import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import {fileURLToPath} from 'node:url';
import ciInfo from 'ci-info';
import {test} from 'tap';
import Api from '../lib/api.js';
import {normalizeGlobs} from '../lib/globs.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const ROOT_DIR = path.join(__dirname, '..');
async function apiCreator(options = {}) {
options.projectDir ??= ROOT_DIR;
options.chalkOptions = {level: 0};
options.concurrency = 2;
options.extensions ??= ['cjs'];
options.experiments = {};
options.globs = normalizeGlobs({
files: options.files, ignoredByWatcher: options.watchMode?.ignoreChanges, extensions: options.extensions, providers: [],
});
options.nodeArguments = process.execArgv;
const instance = new Api(options);
return instance;
}
const options = [
{workerThreads: true},
{workerThreads: false},
];
for (const opt of options) {
test(`fail-fast mode - workerThreads: ${opt.workerThreads} - single file & serial`, async t => {
const api = await apiCreator({
...opt,
failFast: true,
});
const tests = [];
api.on('run', plan => {
plan.status.on('stateChange', evt => {
if (evt.type === 'test-failed') {
tests.push({
ok: false,
title: evt.title,
});
} else if (evt.type === 'test-passed') {
tests.push({
ok: true,
title: evt.title,
});
}
});
});
return api.run({files: [path.join(__dirname, 'fixture/fail-fast/single-file/test.cjs')]})
.then(runStatus => {
t.ok(api.options.failFast);
t.strictSame(tests, [{
ok: true,
title: 'first pass',
}, {
ok: false,
title: 'second fail',
}, {
ok: true,
title: 'third pass',
}]);
t.equal(runStatus.stats.passedTests, 2);
t.equal(runStatus.stats.failedTests, 1);
});
});
test(`fail-fast mode - workerThreads: ${opt.workerThreads} - multiple files & serial`, async t => {
const api = await apiCreator({
...opt,
failFast: true,
serial: true,
});
const tests = [];
api.on('run', plan => {
plan.status.on('stateChange', evt => {
if (evt.type === 'test-failed') {
tests.push({
ok: false,
testFile: evt.testFile,
title: evt.title,
});
} else if (evt.type === 'test-passed') {
tests.push({
ok: true,
testFile: evt.testFile,
title: evt.title,
});
}
});
});
return api.run({
files: [
path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.cjs'),
path.join(__dirname, 'fixture/fail-fast/multiple-files/passes.cjs'),
],
})
.then(runStatus => {
t.ok(api.options.failFast);
t.strictSame(tests, [{
ok: true,
testFile: path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.cjs'),
title: 'first pass',
}, {
ok: false,
testFile: path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.cjs'),
title: 'second fail',
}]);
t.equal(runStatus.stats.passedTests, 1);
t.equal(runStatus.stats.failedTests, 1);
});
});
test(`fail-fast mode - workerThreads: ${opt.workerThreads} - multiple files & interrupt`, {skip: ciInfo.isCI}, async t => {
const api = await apiCreator({
...opt,
failFast: true,
concurrency: 2,
});
const tests = [];
api.on('run', plan => {
plan.status.on('stateChange', evt => {
if (evt.type === 'test-failed') {
tests.push({
ok: false,
testFile: evt.testFile,
title: evt.title,
});
} else if (evt.type === 'test-passed') {
tests.push({
ok: true,
testFile: evt.testFile,
title: evt.title,
});
}
});
});
const fails = path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.cjs');
const passesSlow = path.join(__dirname, 'fixture/fail-fast/multiple-files/passes-slow.cjs');
const runStatus = await api.run({files: [fails, passesSlow]});
t.ok(api.options.failFast);
t.ok(runStatus.stats.passedTests >= 2);
t.ok(runStatus.stats.passedTests <= 3);
t.equal(runStatus.stats.failedTests, 1);
t.strictSame(tests.filter(({testFile}) => testFile === fails), [{
ok: true,
testFile: path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.cjs'),
title: 'first pass',
}, {
ok: false,
testFile: path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.cjs'),
title: 'second fail',
}, {
ok: true,
testFile: path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.cjs'),
title: 'third pass',
}]);
if (runStatus.stats.passedTests === 3) {
t.strictSame(tests.filter(({testFile}) => testFile === passesSlow), [{
ok: true,
testFile: path.join(__dirname, 'fixture/fail-fast/multiple-files/passes-slow.cjs'),
title: 'first pass',
}]);
}
});
test(`fail-fast mode - workerThreads: ${opt.workerThreads} - crash & serial`, async t => {
const api = await apiCreator({
...opt,
failFast: true,
serial: true,
});
const tests = [];
const workerFailures = [];
api.on('run', plan => {
plan.status.on('stateChange', evt => {
switch (evt.type) {
case 'test-failed': {
tests.push({
ok: false,
title: evt.title,
});
break;
}
case 'test-passed': {
tests.push({
ok: true,
title: evt.title,
});
break;
}
case 'worker-failed': {
workerFailures.push(evt);
break;
}
}
});
});
return api.run({
files: [
path.join(__dirname, 'fixture/fail-fast/crash/crashes.cjs'),
path.join(__dirname, 'fixture/fail-fast/crash/passes.cjs'),
],
})
.then(runStatus => {
t.ok(api.options.failFast);
t.strictSame(tests, []);
t.equal(workerFailures.length, 1);
t.equal(workerFailures[0].testFile, path.join(__dirname, 'fixture', 'fail-fast', 'crash', 'crashes.cjs'));
t.equal(runStatus.stats.passedTests, 0);
t.equal(runStatus.stats.failedTests, 0);
});
});
test(`fail-fast mode - workerThreads: ${opt.workerThreads} - timeout & serial`, {skip: ciInfo.isCI}, async t => {
const api = await apiCreator({
...opt,
failFast: true,
serial: true,
timeout: '100ms',
});
const tests = [];
const timeouts = [];
api.on('run', plan => {
plan.status.on('stateChange', evt => {
switch (evt.type) {
case 'test-failed': {
tests.push({
ok: false,
title: evt.title,
});
break;
}
case 'test-passed': {
tests.push({
ok: true,
title: evt.title,
});
break;
}
case 'timeout': {
timeouts.push(evt);
break;
}
}
});
});
return api.run({
files: [
path.join(__dirname, 'fixture/fail-fast/timeout/fails.cjs'),
path.join(__dirname, 'fixture/fail-fast/timeout/passes.cjs'),
path.join(__dirname, 'fixture/fail-fast/timeout/passes-slow.cjs'),
],
})
.then(runStatus => {
t.ok(api.options.failFast);
t.strictSame(tests, []);
t.equal(timeouts.length, 1);
t.equal(timeouts[0].period, 100);
t.equal(runStatus.stats.passedTests, 0);
t.equal(runStatus.stats.failedTests, 0);
});
});
test(`fail-fast mode - workerThreads: ${opt.workerThreads} - no errors`, async t => {
const api = await apiCreator({
...opt,
failFast: true,
});
return api.run({
files: [
path.join(__dirname, 'fixture/fail-fast/without-error/a.cjs'),
path.join(__dirname, 'fixture/fail-fast/without-error/b.cjs'),
],
})
.then(runStatus => {
t.ok(api.options.failFast);
t.equal(runStatus.stats.passedTests, 2);
t.equal(runStatus.stats.failedTests, 0);
});
});
test(`serial execution mode - workerThreads: ${opt.workerThreads}`, async t => {
const api = await apiCreator({
...opt,
serial: true,
});
return api.run({files: [path.join(__dirname, 'fixture/serial.cjs')]})
.then(runStatus => {
t.ok(api.options.serial);
t.equal(runStatus.stats.passedTests, 3);
t.equal(runStatus.stats.failedTests, 0);
});
});
test(`run from package.json folder by default - workerThreads: ${opt.workerThreads}`, async t => {
const api = await apiCreator(opt);
return api.run({files: [path.join(__dirname, 'fixture/process-cwd-default.cjs')]})
.then(runStatus => {
t.equal(runStatus.stats.passedTests, 1);
});
});
test(`absolute paths - workerThreads: ${opt.workerThreads}`, async t => {
const api = await apiCreator(opt);
return api.run({files: [path.resolve('test-tap/fixture/es2015.cjs')]})
.then(runStatus => {
t.equal(runStatus.stats.passedTests, 1);
});
});
test(`symlink to directory containing test files - workerThreads: ${opt.workerThreads}`, async t => {
const api = await apiCreator({...opt, files: ['test-tap/fixture/symlink/*.cjs']});
return api.run()
.then(runStatus => {
t.equal(runStatus.stats.passedTests, 1);
});
});
test(`symlink to test file directly - workerThreads: ${opt.workerThreads}`, async t => {
const api = await apiCreator(opt);
return api.run({files: [path.join(__dirname, 'fixture/symlinkfile.cjs')]})
.then(runStatus => {
t.equal(runStatus.stats.passedTests, 1);
});
});
test(`test file in node_modules is ignored - workerThreads: ${opt.workerThreads}`, async t => {
t.plan(1);
const api = await apiCreator(opt);
return api.run({files: [path.join(__dirname, 'fixture/ignored-dirs/node_modules/test.cjs')]})
.then(runStatus => {
t.equal(runStatus.stats.declaredTests, 0);
});
});
test(`caching is enabled by default - workerThreads: ${opt.workerThreads}`, async t => {
fs.rmSync(path.join(__dirname, 'fixture/caching/node_modules'), {recursive: true, force: true});
const api = await apiCreator({
...opt,
projectDir: path.join(__dirname, 'fixture/caching'),
});
return api.run({files: [path.join(__dirname, 'fixture/caching/test.cjs')]})
.then(() => {
const files = fs.readdirSync(path.join(__dirname, 'fixture/caching/node_modules/.cache/ava'));
if (files.length === 1) {
t.equal(files.filter(x => x.startsWith('failing-tests.json')).length, 1);
} else {
t.equal(files.length, 0);
}
});
});
test(`caching can be disabled - workerThreads: ${opt.workerThreads}`, async t => {
fs.rmSync(path.join(__dirname, 'fixture/caching/node_modules'), {recursive: true, force: true});
const api = await apiCreator({
...opt,
projectDir: path.join(__dirname, 'fixture/caching'),
cacheEnabled: false,
});
return api.run({files: [path.join(__dirname, 'fixture/caching/test.cjs')]})
.then(() => {
t.notOk(fs.existsSync(path.join(__dirname, 'fixture/caching/node_modules/.cache/ava')));
});
});
test(`test file with only skipped tests does not create a failure - workerThreads: ${opt.workerThreads}`, async t => {
const api = await apiCreator();
return api.run({...opt, files: [path.join(__dirname, 'fixture/skip-only.cjs')]})
.then(runStatus => {
t.equal(runStatus.stats.selectedTests, 1);
t.equal(runStatus.stats.skippedTests, 1);
t.equal(runStatus.stats.failedTests, 0);
});
});
test(`test file with only skipped tests does not run hooks - workerThreads: ${opt.workerThreads}`, async t => {
const api = await apiCreator(opt);
return api.run({files: [path.join(__dirname, 'fixture/hooks-skipped.cjs')]})
.then(runStatus => {
t.equal(runStatus.stats.selectedTests, 1);
t.equal(runStatus.stats.skippedTests, 1);
t.equal(runStatus.stats.passedTests, 0);
t.equal(runStatus.stats.failedTests, 0);
t.equal(runStatus.stats.failedHooks, 0);
});
});
test(`verify test count - workerThreads: ${opt.workerThreads}`, async t => {
t.plan(4);
const api = await apiCreator(opt);
return api.run({
files: [
path.join(__dirname, 'fixture/test-count.cjs'),
path.join(__dirname, 'fixture/test-count-2.cjs'),
path.join(__dirname, 'fixture/test-count-3.cjs'),
],
}).then(runStatus => {
t.equal(runStatus.stats.passedTests, 4, 'pass count');
t.equal(runStatus.stats.failedTests, 3, 'fail count');
t.equal(runStatus.stats.skippedTests, 3, 'skip count');
t.equal(runStatus.stats.todoTests, 3, 'todo count');
});
});
test(`using --match with matching tests will only report those passing tests - workerThreads: ${opt.workerThreads}`, async t => {
t.plan(3);
const api = await apiCreator({
...opt,
match: ['this test will match'],
});
api.on('run', plan => {
plan.status.on('stateChange', evt => {
if (evt.type === 'selected-test') {
t.match(evt.testFile, /match-no-match-2/);
t.equal(evt.title, 'this test will match');
}
});
});
return api.run({
files: [
path.join(__dirname, 'fixture/match-no-match.cjs'),
path.join(__dirname, 'fixture/match-no-match-2.cjs'),
path.join(__dirname, 'fixture/test-count.cjs'),
],
}).then(runStatus => {
t.equal(runStatus.stats.passedTests, 1);
});
});
}
test('run from package.json folder by default', async t => {
const api = await apiCreator({
workerThreads: false,
});
return api.run({files: [path.join(__dirname, 'fixture/process-cwd-default.cjs')]})
.then(runStatus => {
t.equal(runStatus.stats.passedTests, 1);
});
});