javascriptreactjstestingjestjs

Switching from ReactDOM.render to createRoot makes simple jest test fail


I just started studying "Mastering React Test-Driven Development" by Daniel Irvine, and I figured that it shouldn't be too hard to convert the examples to React 18. But I am running into trouble converting the very first test in the book using Jest.

The book doesn't use create-react-app or anything, but instead builds the React apps from scratch, so I'm having trouble finding relevant examples of how to convert the code.

When written as in the book, in React 17 style, the test passes. But if I replace ReactDOM.render() with createRoot(), the test fails.

My application directory looks like:

├── package.json
├── package-lock.json
├── src
│   └── Appointment.js
└── test
    └── Appointment.test.js

and the file contents are:

package.json:

{
  "name": "appointments",
  "version": "1.0.0",
  "description": "Appointments project from Mastering React Test-Driven Development.",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "repository": {
    "type": "git",
    "url": "example.com"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/plugin-transform-runtime": "^7.18.6",
    "@babel/preset-env": "^7.18.6",
    "@babel/preset-react": "^7.18.6",
    "jest": "^28.1.2",
    "jest-environment-jsdom": "^28.1.3"
  },
  "dependencies": {
    "@babel/runtime": "^7.18.6",
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "jest": {
    "testEnvironment": "jsdom"
  }
}

src/Appointment.js:

import React from 'react';

export const Appointment = () => <div>Ashley</div>;

test/Appointment.test.js:

import React from 'react';
import ReactDOM from 'react-dom';
// import {createRoot} from 'react-dom/client';

import {Appointment} from '../src/Appointment';

describe('Appointment', () => {
  it("renders the customer's first name.", () => {
    const customer = {firstName: 'Ashley'};
    const component = <Appointment customer={customer} />;
    const container = document.createElement('div');
    document.body.appendChild(container);

    ReactDOM.render(component, container);

    // const root = createRoot(container);
    // root.render(component);

    expect(document.body.textContent).toMatch('Ashley');
  });
});

With ReactDOM.render(), the test passes, but I get the following error:

  console.error
    Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

      11 |     document.body.appendChild(container);
      12 |
    > 13 |     ReactDOM.render(component, container);
         |              ^
      14 |
      15 |     expect(document.body.textContent).toMatch('Ashley');
      16 |   });

      at printWarning (node_modules/react-dom/cjs/react-dom.development.js:86:30)
      at error (node_modules/react-dom/cjs/react-dom.development.js:60:7)
      at Object.render (node_modules/react-dom/cjs/react-dom.development.js:29670:5)
      at Object.render (test/Appointment.test.js:13:14)

I looked up how to convert ReactDOM.render() to createRoot(), and changed the test to:

import React from 'react';
// import ReactDOM from 'react-dom';
import {createRoot} from 'react-dom/client';

import {Appointment} from '../src/Appointment';

describe('Appointment', () => {
  it("renders the customer's first name.", () => {
    const customer = {firstName: 'Ashley'};
    const component = <Appointment customer={customer} />;
    const container = document.createElement('div');
    document.body.appendChild(container);

    // ReactDOM.render(component, container);

    const root = createRoot(container);
    root.render(component);

    expect(document.body.textContent).toMatch('Ashley');
  });
});

and the test fails as follows:


> appointments@1.0.0 test
> jest

 FAIL  test/Appointment.test.js
  Appointment
    ✕ renders the customer's first name. (9 ms)

  ● Appointment › renders the customer's first name.

    expect(received).toMatch(expected)

    Expected substring: "Ashley"
    Received string:    ""

      17 |     root.render(component);
      18 |
    > 19 |     expect(document.body.textContent).toMatch('Ashley');
         |                                       ^
      20 |   });
      21 | });
      22 |

      at Object.toMatch (test/Appointment.test.js:19:39)
      at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:317:13)
      at runJest (node_modules/@jest/core/build/runJest.js:407:19)
      at _run10000 (node_modules/@jest/core/build/cli/index.js:339:7)
      at runCLI (node_modules/@jest/core/build/cli/index.js:190:3)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.979 s, estimated 1 s
Ran all test suites.

How can I get this test to pass using createRoot()?


Solution

  • After some more digging around, I've found that act() can be used in React 18 to force renders to occur before test asserts are checked. This allows for the tests to run immediately without waiting for Jest's done() to timeout when a test fails.

    globals.IS_REACT_ACT_ENVIRONMENT must be set to true in the Jest configuration. Here I've updated package.json:

    package.json:

    ...
      "jest": {
        "testEnvironment": "jsdom",
        "globals": {
          "IS_REACT_ACT_ENVIRONMENT": true
        }
      }
    ...
    

    The test can then be updated to use act() from react-dom/test-utils:

    import React from 'react';
    import {createRoot} from 'react-dom/client';
    import {act} from 'react-dom/test-utils';
    
    import {Appointment} from '../src/Appointment';
    
    describe('Appointment', () => {
      it("renders the customer's first name.", () => {
        const customer = {firstName: 'Ashley'};
        const component = <Appointment customer={customer} />;
        const container = document.createElement('div');
        document.body.appendChild(container);
    
        const root = createRoot(container);
    
        act(() => root.render(component));
    
        expect(document.body.textContent).toMatch('Ashley');
      });
    });
    

    Resources: