With React Native Testing Library and jest.useFakeTimers(), how do waitFor and userEvent.setup advance the fake clock, and when must you pass advanceTimers yourself?
answer
- RNTL detects Jest's fake timers
- waitFor steps the clock by interval
- timeout is measured in fake milliseconds
- default advanceTimers calls jest.advanceTimersByTime
- custom advanceTimers for non-Jest clocks
basics
~10 sRNTL detects Jest fake timers: waitFor and findBy advance the fake clock by interval until the timeout is spent, and userEvent.setup's default advanceTimers calls jest.advanceTimersByTime. Pass advanceTimers only for a non-Jest fake clock.
solid answer
~50 sRNTL inspects the global `setTimeout` to tell whether Jest's fake timers (modern or legacy) are on. If they are, `waitFor` (and so every `findBy*`) stops using real time: it checks once, then repeatedly advances the fake clock by `interval` inside `act`, checks again and flushes microtasks, until the check succeeds or it has advanced a total of `timeout`. So `timeout` is measured in fake milliseconds: a component timer of 3000 ms needs `{ timeout: 3000 }`, but the test still finishes in real milliseconds. `userEvent.setup()`'s default `advanceTimers` calls `jest.advanceTimersByTime` when Jest fake timers are on, so `press`'s 130 ms minimum, `longPress`'s 500 ms and any `delay` elapse on the fake clock without help. You pass `advanceTimers` only when time is faked by something other than Jest. Switching between real and fake timers while a `waitFor` is pending makes it reject.
code
typescript · 20 linesimport { render, screen, userEvent, waitFor } from '@testing-library/react-native';
jest.useFakeTimers();
test('reminder banner appears after 3 seconds', async () => {
await render(<DueDateReminder bookId="b1" />);
// fake-time budget must cover the component's 3000 ms timer
expect(
await screen.findByText('Due tomorrow', {}, { timeout: 3000 }),
).toBeOnTheScreen();
});
test('long press opens book actions', async () => {
const user = userEvent.setup(); // default advanceTimers already drives Jest's fake clock
await render(<BookRow title="Dune" />);
await user.longPress(screen.getByText('Dune'));
expect(screen.getByText('Book actions')).toBeOnTheScreen();
});go deeper
Know that RNTL notices jest.useFakeTimers() and moves the fake clock during findBy, waitFor and userEvent, so you do not advance it by hand around them.
Explain the fake-timer loop: step by interval inside act, check, flush microtasks, stop when the fake timeout budget is spent, and what the default advanceTimers does.
Size timeouts in fake time for long component timers, avoid mid-wait timer switches, and recognise a hang caused by a non-Jest fake clock without advanceTimers.
Decide the suite default, fake or real timers, per test type, weighing speed and determinism against tests that must see real scheduling, and document the helper setup.
## Why fake timers matter in RNTL tests **React Native Testing Library (RNTL)** interactions and screens often involve real waiting: `userEvent`'s `press` holds the press for at least 130 ms because React Native's `Pressability` does, `longPress` holds for 500 ms by default, and the catalog app's search field debounces keystrokes by 400 ms before calling `searchBooks`. With real timers each of those is real wall-clock time. With Jest's fake timers the waits become instant, but only if something moves the fake clock forward. RNTL does that for you in two places. ## Detection RNTL checks the global `setTimeout` before waiting. If Jest has replaced it (a mock function for **legacy** fake timers, or a clock-backed one for **modern** fake timers), RNTL switches to fake-timer mode. The detection only recognises Jest's fake timers, and it can be disabled with the `RNTL_SKIP_AUTO_DETECT_FAKE_TIMERS` environment variable. ## How waitFor and findBy behave under fake timers In fake-timer mode `waitFor` runs this loop: 1. Check the expectation once. 2. If the fake-time budget (`timeout`) is used up, reject with the last error. 3. Advance the fake clock by `interval` (50 ms) inside `act`, using `jest.advanceTimersByTimeAsync` for modern timers. 4. Check the expectation again, then flush pending microtasks so resolved promises can update state. 5. Repeat from step 2. Consequences worth stating in an interview: - **`timeout` counts fake time.** With the default 1000 ms, `waitFor` advances the clock by at most 1000 ms in total. A component that shows a banner after `setTimeout(..., 3000)` needs `{ timeout: 3000 }` or more, and the test still runs in milliseconds. - **You do not advance the clock by hand** around a `findBy*` or `waitFor`; doing so as well can skip past states you meant to observe. - **Do not switch timer modes mid-wait.** Calling `jest.useRealTimers()` while a fake-timer `waitFor` is pending, or the reverse, makes it reject with an error saying the timers changed during the wait. Await every async utility before switching. ## How userEvent behaves under fake timers `userEvent.setup(options)` takes two options: | Option | Default | Purpose | |---|---|---| | `delay` | `0` | Pause between consecutive inputs, such as keystrokes in `type` | | `advanceTimers` | detects Jest fake timers | Function called with each pause length to move a fake clock | Every internal pause starts a `setTimeout` and calls `advanceTimers(ms)` alongside it. The default implementation calls `jest.advanceTimersByTime(ms)` when Jest fake timers are on and does nothing under real timers. That is why, in RNTL, `userEvent.setup()` with no options works under `jest.useFakeTimers()`: the 130 ms press, the 500 ms long press and the typing delay all complete on the fake clock. You pass `advanceTimers` yourself only when time is faked by **something other than Jest**, for example a separate fake-clock library installed by a test helper; the default would not detect it, the pause's `setTimeout` would never fire, and the interaction would hang until Jest's test timeout. Passing `advanceTimers: jest.advanceTimersByTime`, the common web idiom, does the same thing under Jest fake timers, so in RNTL it is redundant. ## The catalog search test ```tsx jest.useFakeTimers(); test('debounced search shows one result', async () => { mockSearchBooks.mockResolvedValue([{ id: 'b7', title: 'Middlemarch' }]); const user = userEvent.setup(); await render(<CatalogSearch />); await user.type(screen.getByLabelText('Search catalog'), 'middle'); expect(await screen.findByText('Middlemarch')).toBeOnTheScreen(); expect(mockSearchBooks).toHaveBeenCalledTimes(1); }); ``` Typing advances the fake clock through each keystroke's zero-length pause, restarting the 400 ms debounce each time. `findByText` then steps the clock in 50 ms increments; at 400 ms the debounce fires, the mocked search resolves, microtasks are flushed, and the result renders well inside the 1000 ms fake budget. The call count is exactly one because the debounce collapsed the keystrokes. ## Why the loop is built this way Advancing in fixed `interval` steps, rather than jumping straight to the next scheduled timer, keeps the loop bounded: code that keeps scheduling new timers cannot trap `waitFor` forever, because the fake-time budget always runs out. Flushing microtasks after each check lets promise chains started by a timer, such as the debounced search call resolving and calling `setState`, finish before the next step. ## Pitfalls - Enabling fake timers after `render` means timers the mount already scheduled are real ones. - A `setTimeout` of more than the `timeout` needs a larger `timeout`, not a manual advance plus a default wait. - Jest's fake-timer API itself (`runAllTimers`, `runOnlyPendingTimers`, restoring real timers after each test) is Jest's, not RNTL's; RNTL only drives the clock inside its own waits and events.
- Why does RNTL's waitFor advance the clock in interval-sized steps rather than jumping straight to the next timer?Stepping by `interval` bounds the work per iteration. Jumping to the next timer could loop forever if some code keeps scheduling new timers faster than the component's own timer is reached; fixed steps guarantee the loop ends when the fake timeout budget is spent.
- Under fake timers, why does waitFor flush microtasks after each step?Advancing the clock fires timer callbacks, but work they start, such as a mocked fetch promise resolving and calling setState, runs in microtasks. Flushing them before the next check lets those promise chains finish, so a result that depends on a timer and then a promise can appear within the same step.
saying these in an interview costs you the question
- Under fake timers you must call jest.advanceTimersByTime before every findBy query.
- findBy's 1000 ms timeout means one real second even with fake timers on.
- RNTL's userEvent hangs under Jest fake timers unless advanceTimers is passed.
- Switching to real timers in the middle of a pending waitFor is safe.
- RNTL detects any fake-clock library, not only Jest's fake timers.