ava/test-tap/runner.js
Mark Wubben 6f62c2d51c Remove superfluous dependencies
* Remove get-stream dependency
* Remove delay dependency
* Remove fs-extra
* Remove temp-write
2023-06-25 15:23:04 +02:00

791 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
});
});
});