Skip to content

Commit a3e48a7

Browse files
fix: setImmediate fake timer issue with jsdom (#1928)
1 parent 8b17683 commit a3e48a7

2 files changed

Lines changed: 47 additions & 6 deletions

File tree

src/helpers/__tests__/timers.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,39 @@ describe('timers', () => {
55
jest.useFakeTimers();
66
expect(jestFakeTimersAreEnabled()).toEqual(false);
77
});
8+
9+
it('setImmediate polyfill keeps using real timers when fake timers are enabled', async () => {
10+
// Regression test for https://github.com/callstack/react-native-testing-library/issues/1767.
11+
// The module captures its timer functions once at import time. When the environment
12+
// has no native `setImmediate` (e.g. jsdom/browser) it falls back to a polyfill built
13+
// on `setTimeout`; that `setTimeout` must be the real one, so the polyfill keeps
14+
// working after fake timers are re-applied (as happens during auto-cleanup).
15+
delete process.env.RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS;
16+
jest.useRealTimers();
17+
18+
// Simulate a missing native `setImmediate` before the module captures its functions.
19+
const originalSetImmediate = globalThis.setImmediate;
20+
// @ts-expect-error force the polyfill fallback
21+
delete globalThis.setImmediate;
22+
23+
let setImmediate!: (fn: () => void) => unknown;
24+
try {
25+
jest.isolateModules(() => {
26+
// eslint-disable-next-line @typescript-eslint/no-require-imports
27+
setImmediate = require('../timers').setImmediate;
28+
});
29+
} finally {
30+
globalThis.setImmediate = originalSetImmediate;
31+
}
32+
33+
jest.useFakeTimers({ legacyFakeTimers: false });
34+
try {
35+
// Before the fix this never resolved (the polyfill read the fake `setTimeout`),
36+
// so the test would hang until the Jest timeout.
37+
const resolved = await new Promise<boolean>((resolve) => setImmediate(() => resolve(true)));
38+
expect(resolved).toBe(true);
39+
} finally {
40+
jest.useRealTimers();
41+
}
42+
}, 10_000);
843
});

src/helpers/timers.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,22 +63,28 @@ function getFakeTimersConfigFromType(type: FakeTimersTypes) {
6363

6464
const jestFakeTimersAreEnabled = (): boolean => Boolean(getJestFakeTimersType());
6565

66-
// we only run our tests in node, and setImmediate is supported in node.
67-
function setImmediatePolyfill(fn: () => void) {
68-
return globalObj.setTimeout(fn, 0);
69-
}
70-
7166
type BindTimeFunctions = {
7267
clearTimeoutFn: typeof clearTimeout;
7368
setImmediateFn: typeof setImmediate;
7469
setTimeoutFn: typeof setTimeout;
7570
};
7671

7772
function bindTimeFunctions(): BindTimeFunctions {
73+
// Capture the current (real) `setTimeout` so the `setImmediate` polyfill keeps
74+
// using real timers even after fake timers are re-applied. Reading
75+
// `globalObj.setTimeout` lazily inside the polyfill would pick up fake timers.
76+
const realSetTimeout = globalObj.setTimeout;
77+
78+
// `setImmediate` exists in Node, but not in environments such as jsdom, so we
79+
// fall back to a `setTimeout`-based polyfill there (see #1767).
80+
function setImmediatePolyfill(fn: () => void) {
81+
return realSetTimeout(fn, 0);
82+
}
83+
7884
return {
7985
clearTimeoutFn: globalObj.clearTimeout,
8086
setImmediateFn: globalObj.setImmediate || setImmediatePolyfill,
81-
setTimeoutFn: globalObj.setTimeout,
87+
setTimeoutFn: realSetTimeout,
8288
};
8389
}
8490

0 commit comments

Comments
 (0)