import path from 'node:path';
import {setTimeout as delay} from 'node:timers/promises';
import {fileURLToPath} from 'node:url';
import ciInfo from 'ci-info';
import sinon from 'sinon';
import {test} from 'tap';
import './helper/chalk0.js';
import * as snapshotManager from '../lib/snapshot-manager.js';
import Test from '../lib/test.js';
import {set as setOptions} from '../lib/worker/options.cjs';
import {ava} from './helper/ava-test.js';
setOptions({});
const failingTestHint = 'Test was expected to fail, but succeeded, you should stop marking the test as failing';
test('run test', t => ava(a => {
a.fail();
}).run().then(result => {
t.equal(result.passed, false);
}));
test('multiple asserts', t => {
const instance = ava(a => {
a.pass();
a.pass();
a.pass();
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.assertCount, 3);
});
});
test('plan assertions', t => {
const instance = ava(a => {
a.plan(2);
a.pass();
a.pass();
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.planCount, 2);
t.equal(instance.assertCount, 2);
});
});
test('plan assertion can be skipped', t => {
const instance = ava(a => {
a.plan.skip(2);
a.pass();
a.pass();
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.planCount, null);
t.equal(instance.assertCount, 2);
});
});
test('plan assertion skip() is bound', t => {
const instance = ava(a => {
(a.plan.skip)(2);
a.pass();
a.pass();
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.planCount, null);
t.equal(instance.assertCount, 2);
});
});
test('run more assertions than planned', t => ava(a => {
a.plan(2);
a.pass();
a.pass();
a.pass();
}).run().then(result => {
t.equal(result.passed, false);
t.ok(result.error);
t.match(result.error.message, /Planned for 2 assertions, but got 3\./);
t.equal(result.error.name, 'AssertionError');
}));
test('fails if no assertions are run', t => ava(() => {}).run().then(result => {
t.equal(result.passed, false);
t.ok(result.error);
t.equal(result.error.name, 'Error');
t.match(result.error.message, /Test finished without running any assertions/);
}));
test('fails if no assertions are run, unless so planned', t => ava(a => a.plan(0)).run().then(result => {
t.equal(result.passed, true);
}));
test('wrap non-assertion errors', t => {
const error = new Error();
return ava(() => {
throw error;
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.message, 'Error thrown in test');
t.equal(result.error.name, 'AssertionError');
t.equal(result.error.formattedDetails.length, 1);
t.equal(result.error.formattedDetails[0].label, 'Error thrown in test:');
t.match(result.error.formattedDetails[0].formatted, /Error/);
});
});
test('title returns the test title', t => {
t.plan(1);
return new Test({
fn(a) {
t.equal(a.title, 'foo');
a.pass();
},
metadata: {type: 'test'},
title: 'foo',
}).run();
});
test('handle non-assertion errors even when planned', t => {
const error = new Error('bar');
return ava(a => {
a.plan(1);
throw error;
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.name, 'AssertionError');
t.equal(result.error.message, 'Error thrown in test');
});
});
test('handle testing of arrays', t => {
const instance = ava(a => {
a.deepEqual(['foo', 'bar'], ['foo', 'bar']);
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.assertCount, 1);
});
});
test('handle falsy testing of arrays', t => {
const instance = ava(a => {
a.notDeepEqual(['foo', 'bar'], ['foo', 'bar', 'cat']);
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.assertCount, 1);
});
});
test('handle testing of objects', t => {
const instance = ava(a => {
a.deepEqual({
foo: 'foo',
bar: 'bar',
}, {
foo: 'foo',
bar: 'bar',
});
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.assertCount, 1);
});
});
test('handle falsy testing of objects', t => {
const instance = ava(a => {
a.notDeepEqual({
foo: 'foo',
bar: 'bar',
}, {
foo: 'foo',
bar: 'bar',
cat: 'cake',
});
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.assertCount, 1);
});
});
test('more assertions than planned should emit an assertion error', t => ava(a => {
a.plan(1);
a.pass();
a.pass();
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.name, 'AssertionError');
}));
test('record test duration', t => ava(async a => {
await delay(1234);
a.pass();
}).run().then(result => {
t.equal(result.passed, true);
t.ok(result.duration >= 1000);
}));
test('fails with the first assertError', t => ava(a => {
a.plan(2);
a.is(1, 2);
a.is(3, 4);
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.name, 'AssertionError');
t.equal(result.error.formattedDetails.length, 1);
t.equal(result.error.formattedDetails[0].label, 'Difference (- actual, + expected):');
t.match(result.error.formattedDetails[0].formatted, /- 1\n\+ 2/);
}));
test('failing pending assertion causes test to fail, not promise rejection', t => ava(a => a.throwsAsync(Promise.resolve()).then(() => {
throw new Error('Should be ignored');
})).run().then(result => {
t.equal(result.passed, false);
t.notMatch(result.error.message, /Rejected promise returned by test/);
}));
test('fails with thrown falsy value', t => ava(() => {
throw 0;
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.message, 'Error thrown in test');
t.equal(result.error.name, 'AssertionError');
t.equal(result.error.formattedDetails.length, 1);
t.equal(result.error.formattedDetails[0].label, 'Error thrown in test:');
t.match(result.error.formattedDetails[0].formatted, /0/);
}));
test('fails with thrown non-error object', t => {
const object = {foo: 'bar'};
return ava(() => {
throw object;
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.message, 'Error thrown in test');
t.equal(result.error.name, 'AssertionError');
t.equal(result.error.formattedDetails.length, 1);
t.equal(result.error.formattedDetails[0].label, 'Error thrown in test:');
t.match(result.error.formattedDetails[0].formatted, /.*{.*\n.*foo: 'bar'/);
});
});
test('skipped assertions count towards the plan', t => {
const instance = ava(a => {
a.plan(16);
a.pass.skip();
a.fail.skip();
a.is.skip(1, 1);
a.not.skip(1, 2);
a.deepEqual.skip({foo: 'bar'}, {foo: 'bar'});
a.notDeepEqual.skip({foo: 'bar'}, {baz: 'thud'});
a.like.skip({foo: 'bar'}, {foo: 'bar'});
a.throws.skip(() => {
throw new Error();
});
a.notThrows.skip(() => {});
a.snapshot.skip({});
a.truthy.skip(true);
a.falsy.skip(false);
a.true.skip(true);
a.false.skip(false);
a.regex.skip('foo', /foo/);
a.notRegex.skip('bar', /foo/);
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.planCount, 16);
t.equal(instance.assertCount, 16);
});
});
test('assertion.skip() is bound', t => {
const instance = ava(a => {
a.plan(16);
(a.pass.skip)();
(a.fail.skip)();
(a.is.skip)(1, 1);
(a.not.skip)(1, 2);
(a.deepEqual.skip)({foo: 'bar'}, {foo: 'bar'});
(a.notDeepEqual.skip)({foo: 'bar'}, {baz: 'thud'});
(a.like.skip)({foo: 'bar'}, {foo: 'bar'});
(a.throws.skip)(() => {
throw new Error();
});
(a.notThrows.skip)(() => {});
(a.snapshot.skip)({});
(a.truthy.skip)(true);
(a.falsy.skip)(false);
(a.true.skip)(true);
(a.false.skip)(false);
(a.regex.skip)('foo', /foo/);
(a.notRegex.skip)('bar', /foo/);
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.planCount, 16);
t.equal(instance.assertCount, 16);
});
});
test('throws and notThrows work with promises', t => {
let asyncCalled = false;
const instance = ava(a => {
a.plan(2);
return Promise.all([
a.throwsAsync(delay(10).then(() => {
throw new Error('foo');
}), {message: 'foo'}),
a.notThrowsAsync(delay(20).then(() => {
asyncCalled = true;
})),
]);
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.planCount, 2);
t.equal(instance.assertCount, 2);
t.equal(asyncCalled, true);
});
});
test('multiple resolving and rejecting promises passed to t.throws/t.notThrows', t => {
const instance = ava(a => {
a.plan(6);
const promises = [];
for (let i = 0; i < 3; i++) {
promises.push(
a.throwsAsync(delay(10).then(() => {
throw new Error('foo');
}), {message: 'foo'}),
a.notThrowsAsync(delay(10)),
);
}
return Promise.all(promises);
});
return instance.run().then(result => {
t.equal(result.passed, true);
t.equal(instance.planCount, 6);
t.equal(instance.assertCount, 6);
});
});
test('fails if test ends while there are pending assertions', t => ava(a => {
a.throwsAsync(Promise.reject(new Error()));
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.name, 'Error');
t.match(result.error.message, /Test finished, but an assertion is still pending/);
}));
test('fails if async test ends while there are pending assertions', t => ava(a => {
a.throwsAsync(async () => {
await delay(100);
throw new Error();
});
return Promise.resolve();
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.name, 'Error');
t.match(result.error.message, /Test finished, but an assertion is still pending/);
}));
test('contextRef', t => {
new Test({
contextRef: {
get() {
return {foo: 'bar'};
},
},
failWithoutAssertions: true,
fn(a) {
a.pass();
t.strictSame(a.context, {foo: 'bar'});
t.end();
},
metadata: {type: 'test'},
onResult() {},
title: 'foo',
}).run();
});
test('failing tests should fail', t => ava.failing('foo', a => {
a.fail();
}).run().then(result => {
t.equal(result.passed, true);
}));
test('failing tests must not pass', t => ava.failing(a => {
a.pass();
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.message, failingTestHint);
}));
test('failing tests must not return a fulfilled promise', t => ava.failing(a => Promise.resolve().then(() => a.pass())).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.message, failingTestHint);
}));
test('failing tests pass when returning a rejected promise', t => ava.failing(a => {
a.plan(1);
return a.notThrowsAsync(delay(10), {value: 'foo'}).then(() => {
throw new Error('reject');
});
}).run().then(result => {
t.equal(result.passed, true);
}));
test('failing tests pass with `t.throwsAsync(nonThrowingPromise)`', t => ava.failing(a => a.throwsAsync(Promise.resolve(10))).run().then(result => {
t.equal(result.passed, true);
}));
test('failing tests fail with `t.notThrowsAsync(throws)`', t => ava.failing(a => a.notThrowsAsync(Promise.resolve('foo'))).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.message, failingTestHint);
}));
test('log from tests', t => ava(a => {
a.log('a log message from a test');
t.ok(true);
a.log('another log message from a test');
a.log({b: 1, c: {d: 2}}, 'complex log', 5, 5.1);
a.log();
(a.log)('bound');
}).run().then(result => {
t.same(
result.logs,
[
'a log message from a test',
'another log message from a test',
'{\n b: 1,\n c: {\n d: 2,\n },\n} complex log 5 5.1',
'bound',
],
);
}));
test('assertions are bound', t =>
ava(a => {
(a.plan)(14);
(a.pass)();
(a.is)(1, 1);
(a.not)(1, 2);
(a.deepEqual)({foo: 'bar'}, {foo: 'bar'});
(a.notDeepEqual)({foo: 'bar'}, {baz: 'thud'});
(a.like)({foo: 'bar'}, {foo: 'bar'});
(a.throws)(() => {
throw new Error();
});
(a.notThrows)(() => {});
(a.truthy)(true);
(a.falsy)(false);
(a.true)(true);
(a.false)(false);
(a.regex)('foo', /foo/);
(a.notRegex)('bar', /foo/);
}).run().then(result => {
t.ok(result.passed);
}),
);
test('snapshot assertion can be skipped', t => {
const projectDir = fileURLToPath(new URL('fixture', import.meta.url));
const manager = snapshotManager.load({
file: path.join(projectDir, 'assert.cjs'),
projectDir,
fixedLocation: null,
recordNewSnapshots: true,
updating: false,
});
return new Test({
compareTestSnapshot: options => manager.compare(options),
updateSnapshots: false,
metadata: {},
title: 'passes',
fn(t) {
t.snapshot.skip({not: {a: 'match'}});
t.snapshot.skip({not: {b: 'match'}});
t.snapshot({name: 'Sindre'});
},
}).run().then(result => {
t.ok(result.passed);
});
});
test('snapshot assertions call options.skipSnapshot when skipped', async t => {
const projectDir = fileURLToPath(new URL('fixture', import.meta.url));
const manager = snapshotManager.load({
file: path.join(projectDir, 'assert.cjs'),
projectDir,
fixedLocation: null,
updating: false,
});
const skipSnapshot = sinon.spy();
const test = new Test({
compareTestSnapshot: options => manager.compare(options),
skipSnapshot,
updateSnapshots: false,
metadata: {},
title: 'passes',
fn(t) {
t.snapshot.skip({not: {a: 'match'}});
t.snapshot.skip({not: {b: 'match'}});
t.snapshot({name: 'Sindre'});
},
});
await test.run();
t.ok(skipSnapshot.calledTwice);
for (const [index, call] of skipSnapshot.getCalls().entries()) {
t.match(call.firstArg, {belongsTo: 'passes', index});
}
});
test('snapshot assertion can be skipped when updating snapshots', t => new Test({
updateSnapshots: true,
metadata: {},
title: 'passes',
fn(t) {
t.snapshot.skip({not: {a: 'match'}});
},
}).run().then(result => {
t.ok(result.passed);
}));
test('implementation runs with null scope', t => ava(function (a) {
a.pass();
t.equal(this, null);
}).run());
test('timeout with promise', t => ava(a => {
a.timeout(10);
return delay(200);
}).run().then(result => {
t.equal(result.passed, false);
t.match(result.error.message, /timeout/);
}));
test('timeout is refreshed on assert', {skip: ciInfo.isCI}, t => ava(async a => {
a.timeout(100);
a.plan(3);
await Promise.all([
delay(50).then(() => a.pass()),
delay(100).then(() => a.pass()),
delay(150).then(() => a.pass()),
delay(200),
]);
}).run().then(result => {
t.equal(result.passed, true);
}));
test('timeout can be cleared', {skip: ciInfo.isCI}, t => ava(async a => {
a.timeout(100);
a.plan(2);
await Promise.all([
delay(50).then(() => a.pass()),
delay(100).then(() => a.timeout.clear()),
delay(350).then(() => a.pass()),
]);
}).run().then(result => {
t.equal(result.passed, true);
}));
test('teardown passing test', t => {
const teardown = sinon.spy();
return ava(a => {
a.teardown(teardown);
a.pass();
}).run().then(result => {
t.equal(result.passed, true);
t.ok(teardown.calledOnce);
});
});
test('teardown failing test', t => {
const teardown = sinon.spy();
return ava(a => {
a.teardown(teardown);
a.fail();
}).run().then(result => {
t.equal(result.passed, false);
t.ok(teardown.calledOnce);
});
});
test('teardown awaits promise', t => {
let tornDown = false;
const teardownPromise = delay(200).then(() => {
tornDown = true;
});
return ava(a => {
a.teardown(() => teardownPromise);
a.pass();
}).run().then(result => {
t.equal(result.passed, true);
t.ok(tornDown);
});
});
test('teardowns run in reverse order', t => {
let resolveA;
const teardownA = sinon.stub().returns(new Promise(resolve => {
resolveA = resolve;
}));
const teardownB = sinon.stub().resolves(delay(200));
return ava(a => {
a.teardown(teardownA);
a.teardown(() => teardownB().then(resolveA));
a.pass();
}).run().then(result => {
t.equal(result.passed, true);
t.ok(teardownB.calledBefore(teardownA));
});
});
test('teardown without function callback fails', t => ava(a => a.throwsAsync(async () => {
a.teardown(false);
}, {message: 'Expected a function'})).run().then(result => {
t.equal(result.passed, true);
}));
test('teardown errors fail the test', t => {
const teardown = sinon.stub().throws('TeardownError');
return ava(a => {
a.teardown(teardown);
a.pass();
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.name, 'TeardownError');
t.ok(teardown.calledOnce);
});
});
test('teardown errors are hidden behind assertion errors', t => {
const teardown = sinon.stub().throws('TeardownError');
return ava(a => {
a.teardown(teardown);
a.fail();
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.name, 'AssertionError');
t.ok(teardown.calledOnce);
});
});
test('teardown errors do not stop next teardown from running', t => {
const teardownA = sinon.spy();
const teardownB = sinon.stub().throws('TeardownError');
return ava(a => {
a.teardown(teardownA);
a.teardown(teardownB);
a.pass();
}).run().then(result => {
t.equal(result.passed, false);
t.equal(result.error.name, 'TeardownError');
t.ok(teardownA.calledOnce);
t.ok(teardownB.calledOnce);
t.ok(teardownB.calledBefore(teardownA));
});
});
test('teardowns cannot be registered by teardowns', async t => {
const result = await ava(a => {
a.teardown(() => {
a.teardown(() => {});
});
a.pass();
}).run();
t.equal(result.passed, false);
t.match(result.error.message, /cannot be used during teardown/);
});
test('.log() is bound', t => ava(a => {
const {log} = a;
for (const value of [1, 2, 3]) {
log('value: ' + value);
}
for (const value of ['value foo', 'value bar']) {
log(value);
}
}).run().then(result => {
t.same(result.logs, [
'value: 1',
'value: 2',
'value: 3',
'value foo',
'value bar',
]);
}));
test('.plan() is bound', t => ava(a => {
const {plan} = a;
plan(3);
a.pass();
a.is(2, 2);
a.truthy('string');
}).run().then(result => {
t.ok(result.passed);
}));
test('.timeout() is bound', t => ava(a => {
const {timeout} = a;
timeout(10);
a.pass();
}).run().then(result => {
t.ok(result.passed);
}));
test('.teardown() is bound', t => {
const teardownCallback = sinon.spy();
return ava(a => {
const {teardown} = a;
teardown(teardownCallback);
a.pass();
}).run().then(result => {
t.ok(result.passed);
t.ok(teardownCallback.calledOnce);
});
});
test('t.passed value is true when teardown callback is executed for passing test', t => {
new Test({
fn(a) {
a.teardown(() => {
t.equal(a.passed, true);
t.end();
});
a.pass();
},
metadata: {type: 'test'},
onResult() {},
title: 'foo',
}).run();
});
test('t.passed value is false when teardown callback is executed for failing test', t => {
new Test({
fn(a) {
a.teardown(() => {
t.equal(a.passed, false);
t.end();
});
a.fail();
},
metadata: {type: 'test'},
onResult() {},
title: 'foo',
}).run();
});