How to solve the "update was not wrapped in act()" warning in testing-library-react?
Asked Answered
V

6

149

I'm working with a simple component that does a side effect. My test passes, but I'm getting the warning Warning: An update to Hello inside a test was not wrapped in act(...)..

I'm also don't know if waitForElement is the best way to write this test.

My component

export default function Hello() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
      setPosts(response.data);
    }

    fetchData();
  }, []);

  return (
    <div>
      <ul>
        {
          posts.map(
            post => <li key={post.id}>{post.title}</li>
          )
        }
      </ul>
    </div>
  )
}

My component test

import React from 'react';
import {render, cleanup, act } from '@testing-library/react';
import mockAxios from 'axios';
import Hello from '.';

afterEach(cleanup);

it('renders hello correctly', async () => {
  mockAxios.get.mockResolvedValue({
    data: [
        { id: 1, title: 'post one' },
        { id: 2, title: 'post two' },
      ],
  });

  const { asFragment } = await waitForElement(() => render(<Hello />));

  expect(asFragment()).toMatchSnapshot();
});
Viyella answered 7/2, 2020 at 14:57 Comment(0)
Q
118

Updated answer:

Please refer to @mikaelrs comment below.

No need for the waitFor or waitForElement. You can just use findBy* selectors which return a promise that can be awaited. e.g await findByTestId('list');


Deprecated answer:

Use waitForElement is a correct way, from the docs:

Wait until the mocked get request promise resolves and the component calls setState and re-renders. waitForElement waits until the callback doesn't throw an error

Here is the working example for your case:

index.jsx:

import React, { useState, useEffect } from 'react';
import axios from 'axios';

export default function Hello() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    const fetchData = async () => {
      const response = await axios.get('https://jsonplaceholder.typicode.com/posts');
      setPosts(response.data);
    };

    fetchData();
  }, []);

  return (
    <div>
      <ul data-testid="list">
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
}

index.test.jsx:

import React from 'react';
import { render, cleanup, waitForElement } from '@testing-library/react';
import axios from 'axios';
import Hello from '.';

jest.mock('axios');

afterEach(cleanup);

it('renders hello correctly', async () => {
  axios.get.mockResolvedValue({
    data: [
      { id: 1, title: 'post one' },
      { id: 2, title: 'post two' },
    ],
  });
  const { getByTestId, asFragment } = render(<Hello />);

  const listNode = await waitForElement(() => getByTestId('list'));
  expect(listNode.children).toHaveLength(2);
  expect(asFragment()).toMatchSnapshot();
});

Unit test results with 100% coverage:

 PASS  stackoverflow/60115885/index.test.jsx
  ✓ renders hello correctly (49ms)

-----------|---------|----------|---------|---------|-------------------
File       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------|---------|----------|---------|---------|-------------------
All files  |     100 |      100 |     100 |     100 |                   
 index.jsx |     100 |      100 |     100 |     100 |                   
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 passed, 1 total
Time:        4.98s

index.test.jsx.snapshot:

// Jest Snapshot v1

exports[`renders hello correctly 1`] = `
<DocumentFragment>
  <div>
    <ul
      data-testid="list"
    >
      <li>
        post one
      </li>
      <li>
        post two
      </li>
    </ul>
  </div>
</DocumentFragment>
`;

source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60115885

Quite answered 11/2, 2020 at 8:41 Comment(9)
btw waitForElement is deprecated now. They recommend import { waitFor } from '@testing-library/react' . Good answer tho!Hush
waitFor was needed to mock a property from an html element: await userEvent.type(r.getByRole('textbox'), 'a'); const fullScreenSection = await waitFor(() => r.getByTestId('FullScreenSection')); fullScreenSection.requestFullscreen = jest.fn(); Hintz
No need for the waitFor or waitForElement. You can just use findBy* selectors which return a promise that can be awaited. e.g await findByTestId('list'); ...Jackson
For me, no alternative worked. It's something so inconsistent that I re run the test 10 times and only 1 time I get the warning, with no change to the codeProfessional
What if I don't care about what is displayed after the update and am just asserting if a function that makes a server request is called with the correct data?Barbarese
A quick question related to this - according to the documentations (testing-library.com/docs/queries/about/#types-of-queries), I assume there is not an async version for queryBy? This is because I wanted to verify that an element does not appear in the document and I am currently doing something like this: await waitFor(() => expect(screen.queryByTestId(testId)).not.toBeInTheDocument());Impromptu
@Barbarese were you able to answer your own question? What if I don't care what happens after. I don't want to wait for any elements to appear.Facelifting
@Facelifting nope... I just resorted to awaiting elements I know are basically always present...Barbarese
What if I'm waiting for document.title?Razid
B
22

i had a error:

console.error
  Warning: A suspended resource finished loading inside a test, but the event was not wrapped in act(...).
  
  When testing, code that resolves suspended data should be wrapped into act(...):
  
  act(() => {
    /* finish loading suspended data */
  });
  /* 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

code:

test('check login link', async () => {
    renderRouter({ initialRoute: [home.path] });
    const loginLink = screen.getByTestId(dataTestIds.loginLink);
    expect(loginLink).toBeInTheDocument();
  
    userEvent.click(loginLink);
    const emailInput = screen.getByTestId(dataTestIds.emailInput);
    expect(emailInput).toBeInTheDocument();
}

i have resolved like:

test('check login link', async () => {
  renderRouter({ initialRoute: [home.path] });
  const loginLink = screen.getByTestId(dataTestIds.loginLink);
  expect(loginLink).toBeInTheDocument();

  userEvent.click(loginLink);

  await waitFor(() => {
    const emailInput = screen.getByTestId(dataTestIds.emailInput);
    expect(emailInput).toBeInTheDocument();
  });
}

i have just wrapped in callback fn - waitFor()

Maybe will be useful for someone

Berns answered 19/6, 2022 at 10:56 Comment(3)
You don't need to use waitFor, you can simply change you getByTestId to findByTestId. I think it works out to the same thing.Perni
@lmat-ReinstateMonica you'd still need to await findByTestId before expect.Sound
It would be really helpful if you could also show the import statementsMohler
C
17

WaitFor worked for me, I've tried using findByTestId as mentioned here, but I still got the same action error.

My solution:

it('Should show an error message when pressing “Next” with no email', async () => {
const { getByTestId, getByText  } = render(
  <Layout buttonText={'Common:Actions.Next'} onValidation={() => validationMock}
  />
);

const validationMock: ValidationResults = {
  email: {
    state: ValidationState.ERROR,
    message: 'Email field cannot be empty'
  }
};

await waitFor(() => {
  const nextButton = getByTestId('btn-next');
  fireEvent.press(nextButton);
});

expect(getByText('Email field cannot be empty')).toBeDefined();
Cimbura answered 30/9, 2022 at 15:3 Comment(1)
2024 and waitFor worked for meLicketysplit
U
4

For me, the solution was to wait for waitForNextUpdate

it('useMyHook test', async() => {
      const {
        result,
        waitForNextUpdate
      } = renderHook(() =>
        useMyHook(),
      );
      await waitForNextUpdate()
      expect(result.current).toEqual([])
    }
Uprising answered 5/10, 2021 at 16:59 Comment(3)
This is a timebomb. If your code changes so that, for instance, another fetch is performed or a useEffect triggers another useEffect which then updates state, you'll need another waitForNextUpdate. In other words, you have to put in enough waitForNextUpdate to get through all your promise resolutions which may change in the future! There is a more general solution.Perni
What is that more general solution?Jannjanna
waitForNextUpdate is no longer even in the APIChickamauga
P
4

slideshowp2's answer above is good but pretty specific to your particular example. (His answer doesn't seem to work because it doesn't wait for the axios promise to resolve; there is always a list testid present, but that is easily fixed.)

If your code changes so that, for instance, after the list testId is found, the asserts run, then another useEffect is triggered which causes state updates about which you don't care, you'll get the same act problem again. A general solution is to wrap the render in act to make sure all updates are done before proceeding with assertions and the end of the test. Also, those assertions won't need to waitFor anything. Rewrite the test body as follows:

axios.get.mockResolvedValue({
  data: [
    { id: 1, title: 'post one' },
    { id: 2, title: 'post two' },
  ],
});
let getByTestId;
let asFragment;
await act(()=>{
  const component = render(<Hello />);
  getByTestId = component.getByTestId;
  asFragment = component.asFragment;
});
const listNode = getByTestId('list');
expect(listNode.children).toHaveLength(2);
expect(asFragment()).toMatchSnapshot();

(Import act from the testing library.)

Note that render is wrapped in act, and finding the list is done using getBy* which is not asynchronous! All the promise resolutions are complete before the getByTestId call, so no state updates happen after the end of the test.

Perni answered 9/9, 2022 at 16:51 Comment(4)
What if for example I'm testing the appearance of a loading spinner and don't want to wait for the promise (API call) to be resolved? Then act(...) warnings will be visible no matter what. Right?Facelifting
There are hacks you can use (act(<anything with a promise>) or await new Promise(process.nextTick)) which can cause the runtime to go through the promises once more. This will remove the warning, but if you have a promise that spawns another promise, you'll need two "hacks", etc. In general, I have found no way to do that without warnings. Good luck out there!Perni
or await act(() => Promise.resolve()) or await act(async () => '')Homemaking
This worked for me. The first answer isn't enough, with this answer I solve this bug.Intersidereal
F
0

A common source of this error in particular is that the code triggering the state changes is not waiting for the state changes to happen. Seems obvios, right? but maybe is not that obvious when triggering so many state updates. Let me illustrate my case:

// Assert
const user = userEvent.setup();
render(<RouterProvider router={router} />);
// Get a reference to the input elements on the form
const titleField = screen.getByRole("textbox", { name: /title/i });
const descriptionField = screen.getByRole("textbox", {
  name: /description/i,
});
// Get a reference to the submit button
const saveButton = screen.getByRole("button", { name: /Save/i });

// Act
// Fill the inputs 
user.type(titleField, "Go to party");
user.type(descriptionField, "Let's have some fun!");

// Click on save todo
await user.click(saveButton);

// Assert
// expect sentences

Everything looks so fine, right? But .type is also an async method and it is triggering an state update. SOLUTION

// Act
// Fill the inputs (and wait for the state to update)
await user.type(titleField, "Go to party");
await user.type(descriptionField, "Let's have some fun!");

// Click on save todo
await user.click(saveButton);

// Assert
// expect sentences
Flagstad answered 13/11, 2023 at 21:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.