Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/helpers/__tests__/timers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,39 @@ describe('timers', () => {
jest.useFakeTimers();
expect(jestFakeTimersAreEnabled()).toEqual(false);
});

it('setImmediate polyfill keeps using real timers when fake timers are enabled', async () => {
// Regression test for https://github.com/callstack/react-native-testing-library/issues/1767.
// The module captures its timer functions once at import time. When the environment
// has no native `setImmediate` (e.g. jsdom/browser) it falls back to a polyfill built
// on `setTimeout`; that `setTimeout` must be the real one, so the polyfill keeps
// working after fake timers are re-applied (as happens during auto-cleanup).
delete process.env.RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS;
jest.useRealTimers();

// Simulate a missing native `setImmediate` before the module captures its functions.
const originalSetImmediate = globalThis.setImmediate;
// @ts-expect-error force the polyfill fallback
delete globalThis.setImmediate;

let setImmediate!: (fn: () => void) => unknown;
try {
jest.isolateModules(() => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
setImmediate = require('../timers').setImmediate;
});
} finally {
globalThis.setImmediate = originalSetImmediate;
}

jest.useFakeTimers({ legacyFakeTimers: false });
try {
// Before the fix this never resolved (the polyfill read the fake `setTimeout`),
// so the test would hang until the Jest timeout.
const resolved = await new Promise<boolean>((resolve) => setImmediate(() => resolve(true)));
expect(resolved).toBe(true);
} finally {
jest.useRealTimers();
}
}, 10_000);
});
18 changes: 12 additions & 6 deletions src/helpers/timers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,28 @@ function getFakeTimersConfigFromType(type: FakeTimersTypes) {

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

// we only run our tests in node, and setImmediate is supported in node.
function setImmediatePolyfill(fn: () => void) {
return globalObj.setTimeout(fn, 0);
}

type BindTimeFunctions = {
clearTimeoutFn: typeof clearTimeout;
setImmediateFn: typeof setImmediate;
setTimeoutFn: typeof setTimeout;
};

function bindTimeFunctions(): BindTimeFunctions {
// Capture the current (real) `setTimeout` so the `setImmediate` polyfill keeps
// using real timers even after fake timers are re-applied. Reading
// `globalObj.setTimeout` lazily inside the polyfill would pick up fake timers.
const realSetTimeout = globalObj.setTimeout;

// `setImmediate` exists in Node, but not in environments such as jsdom, so we
// fall back to a `setTimeout`-based polyfill there (see #1767).
function setImmediatePolyfill(fn: () => void) {
return realSetTimeout(fn, 0);
}

return {
clearTimeoutFn: globalObj.clearTimeout,
setImmediateFn: globalObj.setImmediate || setImmediatePolyfill,
setTimeoutFn: globalObj.setTimeout,
setTimeoutFn: realSetTimeout,
};
}

Expand Down