Getting not wrapped in act error despite already wrapped

Viewed 612

I am testing my React component to verify a callback function behavior delayed by setTimeout but I am receiving the error below despite my fake timer is already triggered within an act block.

The error is as below:

C:\dev\node\node.exe --require "C:\dev\JetBrains\IntelliJ IDEA 2021.1.1\plugins\JavaScriptLanguage\helpers\jest-intellij\lib\jest-intellij-stdin-fix.js" C:\Users\Reph0\Desktop\my-app\node_modules\react-scripts\bin\react-scripts.js test --colors --reporters "C:\dev\JetBrains\IntelliJ IDEA 2021.1.1\plugins\JavaScriptLanguage\helpers\jest-intellij\lib\jest-intellij-reporter.js" --verbose "--testNamePattern=^Timeout component should randomize value until more than 0\.9$" --runTestsByPath C:/Users/Reph0/Desktop/my-app/src/TimeoutComponent.spec.tsx
  console.error
    Warning: An update to TimeoutComponent inside a test was not wrapped in act(...).
    
    When testing, code that causes React state updates should be wrapped into act(...):
    
    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */
    
    This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
        at TimeoutComponent (C:\Users\Reph0\Desktop\my-app\src\TimeoutComponent.tsx:8:35)

The component

import * as React from "react";

interface Props {
  getValue: () => Promise<number>;
}

const TimeoutComponent: React.FC<Props> = (props) => {
  const [value, setValue] = React.useState<number>(0);

  React.useEffect(() => {
    if (value < 0.9) {
      const timeoutId = setTimeout(() => props.getValue().then(setValue), 300);
      return () => {
        clearTimeout(timeoutId);
      };
    }
  }, [value]);

  return (
    <>
      <p>
        Value: <span id="value">{value}</span>
      </p>
    </>
  );
};

export default TimeoutComponent;

The test

import { render } from "@testing-library/react";
import React from "react";
import { act } from "react-dom/test-utils";
import TimeoutComponent from "./TimeoutComponent";

const getValue = jest.fn();

describe("Timeout component", () => {
  beforeEach(() => {
    jest.resetAllMocks();
    jest.useFakeTimers();
  });

  afterEach(() => {
    jest.useRealTimers();
  });

  it("should randomize value until more than 0.9", () => {
    getValue
      .mockResolvedValueOnce(0.1)
      .mockResolvedValueOnce(0.2)
      .mockResolvedValueOnce(0.3)
      .mockResolvedValueOnce(0.4)
      .mockResolvedValueOnce(0.7)
      .mockResolvedValueOnce(0.92)
      .mockResolvedValueOnce(0.6);

    render(<TimeoutComponent getValue={getValue} />);

    act(() => {
      jest.runAllTimers();
    });

    expect(getValue).toBeCalledTimes(6);
  });
});

Here is my package.json

{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "@types/jest": "^26.0.15",
    "@types/node": "^12.0.0",
    "@types/react": "^17.0.0",
    "@types/react-dom": "^17.0.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-scripts": "4.0.3",
    "typescript": "^4.1.2",
    "web-vitals": "^1.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Here is the link to my codesandbox but seems like jest.runAllTimers() doesn't work well there.

Appreciate if someone can give a hand :)

1 Answers

The error happened because the state is changing during testing. We can wait for state change and do the assertion afterward.

For your case, we can wait for the content where it does not contain 0.6 (state will not be changed after 0.92) and determine whether getValue has been called 6 times before.

it("should randomize value until more than 0.9", async () => {
    getValue
      .mockResolvedValueOnce(0.1)
      .mockResolvedValueOnce(0.2)
      .mockResolvedValueOnce(0.3)
      .mockResolvedValueOnce(0.4)
      .mockResolvedValueOnce(0.7)
      .mockResolvedValueOnce(0.92)
      .mockResolvedValueOnce(0.6);

    const result = render(<TimeoutComponent getValue={getValue} />);
    try {
      jest.runAllTimers();

      // wait until the content contains 0.6 
      // will timeout as 0.6 will never be displayed; and 
      // will go to catch and finally block
      await waitFor(() => {
        result.getByText(/0.6/)
      })
    } catch (e) {
      console.log("timeout for waiting for 0.6 appear: ", e.message)
    } finally {
      // get the latest value in the content before timeout error, must be 0.92
      expect(result.container.getElementsByTagName("span")[0].innerHTML).toEqual("0.92");

      // get the number of time getValue has been called before timeout error, must be 6
      expect(getValue).toBeCalledTimes(6);
    }
});

Reference of waitFor:

https://testing-library.com/docs/guide-disappearance/#waiting-for-appearance

Related