Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
test_runner: clean up promisified interval generation
* yield from loop instead of setting up custom iterator
* cancel abort listener on exit
* do not call <Array>.at(0)
  • Loading branch information
Renegade334 committed Jun 24, 2025
commit 942bc294068a2a5fbea2b4badbe520be7b4448d6
64 changes: 19 additions & 45 deletions lib/internal/test_runner/mock/mock_timers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict';

const {
ArrayPrototypeAt,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
DatePrototypeGetTime,
Expand All @@ -16,7 +15,6 @@ const {
ObjectGetOwnPropertyDescriptors,
Promise,
Symbol,
SymbolAsyncIterator,
SymbolDispose,
globalThis,
} = primordials;
Expand All @@ -35,6 +33,8 @@ const {
},
} = require('internal/errors');

const { addAbortListener } = require('internal/events/abort_listener');

const { TIMEOUT_MAX } = require('internal/timers');

const PriorityQueue = require('internal/priority_queue');
Expand Down Expand Up @@ -423,62 +423,36 @@ class MockTimers {
}

async * #setIntervalPromisified(interval, result, options) {
const context = this;
const emitter = new EventEmitter();

let abortListener;
if (options?.signal) {
validateAbortSignal(options.signal, 'options.signal');

if (options.signal.aborted) {
throw abortIt(options.signal);
}

const onAbort = (reason) => {
emitter.emit('data', { __proto__: null, aborted: true, reason });
};

kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
options.signal.addEventListener('abort', onAbort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
abortListener = addAbortListener(options.signal, () => {
emitter.emit('error', abortIt(options.signal));
});
}

const eventIt = EventEmitter.on(emitter, 'data');
const callback = () => {
emitter.emit('data', result);
};

const timer = this.#createTimer(true, callback, interval, options);
const clearListeners = () => {
emitter.removeAllListeners();
context.#clearTimer(timer);
};
const iterator = {
__proto__: null,
[SymbolAsyncIterator]() {
return this;
},
async next() {
const result = await eventIt.next();
const value = ArrayPrototypeAt(result.value, 0);
if (value?.aborted) {
iterator.return();
throw abortIt(options.signal);
}
const timer = this.#createTimer(true,
() => emitter.emit('data'),
interval,
options);

return {
__proto__: null,
done: result.done,
value,
};
},
async return() {
clearListeners();
return eventIt.return();
},
};
yield* iterator;
try {
// eslint-disable-next-line no-unused-vars
for await (const event of eventIt) {
yield result;
}
} finally {
abortListener?.[SymbolDispose]();
this.#clearInterval(timer);
}
}

#setImmediate(callback, ...args) {
Expand Down
20 changes: 20 additions & 0 deletions test/parallel/test-runner-mock-timers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ process.env.NODE_TEST_KNOWN_GLOBALS = 0;
const common = require('../common');

const assert = require('node:assert');
const { getEventListeners } = require('node:events');
const { it, mock, describe } = require('node:test');
const nodeTimers = require('node:timers');
const nodeTimersPromises = require('node:timers/promises');
Expand Down Expand Up @@ -422,6 +423,8 @@ describe('Mock Timers Test Suite', () => {
});

describe('timers/promises', () => {
const hasAbortListener = (signal) => !!getEventListeners(signal, 'abort').length;

describe('setTimeout Suite', () => {
it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
Expand Down Expand Up @@ -728,6 +731,23 @@ describe('Mock Timers Test Suite', () => {
});
});

it('should clear the abort listener when the interval returns', async (t) => {
t.mock.timers.enable({ apis: ['setInterval'] });

const abortController = new AbortController();
const intervalIterator = nodeTimersPromises.setInterval(1, Date.now(), {
signal: abortController.signal,
});

const first = intervalIterator.next();
t.mock.timers.tick();

await first;
assert(hasAbortListener(abortController.signal));
await intervalIterator.return();
assert(!hasAbortListener(abortController.signal));
});

it('should abort operation given an abort controller signal on a real use case', async (t) => {
t.mock.timers.enable({ apis: ['setInterval'] });
const controller = new AbortController();
Expand Down