React Native testing - act without await
Asked Answered
A

2

14

Below test is passing but I get the following warning twice and I don't know why. Could someone help me to figure it out?

    console.error
    Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);
      at printWarning (../../node_modules/react-test-renderer/cjs/react-test-renderer.development.js:120:30)
      at error (../../node_modules/react-test-renderer/cjs/react-test-renderer.development.js:92:5)
      at ../../node_modules/react-test-renderer/cjs/react-test-renderer.development.js:14953:13
      at tryCallOne (../../node_modules/react-native/node_modules/promise/lib/core.js:37:12)
      at ../../node_modules/react-native/node_modules/promise/lib/core.js:123:15
      at flush (../../node_modules/asap/raw.js:50:29)
import { fireEvent } from '@testing-library/react-native'
import { renderScreen } from 'test/render'

describe('screens/home', () => {
  it('should render and redirect to the EventScreen', async () => {
    const {
      getByA11yLabel,
      findByA11yLabel,
      findAllByA11yLabel,
      toJSON
    } = renderScreen('Main')
    expect(toJSON()).toMatchSnapshot('Default render')

    const title = 'New event'
    const titleInput = getByA11yLabel('event.title')

    // Change title - sync fn
    fireEvent.changeText(titleInput, title)

    // Create button should be visible
    const createButton = await findByA11yLabel('event.create')
    expect(titleInput.props.value).toBe(title)
    expect(createButton).toBeTruthy()
    expect(toJSON()).toMatchSnapshot('Change title')

    // Create event - async fn
    fireEvent.press(createButton)

    // The app should be redirected to the EventScreen
    const titleInputs = await findAllByA11yLabel('event.title')
    const upsertButton = await findByA11yLabel('event.upsert')
    expect(toJSON()).toMatchSnapshot('Create event')
    expect(titleInputs).toHaveLength(2)
    expect(titleInputs[0].props.value).toBe('') // @MainScreen
    expect(titleInputs[1].props.value).toBe(title) // @EventScreen
    expect(upsertButton).toBeTruthy()
  })
})
  • As far as I know, there is no need to wrap fireEvent with an act- link
  • findBy* also are automatically wrapped with act - link
  • Related issue in GitHub is still open

Dependencies:

  • react: 16.13.1
  • expo: 39.0.4
  • jest: 26.6.3
  • ts-jest: 26.4.4
  • jest-expo: 39.0.0
  • @testing-library/jest-native: 3.4.3
  • @testing-library/react: 11.2.2
  • @testing-library/react-native: 7.1.0
  • react-test-renderer: 16.13.1
  • typescript: 4.1.2
Attach answered 22/11, 2020 at 9:17 Comment(0)
A
4

If you've exhausted all other debugging efforts and are pretty sure your code is written correctly, it may be related to react-native/jest-preset replacing global.Promise with a mock (see issue).

The solution to the problem, in this case, is to override/patch the jest preset to first save the original global Promise, apply the react-native/jest-preset and then restore the original Promise (overwriting the mocked version). This allowed me to use await in the tests that were unrelated to rendering without triggering the dreaded

console.error
Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);

This snippet shows one way to perform this patch: https://github.com/sbalay/without_await/commit/64a76486f31bdc41f5c240d28263285683755938

Ambi answered 16/9, 2021 at 2:58 Comment(2)
this sounds great but I am using jest-expo and this solution does not help me.Fisken
did you find a way to do it with jest-expo @JoeyGough ??Alleviate
S
2

I was facing the same problem. For my case I was using useEffect in my component. And while test it prompted me to wrap the rendering inside an act() call. Once I did that i.e. act(async () => ...) my initial problem was solved but I was getting the above mentioned error (Warning: You called act(async () => ...) without await.). I had to use await act(async () => ...) in my test to fix that. Though I am still not sure why it was required.

For reference I am adding a complete example component and corresponding test using await act(async () => ...);

LocationComponent.tsx

/** @jsx jsx */
import { jsx } from 'theme-ui';
import { FunctionComponent, useEffect, useState } from 'react';

type Coordinate = {
  latitude: number;
  longitude: number;
};

const LocationComponent: FunctionComponent<any> = () => {
  const [coordinate, setCoordinate] = useState<Coordinate>();
  const [sharedLocation, setSharedLocation] = useState<boolean>();
  useEffect(() => {
    let mounted = true;

    if (!coordinate && navigator) {
      navigator.geolocation.getCurrentPosition(function (position) {
        setCoordinate({
          latitude: position.coords.latitude,
          longitude: position.coords.longitude,
        });
      });
      navigator.permissions
        .query({ name: 'geolocation' })
        .then(function (result) {
          if (mounted) setSharedLocation(result.state === 'granted');
        });
    }

    return () => (mounted = false);
  });

  return (
    <>
      <div>Location shared:{sharedLocation ? 'Yes' : 'No'}</div>
      <div>Latitude:{coordinate?.latitude}</div>
      <div>Longitude:{coordinate?.longitude}</div>
    </>
  );
};
export default LocationComponent;

LocationComponent.spec.tsx

import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import LocationComponent from '../../../../../src/components/scheduler/location/LocationComponent';

const TEST_COORDS = {
  latitude: 41.8817089,
  longitude: -87.643301,
};

global.navigator.permissions = {
  query: jest
    .fn()
    .mockImplementationOnce(() => Promise.resolve({ state: 'granted' })),
};

global.navigator.geolocation = {
  getCurrentPosition: jest.fn().mockImplementationOnce((success) =>
    Promise.resolve(
      success({
        coords: TEST_COORDS,
      })
    )
  ),
};

describe("Location Component when location share is 'granted'", () => {
  it('should display current location details', async () => {
    await act(async () => {
      const { getByText } = render(<LocationComponent />);

      /*expect(
        await waitFor(() => getByText('Location shared:Yes'))
      ).toBeInTheDocument();*/
      expect(
        await waitFor(() => getByText('Latitude:41.8817089'))
      ).toBeInTheDocument();
      expect(
        await waitFor(() => getByText('Longitude:-87.643301'))
      ).toBeInTheDocument();
    });
  });
});

Studied answered 25/11, 2020 at 2:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.