From b451d58e9ad37dbb756f1553cbe35e3337be518c Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Tue, 29 Jul 2025 11:44:47 +0200 Subject: [PATCH 1/2] test_runner: make it compatible with fake timers Signed-off-by: Matteo Collina --- lib/internal/test_runner/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 2203bbd3497659..97d53c097261d7 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -149,7 +149,7 @@ function stopTest(timeout, signal) { disposeFunction = () => { abortListener[SymbolDispose](); - timer[SymbolDispose](); + clearTimeout(timer); }; } From 2779c84ba2f3ba8570414e052250848cc595a8c2 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Wed, 4 Mar 2026 15:59:18 +0100 Subject: [PATCH 2/2] test_runner: add test for fake timers compatibility with timeout Signed-off-by: Matteo Collina --- .../test-runner/mock-timers-with-timeout.js | 43 +++++++++++++++++++ .../test-runner-mock-timers-with-timeout.js | 14 ++++++ 2 files changed, 57 insertions(+) create mode 100644 test/fixtures/test-runner/mock-timers-with-timeout.js create mode 100644 test/parallel/test-runner-mock-timers-with-timeout.js diff --git a/test/fixtures/test-runner/mock-timers-with-timeout.js b/test/fixtures/test-runner/mock-timers-with-timeout.js new file mode 100644 index 00000000000000..4eb94ec5d6d8e9 --- /dev/null +++ b/test/fixtures/test-runner/mock-timers-with-timeout.js @@ -0,0 +1,43 @@ +'use strict'; + +// Simulate @sinonjs/fake-timers: patch the timers module BEFORE +// the test runner is loaded, so the test runner captures the patched +// versions at import time. +const nodeTimers = require('node:timers'); +const originalSetTimeout = nodeTimers.setTimeout; +const originalClearTimeout = nodeTimers.clearTimeout; + +const fakeTimers = new Map(); +let nextId = 1; + +nodeTimers.setTimeout = (fn, delay, ...args) => { + const id = nextId++; + const timer = originalSetTimeout(fn, delay, ...args); + fakeTimers.set(id, timer); + // Sinon fake timers return an object with unref/ref but without + // Symbol.dispose, which would cause the test runner to throw. + return { id, unref() {}, ref() {} }; +}; + +nodeTimers.clearTimeout = (id) => { + if (id != null && typeof id === 'object') id = id.id; + const timer = fakeTimers.get(id); + if (timer) { + originalClearTimeout(timer); + fakeTimers.delete(id); + } +}; + +// Now load the test runner - it will capture our patched setTimeout/clearTimeout +const { test } = require('node:test'); + +test('test with fake timers and timeout', { timeout: 10_000 }, () => { + // This test verifies that the test runner works when setTimeout returns + // an object without Symbol.dispose (like sinon fake timers). + // Previously, the test runner called timer[Symbol.dispose]() which would + // throw TypeError on objects returned by fake timer implementations. +}); + +// Restore +nodeTimers.setTimeout = originalSetTimeout; +nodeTimers.clearTimeout = originalClearTimeout; diff --git a/test/parallel/test-runner-mock-timers-with-timeout.js b/test/parallel/test-runner-mock-timers-with-timeout.js new file mode 100644 index 00000000000000..67f266851fe1ed --- /dev/null +++ b/test/parallel/test-runner-mock-timers-with-timeout.js @@ -0,0 +1,14 @@ +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('node:assert'); +const { spawnSync } = require('node:child_process'); +const { test } = require('node:test'); + +test('mock timers do not break test timeout cleanup', async () => { + const fixture = fixtures.path('test-runner', 'mock-timers-with-timeout.js'); + const cp = spawnSync(process.execPath, ['--test', fixture], { + timeout: 30_000, + }); + assert.strictEqual(cp.status, 0, `Test failed:\nstdout: ${cp.stdout}\nstderr: ${cp.stderr}`); +});