diff --git a/docs/api/vi.md b/docs/api/vi.md
index 43760c14f..f7480bf59 100644
--- a/docs/api/vi.md
+++ b/docs/api/vi.md
@@ -1219,6 +1219,14 @@ vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] })
vi.useFakeTimers({ toNotFake: ['setInterval'] })
```
+The timers exported by `node:timers` and `node:timers/promises` are not faked by default. Enable [`fakeNodeBuiltins`](/config/faketimers#faketimers-fakenodebuiltins) to fake them as well:
+
+```ts
+import { setTimeout as sleep } from 'node:timers/promises'
+
+vi.useFakeTimers({ fakeNodeBuiltins: true })
+```
+
### 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 14c4544eb..a30a10dbe 100644
--- a/docs/config/faketimers.md
+++ b/docs/config/faketimers.md
@@ -59,6 +59,25 @@ Tells @sinonjs/fake-timers to increment mocked time automatically based on the r
Relevant only when using with `shouldAdvanceTime: true`. increment mocked time by advanceTimeDelta ms every advanceTimeDelta ms change in the real system time.
+## fakeTimers.fakeNodeBuiltins 5.1.0 {#faketimers-fakenodebuiltins}
+
+- **Type:** `boolean`
+- **Default:** `false`
+
+Also fake the timers exported by the `node:timers` and `node:timers/promises` modules, following the same `toFake` and `toNotFake` selection as the global timers. This covers the `setTimeout`, `setInterval`, `setImmediate` and `clear*` functions of `node:timers`, and the promise-based `setTimeout`, `setInterval` and `setImmediate` of `node:timers/promises`. Named imports are updated together with the module objects.
+
+```ts
+import { setTimeout as sleep } from 'node:timers/promises'
+
+vi.useFakeTimers({ fakeNodeBuiltins: true })
+
+const promise = sleep(1000)
+await vi.advanceTimersByTimeAsync(1000)
+await promise // resolved by the fake clock
+```
+
+This option has no effect in the browser.
+
## fakeTimers.shouldClearNativeTimers
- **Type:** `boolean`
diff --git a/packages/vitest/src/integrations/mock/timers.ts b/packages/vitest/src/integrations/mock/timers.ts
index 4f5922392..d7a95c0bf 100644
--- a/packages/vitest/src/integrations/mock/timers.ts
+++ b/packages/vitest/src/integrations/mock/timers.ts
@@ -5,12 +5,8 @@
* LICENSE file in the root directory of https://github.com/jestjs/jest.
*/
-import type {
- Clock,
- FakeMethod,
- Config as FakeTimersConfig,
- FakeTimers as FakeTimersContext,
-} from '@sinonjs/fake-timers'
+import type { Clock, FakeMethod, FakeTimers as FakeTimersContext } from '@sinonjs/fake-timers'
+import type { FakeTimersConfig } from '../../runtime/config'
import { withGlobal } from '@sinonjs/fake-timers'
import { isChildProcess } from '../../runtime/utils'
diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts
index f26ba158b..512aa16d6 100644
--- a/packages/vitest/src/integrations/vi.ts
+++ b/packages/vitest/src/integrations/vi.ts
@@ -1,4 +1,3 @@
-import type { Config as FakeTimersConfig } from '@sinonjs/fake-timers'
import type {
MaybeMocked,
MaybeMockedDeep,
@@ -7,7 +6,7 @@ import type {
MockInstance,
} from '@vitest/spy'
import type { Disposable } from 'vitest/optional-runtime-types.js'
-import type { RuntimeOptions, SerializedConfig } from '../runtime/config'
+import type { FakeTimersConfig, RuntimeOptions, SerializedConfig } from '../runtime/config'
import type { VitestMocker } from '../runtime/moduleRunner/moduleMocker'
import type { MockFactoryWithHelper, MockOptions } from '../types/mocker'
import {
diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts
index e56dd7816..9ec40a91e 100644
--- a/packages/vitest/src/node/types/config.ts
+++ b/packages/vitest/src/node/types/config.ts
@@ -1,4 +1,3 @@
-import type { Config as FakeTimersConfig } from '@sinonjs/fake-timers'
import type { PrettyFormatOptions } from '@vitest/pretty-format'
import type { SnapshotStateOptions } from '@vitest/snapshot'
import type { Arrayable } from '@vitest/utils'
@@ -12,7 +11,7 @@ import type {
UserConfig as ViteUserConfig,
} from 'vite'
import type { ChaiConfig } from '../../integrations/chai/config'
-import type { SerializedConfig } from '../../runtime/config'
+import type { FakeTimersConfig, SerializedConfig } from '../../runtime/config'
import type {
SequenceHooks,
SequenceSetupFiles,
diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts
index 4ed880a37..da1b44c22 100644
--- a/packages/vitest/src/runtime/config.ts
+++ b/packages/vitest/src/runtime/config.ts
@@ -1,4 +1,4 @@
-import type { Config as FakeTimersConfig } from '@sinonjs/fake-timers'
+import type { Config as SinonFakeTimersConfig } from '@sinonjs/fake-timers'
import type { PrettyFormatOptions } from '@vitest/pretty-format'
import type { SnapshotEnvironment, SnapshotUpdateState } from '@vitest/snapshot'
import type { DiffOptions, SerializedDiffOptions } from '@vitest/utils/diff'
@@ -10,6 +10,15 @@ import type {
TestTagDefinition,
} from './runner/types'
+export interface FakeTimersConfig extends SinonFakeTimersConfig {
+ /**
+ * Also fake the timers exported by `node:timers` and `node:timers/promises`.
+ * Has no effect in the browser.
+ * @default false
+ */
+ fakeNodeBuiltins?: boolean
+}
+
/**
* Config that tests have access to.
*/
diff --git a/packages/vitest/src/runtime/external-executor.ts b/packages/vitest/src/runtime/external-executor.ts
index 4d5b4060d..4eabb78c6 100644
--- a/packages/vitest/src/runtime/external-executor.ts
+++ b/packages/vitest/src/runtime/external-executor.ts
@@ -110,6 +110,10 @@ export class ExternalModulesExecutor {
return this.cjs.createRequire(identifier)
}
+ syncBuiltinESMExports(): void {
+ this.cjs.syncBuiltinESMExports()
+ }
+
#esmSyntaxCache = new Map()
// require() dispatches to the sync ESM loader only for files that are
diff --git a/packages/vitest/src/runtime/runVmTests.ts b/packages/vitest/src/runtime/runVmTests.ts
index 264ebd6d3..f5e0b8687 100644
--- a/packages/vitest/src/runtime/runVmTests.ts
+++ b/packages/vitest/src/runtime/runVmTests.ts
@@ -15,6 +15,7 @@ import * as VitestIndex from '../public/index'
import { collectDependencies, getEvaluatedImports } from '../utils/module-dependencies'
import { detectAsyncLeaks } from './detect-async-leaks'
import { closeInspector } from './inspector'
+import { VITEST_VM_CONTEXT_SYMBOL } from './moduleRunner/startVitestModuleRunner'
import { collectTests, startTests } from './runner/run'
import { resolveTestRunner } from './runners'
import { setupCommonEnv } from './setup-common'
@@ -58,11 +59,13 @@ export async function run(
process.env.SSR = '1'
}
+ const { externalModulesExecutor } = (globalThis as any)[VITEST_VM_CONTEXT_SYMBOL]
// @ts-expect-error not typed global for patched timers
globalThis.__vitest_required__ = {
util,
timers,
timersPromises,
+ syncBuiltinESMExports: () => externalModulesExecutor.syncBuiltinESMExports(),
}
await traces.$('vitest.runtime.coverage.start', () =>
diff --git a/packages/vitest/src/runtime/setup-node.ts b/packages/vitest/src/runtime/setup-node.ts
index 5c93677a0..6cc6a1670 100644
--- a/packages/vitest/src/runtime/setup-node.ts
+++ b/packages/vitest/src/runtime/setup-node.ts
@@ -1,6 +1,6 @@
import type { Environment } from '../types/environment'
import type { SerializedConfig } from './config'
-import { createRequire } from 'node:module'
+import { createRequire, syncBuiltinESMExports } from 'node:module'
import timers from 'node:timers'
import timersPromises from 'node:timers/promises'
import util from 'node:util'
@@ -53,6 +53,7 @@ export async function setupGlobalEnv(
util,
timers,
timersPromises,
+ syncBuiltinESMExports,
}
if (!config.disableConsoleIntercept) {
diff --git a/packages/vitest/src/runtime/vm/commonjs-executor.ts b/packages/vitest/src/runtime/vm/commonjs-executor.ts
index 6d12b716e..aad722380 100644
--- a/packages/vitest/src/runtime/vm/commonjs-executor.ts
+++ b/packages/vitest/src/runtime/vm/commonjs-executor.ts
@@ -54,6 +54,7 @@ export class CommonjsExecutor {
private moduleCache = new Map()
private builtinCache: Record = Object.create(null)
+ private coreModules: { module: VMSyntheticModule; exports: any; keys: string[] }[] = []
private extensions: Record unknown> =
Object.create(null)
@@ -393,9 +394,24 @@ export class CommonjsExecutor {
{ context: this.context, identifier },
)
this.moduleCache.set(identifier, module)
+ this.coreModules.push({ module, exports, keys })
return module
}
+ // the context sees builtins through synthetic modules whose exports are
+ // copied once, so they need the same sync Node does for its own bindings
+ public syncBuiltinESMExports(): void {
+ _Module.syncBuiltinESMExports()
+ for (const { module, exports, keys } of this.coreModules) {
+ if (module.status !== 'linked' && module.status !== 'evaluated') {
+ continue
+ }
+ for (const key of keys) {
+ module.setExport(key, exports[key])
+ }
+ }
+ }
+
public getCjsSyntheticModule(path: string, identifier: string): VMSyntheticModule {
if (this.moduleCache.has(identifier)) {
return this.moduleCache.get(identifier)!
diff --git a/patches/@sinonjs__fake-timers@15.4.0.patch b/patches/@sinonjs__fake-timers@15.4.0.patch
index 8d2f73cd8..8cd275583 100644
--- a/patches/@sinonjs__fake-timers@15.4.0.patch
+++ b/patches/@sinonjs__fake-timers@15.4.0.patch
@@ -1,26 +1,27 @@
diff --git a/src/fake-timers-src.js b/src/fake-timers-src.js
-index 241a7ad3eeb8b5eb691439fab31a87ef13c76345..2df1b2086670bfa06cdb148a9dbe61cd41680f2e 100644
+index 241a7ad3eeb8b5eb691439fab31a87ef13c76345..65e01d3f7ddf04889512a2d681eb9b9ae27f164c 100644
--- a/src/fake-timers-src.js
+++ b/src/fake-timers-src.js
-@@ -2,14 +2,14 @@
+@@ -2,18 +2,6 @@
const globalObject = require("@sinonjs/commons").global;
let timersModule, timersPromisesModule;
-if (typeof require === "function" && typeof module === "object") {
-+if (typeof __vitest_required__ !== 'undefined') {
- try {
+- try {
- timersModule = require("timers");
-+ timersModule = __vitest_required__.timers;
- } catch {
- // ignored
- }
- try {
+- } catch {
+- // ignored
+- }
+- try {
- timersPromisesModule = require("timers/promises");
-+ timersPromisesModule = __vitest_required__.timersPromises;
- } catch {
- // ignored
- }
-@@ -537,7 +537,7 @@ function withGlobal(_global) {
+- } catch {
+- // ignored
+- }
+-}
+
+ /**
+ * @typedef {"nextAsync" | "manual" | "interval"} TickMode
+@@ -537,7 +525,7 @@ function withGlobal(_global) {
isPresent.hrtime && typeof _global.process.hrtime.bigint === "function";
isPresent.nextTick =
_global.process && typeof _global.process.nextTick === "function";
@@ -29,3 +30,45 @@ index 241a7ad3eeb8b5eb691439fab31a87ef13c76345..2df1b2086670bfa06cdb148a9dbe61cd
isPresent.performance =
_global.performance && typeof _global.performance.now === "function";
const hasPerformancePrototype =
+@@ -2641,6 +2629,13 @@ function withGlobal(_global) {
+ }
+ }
+
++ if (
++ clock.timersModuleMethods !== undefined ||
++ clock.timersPromisesModuleMethods !== undefined
++ ) {
++ _global.__vitest_required__.syncBuiltinESMExports();
++ }
++
+ // Prevent multiple executions which will completely remove these props
+ clock.methods = [];
+ }
+@@ -2809,6 +2804,13 @@ function withGlobal(_global) {
+ handleMissingTimer("performance");
+ }
+ }
++ const nodeModules = config.fakeNodeBuiltins
++ ? _global.__vitest_required__
++ : undefined;
++ timersModule = nodeModules ? nodeModules.timers : undefined;
++ timersPromisesModule = nodeModules
++ ? nodeModules.timersPromises
++ : undefined;
+ if (_global === globalObject && timersModule) {
+ clock.timersModuleMethods = [];
+ }
+@@ -3091,6 +3093,13 @@ function withGlobal(_global) {
+ }
+ }
+
++ if (
++ clock.timersModuleMethods !== undefined ||
++ clock.timersPromisesModuleMethods !== undefined
++ ) {
++ nodeModules.syncBuiltinESMExports();
++ }
++
+ return clock;
+ }
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5674aeb7c..0393273cb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -182,7 +182,7 @@ overrides:
vitest: workspace:*
patchedDependencies:
- '@sinonjs/fake-timers@15.4.0': 8fb375421f30746697394082b6fcf4a218fe07a8db36474ac335c48aff32c0cb
+ '@sinonjs/fake-timers@15.4.0': eb7d99deb6e6015df71726ab01d2317cea8d805b762a5e4b085d04ab3c0f1cf3
acorn@8.11.3: 62f89b815dbd769c8a4d5b19b1f6852f28922ecb581d876c8a8377d05c2483c4
cac@6.7.14: a8f0f3517a47ce716ed90c0cfe6ae382ab763b021a664ada2a608477d0621588
rrweb-snapshot@2.1.1: b66b30796877352a5f887f3f4115c4e7265ddc11772af2223da4bd92293716f6
@@ -1012,7 +1012,7 @@ importers:
version: 1.9.1
'@sinonjs/fake-timers':
specifier: 15.4.0
- version: 15.4.0(patch_hash=8fb375421f30746697394082b6fcf4a218fe07a8db36474ac335c48aff32c0cb)
+ version: 15.4.0(patch_hash=eb7d99deb6e6015df71726ab01d2317cea8d805b762a5e4b085d04ab3c0f1cf3)
'@types/jsdom':
specifier: ^30.0.0
version: 30.0.0
@@ -13589,7 +13589,7 @@ snapshots:
dependencies:
type-detect: 4.0.8
- '@sinonjs/fake-timers@15.4.0(patch_hash=8fb375421f30746697394082b6fcf4a218fe07a8db36474ac335c48aff32c0cb)':
+ '@sinonjs/fake-timers@15.4.0(patch_hash=eb7d99deb6e6015df71726ab01d2317cea8d805b762a5e4b085d04ab3c0f1cf3)':
dependencies:
'@sinonjs/commons': 3.0.1
@@ -18397,7 +18397,7 @@ snapshots:
sinon@22.1.0:
dependencies:
'@sinonjs/commons': 3.0.1
- '@sinonjs/fake-timers': 15.4.0(patch_hash=8fb375421f30746697394082b6fcf4a218fe07a8db36474ac335c48aff32c0cb)
+ '@sinonjs/fake-timers': 15.4.0(patch_hash=eb7d99deb6e6015df71726ab01d2317cea8d805b762a5e4b085d04ab3c0f1cf3)
'@sinonjs/samsam': 10.0.2
diff: 9.0.0
diff --git a/test/e2e/test/fake-timers-node-builtins.test.ts b/test/e2e/test/fake-timers-node-builtins.test.ts
new file mode 100644
index 000000000..3188bc7af
--- /dev/null
+++ b/test/e2e/test/fake-timers-node-builtins.test.ts
@@ -0,0 +1,102 @@
+import { expect, test } from 'vitest'
+import { runInlineTests } from '../../test-utils'
+
+const testFile = `
+import { createRequire } from 'node:module'
+import { setTimeout as nodeSetTimeout } from 'node:timers'
+import timers from 'node:timers'
+import { setTimeout as sleep } from 'node:timers/promises'
+import { afterEach, expect, test, vi } from 'vitest'
+
+const require = createRequire(import.meta.url)
+const requiredTimers = require('node:timers')
+
+afterEach(() => {
+ vi.useRealTimers()
+})
+
+test('node:timers is not faked by default', () => {
+ vi.useFakeTimers()
+ nodeSetTimeout(() => {}, 0)
+ timers.setTimeout(() => {}, 0)
+ expect(vi.getTimerCount()).toBe(0)
+})
+
+test('fakes node:timers', () => {
+ vi.useFakeTimers({ fakeNodeBuiltins: true })
+ const callback = vi.fn()
+ nodeSetTimeout(callback, 1000)
+ timers.setTimeout(callback, 1000)
+ requiredTimers.setTimeout(callback, 1000)
+ expect(vi.getTimerCount()).toBe(3)
+ vi.advanceTimersByTime(1000)
+ expect(callback).toHaveBeenCalledTimes(3)
+})
+
+test('fakes node:timers/promises', async () => {
+ vi.useFakeTimers({ fakeNodeBuiltins: true })
+ let resolved = false
+ const promise = sleep(1000).then(() => {
+ resolved = true
+ })
+ await vi.advanceTimersByTimeAsync(999)
+ expect(resolved).toBe(false)
+ await vi.advanceTimersByTimeAsync(1)
+ await promise
+ expect(resolved).toBe(true)
+})
+
+test('useRealTimers restores the modules', async () => {
+ vi.useFakeTimers({ fakeNodeBuiltins: true })
+ vi.useRealTimers()
+ await new Promise(resolve => nodeSetTimeout(resolve, 1))
+ await sleep(1)
+})
+`
+
+test.for(['threads', 'forks', 'vmThreads', 'vmForks'] as const)(
+ '%s fakes node:timers with fakeNodeBuiltins',
+ async (pool) => {
+ const { stderr, testTree } = await runInlineTests({ 'timers.test.js': testFile }, { pool })
+
+ expect(stderr).toBe('')
+ expect(testTree()).toMatchInlineSnapshot(`
+ {
+ "timers.test.js": {
+ "fakes node:timers": "passed",
+ "fakes node:timers/promises": "passed",
+ "node:timers is not faked by default": "passed",
+ "useRealTimers restores the modules": "passed",
+ },
+ }
+ `)
+ },
+)
+
+test('fakeNodeBuiltins can be enabled in the config', async () => {
+ const { stderr, testTree } = await runInlineTests(
+ {
+ 'timers.test.js': `
+ import { setTimeout as sleep } from 'node:timers/promises'
+ import { expect, test, vi } from 'vitest'
+
+ test('node:timers/promises is faked', () => {
+ vi.useFakeTimers()
+ sleep(1000)
+ expect(vi.getTimerCount()).toBe(1)
+ vi.useRealTimers()
+ })
+ `,
+ },
+ { fakeTimers: { fakeNodeBuiltins: true } },
+ )
+
+ expect(stderr).toBe('')
+ expect(testTree()).toMatchInlineSnapshot(`
+ {
+ "timers.test.js": {
+ "node:timers/promises is faked": "passed",
+ },
+ }
+ `)
+})