Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
robingullo opened this issue Feb 24, 2023 · 9 comments

Comments

@robingullo
Copy link

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.

@mdjastrzebski
Copy link
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?

@robingullo
Copy link
Author

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

@robingullo
Copy link
Author

I encountered the issue initally with React Query btw

@pierrezimmermannbam
Copy link
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

@matthieugicquel
Copy link
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"],
  },

@mdjastrzebski
Copy link
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?

@robingullo
Copy link
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...

@AugustinLF
Copy link
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.

@mdjastrzebski
Copy link
Member

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

@mdjastrzebski mdjastrzebski closed this as not planned Won't fix, can't repro, duplicate, stale Jul 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants