diff --git a/docs/api/vi.md b/docs/api/vi.md index 1a0d99c58..1f040959d 100644 --- a/docs/api/vi.md +++ b/docs/api/vi.md @@ -1051,7 +1051,7 @@ vi.useRealTimers() ### vi.useFakeTimers ```ts -function useFakeTimers(config?: FakeTimerInstallOpts): Vitest +function useFakeTimers(config?: FakeTimersConfig): Vitest ``` To enable mocking timers, you need to call this method. It will wrap all further calls to timers (such as `setTimeout`, `setInterval`, `clearTimeout`, `clearInterval`, `setImmediate`, `clearImmediate`, and `Date`) until [`vi.useRealTimers()`](#vi-userealtimers) is called. @@ -1065,6 +1065,16 @@ The implementation is based internally on [`@sinonjs/fake-timers`](https://githu But you can enable it by specifying the option in `toFake` argument: `vi.useFakeTimers({ toFake: ['nextTick', 'queueMicrotask'] })`. ::: +You can use `toFake` to specify which timers to mock, or `toNotFake` to specify which timers to keep native. Note that `toFake` and `toNotFake` cannot be specified together. + +```ts +// only mock setTimeout and clearTimeout +vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) + +// mock all timers except setInterval +vi.useFakeTimers({ toNotFake: ['setInterval'] }) +``` + ### vi.setTimerTickMode 4.1.0 {#vi-settimertickmode} - **Type:** `(mode: 'manual' | 'nextTimerAsync') => Vitest | (mode: 'interval', interval?: number) => Vitest` diff --git a/docs/config/faketimers.md b/docs/config/faketimers.md index 68b43e1c2..2336bc3c9 100644 --- a/docs/config/faketimers.md +++ b/docs/config/faketimers.md @@ -5,7 +5,7 @@ outline: deep # fakeTimers -- **Type:** `FakeTimerInstallOpts` +- **Type:** `FakeTimerConfig` Options that Vitest will pass down to [`@sinon/fake-timers`](https://npmx.dev/package/@sinonjs/fake-timers) when using [`vi.useFakeTimers()`](/api/vi#vi-usefaketimers). @@ -21,12 +21,23 @@ Installs fake timers with the specified Unix epoch. - **Type:** `('setTimeout' | 'clearTimeout' | 'setImmediate' | 'clearImmediate' | 'setInterval' | 'clearInterval' | 'Date' | 'nextTick' | 'hrtime' | 'requestAnimationFrame' | 'cancelAnimationFrame' | 'requestIdleCallback' | 'cancelIdleCallback' | 'performance' | 'queueMicrotask')[]` - **Default:** everything available globally except `nextTick` and `queueMicrotask` -An array with names of global methods and APIs to fake. - -To only mock `setTimeout()` and `nextTick()`, specify this property as `['setTimeout', 'nextTick']`. +An array with names of global methods and APIs to fake. For example, to only mock `setTimeout()` and `nextTick()`, specify this property as `['setTimeout', 'nextTick']`. Mocking `nextTick` is not supported when running Vitest inside `node:child_process` by using `--pool=forks`. NodeJS uses `process.nextTick` internally in `node:child_process` and hangs when it is mocked. Mocking `nextTick` is supported when running Vitest with `--pool=threads`. +## fakeTimers.toNotFake + +- **Type:** `('setTimeout' | 'clearTimeout' | 'setImmediate' | 'clearImmediate' | 'setInterval' | 'clearInterval' | 'Date' | 'nextTick' | 'hrtime' | 'requestAnimationFrame' | 'cancelAnimationFrame' | 'requestIdleCallback' | 'cancelIdleCallback' | 'performance' | 'queueMicrotask')[]` +- **Default:** `[]` + +An array with names of global methods and APIs to keep native. All other available timers will be mocked. For example, to keep `setInterval()` native and mock all other timers, specify this property as `['setInterval']`. + +Mocking `nextTick` is not supported when running Vitest inside `node:child_process` by using `--pool=forks`. When running with `--pool=forks`, Vitest automatically adds `nextTick` to the `toNotFake` array. + +::: warning +Using both `toFake` and `toNotFake` together is not supported. +::: + ## fakeTimers.loopLimit - **Type:** `number` diff --git a/packages/vitest/package.json b/packages/vitest/package.json index fa33c24a7..9046ad698 100644 --- a/packages/vitest/package.json +++ b/packages/vitest/package.json @@ -192,7 +192,7 @@ "@edge-runtime/vm": "^5.0.0", "@jridgewell/trace-mapping": "catalog:", "@opentelemetry/api": "^1.9.0", - "@sinonjs/fake-timers": "15.0.0", + "@sinonjs/fake-timers": "15.3.2", "@types/estree": "catalog:", "@types/istanbul-lib-coverage": "catalog:", "@types/istanbul-reports": "catalog:", @@ -200,7 +200,6 @@ "@types/node": "^24.12.0", "@types/picomatch": "^4.0.2", "@types/prompts": "^2.4.9", - "@types/sinonjs__fake-timers": "^15.0.1", "@vitest/expect": "workspace:*", "@vitest/snapshot": "workspace:*", "acorn": "8.11.3", diff --git a/packages/vitest/src/defaults.ts b/packages/vitest/src/defaults.ts index 77bf14ffe..3d29a547f 100644 --- a/packages/vitest/src/defaults.ts +++ b/packages/vitest/src/defaults.ts @@ -88,7 +88,7 @@ export const configDefaults: Readonly<{ include: never[] } coverage: CoverageOptions - fakeTimers: import('@sinonjs/fake-timers').FakeTimerInstallOpts + fakeTimers: import('@sinonjs/fake-timers').Config maxConcurrency: number dangerouslyIgnoreUnhandledErrors: boolean typecheck: { diff --git a/packages/vitest/src/integrations/mock/timers.ts b/packages/vitest/src/integrations/mock/timers.ts index ae60e349a..fca90e896 100644 --- a/packages/vitest/src/integrations/mock/timers.ts +++ b/packages/vitest/src/integrations/mock/timers.ts @@ -6,9 +6,10 @@ */ import type { - FakeTimerInstallOpts, - FakeTimerWithContext, - InstalledClock, + Clock, + FakeMethod, + Config as FakeTimersConfig, + FakeTimers as FakeTimersContext, } from '@sinonjs/fake-timers' import { withGlobal } from '@sinonjs/fake-timers' import { isChildProcess } from '../../runtime/utils' @@ -16,7 +17,7 @@ import { mockDate, RealDate, resetDate } from './date' export class FakeTimers { private _global: typeof globalThis - private _clock!: InstalledClock + private _clock!: Clock // | _fakingTime | _fakingDate | // +-------------+-------------+ // | false | falsy | initial @@ -25,8 +26,8 @@ export class FakeTimers { // | true | truthy | unreachable private _fakingTime: boolean private _fakingDate: Date | null - private _fakeTimers: FakeTimerWithContext - private _userConfig?: FakeTimerInstallOpts + private _fakeTimers: FakeTimersContext + private _userConfig?: FakeTimersConfig private _now = RealDate.now constructor({ @@ -34,7 +35,7 @@ export class FakeTimers { config, }: { global: typeof globalThis - config: FakeTimerInstallOpts + config: FakeTimersConfig }) { this._userConfig = config @@ -154,22 +155,28 @@ export class FakeTimers { this._clock.uninstall() } - const toFake = Object.keys(this._fakeTimers.timers) - // Do not mock timers internally used by node by default. It can still be mocked through userConfig. - .filter( - timer => timer !== 'nextTick' && timer !== 'queueMicrotask', - ) as (keyof FakeTimerWithContext['timers'])[] - - if (this._userConfig?.toFake?.includes('nextTick') && isChildProcess()) { + let toFake = this._userConfig?.toFake + if (isChildProcess() && toFake?.includes('nextTick')) { throw new Error( 'process.nextTick cannot be mocked inside child_process', ) } + let toNotFake = this._userConfig?.toNotFake + if (toFake === undefined && toNotFake === undefined) { + // Do not mock timers internally used by node by default. It can still be mocked through userConfig. + toFake = (Object.keys(this._fakeTimers.timers) as FakeMethod[]) + .filter(timer => timer !== 'nextTick' && timer !== 'queueMicrotask') + } + if (isChildProcess() && toNotFake && !toNotFake.includes('nextTick')) { + toNotFake = [...toNotFake, 'nextTick'] + } + this._clock = this._fakeTimers.install({ now: fakeDate, ...this._userConfig, - toFake: this._userConfig?.toFake || toFake, + ...(toFake && { toFake }), + ...(toNotFake && { toNotFake }), ignoreMissingTimers: true, }) @@ -228,7 +235,7 @@ export class FakeTimers { } } - configure(config: FakeTimerInstallOpts): void { + configure(config: FakeTimersConfig): void { this._userConfig = config } diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts index f8d94f99e..54f081de2 100644 --- a/packages/vitest/src/integrations/vi.ts +++ b/packages/vitest/src/integrations/vi.ts @@ -1,4 +1,4 @@ -import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' +import type { Config as FakeTimersConfig } from '@sinonjs/fake-timers' import type { MaybeMocked, MaybeMockedDeep, @@ -27,7 +27,7 @@ export interface VitestUtils { /** * This method wraps all further calls to timers until [`vi.useRealTimers()`](https://vitest.dev/api/vi#vi-userealtimers) is called. */ - useFakeTimers: (config?: FakeTimerInstallOpts) => VitestUtils + useFakeTimers: (config?: FakeTimersConfig) => VitestUtils /** * Restores mocked timers to their original implementations. All timers that were scheduled before will be discarded. */ @@ -496,7 +496,7 @@ function createVitest(): VitestUtils { const _envBooleans = ['PROD', 'DEV', 'SSR'] const utils: VitestUtils = { - useFakeTimers(config?: FakeTimerInstallOpts) { + useFakeTimers(config?: FakeTimersConfig) { if (isChildProcess()) { if ( config?.toFake?.includes('nextTick') diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index 020384634..452d86616 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -1,4 +1,4 @@ -import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' +import type { Config as FakeTimersConfig } from '@sinonjs/fake-timers' import type { PrettyFormatOptions } from '@vitest/pretty-format' import type { SequenceHooks, SequenceSetupFiles, SerializableRetry, TestTagDefinition } from '@vitest/runner' import type { SnapshotStateOptions } from '@vitest/snapshot' @@ -617,7 +617,7 @@ export interface InlineConfig { /** * Options for @sinon/fake-timers */ - fakeTimers?: FakeTimerInstallOpts + fakeTimers?: FakeTimersConfig /** * Custom handler for console.log in tests. diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index c1d6c0418..a8e2e298b 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -1,4 +1,4 @@ -import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' +import type { Config as FakeTimersConfig } from '@sinonjs/fake-timers' import type { PrettyFormatOptions } from '@vitest/pretty-format' import type { SequenceHooks, SequenceSetupFiles, SerializableRetry, TestTagDefinition } from '@vitest/runner' import type { SnapshotEnvironment, SnapshotUpdateState } from '@vitest/snapshot' @@ -34,7 +34,7 @@ export interface SerializedConfig { unstubGlobals: boolean unstubEnvs: boolean // TODO: make optional - fakeTimers: FakeTimerInstallOpts + fakeTimers: FakeTimersConfig maxConcurrency: number defines: Record expect: { diff --git a/patches/@sinonjs__fake-timers@15.0.0.patch b/patches/@sinonjs__fake-timers@15.3.2.patch similarity index 86% rename from patches/@sinonjs__fake-timers@15.0.0.patch rename to patches/@sinonjs__fake-timers@15.3.2.patch index ec8ed6bd8..779976520 100644 --- a/patches/@sinonjs__fake-timers@15.0.0.patch +++ b/patches/@sinonjs__fake-timers@15.3.2.patch @@ -1,5 +1,5 @@ diff --git a/src/fake-timers-src.js b/src/fake-timers-src.js -index a9bcfd1ca..942539085 100644 +index 7d4cfa1b6b8aebc8575e08bd432f8f8ca43d3c4f..09d12bec356aa1048f09771e915075fdecb065b9 100644 --- a/src/fake-timers-src.js +++ b/src/fake-timers-src.js @@ -2,14 +2,14 @@ @@ -11,16 +11,16 @@ index a9bcfd1ca..942539085 100644 try { - timersModule = require("timers"); + timersModule = __vitest_required__.timers; - } catch (e) { + } catch { // ignored } try { - timersPromisesModule = require("timers/promises"); + timersPromisesModule = __vitest_required__.timersPromises; - } catch (e) { + } catch { // ignored } -@@ -197,7 +197,7 @@ function withGlobal(_global) { +@@ -510,7 +510,7 @@ function withGlobal(_global) { isPresent.hrtime && typeof _global.process.hrtime.bigint === "function"; isPresent.nextTick = _global.process && typeof _global.process.nextTick === "function"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 928275647..8034554ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,9 +180,9 @@ overrides: vitest: workspace:* patchedDependencies: - '@sinonjs/fake-timers@15.0.0': - hash: 8f3309cba0158608885141fb640e96b064570f7399136966ff13523bdaf678b2 - path: patches/@sinonjs__fake-timers@15.0.0.patch + '@sinonjs/fake-timers@15.3.2': + hash: 5513cab538aec68ba021f971d90c859c11658db640ba76d316c8df0d2915b0b0 + path: patches/@sinonjs__fake-timers@15.3.2.patch acorn@8.11.3: hash: 62f89b815dbd769c8a4d5b19b1f6852f28922ecb581d876c8a8377d05c2483c4 path: patches/acorn@8.11.3.patch @@ -1115,8 +1115,8 @@ importers: specifier: ^1.9.0 version: 1.9.0 '@sinonjs/fake-timers': - specifier: 15.0.0 - version: 15.0.0(patch_hash=8f3309cba0158608885141fb640e96b064570f7399136966ff13523bdaf678b2) + specifier: 15.3.2 + version: 15.3.2(patch_hash=5513cab538aec68ba021f971d90c859c11658db640ba76d316c8df0d2915b0b0) '@types/estree': specifier: 'catalog:' version: 1.0.8 @@ -1138,9 +1138,6 @@ importers: '@types/prompts': specifier: ^2.4.9 version: 2.4.9 - '@types/sinonjs__fake-timers': - specifier: ^15.0.1 - version: 15.0.1 '@vitest/expect': specifier: workspace:* version: link:../expect @@ -4841,11 +4838,8 @@ packages: '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - '@sinonjs/fake-timers@15.0.0': - resolution: {integrity: sha512-dlUB2oL+hDIYkIq/OWFBDhQAuU6kDey3eeMiYpVb7UXHhkMq/r1HloKXAbJwJZpYWkFWsydLjMqDpueMUEOjXQ==} - - '@sinonjs/fake-timers@15.1.1': - resolution: {integrity: sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==} + '@sinonjs/fake-timers@15.3.2': + resolution: {integrity: sha512-mrn35Jl2pCpns+mE3HaZa1yPN5EYCRgiMI+135COjr2hr8Cls9DXqIZ57vZe2cz7y2XVSq92tcs6kGQcT1J8Rw==} '@sinonjs/samsam@9.0.3': resolution: {integrity: sha512-ZgYY7Dc2RW+OUdnZ1DEHg00lhRt+9BjymPKHog4PRFzr1U3MbK57+djmscWyKxzO1qfunHqs4N45WWyKIFKpiQ==} @@ -5206,9 +5200,6 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/sinonjs__fake-timers@15.0.1': - resolution: {integrity: sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==} - '@types/sinonjs__fake-timers@8.1.5': resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} @@ -13256,11 +13247,7 @@ snapshots: dependencies: type-detect: 4.0.8 - '@sinonjs/fake-timers@15.0.0(patch_hash=8f3309cba0158608885141fb640e96b064570f7399136966ff13523bdaf678b2)': - dependencies: - '@sinonjs/commons': 3.0.1 - - '@sinonjs/fake-timers@15.1.1': + '@sinonjs/fake-timers@15.3.2(patch_hash=5513cab538aec68ba021f971d90c859c11658db640ba76d316c8df0d2915b0b0)': dependencies: '@sinonjs/commons': 3.0.1 @@ -13604,8 +13591,6 @@ snapshots: '@types/resolve@1.20.2': {} - '@types/sinonjs__fake-timers@15.0.1': {} - '@types/sinonjs__fake-timers@8.1.5': {} '@types/statuses@2.0.6': {} @@ -18701,7 +18686,7 @@ snapshots: sinon@21.0.3: dependencies: '@sinonjs/commons': 3.0.1 - '@sinonjs/fake-timers': 15.1.1 + '@sinonjs/fake-timers': 15.3.2(patch_hash=5513cab538aec68ba021f971d90c859c11658db640ba76d316c8df0d2915b0b0) '@sinonjs/samsam': 9.0.3 diff: 8.0.3 supports-color: 7.2.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e55a5f21c..74eb8051c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -39,7 +39,7 @@ overrides: vite: $vite vitest: workspace:* patchedDependencies: - '@sinonjs/fake-timers@15.0.0': patches/@sinonjs__fake-timers@15.0.0.patch + '@sinonjs/fake-timers@15.3.2': patches/@sinonjs__fake-timers@15.3.2.patch acorn@8.11.3: patches/acorn@8.11.3.patch cac@6.7.14: patches/cac@6.7.14.patch istanbul-lib-instrument: patches/istanbul-lib-instrument.patch diff --git a/test/unit/test/fixtures/timers.suite.ts b/test/unit/test/fixtures/timers.suite.ts index 1f26dbedd..9122734ae 100644 --- a/test/unit/test/fixtures/timers.suite.ts +++ b/test/unit/test/fixtures/timers.suite.ts @@ -51,7 +51,7 @@ describe('FakeTimers', () => { expect(global.clearInterval).not.toBe(undefined) }) - it.skipIf(isChildProcess)('mocks process.nextTick if it exists on global', () => { + it.skipIf(isChildProcess)('mocks process.nextTick when toFake includes nextTick', () => { const origNextTick = () => {} const global = { Date: FakeDate, @@ -81,7 +81,7 @@ describe('FakeTimers', () => { expect(global.process.nextTick).toBe(origNextTick) }) - it.runIf(isChildProcess)('throws when is child_process and tries to mock nextTick', () => { + it.runIf(isChildProcess)('throws when is child_process and toFake includes nextTick', () => { const global = { Date: FakeDate, process, setTimeout, clearTimeout } const timers = new FakeTimers({ global, config: { toFake: ['nextTick'] } }) @@ -135,6 +135,90 @@ describe('FakeTimers', () => { expect(global.setImmediate).toBeUndefined(); expect(global.clearImmediate).toBeUndefined(); }) + + it('leaves listed methods native when toNotFake is used', () => { + const origSetTimeout = setTimeout + const origSetImmediate = () => {} + const origClearImmediate = () => {} + const global = { + Date: FakeDate, + clearImmediate: origClearImmediate, + clearTimeout, + setImmediate: origSetImmediate, + setTimeout, + } + const timers = new FakeTimers({ global, config: { toNotFake: ['setImmediate', 'clearImmediate', 'nextTick'] } }) + timers.useFakeTimers() + expect(global.setTimeout).not.toBe(origSetTimeout) + expect(global.setImmediate).toBe(origSetImmediate) + expect(global.clearImmediate).toBe(origClearImmediate) + }) + + it.skipIf(isChildProcess)('mocks process.nextTick when toNotFake does not include nextTick', () => { + const origNextTick = () => {} + const global = { + Date: FakeDate, + clearTimeout, + process: { + nextTick: origNextTick, + }, + setTimeout, + } + const timers = new FakeTimers({ global, config: { toNotFake: [] } }) + timers.useFakeTimers() + expect(global.process.nextTick).not.toBe(origNextTick) + }) + + it.runIf(isChildProcess)('does not mock process.nextTick when toNotFake does not include nextTick and is child_process', () => { + const origNextTick = () => {} + const global = { + Date: FakeDate, + clearTimeout, + process: { + nextTick: origNextTick, + }, + setTimeout, + } + const timers = new FakeTimers({ global, config: { toNotFake: [] } }) + timers.useFakeTimers() + expect(global.process.nextTick).toBe(origNextTick) + }) + + it.skipIf(isChildProcess)('does not mock process.nextTick when toNotFake includes nextTick', () => { + const origNextTick = () => {} + const global = { + Date: FakeDate, + clearTimeout, + process: { + nextTick: origNextTick, + }, + setTimeout, + } + const timers = new FakeTimers({ global, config: { toNotFake: ['nextTick'] } }) + timers.useFakeTimers() + expect(global.process.nextTick).toBe(origNextTick) + }) + + it("toFake and toNotFake cannot be used together", () => { + const global = { + Date: FakeDate, + setTimeout, + clearTimeout, + setInterval, + clearInterval, + } + const timers = new FakeTimers({ + global, + config: { + toFake: [], + toNotFake: [], + }, + }) + expect(() => timers.useFakeTimers()) + .toThrowErrorMatchingInlineSnapshot( + `[TypeError: config.toFake and config.toNotFake cannot be used together]` + ) + }) }) describe('runAllTicks', () => { @@ -1138,8 +1222,8 @@ describe('FakeTimers', () => { 'mock1', 'mock2', 'mock2', - 'mock3', 'mock5', + 'mock3', 'mock1', 'mock2', ]) @@ -1246,8 +1330,8 @@ describe('FakeTimers', () => { 'mock1', 'mock2', 'mock2', - 'mock3', 'mock5', + 'mock3', 'mock1', 'mock2', ])