Tests should be composable.
Consider a search component that uses a 300ms debounce. You've already verified the debounce logic itself in a dedicated test. When other tests interact with this component, they shouldn't depend on the debounce's existence. If you change the debounce from 300ms to 500ms, ideally only the debounce test fails — not the 15 other tests that simply type into the search field.
Let's examine this problem and explore several solutions.
The Problem with Manual Fake Timers
Using fake timers in manual mode breaks composability by tying every test to the debounce implementation.
vi.useFakeTimers();
mountCookbookSearch();
await page.getByLabel('Keywords').fill('Marmicode');
await vi.advanceTimersByTimeAsync(310);
await expect
.element(page.getByRole('heading'))
.toHaveTextContent('Angular Testing Cookbook | Marmicode');
This test explicitly references the 300ms debounce — notice the 310ms, accounting for the fact that time isn't perfectly precise. If the debounce delay changes to 500ms, this test breaks even though it's actually testing search results, not timing.
Using vi.runAllTimersAsync() can flush all pending timers without specifying a duration. This reduces coupling to the exact delay value, but the test still needs to know that timers are involved. Manual mode also freezes every timer, including framework internals such as Angular's synchronization or React's scheduler fallbacks. Understanding those internals can become necessary just to get a test to run.
There's a better way.
Solution #1 — Real Time and Polling
In Vitest Browser Mode, DOM interactions via the page API and assertions like expect.element use polling. They retry until the element is found or the assertion passes, up to a specified timeout.
mountCookbookSearch();
await page.getByLabel('Keywords').fill('Marmicode');
// This will keep retrying until the debounce hits and the
// "Angular Testing Cookbook" is the only cookbook remaining.
await expect
.element(page.getByRole('heading'))
.toHaveTextContent('Angular Testing Cookbook | Marmicode');
No fake timers, no manual time advancement. The test simply waits for the heading to appear.
This approach works and stays composable, but it comes at a price: the test runs as slowly as the debounce takes.
Solution #2 — Dynamic Timing Configuration
Another option is making the delay configurable and overriding it in tests.
This stays composable and fast. When you control the code, this is frequently the ideal approach. I detail this pattern in the Controlling Time in Tests chapter of my cookbook.
Solution #3 — "Fast-Forward" Mode
Vitest 4.1 adds a way to control automatic time advancement for fake timers using vi.setTimerTickMode('nextTimerAsync') — what I call "fast-forward" mode.
With manual fake timers, you'd call vi.advanceTimersByTimeAsync(310) and need to know the delay. "Fast-forward" mode, however, advances the clock automatically, as quickly and as far as needed. You don't need to know the delay, and you don't manually advance anything.
vi.useFakeTimers().setTimerTickMode('nextTimerAsync'); // 👈
mountCookbookSearch();
await page.getByLabel('Keywords').fill('Marmicode');
await expect
.element(page.getByRole('heading'))
.toHaveTextContent('Angular Testing Cookbook | Marmicode');
The 300ms debounce gets skipped instantly. Even if you change it to 5 seconds, the test still passes in milliseconds.
This keeps tests composable — they don't know about the delay — and fast, without any real waiting.
Remember to restore real timers when you're done.
Special Thanks to Andrew and Vladimir
"Fast-forward" mode came to life thanks to Andrew Scott from the Angular team, working alongside Vladimir Sheremet from the Vitest team.
Going Deeper
I explore the complete decision tree — manual mode, "fast-forward" mode, dynamic timing configuration, and their trade-offs — in the Controlling Time in Tests chapter of my Angular Testing Cookbook. It includes step-by-step recipes. 👨🏻🍳
I'm Younes Jaaidi, Angular GDE & Nx Champion. I help teams write tests that survive refactoring through a video course, a 3-day hands-on workshop, and a free cookbook.
