current commit
260917.12 · test: unskip `test-watch-create-isolation-none`
'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,
};