test_runner: emit event when file changes in watch mode

PR-URL: https://github.com/nodejs/node/pull/57903
Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
Reviewed-By: Jake Yuesong Li <jake.yuesong@gmail.com>
This commit is contained in:
Jacopo Martinelli 2025-05-30 21:32:11 +02:00 committed by GitHub
parent 7622f0d050
commit 95249083ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 69 additions and 0 deletions

View File

@ -1121,6 +1121,9 @@ const customReporter = new Transform({
case 'test:watch:drained': case 'test:watch:drained':
callback(null, 'test watch queue drained'); callback(null, 'test watch queue drained');
break; break;
case 'test:watch:restarted':
callback(null, 'test watch restarted due to file change');
break;
case 'test:start': case 'test:start':
callback(null, `test ${event.data.name} started`); callback(null, `test ${event.data.name} started`);
break; break;
@ -1166,6 +1169,9 @@ const customReporter = new Transform({
case 'test:watch:drained': case 'test:watch:drained':
callback(null, 'test watch queue drained'); callback(null, 'test watch queue drained');
break; break;
case 'test:watch:restarted':
callback(null, 'test watch restarted due to file change');
break;
case 'test:start': case 'test:start':
callback(null, `test ${event.data.name} started`); callback(null, `test ${event.data.name} started`);
break; break;
@ -1210,6 +1216,9 @@ export default async function * customReporter(source) {
case 'test:watch:drained': case 'test:watch:drained':
yield 'test watch queue drained\n'; yield 'test watch queue drained\n';
break; break;
case 'test:watch:restarted':
yield 'test watch restarted due to file change\n';
break;
case 'test:start': case 'test:start':
yield `test ${event.data.name} started\n`; yield `test ${event.data.name} started\n`;
break; break;
@ -1250,6 +1259,9 @@ module.exports = async function * customReporter(source) {
case 'test:watch:drained': case 'test:watch:drained':
yield 'test watch queue drained\n'; yield 'test watch queue drained\n';
break; break;
case 'test:watch:restarted':
yield 'test watch restarted due to file change\n';
break;
case 'test:start': case 'test:start':
yield `test ${event.data.name} started\n`; yield `test ${event.data.name} started\n`;
break; break;
@ -3175,6 +3187,10 @@ generated for each test file in addition to a final cumulative summary.
Emitted when no more tests are queued for execution in watch mode. Emitted when no more tests are queued for execution in watch mode.
### Event: `'test:watch:restarted'`
Emitted when one or more tests are restarted due to a file change in watch mode.
## Class: `TestContext` ## Class: `TestContext`
<!-- YAML <!-- YAML

View File

@ -481,6 +481,7 @@ function watchFiles(testFiles, opts) {
// Reset the topLevel counter // Reset the topLevel counter
opts.root.harness.counters.topLevel = 0; opts.root.harness.counters.topLevel = 0;
} }
await runningSubtests.get(file); await runningSubtests.get(file);
runningSubtests.set(file, runTestFile(file, filesWatcher, opts)); runningSubtests.set(file, runTestFile(file, filesWatcher, opts));
} }
@ -508,6 +509,8 @@ function watchFiles(testFiles, opts) {
// Reset the root start time to recalculate the duration // Reset the root start time to recalculate the duration
// of the run // of the run
opts.root.clearExecutionTime(); opts.root.clearExecutionTime();
opts.root.reporter[kEmitMessage]('test:watch:restarted');
// Restart test files // Restart test files
if (opts.isolation === 'none') { if (opts.isolation === 'none') {
PromisePrototypeThen(restartTestFile(kIsolatedProcessName), undefined, (error) => { PromisePrototypeThen(restartTestFile(kIsolatedProcessName), undefined, (error) => {

View File

@ -257,6 +257,56 @@ describe('test runner watch mode', () => {
assert.notDeepStrictEqual(durations[0][1], durations[1][1]); assert.notDeepStrictEqual(durations[0][1], durations[1][1]);
}); });
it('should emit test:watch:restarted when file is updated', async () => {
let alreadyDrained = false;
const events = [];
const testWatchRestarted = common.mustCall(1);
const controller = new AbortController();
const stream = run({
cwd: tmpdir.path,
watch: true,
signal: controller.signal,
}).on('data', function({ type }) {
events.push(type);
if (type === 'test:watch:restarted') {
testWatchRestarted();
}
if (type === 'test:watch:drained') {
if (alreadyDrained) {
controller.abort();
}
alreadyDrained = true;
}
});
await once(stream, 'test:watch:drained');
writeFileSync(join(tmpdir.path, 'test.js'), fixtureContent['test.js']);
// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
assert.partialDeepStrictEqual(events, [
'test:watch:drained',
'test:watch:restarted',
'test:watch:drained',
]);
});
it('should not emit test:watch:restarted since watch mode is disabled', async () => {
const stream = run({
cwd: tmpdir.path,
watch: false,
});
stream.on('test:watch:restarted', common.mustNotCall());
writeFileSync(join(tmpdir.path, 'test.js'), fixtureContent['test.js']);
// eslint-disable-next-line no-unused-vars
for await (const _ of stream);
});
describe('test runner watch mode with different cwd', () => { describe('test runner watch mode with different cwd', () => {
it( it(
'should execute run using a different cwd for the runner than the process cwd', 'should execute run using a different cwd for the runner than the process cwd',