testing-library / testing-library/dom-testing-library
`waitFor` introduces unexpected error with rejected Promise
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 3.3k
- Forks
- 474
- PR merge metrics
- No merged PRs in 30d
Description
@testing-library/domversion: 8.20.0- Testing Framework and version:
jest@29.4.1 - DOM Environment:
jest-environment-jsdom@29.4.1
What you did:
Here's a minimal test to reproduce the issue
import * as React from "react";
import "@testing-library/jest-dom"
import { render, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event";
type Props = {reticulate: () => Promise<number>;}
function SplineReticulator({reticulate}: Props) {
const openWindow = async () => {
return new Promise<Window>((res) => {
setTimeout(() => res(window.open('https://www.example.com')!), 250)
})
}
const handler = () => {
const promise = new Promise<number>(async (res, rej) => {
try {
const result = reticulate();
// Uncommenting this line makes the tests pass.
// result.catch(() => {});
await openWindow();
const answer = await result;
return res(answer);
} catch (err) {
return rej(err);
}
});
promise.then(
(result) => { console.log('good!', result) },
(reason) => { console.error('bad!', reason.message) }
);
}
return (
<button onClick={() => handler()}>Click me!</button>
)
}
it('reticulates splines', async () => {
window.open = jest.fn().mockReturnValue({ close: jest.fn(), closed: false });
const reticulate = jest.fn().mockRejectedValue(new Error('oops!'));
const {getByText} = render(<SplineReticulator reticulate={reticulate} />);
userEvent.click(getByText("Click me!"));
await waitFor(() => expect(window.open).toHaveBeenCalled());
});
What happened:
Running this change as-is produces the following error:
Console output
at 09:45:09 PM $ npx jest repro.test.tsx
console.error
bad! oops!
30 | promise.then(
31 | (result) => { console.log('good!', result) },
> 32 | (reason) => { console.error('bad!', reason.message) }
| ^
33 | );
34 | }
35 |
at src/repro.test.tsx:32:29
(node:97479) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
(Use `node --trace-warnings ...` to show where the warning was created)
FAIL src/repro.test.tsx
✕ reticulates splines (322 ms)
● reticulates splines
oops!
41 | it('reticulates splines', async () => {
42 | window.open = jest.fn().mockReturnValue({ close: jest.fn(), closed: false });
> 43 | const reticulate = jest.fn().mockRejectedValue(new Error('oops!'));
| ^
44 |
45 | const {getByText} = render(<SplineReticulator reticulate={reticulate} />);
46 | userEvent.click(getByText("Click me!"));
at src/repro.test.tsx:43:50
at src/repro.test.tsx:31:71
at Object.<anonymous>.__awaiter (src/repro.test.tsx:27:12)
at Object.<anonymous> (src/repro.test.tsx:41:38)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.002 s
Ran all test suites matching /repro.test.tsx/i.
However, if I uncomment Line 20, that section of code now looks like:
const promise = new Promise<number>(async (res, rej) => {
try {
const result = reticulate();
result.catch(() => {});
await openWindow();
const answer = await result;
return res(answer);
} catch (err) {
return rej(err);
}
});
and the tests pass as expected:
Console output
at 09:53:13 PM $ npx jest repro.test.tsx
console.error
bad! oops!
28 | promise.then(
29 | (result) => { console.log('good!', result) },
> 30 | (reason) => { console.error('bad!', reason.message) }
| ^
31 | );
32 | }
33 |
at src/repro.test.tsx:30:29
PASS src/repro.test.tsx
✓ reticulates splines (323 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.992 s, estimated 2 s
Ran all test suites matching /repro.test.tsx/i.
Reproduction:
You can also find this test file available at https://gist.github.com/Dru89/8c37aa36b8fe7093b97761283e9feece
The jest config is basically just the default configuration, with added support for React. I have tried this using both "fake timers" and "real timers" in jest, and it produces the same result.
Problem description:
As you can see, I'm not actually doing anything with that call to result.catch(() => {}), and the promise does get handled, albeit after the call to await openWindow().
My best guess about why this happens comes from a hint in the warnings from Node:
(node:3233) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)
As I mentioned, the promise does actually get handled with the call to await result; and then gets caught by the surrounding try/catch blocks, but I think maybe something in the waitFor might be causing fulfilled promises to settle, which triggers some "unhandled promise rejections" in jest/node or similar?
That theory is buoyed by the fact that if I change the reticulate mock to reject the promise after a delay, that also makes the tests pass. Basically, take the above reproduction, but change reticulate to be:
const reticulate = jest.fn(() => {
return new Promise<number>((res, rej) => {
setTimeout(() => {
rej(new Error('oops!'));
}, 1000);
});
});
Suggested solution:
I don't have any suggestions for possible solutions here. And I'm not even really sure if there's anything that could be done, if the problem is basically that waitFor is "running out" any remaining promises, and jest is maybe just listening for those errors somewhere.
But it was a weird bug that I couldn't find any existing examples for after searching, and wanted to at least document this in case it helped anyone else. (And maybe it is an issue that could be fixed in how waitFor is implemented? 🤷)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the minimal TSX reproduction and run npx jest repro.test.tsx, then inspect the waitFor behavior involved in the rejected Promise case. Compare the immediate and delayed rejection variants and determine whether the warning originates in waitFor or the test environment; done means a confirmed diagnosis and a focused regression test or documented disposition.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, typescript
- Domain
- testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100