When testing, code that causes React state updates should be wrapped into act
Asked Answered
M

10

96

I have this test:

import {
  render,
  cleanup,
  waitForElement
} from '@testing-library/react'

const TestApp = () => {
  const { loading, data, error } = useFetch<Person>('https://example.com', { onMount: true });

  return (
    <>
      {loading && <div data-testid="loading">loading...</div>}
      {error && <div data-testid="error">{error.message}</div>}
      {data && 
        <div>
          <div data-testid="person-name">{data.name}</div>
          <div data-testid="person-age">{data.age}</div>
        </div>
      }
    </>
  );
};

  describe("useFetch", () => {
    const renderComponent = () => render(<TestApp/>);

    it('should be initially loading', () => {
      const { getByTestId } = renderComponent();

      expect(getByTestId('loading')).toBeDefined();
    })
  });

The test passes but I get the following warning:

Warning: An update to TestApp 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
    in TestApp

console.error node_modules/react-dom/cjs/react-dom.development.js:506 Warning: An update to TestApp 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
    in TestApp
Management answered 23/6, 2019 at 8:12 Comment(1)
Read github.com/testing-library/react-testing-library/issues/281Emasculate
G
113

The key is to await act and then use async arrow function.

await act( async () => render(<TestApp/>));

Source:

https://mcmap.net/q/219308/-testing-react-components-that-fetches-data-using-hooks

Gyre answered 24/9, 2020 at 14:36 Comment(10)
that source is using enzyme mount but here the post is using the react testing library, personally I tried this without successScissors
Worked for me when using react-hook-form.Wheeling
If you vote down please comment why. Hard to improve answers otherwiseGyre
This worked for me!!Amanda
This worked for me and I'm using renderHook from the @testing-library/react (version 13.2.0) in React 18Danit
Lots of fancy solutions out there that didn't work for me, then this one-liner solution did the miracle. Thanks op. Worked like a charm.Johannajohannah
I'm using Jest and RTL for unit tests in a React TypeScript application. This worked for me as well. You would only need to wrap your render like so await act(()=> render(...)) Worked like a charm indeed.Altocumulus
worked using @testing-library/react @testing-library/jest-dom This issue displayed from a component contained in another component did this and worked flawlessly.Malefaction
What's the async for?Concourse
I'm downvoting this because the simultaneous use of act, async and render in one statement has shadowed the real reason that this works. The only part that works is await. Add await waitFor(() => {}) below the render line also works. See my answer.Haifa
W
55

Try asserting inside 'await waitFor()' - for this your it() function should be async

it('should be initially loading', async () => {
  const { getByTestId } = renderComponent();

  await waitFor(() => {
    expect(getByTestId('loading')).toBeDefined();
  });
});

Keep calm and happy coding

Wite answered 10/7, 2021 at 21:19 Comment(0)
S
9

Try using await inside act

import { act } from 'react-dom/test-utils';
await act(async () => {
            wrapper = mount(Commponent);
            wrapper.find('button').simulate('click');
        });
Sample answered 6/1, 2022 at 16:50 Comment(1)
This worked for me, though I'm not really sure why? The example here is very similar to my code, there is nothing awaited in the inner function, and we aren't returning any other promise, so it seems like it should just be a promise that returns immediately. I guess it just waits until the next tick, which is enough?Snapp
Z
5

I was getting the same issue which gets resolved by using async queries (findBy*) instead of getBy* or queryBy*.

expect(await screen.findByText(/textonscreen/i)).toBeInTheDocument(); 

Async query returns a Promise instead of element, which resolves when an element is found which matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of 1000ms. If you need to find more than one element, use findAllBy.

https://testing-library.com/docs/dom-testing-library/api-async/

But as you know it wont work properly if something is not on screen. So for queryBy* one might need to update test case accordingly

[Note: Here there is no user event just simple render so findBy will work otherwise we need to put user Event in act ]

Zoosporangium answered 14/7, 2022 at 16:8 Comment(0)
S
2
    test('handles server ok', async () => {
    render(
      <MemoryRouter>
        <Login />
      </MemoryRouter>
    )

    await waitFor(() => fireEvent.click(screen.getByRole('register')))

    let domInfo
    await waitFor(() => (domInfo = screen.getByRole('infoOk')))

    // expect(domInfo).toHaveTextContent('登陆成功')
  })

I solved the problem in this way,you can try it.

Shulman answered 28/3, 2022 at 6:18 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Vivacity
P
1

React app with react testing library:

I tried a lot of things, what worked for me was to wait for something after the fireevent so that nothing happens after the test is finished.

In my case it was a calendar that opened when the input field got focus. I fireed the focus event and checked that the resulting focus event occured and finished the test. I think maybe that the calendar opened after my test was finished but before the system was done, and that triggered the warning. Waiting for the calendar to show before finishing did the trick.

fireEvent.focus(inputElement);

await waitFor(async () => {
  expect(await screen.findByText('December 2022')).not.toBeNull();
});
expect(onFocusJestFunction).toHaveBeenCalledTimes(1);
// End

Hopes this helps someone, I just spent half a day on this.

Predestine answered 5/12, 2022 at 15:17 Comment(0)
T
1

This is just a warning in react-testing-library (RTL). you do not have to use act in RTL because it is already using it behind the scenes. If you are not using RTL, you have to use act

import {act} from "react-dom/test-utils"
test('',{
    act(()=>{
        render(<TestApp/>)
    })
})

You will see that warning when your component does data fetching. Because data fetching is async, when you render the component inside act(), behing the scene all the data fetching and state update will be completed first and then act() will finish. So you will be rendering the component, with the latest state update

Easiest way to get rid of this warning in RTL, you should run async query functions findBy*

test("test", async () => {
  render(
    <MemoryRouter>
      <TestApp />
    </MemoryRouter>
  );

  await screen.findByRole("button");
});
Templia answered 2/2, 2023 at 18:12 Comment(0)
L
0

I don't see the stack of the act error, but I guess, it is triggered by the end of the loading when this causes to change the TestApp state to change and rerender after the test finished. So waiting for the loading to disappear at the end of the test should solve this issue.

describe("useFetch", () => {
  const renderComponent = () => render(<TestApp/>);

  it('should be initially loading', async () => {
    const { getByTestId } = renderComponent();

    expect(getByTestId('loading')).toBeDefined();
    await waitForElementToBeRemoved(() => queryByTestId('loading'));
  });
});
Lobelia answered 24/6, 2022 at 14:28 Comment(0)
H
0

This may be caused by the issue unmount timing leads to "test was not wrapped in act". In short, react states are still getting updated even after the test has ended.

One straightforward way is to explicitly unmount before the end of the test:

    it('should be initially loading', () => {
      const { getByTestId, unmount } = render(<MyApp/>);
      expect(getByTestId('loading')).toBeDefined();
      unmount();
    })

If you are willing to change the test function to be async, calling await waitFor(() => {}) after render also works by giving an opportunity for the app to finish updating the states, but this may be less reliable and I would recommend the method above as it cuts the throat of the issue:

    it('should be initially loading', async () => {
      const { getByTestId } = render(<MyApp/>);
      await waitFor(() => {});
      expect(getByTestId('loading')).toBeDefined();
    })
Haifa answered 19/2 at 9:23 Comment(0)
L
0

after you do something that update the state, add this

const pause = async (ms) => {
  await act(async () => {
    await sleep(ms || 100);
  });
};
await pause()
Letta answered 19/2 at 10:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.