'use strict';
const common = require('./index.js');
const tmpdir = require('./tmpdir.js');
const fixtures = require('./fixtures.js');
const { writeFileSync, readdirSync, readFileSync, renameSync, unlinkSync } = require('node:fs');
const { spawn } = require('node:child_process');
const { once } = require('node:events');
const assert = require('node:assert');
const { setTimeout } = require('node:timers/promises');
function skipIfNoWatch() {
if (common.isIBMi) {
common.skip('IBMi does not support `fs.watch()`');
}
if (common.isAIX) {
common.skip('folder watch capability is limited in AIX.');
}
}
function skipIfNoWatchModeSignals() {
if (common.isWindows) {
common.skip('no signals on Windows');
}
if (common.isIBMi) {
common.skip('IBMi does not support `fs.watch()`');
}
if (common.isAIX) {
common.skip('folder watch capability is limited in AIX.');
}
}
const fixturePaths = {};
const fixtureContent = {};
function refreshForTestRunnerWatch() {
tmpdir.refresh();
const files = readdirSync(fixtures.path('test-runner-watch'));
for (const file of files) {
const src = fixtures.path('test-runner-watch', file);
const dest = tmpdir.resolve(file);
fixturePaths[file] = dest;
fixtureContent[file] = readFileSync(src, 'utf8');
writeFileSync(dest, fixtureContent[file]);
}
}
async function performFileOperation(operation, useRunApi, timeout = 1000) {
if (useRunApi) {
const interval = setInterval(() => {
operation();
clearInterval(interval);
}, common.platformTimeout(timeout));
} else {
operation();
await setTimeout(common.platformTimeout(timeout));
}
}
function assertTestOutput(run, shouldCheckRecursion = false, expectations) {
if (shouldCheckRecursion) {
assert.doesNotMatch(run, /run\(\) is being called recursively/);
}
assert.match(run, new RegExp(`\n${Object.entries({
tests: 1,
suites: 0,
pass: 1,
fail: 0,
cancelled: 0,
skipped: 0,
todo: 0,
...expectations,
}).map((t) => `. ${t.join(' ')}`).join('\n')}\n`));
}
async function testRunnerWatch({
fileToUpdate,
file,
action = 'update',
fileToCreate,
isolation,
useRunApi = false,
cwd = tmpdir.path,
runnerCwd,
}) {
const ran1 = Promise.withResolvers();
const ran2 = Promise.withResolvers();
let args;
if (useRunApi) {
// Use the fixture that calls run() API
const runner = fixtures.path('test-runner-watch.mjs');
args = [runner];
if (file) args.push('--file', file);
if (runnerCwd) args.push('--cwd', runnerCwd);
if (isolation) args.push('--isolation', isolation);
} else {
// Use CLI --watch --test flags
args = ['--watch', '--test', '--test-reporter=spec',
isolation ? `--test-isolation=${isolation}` : '',
file ? fixturePaths[file] : undefined].filter(Boolean);
}
const child = spawn(process.execPath, args,
{ encoding: 'utf8', stdio: 'pipe', cwd });
let stdout = '';
let currentRun = '';
const runs = [];
child.stdout.on('data', (data) => {
stdout += data.toString();
currentRun += data.toString();
const testRuns = stdout.match(/^\S+ duration_ms\s\d+/gm);
if (testRuns?.length >= 1) ran1.resolve();
if (testRuns?.length >= 2) ran2.resolve();
});
const testUpdate = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const content = fixtureContent[fileToUpdate];
const path = fixturePaths[fileToUpdate];
await performFileOperation(
() => writeFileSync(path, content),
useRunApi,
);
await ran2.promise;
runs.push(currentRun);
child.kill();
await once(child, 'exit');
assert.strictEqual(runs.length, 2);
for (const run of runs) {
assertTestOutput(run, useRunApi);
}
};
const testRename = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const fileToRenamePath = tmpdir.resolve(fileToUpdate);
const newFileNamePath = tmpdir.resolve(`test-renamed-${fileToUpdate}`);
await performFileOperation(
() => renameSync(fileToRenamePath, newFileNamePath),
useRunApi,
);
await ran2.promise;
runs.push(currentRun);
child.kill();
await once(child, 'exit');
assert.strictEqual(runs.length, 2);
const [firstRun, secondRun] = runs;
assertTestOutput(firstRun, useRunApi);
if (action === 'rename2') {
assert.match(secondRun, /MODULE_NOT_FOUND/);
return;
}
assertTestOutput(secondRun, useRunApi);
};
const testDelete = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const fileToDeletePath = tmpdir.resolve(fileToUpdate);
if (useRunApi) {
const { existsSync } = require('node:fs');
const interval = setInterval(() => {
if (existsSync(fileToDeletePath)) {
unlinkSync(fileToDeletePath);
} else {
ran2.resolve();
clearInterval(interval);
}
}, common.platformTimeout(1000));
await ran2.promise;
} else {
unlinkSync(fileToDeletePath);
await setTimeout(common.platformTimeout(2000));
ran2.resolve();
}
runs.push(currentRun);
child.kill();
await once(child, 'exit');
assert.strictEqual(runs.length, 2);
for (const run of runs) {
assert.doesNotMatch(run, /MODULE_NOT_FOUND/);
}
};
const testCreate = async () => {
await ran1.promise;
runs.push(currentRun);
currentRun = '';
const newFilePath = tmpdir.resolve(fileToCreate);
await performFileOperation(
() => writeFileSync(newFilePath, 'module.exports = {};'),
useRunApi,
);
await ran2.promise;
runs.push(currentRun);
child.kill();
await once(child, 'exit');
assertTestOutput(runs[0], false);
assertTestOutput(runs[1], false, isolation === 'none' && {
tests: 2,
pass: 2,
});
};
action === 'update' && await testUpdate();
action === 'rename' && await testRename();
action === 'rename2' && await testRename();
action === 'delete' && await testDelete();
action === 'create' && await testCreate();
return runs;
}
module.exports = {
skipIfNoWatch,
skipIfNoWatchModeSignals,
testRunnerWatch,
refreshForTestRunnerWatch,
fixtureContent,
fixturePaths,
};