Skip to content

Test with suspending component times out with fakeTimers: { enableGlobally: true } #1347

Closed as not planned

Description

@robingullo

Describe the bug

I've set up a simple test with a suspending component (no timers, just a return in async / await) that eventually renders a text. The test waits for the text with await findByText.
With jest.useFakeTimers(), the test pass.
With fakeTimers: { enableGlobally: true } in jest.config.js, the test fails.

Expected behavior

The test should pass in both cases.

Steps to Reproduce

  • Clone the examples/basic directory from the repo
  • Add this test:
import * as React from 'react';
import { render, screen } from '@testing-library/react-native';
import { Text, View } from 'react-native';

beforeEach(() => {
  jest.useFakeTimers();

  // Logs a warning if modern fake timers are not set
  jest.setSystemTime(Date.now());
});

test('should work', async () => {
  let loading = true;
  let data: string | undefined = undefined;

  const Suspending = () => {
    if (loading)
      throw (async () => {
        return await 'result';
      })().then((result) => {
        data = result;
        loading = false;
      });
    return <Text>{data}</Text>;
  };

  render(
    <View>
      <React.Suspense fallback={<Text>Loading</Text>}>
        <Suspending />
      </React.Suspense>
    </View>
  );

  await screen.findByText('result');
});
  • Run the test: it passes ✅
  • In jest.config.js, add the line fakeTimers: { enableGlobally: true }
  • Run the test: it fails ❌

Versions

npmPackages:
@testing-library/react-native: ^11.4.0 => 11.5.2
react: 18.1.0 => 18.1.0
react-native: 0.70.5 => 0.70.5
react-test-renderer: 18.1.0 => 18.1.0

Note:
I have the same result with RN 0.71.3 and the standard 'react-native' preset.

Activity

  1. mdjastrzebski commented on Feb 24, 2023

    @mdjastrzebski
    Member

    Not sure how well RNTL handles React Suspense, I will have to spend some time to check it out.

    BTW why are you throwing a function returning a promise? Also await 'result' seems to be unnecessary as awaiting non-promise value just returns it. Shouldn't the Suspending component throw a promise?

  2. robingullo commented on Feb 27, 2023

    @robingullo
    Author

    I think it throws a promise, the arrow function is called immediately. I admit it might not be the clearest syntax.

  3. robingullo commented on Feb 27, 2023

    @robingullo
    Author

    I encountered the issue initally with React Query btw

  4. pierrezimmermannbam commented on Feb 28, 2023

    @pierrezimmermannbam
    Collaborator

    I've started to look into this and it's not easy. I haven't made much progress so far but I did a reproduction of the issue using nearly no code from RNTL:

    import * as React from 'react';
    import { Text, View } from 'react-native';
    import Renderer from 'react-test-renderer';
    import { getQueriesForElement } from '../..';
    
    jest.useFakeTimers();
    
    test.only('should work', async () => {
      let loading = true;
      let data: string | undefined = undefined;
    
      const Suspending = () => {
        if (loading)
          throw (async () => {
            return await 'result';
          })().then((result) => {
            data = result;
            loading = false;
            return result;
          });
        return <Text>{data}</Text>;
      };
    
      const instance = Renderer.create(
        <View>
          <React.Suspense fallback={<Text>Loading</Text>}>
            <Suspending />
          </React.Suspense>
        </View>
      );
    
      const queries = getQueriesForElement(instance.root);
    
      jest.useRealTimers();
      await new Promise((resolve) => setImmediate(resolve));
      jest.useFakeTimers();
    
      expect(queries.getByText('result')).toBeTruthy();
    });

    I'm only using getQueriesForElement but I'm pretty sure it's not the source of this bug. That would mean that the problem lies elsewhere, although I'm not sure yet where, but I'd guess either jest or react-test-renderer

    Also in both cases the promise resolves correctly but in one case we still see the loading state so it's definitely related to Suspense having a different behavior in both situations somehow

  5. matthieugicquel commented on Mar 16, 2023

    @matthieugicquel
    Contributor

    I've noticed that issue is linked to the faking of setImmediate only. With this config, the test passes:

      fakeTimers: {
        enableGlobally: true,
        doNotFake: ["setImmediate"],
      },
  6. mdjastrzebski commented on Mar 16, 2023

    @mdjastrzebski
    Member

    @robingullo, @pierrezimmermannbam, @matthieugicquel if I understand the above conclusions, seem to indicate that this is a Jest issue, and not a RNTL one. Do you agree with that conclussion?

  7. robingullo commented on Mar 16, 2023

    @robingullo
    Author

    @mdjastrzebski I found a similar issue on the react repo, with a deep analysis. It looks like a more general React / Jest incompatibility. The issue explains how it can fixed by switching to real timers before module imports:

    jest.useRealTimers(); // fails with jest.useFakeTimers()
    const { Suspense } = require('react');
    const { render } = require('@testing-library/react-native');
    const { Text, View } = require('react-native');
    
    jest.useFakeTimers();
    
    beforeEach(() => {
      jest.useFakeTimers();
    
      // Logs a warning if modern fake timers are not set
      jest.setSystemTime(Date.now());
    });
    
    test('should work', async () => {
      let loading = true;
      let data: string | undefined = undefined;
    
      const Suspending = () => {
        if (loading)
          throw Promise.resolve('result').then((result) => {
            data = result;
            loading = false;
          });
        return <Text>{data}</Text>;
      };
    
      const screen = render(
        <View>
          <Suspense fallback={<Text>Loading</Text>}>
            <Suspending />
          </Suspense>
        </View>,
      );
    
      await screen.findByText('result');
    });
    

    But of course it works only with require...

  8. AugustinLF commented on Mar 16, 2023

    @AugustinLF
    Collaborator

    I've had to do the same doNotFake: ["setImmediate"] config change, even when not using any suspense feature though. And that was only needed when enabling globally, but not locally. But when I tried that on the example repo, I couldn't reproduce.

  9. mdjastrzebski commented on Jul 21, 2023

    @mdjastrzebski
    Member

    Closing as both stale and non-actionable at the moment. There is known workaround (Use jest.useFakeTimers() instead of { enableGlobally: true }.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions