132

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();
});
Lin Du
  • 88,126
  • 95
  • 281
  • 483
Pablo Darde
  • 5,844
  • 10
  • 37
  • 55

5 Answers5

104

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

Lin Du
  • 88,126
  • 95
  • 281
  • 483
  • 27
    btw `waitForElement` is deprecated now. They recommend `import { waitFor } from '@testing-library/react'` . Good answer tho! – victorkurauchi May 21 '20 at 14:44
  • 1
    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(); ` – Ambroise Rabier Aug 04 '20 at 12:22
  • 33
    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'); ...` – mikaelrs Sep 15 '20 at 10:43
  • 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 code – Vencovsky Jun 24 '21 at 17:06
  • 3
    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? – Elias Aug 17 '21 at 08:33
  • A quick question related to this - according to the documentations (https://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());` – H.T. Koo Feb 01 '23 at 15:58
  • @Elias 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. – karlosos Feb 15 '23 at 13:05
  • @karlosos nope... I just resorted to awaiting elements I *know* are basically always present... – Elias Feb 16 '23 at 17:13
16

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

Oleksii
  • 269
  • 3
  • 4
10

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();
KarolHarumi
  • 109
  • 1
  • 5
5

For me, the solution was to wait for waitForNextUpdate

it('useMyHook test', async() => {
      const {
        result,
        waitForNextUpdate
      } = renderHook(() =>
        useMyHook(),
      );
      await waitForNextUpdate()
      expect(result.current).toEqual([])
    }
Elfen
  • 903
  • 7
  • 8
  • 1
    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. – lmat - Reinstate Monica Sep 09 '22 at 16:40
  • 3
    What is that more general solution? – A dev Nov 16 '22 at 10:22
3

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.

lmat - Reinstate Monica
  • 7,289
  • 6
  • 48
  • 62
  • 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? – karlosos Feb 15 '23 at 13:08
  • 1
    There are hacks you can use (`act()` 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! – lmat - Reinstate Monica Feb 15 '23 at 21:11
  • 2
    or `await act(() => Promise.resolve())` or `await act(async () => '')` – Mathieu CAROFF Apr 28 '23 at 16:49
  • This worked for me. The first answer isn't enough, with this answer I solve this bug. – Luis Jun 15 '23 at 04:16