* Remove get-stream dependency * Remove delay dependency * Remove fs-extra * Remove temp-write
791 lines
17 KiB
JavaScript
791 lines
17 KiB
JavaScript
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(() => {
|
||
// Note that afterEach and beforeEach run twice because there are two actual tests - "serial" and "concurrent"
|
||
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);
|
||
});
|
||
});
|
||
});
|