diff --git a/doc/api/test.md b/doc/api/test.md index 94f35abd03a04b..c10550bbf5e0cb 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -3933,6 +3933,18 @@ Emitted when no more tests are queued for execution in watch mode. ### Event: `'test:watch:restarted'` + + +* `data` {Object} + * `file` {string|undefined} The path of the file whose change triggered the + restart. This value is `undefined` when the triggering file cannot be + determined. + Emitted when one or more tests are restarted due to a file change in watch mode. ## `getTestContext()` diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index dc6529c220539a..a005d92eaeb245 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -631,7 +631,7 @@ function watchFiles(testFiles, opts) { } // Watch for changes in current filtered files - const onChanged = ({ owners, eventType }) => { + const onChanged = ({ owners, eventType, trigger }) => { if (!opts.hasFiles && (eventType === 'rename' || eventType === 'change')) { const updatedTestFiles = createTestFileList(opts.globPatterns, opts.cwd); const newFileName = ArrayPrototypeFind(updatedTestFiles, (x) => !ArrayPrototypeIncludes(testFiles, x)); @@ -653,7 +653,7 @@ function watchFiles(testFiles, opts) { // Reset the root start time to recalculate the duration // of the run opts.root.clearExecutionTime(); - opts.root.reporter[kEmitMessage]('test:watch:restarted'); + opts.root.reporter[kEmitMessage]('test:watch:restarted', { __proto__: null, file: trigger }); // Restart test files if (opts.isolation === 'none') { diff --git a/test/test-runner/test-run-watch-restarted-file.mjs b/test/test-runner/test-run-watch-restarted-file.mjs new file mode 100644 index 00000000000000..b0d761b66beb79 --- /dev/null +++ b/test/test-runner/test-run-watch-restarted-file.mjs @@ -0,0 +1,47 @@ +// Test run({ watch: true }) reports the triggering file path in the +// test:watch:restarted event data. +import * as common from '../common/index.mjs'; +import { run } from 'node:test'; +import assert from 'node:assert'; +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { once } from 'node:events'; +import tmpdir from '../common/tmpdir.js'; +import { refreshForTestRunnerWatch, skipIfNoWatch, fixtureContent } from '../common/watch.js'; + +skipIfNoWatch(); +refreshForTestRunnerWatch(); + +const changedFile = join(tmpdir.path, 'test.js'); + +let alreadyDrained = false; +const restartedFiles = []; +const onRestarted = common.mustCall((file) => { + restartedFiles.push(file); +}, 1); + +const controller = new AbortController(); +const stream = run({ + cwd: tmpdir.path, + watch: true, + signal: controller.signal, +}).on('data', function({ type, data }) { + if (type === 'test:watch:restarted') { + onRestarted(data.file); + } + if (type === 'test:watch:drained') { + if (alreadyDrained) { + controller.abort(); + } + alreadyDrained = true; + } +}); + +await once(stream, 'test:watch:drained'); + +writeFileSync(changedFile, fixtureContent['test.js']); + +// eslint-disable-next-line no-unused-vars +for await (const _ of stream); + +assert.deepStrictEqual(restartedFiles, [changedFile]);