Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React Native testing - act without await

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
like image 842
Mehmet N. Yarar Avatar asked Nov 22 '20 09:11

Mehmet N. Yarar


Video Answer


1 Answers

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();
    });
  });
});

like image 65
sm7 Avatar answered Oct 08 '22 00:10

sm7