current commit
241019.1 · lib: replace `createDeferredPromise` util with `Promise.withResolvers`
import * as common from '../common/index.mjs';
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { spawn } from 'node:child_process';
import { writeFile, unlink } from 'node:fs/promises';
import { setTimeout } from 'node:timers/promises';
import tmpdir from '../common/tmpdir.js';

if (common.isIBMi)
  common.skip('IBMi does not support `fs.watch()`');

if (common.isAIX)
  common.skip('folder watch capability is limited in AIX.');

tmpdir.refresh();

// This test updates these files repeatedly,
// Reading them from disk is unreliable due to race conditions.
const fixtureContent = {
  'dependency.js': 'module.exports = {};',
  'dependency.mjs': 'export const a = 1;',
  // Test 1
  'test.js': `
const test = require('node:test');
require('./dependency.js');
import('./dependency.mjs');
import('data:text/javascript,');
test('first test has ran');`,
  // Test 2
  'test-2.mjs': `
import test from 'node:test';
import './dependency.js';
import { a } from './dependency.mjs';
import 'data:text/javascript,';
test('second test has ran');`,
  // Test file to be deleted
  'test-to-delete.mjs': `
import test from 'node:test';
import './dependency.js';
import { a } from './dependency.mjs';
import 'data:text/javascript,';
test('test to delete has ran');`,
};

const fixturePaths = Object.fromEntries(Object.keys(fixtureContent)
  .map((file) => [file, tmpdir.resolve(file)]));

async function setupFixtures() {
  return Promise.all(Object.entries(fixtureContent)
    .map(([file, content]) => writeFile(fixturePaths[file], content, { flush: true })));
}

await setupFixtures();

describe('test runner watch mode with more complex setup', () => {
  it('should run tests when a dependency changed after a watched test file being deleted', async () => {
    const ran1 = Promise.withResolvers();
    const ran2 = Promise.withResolvers();
    const child = spawn(process.execPath,
                        ['--watch', '--test'],
                        { encoding: 'utf8', stdio: 'pipe', cwd: tmpdir.path });
    let stdout = '';
    let currentRun = '';
    const runs = [];

    child.stdout.on('data', (data) => {
      stdout += data.toString();
      currentRun += data.toString();
      const testRuns = stdout.match(/duration_ms\s\d+/g);

      if (testRuns?.length >= 2) {
        ran2.resolve();
        return;
      }
      if (testRuns?.length >= 1) ran1.resolve();
    });

    await ran1.promise;
    runs.push(currentRun);
    currentRun = '';
    const fileToDeletePathLocal = tmpdir.resolve('test-to-delete.mjs');
    await unlink(fileToDeletePathLocal);
    await setTimeout(common.platformTimeout(1000));

    const content = fixtureContent['dependency.mjs'];
    const path = fixturePaths['dependency.mjs'];
    await writeFile(path, content, { flush: true });
    await setTimeout(common.platformTimeout(1000));
    await ran2.promise;
    runs.push(currentRun);
    currentRun = '';
    child.kill();

    assert.strictEqual(runs.length, 2);

    const [firstRun, secondRun] = runs;

    assert.match(firstRun, /tests 3/);
    assert.match(firstRun, /pass 3/);
    assert.match(firstRun, /fail 0/);
    assert.match(firstRun, /cancelled 0/);

    assert.match(secondRun, /tests 2/);
    assert.match(secondRun, /pass 2/);
    assert.match(secondRun, /fail 0/);
    assert.match(secondRun, /cancelled 0/);
  });
});