fix(test): make fake timers work with testing-library/react (#25915)

## Summary

Fixes #25869

Two fixes to enable `jest.useFakeTimers()` to work with
`@testing-library/react` and `@testing-library/user-event`:

- Set `setTimeout.clock = true` when fake timers are enabled.
testing-library/react's `jestFakeTimersAreEnabled()` checks for this
property to determine if `jest.advanceTimersByTime()` should be called
when draining the microtask queue. Without this, testing-library never
advances timers.

- Make `advanceTimersByTime(0)` fire `setTimeout(fn, 0)` timers.
`setTimeout(fn, 0)` is internally scheduled with a 1ms delay per HTML
spec. Jest/testing-library expect `advanceTimersByTime(0)` to fire such
"immediate" timers, but we were advancing by 0ms so they never fired.

## Test plan

- [x] All 30 existing fake timer tests pass
- [x] New regression test validates both fixes
- [x] Original user-event reproduction now works (test completes instead
of hanging)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
robobun
2026-01-08 20:25:35 -08:00
committed by GitHub
parent 596e83c918
commit eb5b498c62
2 changed files with 108 additions and 1 deletions

View File

@@ -0,0 +1,81 @@
// https://github.com/oven-sh/bun/issues/25869
// useFakeTimers with testing-library/react hangs when using user-event
import { expect, jest, test } from "bun:test";
// Test that jestFakeTimersAreEnabled() detection works properly.
// testing-library/react checks for setTimeout.clock or setTimeout._isMockFunction
// to determine if fake timers are enabled.
function jestFakeTimersAreEnabled(): boolean {
// @ts-expect-error - checking for Jest fake timers markers
if (typeof jest !== "undefined" && jest !== null) {
return (
// @ts-expect-error - checking for mock function marker
(globalThis.setTimeout as any)._isMockFunction === true ||
Object.prototype.hasOwnProperty.call(globalThis.setTimeout, "clock")
);
}
return false;
}
test("setTimeout.clock is not set before useFakeTimers", () => {
expect(jestFakeTimersAreEnabled()).toBe(false);
expect(Object.prototype.hasOwnProperty.call(globalThis.setTimeout, "clock")).toBe(false);
});
test("setTimeout.clock is set after useFakeTimers", () => {
jest.useFakeTimers();
try {
expect(jestFakeTimersAreEnabled()).toBe(true);
expect(Object.prototype.hasOwnProperty.call(globalThis.setTimeout, "clock")).toBe(true);
} finally {
jest.useRealTimers();
}
});
test("setTimeout.clock is set to false after useRealTimers", () => {
jest.useFakeTimers();
jest.useRealTimers();
// Note: The clock property remains on setTimeout but is set to false.
// This differs from Jest/Sinon which removes the property entirely.
// The value being false is sufficient for most use cases.
expect((globalThis.setTimeout as any).clock).toBe(false);
});
test("advanceTimersByTime(0) fires setTimeout(fn, 0) timers", async () => {
jest.useFakeTimers();
try {
let called = false;
setTimeout(() => {
called = true;
}, 0);
expect(called).toBe(false);
jest.advanceTimersByTime(0);
expect(called).toBe(true);
} finally {
jest.useRealTimers();
}
});
test("user-event style wait pattern does not hang", async () => {
jest.useFakeTimers();
try {
// This is the pattern used by @testing-library/user-event in wait.js
// It was hanging before the fix because:
// 1. advanceTimersByTime(0) didn't fire setTimeout(fn, 0) timers
// 2. jestFakeTimersAreEnabled() returned false, so advanceTimers wasn't called
const delay = 0;
const result = await Promise.all([
new Promise<string>(resolve => globalThis.setTimeout(() => resolve("timeout"), delay)),
Promise.resolve().then(() => {
jest.advanceTimersByTime(delay);
return "advanced";
}),
]);
expect(result).toEqual(["timeout", "advanced"]);
} finally {
jest.useRealTimers();
}
});