Skip to content

feat: automatically advance fake timers in Jest and Vitest - #1324

Open
TrevorBurnham wants to merge 2 commits into
testing-library:mainfrom
TrevorBurnham:fix-fake-timers-clock-detection
Open

feat: automatically advance fake timers in Jest and Vitest#1324
TrevorBurnham wants to merge 2 commits into
testing-library:mainfrom
TrevorBurnham:fix-fake-timers-clock-detection

Conversation

@TrevorBurnham

@TrevorBurnham TrevorBurnham commented Aug 15, 2026

Copy link
Copy Markdown

This PR makes userEvent automatically advance fake timers under both Jest and Vitest, without the need for users to configure advanceTimers manually.

Fixes #1115

Approach

Jest's modern fake timers and Vitest's fake timers are both built on @sinonjs/fake-timers, which exposes the installed clock on each timer function it replaces. So tick that clock when it is present:

export function advanceFakeTimers(delay: number): void {
  const {clock} = globalThis.setTimeout as typeof globalThis.setTimeout & {
    clock?: FakeClock
  }

  if (typeof clock?.tick === 'function') {
    clock.tick(delay)
  }
}

This works however the framework is imported, and only while fake timers are installed, so real timers are never touched. It's the same signal @testing-library/dom uses in jestFakeTimersAreEnabled().

Detection happens per wait() rather than once in createConfig(), so calling userEvent.setup() in a beforeEach before useFakeTimers() also works. An explicit advanceTimers option still takes precedence.

This does not add support for auto-advancing Jest's legacy fake timers, which don't expose a clock. Modern fake timers have been the default since Jest 27 (2021): https://jestjs.io/blog/2021/05/25/jest-27

Relevant docs

The "Using Fake Timers" and "Options → advanceTimers" docs should be updated once this is merged. Currently they only mention Jest, not Vitest, and recommend this piece of setup:

userEvent.setup({advanceTimers: jest.advanceTimersByTime})

Happy to send a docs PR alongside this.

Verification

I've confirmed that timers automatically advance under the latest Vitest and Jest with fake timers enabled.

Covered: click and delayed type() under fake timers with no options, fake timers installed after setup(), toFake: ['Date'] only, explicit advanceTimers still honored, real timers clean.

This repo's test env installs @sinonjs/fake-timers the same way Jest and Vitest do, so the unit tests exercise the real mechanism instead of hand-assigned globals.

Notes for review

  • setTimeout.clock is undocumented @sinonjs/fake-timers internals. @testing-library/dom already relies on it, but it's a notable dependency on an implementation detail.
  • waitFor in @testing-library/dom sniffs the jest global separately, so Vitest users under fake timers may still need the globalThis.jest shim for findBy* queries.
    Making that "just work" would be a natural follow-up.
  • Auto-advancing configs (jest.useFakeTimers({advanceTimers: true}), vi.useFakeTimers({shouldAdvanceTime: true})) now get both auto-advance and an explicit tick(delay). At the default delay: 0 no additional virtual time is advanced, since tick(0) fires the timers already due without moving the clock. With a non-zero delay, the clock moves by that delay on top of auto-advance. Sinon sets clock.attachedInterval only when auto-advance is on, so the tick could be skipped, but advancing by the delay the caller asked for seems more correct than deferring to real time.

@TrevorBurnham
TrevorBurnham force-pushed the fix-fake-timers-clock-detection branch from 812e1c9 to 9e85866 Compare August 15, 2026 23:16
@TrevorBurnham TrevorBurnham changed the title feat: advance fake timers installed by Jest or Vitest feat: automatically advance fake timers in Jest and Vitest Aug 15, 2026
Both frameworks install `@sinonjs/fake-timers`, which exposes the clock
on each timer function it replaces. Ticking that clock advances fake
timers whichever framework installed them - and only while they are
installed, so real timers are untouched.

Fixes testing-library#1115
@TrevorBurnham
TrevorBurnham force-pushed the fix-fake-timers-clock-detection branch from 9e85866 to c9d16de Compare August 23, 2026 22:44
@snowystinger

Copy link
Copy Markdown
Contributor

Thanks, I spent some time looking over this today.

The issue this PR is closing I think is actually addressed by testing-library/react-testing-library#1443
Right now, Jest will also hang if you use fake timers and don't follow this part of the docs https://testing-library.com/docs/user-event/options#advancetimers

So if we're aiming for vitest and jest working the same to start, which I think is the best interpretation of that issue, then I think the first change is to update react-testing-library, not here, that way vitest advanceTimers in the config works without the globalThis.jest shim.

Once those two work the same, then I think we can consider this PR and a different/new Issue to add automatic detection of fake timers. At least right now, vitest just breaks until you have the right setup instead of sometimes working.

One more thing to note, this changes the API default for advanceTimers, it goes from () => Promise.resolve() to () => void. I think this doesn't matter because the only thing that uses it is wait which coerces it to a promise anyways. Though the config is available, so someone else could be relying on it.

@TrevorBurnham

Copy link
Copy Markdown
Author

You're right: testing-library/react-testing-library#1443 fixes #1115 insofar as it makes this setup work with Vitest fake timers:

const user = userEvent.setup({advanceTimers: vi.advanceTimersByTime})

I think detecting and handling the most common fake timer implementations automatically is the better solution for users, but I also realize that it has a larger blast radius and might want to wait for a new major version (is there a roadmap for one?).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

userEvent.click() fails when used with vi.useFakeTimers(), all available solutions are not working

2 participants