From f62951879ba6355092309901efa23164293ffac2 Mon Sep 17 00:00:00 2001 From: Siddhartha Singh Date: Sat, 7 Mar 2026 08:20:39 +0530 Subject: [PATCH] test_runner: sync built-in ESM exports for mock timers Synchronize ESM facades whenever mocked timers are enabled, disabled, or reset. This ensures that 'node:timers/promises' and other built-ins correctly reflect the mocked state when imported in ESM modules. Fixes: https://github.com/nodejs/node/issues/62081 --- lib/internal/test_runner/mock/mock_timers.js | 9 ++-- test/parallel/test-runner-mock-timers-esm.mjs | 45 +++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-runner-mock-timers-esm.mjs diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 643128c3f0031f..666998c023490b 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -24,6 +24,8 @@ const { validateNumber, validateStringArray, } = require('internal/validators'); +const { Module } = require('internal/modules/cjs/loader'); +const { syncBuiltinESMExports } = Module; const { AbortError, @@ -440,9 +442,9 @@ class MockTimers { const eventIt = EventEmitter.on(emitter, 'data'); const timer = this.#createTimer(true, - () => emitter.emit('data'), - interval, - options); + () => emitter.emit('data'), + interval, + options); try { // eslint-disable-next-line no-unused-vars @@ -628,6 +630,7 @@ class MockTimers { const target = activate ? options.toFake : options.toReal; ArrayPrototypeForEach(this.#timersInContext, (timer) => target[timer]()); this.#isEnabled = activate; + syncBuiltinESMExports(); } /** diff --git a/test/parallel/test-runner-mock-timers-esm.mjs b/test/parallel/test-runner-mock-timers-esm.mjs new file mode 100644 index 00000000000000..7a2ecd798a9ac0 --- /dev/null +++ b/test/parallel/test-runner-mock-timers-esm.mjs @@ -0,0 +1,45 @@ +import '../common/index.mjs'; +import { describe, it } from 'node:test'; +import { setTimeout } from 'node:timers/promises'; +import assert from 'node:assert'; + +describe('Mock Timers ESM Regression', () => { + it('should work with node:timers/promises and runAll() in ESM', async (t) => { + t.mock.timers.enable({ apis: ['Date', 'setTimeout'] }); + const startTime = Date.now(); + let t1 = 0; + + await Promise.all([ + (async () => { + await setTimeout(1000); + t1 = Date.now(); + })(), + (async () => { + // Wait for the next tick to ensure setTimeout has been called + await new Promise((resolve) => process.nextTick(resolve)); + t.mock.timers.runAll(); + })(), + ]); + + assert.strictEqual(t1 - startTime, 1000); + }); + + it('should work with node:timers/promises and tick() in ESM', async (t) => { + t.mock.timers.enable({ apis: ['Date', 'setTimeout'] }); + const startTime = Date.now(); + let t1 = 0; + + await Promise.all([ + (async () => { + await setTimeout(500); + t1 = Date.now(); + })(), + (async () => { + await new Promise((resolve) => process.nextTick(resolve)); + t.mock.timers.tick(500); + })(), + ]); + + assert.strictEqual(t1 - startTime, 500); + }); +});