"A test that waits 5 seconds for a toast to disappear is a test you'll skip."
Why Fake Time?
Any code that uses setTimeout, setInterval, requestAnimationFrame, or reads Date.now() / new Date() is time-coupled. If your auto-dismiss toast disappears after 5,000 ms, a real test waits 5,000 ms. Multiply by 50 timer-aware tests in a suite and watch your CI go from a minute to an hour.
Fake timers replace the real timer APIs with controllable versions. setTimeout still queues callbacks, but they don't fire on their own — you advance time manually with vi.advanceTimersByTime(5000), and all callbacks scheduled within that window fire synchronously. The test that used to take 5 seconds takes 5 milliseconds.
The Setup / Teardown Pattern
Fake timers are global — once you enable them, every timer in every collaborator uses the fake clock until you turn them off. The clean pattern is per-test or per-suite:
beforeEach(() => vi.useFakeTimers()) — enable before each test.
afterEach(() => vi.useRealTimers()) — restore after each test.
Forget the cleanup and the next test gets a frozen clock it didn't ask for, leading to "why is this completely unrelated test now hanging?" debugging that eats hours.
The Three Advance Methods
vi.advanceTimersByTime(ms) — moves the clock forward by ms, firing every callback scheduled in that window. The most common one.
vi.runAllTimers() — runs every pending timer at once, even setInterval (until queue drains). Useful when you want all pending work to complete.
vi.runOnlyPendingTimers() — runs only the currently-queued timers, ignoring any new ones scheduled by those callbacks. Useful for breaking out of recursive setTimeout loops.
Fake timers do not advance microtasks automatically. If your timer callback awaits a promise, you need to flush microtasks too — await vi.advanceTimersByTimeAsync(5000) (the Async variant) handles both. Get this wrong and your test sees the timer fire but never sees the promise resolve.
Faking Date Too
Time-coupled code that reads Date.now() directly (or new Date()) is also a candidate for fake clock. vi.setSystemTime('2026-01-01') pins the wall clock to a specific moment; any reads return values consistent with that moment. Combined with vi.useFakeTimers, you get a fully deterministic time environment.
Write a retryWithBackoff(fn, attempts, baseMs) that retries fn up to attempts times, doubling the delay each retry (baseMs, baseMs*2, baseMs*4, ...). Write three tests: (1) succeeds on first try — no delay needed, (2) succeeds on third try — verify the exponential delays using vi.advanceTimersByTime, (3) exhausts all retries — verify it throws. The test for (2) should complete in milliseconds even though the simulated wait is 1+2+4 = 7 seconds.
Hint
If your fn is async, use vi.advanceTimersByTimeAsync and remember to await the promise. The microtask flush is what lets the next retry actually fire.
Progress
Progress is local-only — sign in to sync across devices.