diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 5b2a74c7b62862..c539f6d68af14a 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -409,8 +409,8 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { return subtest.start(); } -function watchFiles(testFiles, root, inspectPort, testNamePatterns) { - const filesWatcher = new FilesWatcher({ throttle: 500, mode: 'filter' }); +function watchFiles(testFiles, root, inspectPort, signal, testNamePatterns) { + const filesWatcher = new FilesWatcher({ throttle: 500, mode: 'filter', signal }); filesWatcher.on('changed', ({ owners }) => { filesWatcher.unfilterFilesOwnedBy(owners); PromisePrototypeThen(SafePromiseAllReturnVoid(testFiles, async (file) => { @@ -432,6 +432,7 @@ function watchFiles(testFiles, root, inspectPort, testNamePatterns) { triggerUncaughtException(error, true /* fromPromise */); })); }); + signal?.addEventListener('abort', () => root.postRun(), { __proto__: null, once: true }); return filesWatcher; } @@ -474,7 +475,7 @@ function run(options) { let postRun = () => root.postRun(); let filesWatcher; if (watch) { - filesWatcher = watchFiles(testFiles, root, inspectPort, testNamePatterns); + filesWatcher = watchFiles(testFiles, root, inspectPort, signal, testNamePatterns); postRun = undefined; } const runFiles = () => { diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 3c756c4b5d77c9..1fa4fc14cd4d4d 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -30,14 +30,18 @@ class FilesWatcher extends EventEmitter { #ownerDependencies = new SafeMap(); #throttle; #mode; + #signal; - constructor({ throttle = 500, mode = 'filter' } = kEmptyObject) { + constructor({ throttle = 500, mode = 'filter', signal } = kEmptyObject) { super(); validateNumber(throttle, 'options.throttle', 0, TIMEOUT_MAX); validateOneOf(mode, 'options.mode', ['filter', 'all']); this.#throttle = throttle; this.#mode = mode; + this.#signal = signal; + + signal?.addEventListener('abort', () => this.clear(), { __proto__: null, once: true }); } #isPathWatched(path) { @@ -89,7 +93,7 @@ class FilesWatcher extends EventEmitter { if (this.#isPathWatched(path)) { return; } - const watcher = watch(path, { recursive }); + const watcher = watch(path, { recursive, signal: this.#signal }); watcher.on('change', (eventType, fileName) => this .#onChange(recursive ? resolve(path, fileName) : path)); this.#watchers.set(path, { handle: watcher, recursive }); diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index 794d55ab1a51d1..866b283981136a 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -117,4 +117,19 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { assert.strictEqual(result[2], 'ok 1 - this should be skipped # SKIP test name does not match pattern\n'); assert.strictEqual(result[5], 'ok 2 - this should be executed\n'); }); + + it('should stop watch mode when abortSignal aborts', async () => { + const controller = new AbortController(); + const result = await run({ files: [join(testFixtures, 'test/random.cjs')], watch: true, signal: controller.signal }) + .compose(async function* (source) { + for await (const chunk of source) { + if (chunk.type === 'test:pass') { + controller.abort(); + yield chunk.data.name; + } + } + }) + .toArray(); + assert.deepStrictEqual(result, ['this should pass']); + }); });