DEV Community

Cover image for Vitest's 4.1 New "Fast-Forward" Mode Skips Timer Delays Instantly

Vitest's 4.1 New "Fast-Forward" Mode Skips Timer Delays Instantly

An important property of tests is that they should be composable.

Here is an example. Say you have a search component with a 300ms debounce. You've already tested the debounce behavior itself — "does it wait 300ms before firing?" — in a dedicated test. Now every other test that involves this component shouldn't care whether the debounce is there or not. Change the delay from 300ms to 500ms? Only the debounce test should break. Not the 15 other tests that just happen to type into that search field.

Let's look at what this problem looks like, then at different approaches to solving it.

The Challenge with Manual Fake Timers

The classic approach of fake timers in manual mode breaks composability by coupling all the tests to the debounce.

vi.useFakeTimers();

mountCookbookSearch();

await page.getByLabel('Keywords').fill('Marmicode');

await vi.advanceTimersByTimeAsync(310);

await expect
  .element(page.getByRole('heading'))
  .toHaveTextContent('Angular Testing Cookbook | Marmicode');
Enter fullscreen mode Exit fullscreen mode

This test knows about the 300ms debounce (note the 310ms — time is not a precise science 😉). Change the debounce to 500ms and this test breaks — even though it's testing search results, not timing.
You could use vi.runAllTimersAsync() instead to flush all pending timers without specifying a duration. That's less coupled to the exact value, but the test still knows it needs to deal with timers at all. And manual mode freezes all timers — including framework internals like Angular's synchronization or React's scheduler fallbacks — which can require deep knowledge of framework internals just to get your test to run.

Let's fix this.

Solution #1 — Real Time and Polling

If you're using Vitest Browser Mode, DOM interactions using the page API and assertions such as expect.element poll. They respectively retry until the element is found or the assertion passes — or until the timeout is reached.

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');
Enter fullscreen mode Exit fullscreen mode

No fake timers. No manual time advancement. The test just waits for the heading to appear.

This works, and it's composable but it has a cost: the test is as slow as the debounce.

Solution #2 — Dynamic Timing Configuration

Another approach is to make the delay configurable and override it in tests.

This is composable and fast. It's often the best approach when you control the code. I cover this pattern in detail in my Controlling Time in Tests cookbook chapter.

Solution #3 — "Fast-Forward" Mode

Vitest 4.1 introduces a way to control how fake timers automatically advance time vi.setTimerTickMode('nextTimerAsync') — what I call "fast-forward" mode.
Unlike manual fake timers where you have to call vi.advanceTimersByTimeAsync(310) and know the delay, "fast-forward" mode advances the clock on its own, as quickly possible and as far as necessary. You don't need to know the delay value. You don't need to 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');
Enter fullscreen mode Exit fullscreen mode

The 300ms debounce is skipped instantly. Change it to 5 seconds — the test still passes in milliseconds. ✨

It's composable (the test doesn't know about the delay), and it's fast (no real waiting).

⚠️ Make sure you restore real timers.

Special thanks to Andrew and Vladimir

"Fast-forward" mode exists thanks to Andrew Scott from the Angular team — alongside Vladimir Sheremet from the Vitest team.

Going Deeper

I cover the full 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. Step-by-step recipes included. 👨🏻‍🍳


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.

Top comments (0)