Skip to content

Conversation

@atscott
Copy link

@atscott atscott commented Oct 16, 2025

Description

This change exposes the setTickMode function from the underlying fake-timers library.
This is to align with the new feature introduced in fake-timers.

The new setTickMode method allows developers to configure whether time should auto advance and what the delta should be after the clock has been created. Prior to this, it could only be done at creation time with shouldAdvanceTime: true.

This also adds a new mode for automatically advancing time that moves more quickly than the existing shouldAdvanceTime, which uses real time.

Testing with mock clocks can often turn into a real struggle when dealing with situations where some work in the test is truly async and other work is captured by the mock clock. When using mock clocks, testers are always forced to write tests with intimate knowledge of when the mock clock needs to be ticked. It is ideal for test code to be written in a way that is independent of whether a mock clock is installed.

shouldAdvanceTime is essentially setInterval(() => clock.tick(ms), ms) while this feature is const loop = () => setTimeout(() => clock.nextAsync().then(() => loop()), 0);

There are two key differences:

  1. shouldAdvanceTime uses clock.tick(ms) so it synchronously runs all timers inside the "ms" of the clock queue. This doesn't allow the microtask queue to empty between the macrotask timers in the clock.
  2. shouldAdvanceTime uses real time to advance the same amount of real time in the mock clock. setTickMode({mode: "nextAsync"}) advances time as quickly possible and as far as necessary.

See: sinonjs/fake-timers@108efae

Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. If the feature is substantial or introduces breaking changes without a discussion, PR might be closed.
  • Ideally, include a test that fails without this PR but passes with it.
  • Please, don't make changes to pnpm-lock.yaml unless you introduce a new test example.
  • Please check Allow edits by maintainers to make review process faster. Note that this option is not available for repositories that are owned by Github organizations.

Tests

  • Run the tests with pnpm test:ci.

Documentation

  • If you introduce new functionality, document it. You can run documentation with pnpm run docs command.

Changesets

  • Changes in changelog are generated from PR name. Please, make sure that it explains your changes in an understandable manner. Please, prefix changeset messages with feat:, fix:, perf:, docs:, or chore:.

@netlify
Copy link

netlify bot commented Oct 16, 2025

Deploy Preview for vitest-dev ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit a832dce
🔍 Latest deploy log https://app.netlify.com/projects/vitest-dev/deploys/691e21e21f374d0008074526
😎 Deploy Preview https://deploy-preview-8726--vitest-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@atscott atscott force-pushed the v15setTickMode branch 12 times, most recently from 9475530 to 4e552d4 Compare October 16, 2025 21:33
@atscott atscott marked this pull request as ready for review October 16, 2025 22:00
@sheremet-va
Copy link
Member

This looks good to me, but I am a bit confused why can't we set the tick in options? Why do we need to call two different functions?

vi.useFakeTimers()
vi.setTimerTickMode({ mode: 'manual' })

Instead of

vi.useFakeTimers({ tickMode: 'manual' }) // also available in the config file

@atscott
Copy link
Author

atscott commented Nov 7, 2025

Thanks for your feedback.

vi.useFakeTimers()
vi.setTimerTickMode({ mode: 'manual' })
Instead of
vi.useFakeTimers({ tickMode: 'manual' }) // also available in the config file

I think this is maybe a "yes and" answer. I think both are useful, e.g. having a default tick mode and wanting to change it for a particular test or within a test. Also, for what it's worth, you could compress the first example since they return the utils: vi.useFakeTimers().setTimerTickMode({mode: 'manual})

The difficulty with adding this to the config object is that there is already a config property (actually two of them) for the 'interval' (shouldAdvanceTime and advanceTimeDelta https://github.com/DefinitelyTyped/DefinitelyTyped/blob/831153d6046c4a9ae60e8ce249fa0b13f9e24879/types/sinonjs__fake-timers/index.d.ts#L377-L387). In addition, this wasn't added to the FakeTimerInstallOpts in sinon fake timers so we'd either have to intersect that config with a Vitest-specific one or go back to Sinon and figure out how to resolve the collision with the shouldAdvanceTime and advanceTimeDelta there.

sheremet-va
sheremet-va previously approved these changes Nov 17, 2025
@sheremet-va sheremet-va added this to the 4.1.0 milestone Nov 17, 2025
@yjaaidi
Copy link
Contributor

yjaaidi commented Nov 18, 2025

Good job! Thanks for this @atscott.

Pedagogically speaking, I am afraid that setTickMode adds cognitive overhead.
While it could be interesting for some edge cases, in most scenarios, users will configure it when calling userFakeTimers so having it in the options offers a better discovery.
Also, the fakeTimers config (https://vitest.dev/config/faketimers) is the first search engine result for "Vitest fake timers". It would be good for discovery to have the option there.

Andrew, is there a reason to name the option nextTimerAsync instead of nextAsync like in sinon?

@sheremet-va do you really want the fakeTimers config to be a pass-through to sinon? Or could this be the opportunity to deprecate shouldAdvanceTime and advanceTimeDelta options and provide some new option? Why not even think of a more user-friendly name like fast-forward?

'manual' 
| 'auto' 
| 'fast-forward'
| {mode: 'auto', delta: number}

I am afraid of the combinatorial complexity. e.g. shouldAdvanceTime: true, advanceTimeDelta: 20, tickMode: 'nextTimerAsync' would work but advanceTimeDelta would just be ignored right? That can be confusing for users.

@atscott
Copy link
Author

atscott commented Nov 18, 2025

Andrew, is there a reason to name the option nextTimerAsync instead of nextAsync like in sinon?

This follows the pattern used with every other API that touches the clock. More specifically, it is a parallel to vi.advanceTimersToNextTimerAsync, as the API is similar to running nextTimerAsync in a loop.

Pedagogically speaking, I am afraid that setTickMode adds cognitive overhead.

I don't disagree entirely but I have also described above how this is more a limitation of the current 1-1 passthrough to sinon options and that setTickMode is useful on its own to configure how the clock ticks after setup.

but advanceTimeDelta would just be ignored right

No, it is not ignored. The config with useFakeTimers is the initial config for the timers and takes effect immediately. setTickMode updates the tick behavior of an existing clock. There are effectively infinite examples of updating configurations to new values.

Edit to the above: Apologies, I was misreading the thread. I understand now that this was a discussion about how to expand the configuration to allow defining this new behavior in the initial config. And yes, I agree that the current configuration isn't quite suited for it.

@yjaaidi
Copy link
Contributor

yjaaidi commented Nov 18, 2025

This follows the pattern used with every other API that touches the clock. More specifically, it is a parallel to vi.advanceTimersToNextTimerAsync, as the API is similar to running nextTimerAsync in a loop.

Ok. It just seemed to me that it only applies to vi methods such as setTickMode being wrapped with setTimerTickMode but not for options. i.e. setTimerTickMode({mode: 'nextAsync')

Edit to the above: Apologies, I was misreading the thread. I understand now that this was a discussion about how to
expand the configuration to allow defining this new behavior in the initial config.

My bad, my last example was not very clear.

And yes, I agree that the current configuration isn't quite suited for it.

🙌

@atscott
Copy link
Author

atscott commented Nov 18, 2025

It just seemed to me that it only applies to vi methods such as setTickMode being wrapped with setTimerTickMode but not for options. i.e. setTimerTickMode({mode: 'nextAsync')

yea, and I think that’s generally true. The reason I felt like changing it here is that the option is trying to reflect the API name it’s conceptually wrapping, which for Vi is advanceTimersToNextTimerAsync. I don’t think either name is great for understandability, but in the absence of greatness, we can at least opt for being literal 🤷‍♂️

This change exposes the `setTickMode` function from the underlying `fake-timers` library.
This is to align with the new feature introduced in `fake-timers`.

The new `setTickMode` method allows developers to configure whether time should auto advance and what the delta should be after the clock has been created. Prior to this, it could only be done at creation time with `shouldAdvanceTime: true`.

This also adds a new mode for automatically advancing time that moves more quickly than the existing shouldAdvanceTime, which uses real time.

Testing with mock clocks can often turn into a real struggle when dealing with situations where some work in the test is truly async and other work is captured by the mock clock. When using mock clocks, testers are always forced to write tests with intimate knowledge of when the mock clock needs to be ticked. It is ideal for test code to be written in a way that is independent of whether a mock clock is installed.

`shouldAdvanceTime` is essentially `setInterval(() => clock.tick(ms), ms)` while this feature is `const loop = () => setTimeout(() => clock.nextAsync().then(() => loop()), 0);`

There are two key differences:
1. `shouldAdvanceTime` uses `clock.tick(ms)` so it synchronously runs all timers inside the "ms" of the clock queue. This doesn't allow the microtask queue to empty between the macrotask timers in the clock.
2. `shouldAdvanceTime` uses real time to advance the same amount of real time in the mock clock. `setTickMode({mode: "nextAsync"})` advances time as quickly possible and as far as necessary.

See: sinonjs/fake-timers@108efae
@atscott
Copy link
Author

atscott commented Jan 2, 2026

@sheremet-va Do you have any opinions on the API naming/shape? Happy to rebase this again but will otherwise leave it alone if there's still some bike-shedding discussions to work through.

@sheremet-va
Copy link
Member

@sheremet-va Do you have any opinions on the API naming/shape? Happy to rebase this again but will otherwise leave it alone if there's still some bike-shedding discussions to work through.

I am fine with the naming, we are just waiting for 4.1 beta (in ~2 weeks).

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.

3 participants