react-native-testing-library icon indicating copy to clipboard operation
react-native-testing-library copied to clipboard

Tests with async useEffect don't seem to complete

Open leepowelldev opened this issue 3 years ago • 6 comments

Describe the bug

So I guess this is on the back of my question (https://github.com/callstack/react-native-testing-library/issues/1066) where I was trying to reproduce and isolate the issue. However, I have come up against something similar.

Repo: https://github.com/leepowelldev/rntl-async-bug

I have created a new component MyComponent which has three useEffects which consume async methods (each has appropriate cleanup functions). The test waits for these effects to run, and then assert the final rendered text. However, this never succeeds and the text can never be found. It seems to hang on the second useEffect and the test times out - resulting in the component being unmounted.

Expected behavior

Test should pass and should be able to find 'Hello world.' - this works as expected in simulator with no errors.

Steps to Reproduce

Clone repo, install deps and run npm test or yarn test

Screenshots

Screenshot 2022-08-18 at 16 51 14

Versions

npmPackages: @testing-library/react-native: ^11.0.0 => 11.0.0 react: 18.0.0 => 18.0.0 react-native: 0.69.4 => 0.69.4 react-test-renderer: 18.0.0 => 18.0.0

leepowelldev avatar Aug 18 '22 15:08 leepowelldev

I believe the issue is the combination of timeouts and promises. From what I notice, the first promise is never resolved. Not sure this should be handled by the library but I do think in this kind of situation what you want to do is use fake timers. Using real timers with timeouts makes tests less predictable and in some cases quite long running so I'd advise against that. I managed to make your test work by using fake timers with the following code :

jest.useFakeTimers();

test("MyComponent", async () => {
  render(<MyComponent />);

  // Commenting this out makes it work but with `act` warnings... however
  // wrapping it in an async `act` causes the test to fail again
  // await wait({ ms:5000 });

  // Run timer of stage 1
  jest.runOnlyPendingTimers();
  // Wait for promise to be resolved
  await waitFor(() => {});

  // Run timer of stage 2
  jest.runOnlyPendingTimers();
  // Wait for promise to be resolved
  await waitFor(() => {});

  // Run timer of stage 3
  jest.runOnlyPendingTimers();
  await screen.findByText("Hello world.", {}, { timeout: 20000 });
}, 30000);

This is not ideal as there are waitFor with empty callbacks which act here as a way to flush promises but there is no observable effect at the end of the first two stages. This is a very contrived example though and it should be rather unlikely that you encounter something similar very often

Hope this helps !

pierrezimmermannbam avatar Aug 19 '22 14:08 pierrezimmermannbam

I'm more concerned about why the first promise doesn't resolve ... I can't see any reason it shouldn't. And I don't think I should be limited to fake timers to make this run. Appreciate this is a contrived example, but I currently have real world code that is similar to this - async useEffects updating state to trigger other useEffects.

I'll certainly take a look over your approach. Thanks.

leepowellnbs avatar Aug 19 '22 14:08 leepowellnbs

@leepowelldev Does the MyComponent test work correctly if we have only 2 promises? What about a single promise? I wonder what might be threshold fold observed behaviour.

mdjastrzebski avatar Aug 19 '22 21:08 mdjastrzebski

@mdjastrzebski a single promise passes, mutiple promises fails. I don't believe this is a problem with timeouts, but possibly an issue with the promises...

I tried changing the wait util from:

function wait({
  ms = 500,
  signal,
}: { ms?: number; signal?: AbortSignal } = {}): Promise<void> {
  return new Promise((resolve, reject) => {
    const id = setTimeout(() => {
      resolve()
    }, ms);

    if (signal) {
      signal.addEventListener('abort', () => {
        clearTimeout(id);
        reject(new Error('Aborted'));
      });
    }
  });
}

to simply:

function wait(): Promise<void> {
  return Promise.resolve();
}

And still got the same issues.

leepowelldev avatar Aug 20 '22 13:08 leepowelldev

FYI I tested this in @testing-library/react and it passes as expected.

leepowellnbs avatar Sep 21 '22 10:09 leepowellnbs

I'm pretty sure it's the same as #1093. If you use react 18, we seem to have issues with some cases. In the linked issue there is an attached PR that should fix that.

AugustinLF avatar Sep 21 '22 11:09 AugustinLF

I've retested the repro repo you submitted with following deps update and all tests pass:

  "dependencies": {
    "react": "18.1.0",
    "react-dom": "18.1.0",
    "react-native": "0.70.1",
  },
  "devDependencies": {
    "@testing-library/react-native": "^11.2.0",
    "react-test-renderer": "18.1.0",
  },

Note: the tests seem to require Node 16, due to AbortController not available in Node 14.

@leepowelldev can you confirm the RNTL v11.2.0 (with potential React, React Test Renderer and React Native updates) does solve the issue for you.

mdjastrzebski avatar Sep 26 '22 12:09 mdjastrzebski

Closing as fixed.

@leepowelldev If the issue still occurs for you on the latest version of RNTL, please provide update the repo with latest RNTL, React, etc deps so that we are able to reproduce it on our end.

mdjastrzebski avatar Sep 30 '22 11:09 mdjastrzebski

@mdjastrzebski sorry, this was on my "todo" list, but it's been a busy few days and it slipped my mind. Thanks for the fix and for testing 🙇

leepowellnbs avatar Sep 30 '22 11:09 leepowellnbs