# Fake timers are paired with explicit clock control

- **Pattern:** `ab-000298` (`ai-slop-test-theater.mock-hygiene.no-fake-timers-without-clock-control`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000298
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000298)

## Why it matters

Calling `vi.useFakeTimers()` or `jest.useFakeTimers()` freezes the JavaScript clock at the moment of the call. Any code that depends on `setTimeout`, `setInterval`, or `Date.now()` then hangs indefinitely — the timer callback never fires because the clock never advances. Tests that use `useFakeTimers` without a corresponding `vi.advanceTimersByTime()` or `vi.runAllTimers()` will either timeout or silently pass without actually executing the time-dependent code path. ISO-25010:2011 testability requires that all branches of code under test are actually exercised.

## Severity rationale

Low because frozen-timer tests typically don't produce false passes — they usually hang or time out — but they do silently skip the time-dependent code branch.

## Remediation

Always pair `vi.useFakeTimers()` with an explicit clock advancement, and restore real timers after the test. The test cannot verify timer-dependent behavior unless the clock is manually advanced:

```ts
it('debounces rapid input', () => {
  vi.useFakeTimers()
  const callback = vi.fn()
  const debounced = debounce(callback, 100)
  debounced()
  debounced()
  debounced()
  vi.advanceTimersByTime(100) // fires the debounced callback
  expect(callback).toHaveBeenCalledTimes(1)
  vi.useRealTimers()
})
```

## Detection

- **ID:** `no-fake-timers-without-clock-control`
- **Severity:** `low`
- **What to look for:** Walk all test files. Count all `vi.useFakeTimers(`/`jest.useFakeTimers(`/`sinon.useFakeTimers(` calls. For each, verify the same file (or test block) contains a corresponding clock-advancement call: `vi.advanceTimersByTime(`/`jest.advanceTimersByTime(`/`clock.tick(`/`vi.runAllTimers(`/`jest.runAllTimers(`. A `useFakeTimers` without advancement freezes the clock — any code that waits for a timer never resolves.
- **Pass criteria:** 100% of `useFakeTimers` calls have a corresponding clock-advancement call in the same test block. Report: "X useFakeTimers calls, Y with clock advancement, 0 frozen."
- **Fail criteria:** At least 1 `useFakeTimers` call has no clock-advancement.
- **Skip (N/A) when:** No `useFakeTimers` calls in any test file.
- **Cross-reference:** For broader test framework patterns, the Code Quality Essentials audit (`code-quality-essentials`) covers test setup conventions.
- **Detail on fail:** `"1 frozen fake timer: tests/debounce.test.ts calls vi.useFakeTimers() but never calls vi.advanceTimersByTime() — debounced code under test never fires"`
- **Remediation:** `vi.useFakeTimers()` freezes the clock — any code that uses `setTimeout`/`setInterval`/`Date.now()` will never advance. Pair it with `vi.advanceTimersByTime()`:

  ```ts
  it('debounces input', () => {
    vi.useFakeTimers()
    const callback = vi.fn()
    const debounced = debounce(callback, 100)
    debounced()
    debounced()
    debounced()
    vi.advanceTimersByTime(100)  // advance the clock
    expect(callback).toHaveBeenCalledTimes(1)
    vi.useRealTimers()
  })
  ```

## External references

- iso-25010 maintainability.testability

Taxons: code-quality

HTML version: https://auditbuffet.com/patterns/ab-000298
