import {setTimeout as delay} from 'node:timers/promises';
import {test} from 'tap';
import Runner from '../lib/runner.js';
import {set as setOptions} from '../lib/worker/options.cjs';
setOptions({});
const noop = () => {};
const promiseEnd = (runner, next) => new Promise(resolve => {
resolve(runner.once('finish'));
next(runner);
}).then(() => runner);
test('nested tests and hooks aren’t allowed', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.chain('test', a => {
t.throws(() => {
runner.chain(noop);
}, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'});
a.pass();
});
});
});
test('tests must be declared synchronously', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.chain('test', a => {
a.pass();
return Promise.resolve();
});
}).then(runner => {
t.throws(() => {
runner.chain(noop);
}, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'});
});
});
test('runner emits "stateChange" events', t => {
const runner = new Runner({file: import.meta.url});
runner.on('stateChange', evt => {
if (evt.type === 'declared-test') {
t.same(evt, {
type: 'declared-test',
title: 'foo',
knownFailing: false,
todo: false,
});
t.end();
}
});
runner.chain('foo', a => {
a.pass();
});
});
test('notifyTimeoutUpdate emits "stateChange" event', t => {
const runner = new Runner({file: import.meta.url});
runner.on('stateChange', evt => {
if (evt.type === 'test-timeout-configured') {
t.same(evt, {
type: 'test-timeout-configured',
period: 120,
});
t.end();
}
});
runner.notifyTimeoutUpdate(120);
});
test('run serial tests before concurrent ones', t => {
const array = [];
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.chain('test', a => {
array.push('c');
a.end();
});
runner.chain.serial('serial', a => {
array.push('a');
a.end();
});
runner.chain.serial('serial 2', a => {
array.push('b');
a.end();
});
}).then(() => {
t.strictSame(array, ['a', 'b', 'c']);
});
});
test('anything can be skipped', t => {
const array = [];
function pusher(title) {
return a => {
array.push(title);
a.pass();
};
}
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.chain.after(pusher('after'));
runner.chain.after.skip(pusher('after.skip'));
runner.chain.afterEach(pusher('afterEach'));
runner.chain.afterEach.skip(pusher('afterEach.skip'));
runner.chain.before(pusher('before'));
runner.chain.before.skip(pusher('before.skip'));
runner.chain.beforeEach(pusher('beforeEach'));
runner.chain.beforeEach.skip(pusher('beforeEach.skip'));
runner.chain('concurrent', pusher('concurrent'));
runner.chain.skip('concurrent.skip', pusher('concurrent.skip'));
runner.chain.serial('serial', pusher('serial'));
runner.chain.serial.skip('serial.skip', pusher('serial.skip'));
}).then(() => {
t.strictSame(array, [
'before',
'beforeEach',
'serial',
'afterEach',
'beforeEach',
'concurrent',
'afterEach',
'after',
]);
});
});
test('test types and titles', t => {
t.plan(10);
const fail = a => a.fail();
const pass = a => a.pass();
const check = (setup, expect) => {
const runner = new Runner({file: import.meta.url});
runner.on('stateChange', evt => {
if (evt.type === 'hook-failed' || evt.type === 'test-failed' || evt.type === 'test-passed') {
const expected = expect.shift();
t.equal(evt.title, expected.title);
}
});
return promiseEnd(runner, () => setup(runner.chain));
};
return Promise.all([
check(chain => {
chain.before(fail);
chain('test', pass);
}, [
{type: 'before', title: 'before hook'},
]),
check(chain => {
chain('test', pass);
chain.after(fail);
}, [
{type: 'test', title: 'test'},
{type: 'after', title: 'after hook'},
]),
check(chain => {
chain('test', pass);
chain.after.always(fail);
}, [
{type: 'test', title: 'test'},
{type: 'after', title: 'after.always hook'},
]),
check(chain => {
chain.beforeEach(fail);
chain('test', fail);
}, [
{type: 'beforeEach', title: 'beforeEach hook for test'},
]),
check(chain => {
chain('test', pass);
chain.afterEach(fail);
}, [
{type: 'test', title: 'test'},
{type: 'afterEach', title: 'afterEach hook for test'},
]),
check(chain => {
chain('test', pass);
chain.afterEach.always(fail);
}, [
{type: 'test', title: 'test'},
{type: 'afterEach', title: 'afterEach.always hook for test'},
]),
]);
});
test('skip test', t => {
t.plan(3);
const array = [];
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'selected-test' && evt.skip) {
t.pass();
}
if (evt.type === 'test-passed') {
t.pass();
}
});
runner.chain('test', a => {
array.push('a');
a.pass();
});
runner.chain.skip('skip', () => {
array.push('b');
});
}).then(() => {
t.strictSame(array, ['a']);
});
});
test('tests must have a non-empty title)', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
t.throws(() => {
runner.chain('', t => t.pass());
}, new TypeError('Tests must have a title'));
});
});
test('test titles must be unique', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.chain('title', t => t.pass());
t.throws(() => {
runner.chain('title', t => t.pass());
}, new Error('Duplicate test title: title'));
});
});
test('tests must have an implementation', t => {
t.plan(1);
const runner = new Runner({file: import.meta.url});
t.throws(() => {
runner.chain('title');
}, new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.'));
});
test('todo test', t => {
t.plan(3);
const array = [];
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'selected-test' && evt.todo) {
t.pass();
}
if (evt.type === 'test-passed') {
t.pass();
}
});
runner.chain('test', a => {
array.push('a');
a.pass();
});
runner.chain.todo('todo');
}).then(() => {
t.strictSame(array, ['a']);
});
});
test('todo tests must not have an implementation', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
t.throws(() => {
runner.chain.todo('todo', () => {});
}, new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.'));
});
});
test('todo tests must have a title', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
t.throws(() => {
runner.chain.todo();
}, new TypeError('`todo` tests require a title'));
});
});
test('todo test titles must be unique', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.chain('title', t => t.pass());
t.throws(() => {
runner.chain.todo('title');
}, new Error('Duplicate test title: title'));
});
});
test('only test', t => {
t.plan(2);
const array = [];
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'selected-test') {
t.pass();
}
});
runner.chain('test', a => {
array.push('a');
a.pass();
});
runner.chain.only('only', a => {
array.push('b');
a.pass();
});
}).then(() => {
t.strictSame(array, ['b']);
});
});
test('options.runOnlyExclusive means only exclusive tests are run', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url, runOnlyExclusive: true}), runner => {
runner.chain('test', () => {
t.fail();
});
runner.chain.only('test 2', () => {
t.pass();
});
});
});
test('options.serial forces all tests to be serial', t => {
t.plan(1);
const array = [];
return promiseEnd(new Runner({file: import.meta.url, serial: true}), runner => {
runner.chain('async', async a => {
await delay(200);
array.push(1);
a.pass();
});
runner.chain('async 2', async a => {
await delay(100);
array.push(2);
a.pass();
});
runner.chain('test', a => {
a.pass();
t.strictSame(array, [1, 2]);
});
});
});
test('options.failFast does not stop concurrent tests from running', t => {
const expected = ['first', 'second'];
t.plan(expected.length);
promiseEnd(new Runner({file: import.meta.url, failFast: true}), runner => {
let block;
let resume;
runner.chain.beforeEach(() => {
if (block) {
return block;
}
block = new Promise(resolve => {
resume = resolve;
});
});
runner.chain('first', a => {
resume();
a.fail();
});
runner.chain('second', a => {
a.pass();
});
runner.on('stateChange', evt => {
if (evt.type === 'test-failed' || evt.type === 'test-passed') {
t.equal(evt.title, expected.shift());
}
});
});
});
test('options.failFast && options.serial stops subsequent tests from running ', t => {
const expected = ['first'];
t.plan(expected.length);
promiseEnd(new Runner({file: import.meta.url, failFast: true, serial: true}), runner => {
let block;
let resume;
runner.chain.beforeEach(() => {
if (block) {
return block;
}
block = new Promise(resolve => {
resume = resolve;
});
});
runner.chain('first', a => {
resume();
a.fail();
});
runner.chain('second', a => {
a.pass();
});
runner.on('stateChange', evt => {
if (evt.type === 'test-failed' || evt.type === 'test-passed') {
t.equal(evt.title, expected.shift());
}
});
});
});
test('options.failFast & failing serial test stops subsequent tests from running ', t => {
const expected = ['first'];
t.plan(expected.length);
promiseEnd(new Runner({file: import.meta.url, failFast: true, serial: true}), runner => {
let block;
let resume;
runner.chain.beforeEach(() => {
if (block) {
return block;
}
block = new Promise(resolve => {
resume = resolve;
});
});
runner.chain.serial('first', a => {
resume();
a.fail();
});
runner.chain.serial('second', a => {
a.pass();
});
runner.chain('third', a => {
a.pass();
});
runner.on('stateChange', evt => {
if (evt.type === 'test-failed' || evt.type === 'test-passed') {
t.equal(evt.title, expected.shift());
}
});
});
});
test('options.match will not run tests with non-matching titles', t => {
t.plan(4);
return promiseEnd(new Runner({file: import.meta.url, match: ['*oo', '!foo']}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'test-passed') {
t.pass();
}
});
runner.chain('mhm. grass tasty. moo', a => {
t.pass();
a.pass();
});
runner.chain('juggaloo', a => {
t.pass();
a.pass();
});
runner.chain('foo', a => {
t.fail();
a.pass();
});
runner.chain('test', a => {
t.fail();
a.pass();
});
});
});
test('options.match hold no effect on hooks with titles', t => {
t.plan(2);
return promiseEnd(new Runner({file: import.meta.url, match: ['!before*']}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'test-passed') {
t.pass();
}
});
let actual;
runner.chain.before('before hook with title', () => {
actual = 'foo';
});
runner.chain('after', a => {
t.equal(actual, 'foo');
a.pass();
});
});
});
test('options.match overrides .only', t => {
t.plan(4);
return promiseEnd(new Runner({file: import.meta.url, match: ['*oo']}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'test-passed') {
t.pass();
}
});
runner.chain('moo', a => {
t.pass();
a.pass();
});
runner.chain.only('boo', a => {
t.pass();
a.pass();
});
});
});
test('options.match matches todo tests', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url, match: ['*oo']}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'selected-test' && evt.todo) {
t.pass();
}
});
runner.chain.todo('moo');
runner.chain.todo('oom');
});
});
test('macros: Additional args will be spread as additional args on implementation function', t => {
t.plan(3);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'test-passed') {
t.pass();
}
});
runner.chain.before((a, ...rest) => {
t.same(rest, ['foo', 'bar']);
a.pass();
}, 'foo', 'bar');
runner.chain('test1', (a, ...rest) => {
t.same(rest, ['foo', 'bar']);
a.pass();
}, 'foo', 'bar');
});
});
test('macros: Customize test names attaching a `title` function', t => {
t.plan(6);
const expectedTitles = [
'defaultA',
'suppliedB',
'defaultC',
];
const expectedArgs = [
['A'],
['B'],
['C'],
];
function macroFn(avaT, ...rest) {
t.same(rest, expectedArgs.shift());
avaT.pass();
}
macroFn.title = (title = 'default', firstArg = undefined) => title + firstArg;
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'declared-test') {
t.equal(evt.title, expectedTitles.shift());
}
});
runner.chain(macroFn, 'A');
runner.chain('supplied', macroFn, 'B');
runner.chain(macroFn, 'C');
});
});
test('macros: test titles must be strings', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
t.throws(() => {
const macro = t => t.pass();
macro.title = () => [];
runner.chain(macro);
}, new TypeError('Test & hook titles must be strings'));
});
});
test('macros: hook titles must be strings', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
t.throws(() => {
const macro = t => t.pass();
macro.title = () => [];
runner.chain.before(macro);
}, new TypeError('Test & hook titles must be strings'));
});
});
test('match applies to macros', t => {
t.plan(1);
function macroFn(avaT) {
avaT.pass();
}
macroFn.title = (title, firstArg) => `${firstArg}bar`;
return promiseEnd(new Runner({file: import.meta.url, match: ['foobar']}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'test-passed') {
t.equal(evt.title, 'foobar');
}
});
runner.chain(macroFn, 'foo');
runner.chain(macroFn, 'bar');
});
});
test('silently skips other tests when .only is used', t => {
t.plan(1);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.on('stateChange', evt => {
if (evt.type === 'test-passed') {
t.pass();
}
});
runner.chain('skip me', a => a.pass());
runner.chain.serial('skip me too', a => a.pass());
runner.chain.only('only me', a => a.pass());
});
});
test('subsequent always hooks are run even if earlier always hooks fail', t => {
t.plan(3);
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.chain('test', a => a.pass());
runner.chain.serial.after.always(a => {
t.pass();
a.fail();
});
runner.chain.serial.after.always(a => {
t.pass();
a.fail();
});
runner.chain.after.always(a => {
t.pass();
a.fail();
});
});
});
test('hooks run concurrently, but can be serialized', t => {
t.plan(7);
let activeCount = 0;
return promiseEnd(new Runner({file: import.meta.url}), runner => {
runner.chain('test', a => a.pass());
runner.chain.before(() => {
t.equal(activeCount, 0);
activeCount++;
return new Promise(resolve => {
setTimeout(resolve, 20);
}).then(() => {
activeCount--;
});
});
runner.chain.before(() => {
t.equal(activeCount, 1);
activeCount++;
return new Promise(resolve => {
setTimeout(resolve, 10);
}).then(() => {
activeCount--;
});
});
runner.chain.serial.before(() => {
t.equal(activeCount, 0);
activeCount++;
return new Promise(resolve => {
setTimeout(resolve, 10);
}).then(() => {
activeCount--;
});
});
runner.chain.before(() => {
t.equal(activeCount, 0);
activeCount++;
return new Promise(resolve => {
setTimeout(resolve, 20);
}).then(() => {
activeCount--;
});
});
runner.chain.before(() => {
t.equal(activeCount, 1);
activeCount++;
return new Promise(resolve => {
setTimeout(resolve, 10);
}).then(() => {
activeCount--;
});
});
runner.chain.serial.before(() => {
t.equal(activeCount, 0);
activeCount++;
return new Promise(resolve => {
setTimeout(resolve, 10);
}).then(() => {
activeCount--;
});
});
runner.chain.serial.before(() => {
t.equal(activeCount, 0);
});
});
});