From b6fbd5847a5d460e9ce435ab52fde91ba1bd287a Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 10 Sep 2023 21:30:04 +0200 Subject: [PATCH] Make assertions throw Fixes #3201. Assertions now throw a `TestFailure` error when they fail. This error is not exported or documented and should not be used or thrown manually. You cannot catch this error in order to recover from a failure, use `t.try()` instead. All assertions except for `throws` and `throwsAsync` now return `true` when they pass. This is useful for some of the assertions in TypeScript where they can be used as a type guard. Committing a failed `t.try()` result now also throws. --- docs/03-assertions.md | 48 ++- docs/recipes/typescript.md | 4 +- lib/assert.js | 300 +++++++----------- lib/test.js | 94 ++++-- test-tap/assert.js | 188 ++++++----- .../report/regular/traces-in-t-throws.cjs | 12 +- test-tap/reporters/default.regular.v16.log | 30 +- test-tap/reporters/default.regular.v18.log | 30 +- test-tap/reporters/default.regular.v20.log | 30 +- test-tap/reporters/tap.failfast.v16.log | 2 +- test-tap/reporters/tap.failfast.v18.log | 2 +- test-tap/reporters/tap.failfast.v20.log | 2 +- test-tap/reporters/tap.failfast2.v16.log | 2 +- test-tap/reporters/tap.failfast2.v18.log | 2 +- test-tap/reporters/tap.failfast2.v20.log | 2 +- test-tap/reporters/tap.regular.v16.log | 16 +- test-tap/reporters/tap.regular.v18.log | 16 +- test-tap/reporters/tap.regular.v20.log | 16 +- test-tap/test.js | 32 +- .../assertions-as-type-guards.cts | 6 - test-types/import-in-cts/throws.cts | 28 +- .../module/assertions-as-type-guards.ts | 6 - test-types/module/snapshot.ts | 2 +- test-types/module/throws.ts | 28 +- types/assertions.d.cts | 118 +++---- 25 files changed, 483 insertions(+), 533 deletions(-) diff --git a/docs/03-assertions.md b/docs/03-assertions.md index 4dcebeecd..1613658ec 100644 --- a/docs/03-assertions.md +++ b/docs/03-assertions.md @@ -21,7 +21,13 @@ test('unicorns are truthy', t => { If multiple assertion failures are encountered within a single test, AVA will only display the *first* one. -Assertions return a boolean indicating whether they passed. You can use this to return early from a test. Note that this does not apply to the "throws" and `snapshot()` assertions. +In AVA 6, assertions return `true` if they've passed and throw otherwise. Catching this error does not cause the test to pass. The error value is undocumented. + +In AVA 5, assertions return a boolean and do not throw. You can use this to return early from a test. The `snapshot()` assertion does not return a value. + +If you use TypeScript you can use some assertions as type guards. + +Note that the "throws" assertions return the error that was thrown (provided the assertion passed). In AVA 5, they return `undefined` if the assertion failed. ## Assertion planning @@ -95,39 +101,39 @@ test('custom assertion', t => { ### `.pass(message?)` -Passing assertion. Returns a boolean indicating whether the assertion passed. +Passing assertion. ### `.fail(message?)` -Failing assertion. Returns a boolean indicating whether the assertion passed. +Failing assertion. ### `.assert(actual, message?)` -Asserts that `actual` is truthy. Returns a boolean indicating whether the assertion passed. +Asserts that `actual` is truthy. ### `.truthy(actual, message?)` -Assert that `actual` is truthy. Returns a boolean indicating whether the assertion passed. +Assert that `actual` is truthy. ### `.falsy(actual, message?)` -Assert that `actual` is falsy. Returns a boolean indicating whether the assertion passed. +Assert that `actual` is falsy. ### `.true(actual, message?)` -Assert that `actual` is `true`. Returns a boolean indicating whether the assertion passed. +Assert that `actual` is `true`. ### `.false(actual, message?)` -Assert that `actual` is `false`. Returns a boolean indicating whether the assertion passed. +Assert that `actual` is `false`. ### `.is(actual, expected, message?)` -Assert that `actual` is the same as `expected`. This is based on [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is). Returns a boolean indicating whether the assertion passed. +Assert that `actual` is the same as `expected`. This is based on [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is). ### `.not(actual, expected, message?)` -Assert that `actual` is not the same as `expected`. This is based on [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is). Returns a boolean indicating whether the assertion passed. +Assert that `actual` is not the same as `expected`. This is based on [`Object.is()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is). ### `.deepEqual(actual, expected, message?)` @@ -135,7 +141,7 @@ Assert that `actual` is deeply equal to `expected`. See [Concordance](https://gi ### `.notDeepEqual(actual, expected, message?)` -Assert that `actual` is not deeply equal to `expected`. The inverse of `.deepEqual()`. Returns a boolean indicating whether the assertion passed. +Assert that `actual` is not deeply equal to `expected`. The inverse of `.deepEqual()`. ### `.like(actual, selector, message?)` @@ -168,12 +174,9 @@ You can also use arrays, but note that any indices in `actual` that are not in ` t.like([1, 2, 3, 4], [1, , 3]) ``` -Finally, this returns a boolean indicating whether the assertion passed. - ### `.throws(fn, expectation?, message?)` -Assert that an error is thrown. `fn` must be a function which should throw. By default, the thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned. - +Assert that an error is thrown. `fn` must be a function which should throw. By default, the thrown value *must* be an error. It is returned so you can run more assertions against it. `expectation` can be an object with one or more of the following properties: * `any`: a boolean only available in AVA 6, if `true` then the thrown value does not need to be an error. Defaults to `false` @@ -208,8 +211,7 @@ test('throws', t => { Assert that an error is thrown. `thrower` can be an async function which should throw, or a promise that should reject. This assertion must be awaited. -By default, the thrown value *must* be an error. It is returned so you can run more assertions against it. If the assertion fails then `undefined` is returned. - +By default, the thrown value *must* be an error. It is returned so you can run more assertions against it. `expectation` can be an object with one or more of the following properties: * `any`: a boolean only available in AVA 6, if `true` then the thrown value does not need to be an error. Defaults to `false` @@ -245,7 +247,7 @@ test('rejects', async t => { ### `.notThrows(fn, message?)` -Assert that no error is thrown. `fn` must be a function which shouldn't throw. Does not return anything. +Assert that no error is thrown. `fn` must be a function which shouldn't throw. ### `.notThrowsAsync(nonThrower, message?)` @@ -259,15 +261,13 @@ test('resolves', async t => { }); ``` -Does not return anything. - ### `.regex(contents, regex, message?)` -Assert that `contents` matches `regex`. Returns a boolean indicating whether the assertion passed. +Assert that `contents` matches `regex`. ### `.notRegex(contents, regex, message?)` -Assert that `contents` does not match `regex`. Returns a boolean indicating whether the assertion passed. +Assert that `contents` does not match `regex`. ### `.snapshot(expected, message?)` @@ -279,7 +279,7 @@ Compares the `expected` value with a previously recorded snapshot. Snapshots are The implementation function behaves the same as any other test function. You can even use macros. The first title argument is always optional. Additional arguments are passed to the implementation or macro function. -`.try()` is an asynchronous function. You must `await` it. The result object has `commit()` and `discard()` methods. You must decide whether to commit or discard the result. If you commit a failed result, your test will fail. +`.try()` is an asynchronous function. You must `await` it. The result object has `commit()` and `discard()` methods. You must decide whether to commit or discard the result. If you commit a failed result, your test will fail. In AVA 6, calling `commit()` on a failed result will throw an error. You can check whether the attempt passed using the `passed` property. Any assertion errors are available through the `errors` property. The attempt title is available through the `title` property. @@ -318,5 +318,3 @@ test('flaky macro', async t => { secondTry.commit(); }); ``` - -Returns a boolean indicating whether the assertion passed. diff --git a/docs/recipes/typescript.md b/docs/recipes/typescript.md index 7ee9308ce..7f7a2e4ab 100644 --- a/docs/recipes/typescript.md +++ b/docs/recipes/typescript.md @@ -177,7 +177,7 @@ Note that, despite the type cast above, when executing `t.context` is an empty o ## Typing `throws` assertions -The `t.throws()` and `t.throwsAsync()` assertions are typed to always return an Error. You can customize the error class using generics: +In AVA 6, the `t.throws()` and `t.throwsAsync()` assertions are typed to always return an `Error`. You can customize the error class using generics: ```ts import test from 'ava'; @@ -206,6 +206,6 @@ test('throwsAsync', async t => { }); ``` -Note that, despite the typing, the assertion returns `undefined` if it fails. Typing the assertions as returning `Error | undefined` didn't seem like the pragmatic choice. +In AVA 5, the assertion is typed to return the `Error` if the assertion passes *or* `undefined` if it fails. [`@ava/typescript`]: https://github.com/avajs/typescript diff --git a/lib/assert.js b/lib/assert.js index afffa87d9..f4d5ff518 100644 --- a/lib/assert.js +++ b/lib/assert.js @@ -261,6 +261,7 @@ export class Assertions { pass = notImplemented, pending = notImplemented, fail = notImplemented, + failPending = notImplemented, skip = notImplemented, compareWithSnapshot = notImplemented, experiments = {}, @@ -271,41 +272,28 @@ export class Assertions { return assertionFn; }; - const checkMessage = (message, assertion) => { + const assertMessage = (message, assertion) => { const result = checkAssertionMessage(message, assertion); - if (result === true) { - return true; + if (result !== true) { + throw fail(result); } - - fail(result); - return false; }; - this.pass = withSkip(() => { - pass(); - return true; - }); + this.pass = withSkip(() => pass()); this.fail = withSkip(message => { - if (!checkMessage(message, 't.fail()')) { - return false; - } + assertMessage(message, 't.fail()'); - fail(new AssertionError(message ?? 'Test failed via `t.fail()`', { + throw fail(new AssertionError(message ?? 'Test failed via `t.fail()`', { assertion: 't.fail()', })); - - return false; }); this.is = withSkip((actual, expected, message) => { - if (!checkMessage(message, 't.is()')) { - return false; - } + assertMessage(message, 't.is()'); if (Object.is(actual, expected)) { - pass(); - return true; + return pass(); } const result = concordance.compare(actual, expected, concordanceOptions); @@ -313,87 +301,70 @@ export class Assertions { const expectedDescriptor = result.expected ?? concordance.describe(expected, concordanceOptions); if (result.pass) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.is()', formattedDetails: [formatDescriptorWithLabel('Values are deeply equal to each other, but they are not the same:', actualDescriptor)], })); } else { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.is()', formattedDetails: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)], })); } - - return false; }); this.not = withSkip((actual, expected, message) => { - if (!checkMessage(message, 't.not()')) { - return false; - } + assertMessage(message, 't.not()'); if (Object.is(actual, expected)) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.not()', formattedDetails: [formatWithLabel('Value is the same as:', actual)], })); - return false; } - pass(); - return true; + return pass(); }); this.deepEqual = withSkip((actual, expected, message) => { - if (!checkMessage(message, 't.deepEqual()')) { - return false; - } + assertMessage(message, 't.deepEqual()'); const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { - pass(); - return true; + return pass(); } const actualDescriptor = result.actual ?? concordance.describe(actual, concordanceOptions); const expectedDescriptor = result.expected ?? concordance.describe(expected, concordanceOptions); - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.deepEqual()', formattedDetails: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)], })); - return false; }); this.notDeepEqual = withSkip((actual, expected, message) => { - if (!checkMessage(message, 't.notDeepEqual()')) { - return false; - } + assertMessage(message, 't.notDeepEqual()'); const result = concordance.compare(actual, expected, concordanceOptions); if (result.pass) { const actualDescriptor = result.actual ?? concordance.describe(actual, concordanceOptions); - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.notDeepEqual()', formattedDetails: [formatDescriptorWithLabel('Value is deeply equal:', actualDescriptor)], })); - return false; } - pass(); - return true; + return pass(); }); this.like = withSkip((actual, selector, message) => { - if (!checkMessage(message, 't.like()')) { - return false; - } + assertMessage(message, 't.like()'); if (!isLikeSelector(selector)) { - fail(new AssertionError('`t.like()` selector must be a non-empty object', { + throw fail(new AssertionError('`t.like()` selector must be a non-empty object', { assertion: 't.like()', formattedDetails: [formatWithLabel('Called with:', selector)], })); - return false; } let comparable; @@ -401,11 +372,10 @@ export class Assertions { comparable = selectComparable(actual, selector); } catch (error) { if (error === CIRCULAR_SELECTOR) { - fail(new AssertionError('`t.like()` selector must not contain circular references', { + throw fail(new AssertionError('`t.like()` selector must not contain circular references', { assertion: 't.like()', formattedDetails: [formatWithLabel('Called with:', selector)], })); - return false; } throw error; @@ -413,18 +383,15 @@ export class Assertions { const result = concordance.compare(comparable, selector, concordanceOptions); if (result.pass) { - pass(); - return true; + return pass(); } const actualDescriptor = result.actual ?? concordance.describe(comparable, concordanceOptions); const expectedDescriptor = result.expected ?? concordance.describe(selector, concordanceOptions); - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.like()', formattedDetails: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)], })); - - return false; }); this.throws = withSkip((...args) => { @@ -433,24 +400,20 @@ export class Assertions { // to the function. let [fn, expectations, message] = args; - if (!checkMessage(message, 't.throws()')) { - return; - } + assertMessage(message, 't.throws()'); if (typeof fn !== 'function') { - fail(new AssertionError('`t.throws()` must be called with a function', { + throw fail(new AssertionError('`t.throws()` must be called with a function', { assertion: 't.throws()', improperUsage: {assertion: 'throws'}, formattedDetails: [formatWithLabel('Called with:', fn)], })); - return; } try { expectations = validateExpectations('t.throws()', expectations, args.length, experiments); } catch (error) { - fail(error); - return; + throw fail(error); } let retval; @@ -460,22 +423,20 @@ export class Assertions { if (isPromise(retval)) { // Here isPromise() checks if something is "promise like". Cast to an actual promise. Promise.resolve(retval).catch(noop); - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.throws()', formattedDetails: [formatWithLabel('Function returned a promise. Use `t.throwsAsync()` instead:', retval)], })); - return; } } catch (error) { actual = error; } if (!actual) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.throws()', formattedDetails: [formatWithLabel('Function returned:', retval)], })); - return; } try { @@ -489,30 +450,32 @@ export class Assertions { pass(); return actual; } catch (error) { - fail(error); + throw fail(error); } }); this.throwsAsync = withSkip(async (...args) => { let [thrower, expectations, message] = args; - if (!checkMessage(message, 't.throwsAsync()')) { - return; + try { + assertMessage(message, 't.throwsAsync()'); + } catch (error) { + Promise.resolve(thrower).catch(noop); + throw error; } if (typeof thrower !== 'function' && !isPromise(thrower)) { - fail(new AssertionError('`t.throwsAsync()` must be called with a function or promise', { + throw fail(new AssertionError('`t.throwsAsync()` must be called with a function or promise', { assertion: 't.throwsAsync()', formattedDetails: [formatWithLabel('Called with:', thrower)], })); - return; } try { expectations = validateExpectations('t.throwsAsync()', expectations, args.length, experiments); } catch (error) { - fail(error); - return; + Promise.resolve(thrower).catch(noop); + throw fail(error); } const handlePromise = async (promise, wasReturned) => { @@ -520,29 +483,29 @@ export class Assertions { const assertionStack = getAssertionStack(); // Handle "promise like" objects by casting to a real Promise. const intermediate = Promise.resolve(promise).then(value => { - throw new AssertionError(message, { + throw failPending(new AssertionError(message, { assertion: 't.throwsAsync()', assertionStack, formattedDetails: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} resolved with:`, value)], - }); + })); }, error => { - assertExpectations({ - assertion: 't.throwsAsync()', - actual: error, - expectations, - message, - prefix: `${wasReturned ? 'Returned promise' : 'Promise'} rejected with`, - assertionStack, - }); - return error; + try { + assertExpectations({ + assertion: 't.throwsAsync()', + actual: error, + expectations, + message, + prefix: `${wasReturned ? 'Returned promise' : 'Promise'} rejected with`, + assertionStack, + }); + return error; + } catch (error_) { + throw failPending(error_); + } }); pending(intermediate); - try { - return await intermediate; - } catch { - // Don't reject the returned promise, even if the assertion fails. - } + return intermediate; }; if (isPromise(thrower)) { @@ -558,63 +521,60 @@ export class Assertions { } if (actual) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.throwsAsync()', cause: actual, formattedDetails: [formatWithLabel('Function threw synchronously. Use `t.throws()` instead:', actual)], })); - return; } if (isPromise(retval)) { return handlePromise(retval, true); } - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.throwsAsync()', formattedDetails: [formatWithLabel('Function returned:', retval)], })); }); this.notThrows = withSkip((fn, message) => { - if (!checkMessage(message, 't.notThrows()')) { - return; - } + assertMessage(message, 't.notThrows()'); if (typeof fn !== 'function') { - fail(new AssertionError('`t.notThrows()` must be called with a function', { + throw fail(new AssertionError('`t.notThrows()` must be called with a function', { assertion: 't.notThrows()', improperUsage: {assertion: 'notThrows'}, formattedDetails: [formatWithLabel('Called with:', fn)], })); - return; } try { fn(); } catch (error) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.notThrows()', cause: error, formattedDetails: [formatWithLabel('Function threw:', error)], })); - return; } - pass(); + return pass(); }); - this.notThrowsAsync = withSkip((nonThrower, message) => { - if (!checkMessage(message, 't.notThrowsAsync()')) { - return Promise.resolve(); + this.notThrowsAsync = withSkip(async (nonThrower, message) => { + try { + assertMessage(message, 't.notThrowsAsync()'); + } catch (error) { + Promise.resolve(nonThrower).catch(noop); + throw error; } if (typeof nonThrower !== 'function' && !isPromise(nonThrower)) { - fail(new AssertionError('`t.notThrowsAsync()` must be called with a function or promise', { + throw fail(new AssertionError('`t.notThrowsAsync()` must be called with a function or promise', { assertion: 't.notThrowsAsync()', formattedDetails: [formatWithLabel('Called with:', nonThrower)], })); - return Promise.resolve(); } const handlePromise = async (promise, wasReturned) => { @@ -622,19 +582,16 @@ export class Assertions { const assertionStack = getAssertionStack(); // Handle "promise like" objects by casting to a real Promise. const intermediate = Promise.resolve(promise).then(noop, error => { - throw new AssertionError(message, { + throw failPending(new AssertionError(message, { assertion: 't.notThrowsAsync()', assertionStack, formattedDetails: [formatWithLabel(`${wasReturned ? 'Returned promise' : 'Promise'} rejected with:`, error)], - }); + })); }); pending(intermediate); - try { - return await intermediate; - } catch { - // Don't reject the returned promise, even if the assertion fails. - } + await intermediate; + return true; }; if (isPromise(nonThrower)) { @@ -645,20 +602,18 @@ export class Assertions { try { retval = nonThrower(); } catch (error) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.notThrowsAsync()', cause: error, formattedDetails: [formatWithLabel('Function threw:', error)], })); - return Promise.resolve(); } if (!isPromise(retval)) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.notThrowsAsync()', formattedDetails: [formatWithLabel('Function did not return a promise. Use `t.notThrows()` instead:', retval)], })); - return Promise.resolve(); } return handlePromise(retval, true); @@ -666,30 +621,25 @@ export class Assertions { this.snapshot = withSkip((expected, message) => { if (disableSnapshots) { - fail(new AssertionError('`t.snapshot()` can only be used in tests', { + throw fail(new AssertionError('`t.snapshot()` can only be used in tests', { assertion: 't.snapshot()', })); - return false; } if (message?.id !== undefined) { - fail(new AssertionError('Since AVA 4, snapshot IDs are no longer supported', { + throw fail(new AssertionError('Since AVA 4, snapshot IDs are no longer supported', { assertion: 't.snapshot()', formattedDetails: [formatWithLabel('Called with id:', message.id)], })); - return false; } - if (!checkMessage(message, 't.snapshot()')) { - return false; - } + assertMessage(message, 't.snapshot()'); if (message === '') { - fail(new AssertionError('The snapshot assertion message must be a non-empty string', { + throw fail(new AssertionError('The snapshot assertion message must be a non-empty string', { assertion: 't.snapshot()', formattedDetails: [formatWithLabel('Called with:', message)], })); - return false; } let result; @@ -706,188 +656,152 @@ export class Assertions { improperUsage.expectedVersion = error.expectedVersion; } - fail(new AssertionError(message ?? 'Could not compare snapshot', { + throw fail(new AssertionError(message ?? 'Could not compare snapshot', { asssertion: 't.snapshot()', improperUsage, })); - return false; } if (result.pass) { - pass(); - return true; + return pass(); } if (result.actual) { - fail(new AssertionError(message ?? 'Did not match snapshot', { + throw fail(new AssertionError(message ?? 'Did not match snapshot', { assertion: 't.snapshot()', formattedDetails: [formatDescriptorDiff(result.actual, result.expected, {invert: true})], })); } else { // This can only occur in CI environments. - fail(new AssertionError(message ?? 'No snapshot available — new snapshots are not created in CI environments', { + throw fail(new AssertionError(message ?? 'No snapshot available — new snapshots are not created in CI environments', { assertion: 't.snapshot()', })); } - - return false; }); this.truthy = withSkip((actual, message) => { - if (!checkMessage(message, 't.truthy()')) { - return false; - } + assertMessage(message, 't.truthy()'); if (actual) { - pass(); - return true; + return pass(); } - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.truthy()', formattedDetails: [formatWithLabel('Value is not truthy:', actual)], })); - return false; }); this.falsy = withSkip((actual, message) => { - if (!checkMessage(message, 't.falsy()')) { - return false; - } + assertMessage(message, 't.falsy()'); if (actual) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.falsy()', formattedDetails: [formatWithLabel('Value is not falsy:', actual)], })); - return false; } - pass(); - return true; + return pass(); }); this.true = withSkip((actual, message) => { - if (!checkMessage(message, 't.true()')) { - return false; - } + assertMessage(message, 't.true()'); if (actual === true) { - pass(); - return true; + return pass(); } - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.true()', formattedDetails: [formatWithLabel('Value is not `true`:', actual)], })); - return false; }); this.false = withSkip((actual, message) => { - if (!checkMessage(message, 't.false()')) { - return false; - } + assertMessage(message, 't.false()'); if (actual === false) { - pass(); - return true; + return pass(); } - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.false()', formattedDetails: [formatWithLabel('Value is not `false`:', actual)], })); - return false; }); this.regex = withSkip((string, regex, message) => { - if (!checkMessage(message, 't.regex()')) { - return false; - } + assertMessage(message, 't.regex()'); if (typeof string !== 'string') { - fail(new AssertionError('`t.regex()` must be called with a string', { + throw fail(new AssertionError('`t.regex()` must be called with a string', { assertion: 't.regex()', formattedDetails: [formatWithLabel('Called with:', string)], })); - return false; } if (!(regex instanceof RegExp)) { - fail(new AssertionError('`t.regex()` must be called with a regular expression', { + throw fail(new AssertionError('`t.regex()` must be called with a regular expression', { assertion: 't.regex()', formattedDetails: [formatWithLabel('Called with:', regex)], })); - return false; } if (!regex.test(string)) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.regex()', formattedDetails: [ formatWithLabel('Value must match expression:', string), formatWithLabel('Regular expression:', regex), ], })); - return false; } - pass(); - return true; + return pass(); }); this.notRegex = withSkip((string, regex, message) => { - if (!checkMessage(message, 't.notRegex()')) { - return false; - } + assertMessage(message, 't.notRegex()'); if (typeof string !== 'string') { - fail(new AssertionError('`t.notRegex()` must be called with a string', { + throw fail(new AssertionError('`t.notRegex()` must be called with a string', { assertion: 't.notRegex()', formattedDetails: [formatWithLabel('Called with:', string)], })); - return false; } if (!(regex instanceof RegExp)) { - fail(new AssertionError('`t.notRegex()` must be called with a regular expression', { + throw fail(new AssertionError('`t.notRegex()` must be called with a regular expression', { assertion: 't.notRegex()', formattedDetails: [formatWithLabel('Called with:', regex)], })); - return false; } if (regex.test(string)) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.notRegex()', formattedDetails: [ formatWithLabel('Value must not match expression:', string), formatWithLabel('Regular expression:', regex), ], })); - return false; } - pass(); - return true; + return pass(); }); this.assert = withSkip((actual, message) => { - if (!checkMessage(message, 't.assert()')) { - return false; - } + assertMessage(message, 't.assert()'); if (!actual) { - fail(new AssertionError(message, { + throw fail(new AssertionError(message, { assertion: 't.assert()', formattedDetails: [formatWithLabel('Value is not truthy:', actual)], })); - return false; } - pass(); - return true; + return pass(); }); } } diff --git a/lib/test.js b/lib/test.js index 1879bcdd2..e821c9f7a 100644 --- a/lib/test.js +++ b/lib/test.js @@ -26,18 +26,29 @@ function formatErrorValue(label, error) { return {label, formatted}; } +class TestFailure extends Error { + constructor() { + super('The test has failed'); + this.name = 'TestFailure'; + } +} + const testMap = new WeakMap(); class ExecutionContext extends Assertions { constructor(test) { super({ pass() { test.countPassedAssertion(); + return true; }, pending(promise) { test.addPendingAssertion(promise); }, fail(error) { - test.addFailedAssertion(error); + return test.addFailedAssertion(error); + }, + failPending(error) { + return test.failPendingAssertion(error); }, skip() { test.countPassedAssertion(); @@ -126,7 +137,7 @@ class ExecutionContext extends Assertions { if (discarded) { test.saveFirstError(new Error('Can’t commit a result that was previously discarded')); - return; + throw this.testFailure; } committed = true; @@ -145,7 +156,7 @@ class ExecutionContext extends Assertions { discard({retainLogs = false} = {}) { if (committed) { test.saveFirstError(new Error('Can’t discard a result that was previously committed')); - return; + throw this.testFailure; } if (discarded) { @@ -274,7 +285,7 @@ export default class Test { }; this.assertCount = 0; - this.assertError = undefined; + this.assertError = null; this.attemptCount = 0; this.calledEnd = false; this.duration = null; @@ -285,6 +296,7 @@ export default class Test { this.pendingAttemptCount = 0; this.planCount = null; this.startedAt = 0; + this.testFailure = null; this.timeoutTimer = null; } @@ -309,7 +321,7 @@ export default class Test { this.logs.push(text); } - addPendingAssertion(promise) { + async addPendingAssertion(promise) { if (this.finishing) { this.saveFirstError(new Error('Assertion started, but test has already finished')); } @@ -322,12 +334,14 @@ export default class Test { this.pendingAssertionCount++; this.refreshTimeout(); - promise - .catch(error => this.saveFirstError(error)) - .then(() => { - this.pendingAssertionCount--; - this.refreshTimeout(); - }); + try { + await promise; + } catch { + // Ignore errors. + } finally { + this.pendingAssertionCount--; + this.refreshTimeout(); + } } addFailedAssertion(error) { @@ -342,6 +356,12 @@ export default class Test { this.assertCount++; this.refreshTimeout(); this.saveFirstError(error); + return this.testFailure; + } + + failPendingAssertion(error) { + this.saveFirstError(error); + return this.testFailure; } finishAttempt({commit, deferredSnapshotRecordings, errors, logs, passed, retainLogs, snapshotCount, startingSnapshotCount}) { @@ -380,12 +400,14 @@ export default class Test { } this.refreshTimeout(); + if (this.testFailure) { + throw this.testFailure; + } } saveFirstError(error) { - if (!this.assertError) { - this.assertError = error; - } + this.assertError ??= error; + this.testFailure = new TestFailure(); } plan(count, planAssertionStack) { @@ -500,55 +522,53 @@ export default class Test { callFn() { try { - return { - ok: true, - retval: this.fn.call(null, this.createExecutionContext()), - }; + return [true, this.fn.call(null, this.createExecutionContext())]; } catch (error) { - return { - ok: false, - error, - }; + return [false, error]; } } run() { this.startedAt = nowAndTimers.now(); - const result = this.callFn(); - if (!result.ok) { - if (isExternalAssertError(result.error)) { + const [syncOk, retval] = this.callFn(); + if (!syncOk) { + if (this.testFailure !== null && retval === this.testFailure) { + return this.finish(); + } + + if (isExternalAssertError(retval)) { this.saveFirstError(new AssertionError('Assertion failed', { - cause: result.error, - formattedDetails: [{label: 'Assertion failed: ', formatted: result.error.message}], + cause: retval, + formattedDetails: [{label: 'Assertion failed: ', formatted: retval.message}], })); } else { this.saveFirstError(new AssertionError('Error thrown in test', { - // TODO: Provide an assertion stack that traces to the test declaration, - // rather than AVA internals. + // TODO: Provide an assertion stack that traces to the test declaration, + // rather than AVA internals. assertionStack: '', - cause: result.error, - formattedDetails: [formatErrorValue('Error thrown in test:', result.error)], + cause: retval, + formattedDetails: [formatErrorValue('Error thrown in test:', retval)], })); } return this.finish(); } - const returnedObservable = result.retval !== null && typeof result.retval === 'object' && typeof result.retval.subscribe === 'function'; - const returnedPromise = isPromise(result.retval); + const returnedObservable = retval !== null && typeof retval === 'object' && typeof retval.subscribe === 'function'; + const returnedPromise = isPromise(retval); let promise; if (returnedObservable) { promise = new Promise((resolve, reject) => { - result.retval.subscribe({ + retval.subscribe({ error: reject, complete: () => resolve(), }); }); } else if (returnedPromise) { // `retval` can be any thenable, so convert to a proper promise. - promise = Promise.resolve(result.retval); + promise = Promise.resolve(retval); } if (promise) { @@ -571,6 +591,10 @@ export default class Test { promise .catch(error => { + if (this.testFailure !== null && error === this.testFailure) { + return; + } + if (isExternalAssertError(error)) { this.saveFirstError(new AssertionError('Assertion failed', { cause: error, diff --git a/test-tap/assert.js b/test-tap/assert.js index 00aed409d..4ba1e4f50 100644 --- a/test-tap/assert.js +++ b/test-tap/assert.js @@ -13,21 +13,40 @@ setOptions({chalkOptions: {level: 0}}); let lastFailure = null; let lastPassed = false; +class AssertionFailed extends Error { + constructor() { + super('Assertion failed'); + this.name = 'AssertionFailed'; + } +} + const AssertionsBase = class extends assert.Assertions { constructor(overwrites = {}) { super({ pass() { lastPassed = true; + // Match behavior in lib/test.js, not great for this test suite but refactoring is a much larger task. + return true; }, pending(promise) { promise.then(() => { lastPassed = true; }, error => { - lastFailure = error; + if (error.name !== 'AssertionFailed') { + lastFailure = error; + } }); }, fail(error) { lastFailure = error; + return new AssertionFailed(); + }, + failPending(error) { + if (error.name !== 'AssertionFailed') { + lastFailure = error; + } + + return new AssertionFailed(); }, skip() {}, experiments: {}, @@ -87,21 +106,41 @@ function add(fn) { return gatheringPromise; } -function failsWith(t, fn, subset, {expectBoolean = true} = {}) { +function failsWith(t, fn, subset) { lastFailure = null; - const retval = fn(); + try { + fn(); + } catch (error) { + if (error.name !== 'AssertionFailed') { + throw error; + } + } + assertFailure(t, subset); - if (expectBoolean) { - t.notOk(retval); +} + +async function failsWithAsync(t, fn, subset) { + lastFailure = null; + try { + await fn(); + } catch (error) { + if (error.name !== 'AssertionFailed') { + throw error; + } } + + assertFailure(t, subset); } function throwsAsyncFails(t, fn, subset) { return add(() => { lastFailure = null; - return fn().then(retval => { - t.equal(retval, undefined); - assertFailure(t, subset); + return fn().catch(error => { + if (error.name === 'AssertionFailed') { + assertFailure(t, subset); + } else { + throw error; + } }); }); } @@ -109,16 +148,27 @@ function throwsAsyncFails(t, fn, subset) { function notThrowsAsyncFails(t, fn, subset) { return add(() => { lastFailure = null; - return fn().then(retval => { - t.equal(retval, undefined); - assertFailure(t, subset); + return fn().catch(error => { + if (error.name === 'AssertionFailed') { + assertFailure(t, subset); + } else { + throw error; + } }); }); } function fails(t, fn) { lastFailure = null; - const retval = fn(); + let retval; + try { + retval = fn(); + } catch (error) { + if (error.name !== 'AssertionFailed') { + throw error; + } + } + if (lastFailure) { t.notOk(retval); } else { @@ -132,7 +182,7 @@ function passes(t, fn, {expectBoolean = true} = {}) { const retval = fn(); if (lastPassed) { if (expectBoolean) { - t.ok(retval); + t.equal(retval, true); } else { t.pass(); } @@ -820,12 +870,12 @@ test('.throws()', gather(t => { // Passes because an error is thrown. passes(t, () => assertions.throws(() => { throw new Error('foo'); - })); + }), {expectBoolean: false}); // Passes when string is thrown, only when any is set to true. passes(t, () => assertions.throws(() => { throw 'foo'; // eslint-disable-line no-throw-literal - }, {any: true})); + }, {any: true}), {expectBoolean: false}); // Passes because the correct error is thrown. passes(t, () => { @@ -833,7 +883,7 @@ test('.throws()', gather(t => { return assertions.throws(() => { throw error; }, {is: error}); - }); + }, {expectBoolean: false}); // Fails because the thrown value is not an error fails(t, () => { @@ -854,7 +904,7 @@ test('.throws()', gather(t => { // Passes because the correct error is thrown. passes(t, () => assertions.throws(() => { throw new TypeError(); - }, {name: 'TypeError'})); + }, {name: 'TypeError'}), {expectBoolean: false}); // Fails because the thrown value is not an error fails(t, () => assertions.throws(() => { @@ -872,14 +922,14 @@ test('.throws()', gather(t => { const error = new TypeError(); error.code = 'ERR_TEST'; throw error; - }, {code: 'ERR_TEST'})); + }, {code: 'ERR_TEST'}), {expectBoolean: false}); // Passes because the correct error is thrown. passes(t, () => assertions.throws(() => { const error = new TypeError(); error.code = 42; throw error; - }, {code: 42})); + }, {code: 42}), {expectBoolean: false}); // Fails because the thrown value is not the right one fails(t, () => assertions.throws(() => { @@ -901,11 +951,11 @@ test('.throws()', gather(t => { passes(t, () => assertions.throws(() => { throw new Error('foo'); - }, undefined)); + }, undefined), {expectBoolean: false}); passes(t, async () => { await assertions.throwsAsync(() => Promise.reject(new Error('foo')), undefined); - }); + }, {expectBoolean: false}); failsWith(t, () => assertions.throws(() => {}, undefined, null), { assertion: 't.throws()', @@ -938,7 +988,7 @@ test('.throws()', gather(t => { passes(t, () => assertions.throws(() => { throw new Error('error'); - }, {message: 'error'})); + }, {message: 'error'}), {expectBoolean: false}); // Fails because the regular expression in the message is incorrect failsWith( @@ -962,7 +1012,7 @@ test('.throws()', gather(t => { passes(t, () => assertions.throws(() => { throw new Error('error'); - }, {message: /error/})); + }, {message: /error/}), {expectBoolean: false}); // Fails because the function in the message returns false failsWith( @@ -986,7 +1036,7 @@ test('.throws()', gather(t => { passes(t, () => assertions.throws(() => { throw new Error('error'); - }, {message: () => true})); + }, {message: () => true}), {expectBoolean: false}); })); test('.throws() returns the thrown error', t => { @@ -1102,15 +1152,13 @@ test('.throws() fails if passed a bad value', t => { t.end(); }); -test('.throwsAsync() fails if passed a bad value', t => { - failsWith(t, () => assertions.throwsAsync('not a function'), { +test('.throwsAsync() fails if passed a bad value', gather(t => { + throwsAsyncFails(t, () => assertions.throwsAsync('not a function'), { assertion: 't.throwsAsync()', message: '`t.throwsAsync()` must be called with a function or promise', formattedDetails: [{label: 'Called with:', formatted: /not a function/}], - }, {expectBoolean: false}); - - t.end(); -}); + }); +})); test('.throws() fails if passed a bad expectation', t => { failsWith(t, () => assertions.throws(() => {}, true), { @@ -1188,78 +1236,78 @@ test('.throws() fails if passed a bad expectation', t => { t.end(); }); -test('.throwsAsync() fails if passed a bad expectation', t => { - failsWith(t, () => assertions.throwsAsync(() => {}, true), { +test('.throwsAsync() fails if passed a bad expectation', async t => { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, true), { assertion: 't.throwsAsync()', message: 'The second argument to `t.throwsAsync()` must be an expectation object, `null` or `undefined`', formattedDetails: [{label: 'Called with:', formatted: /true/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, 'foo'), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, 'foo'), { assertion: 't.throwsAsync()', message: 'The second argument to `t.throwsAsync()` must be an expectation object, `null` or `undefined`', formattedDetails: [{label: 'Called with:', formatted: /foo/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, /baz/), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, /baz/), { assertion: 't.throwsAsync()', message: 'The second argument to `t.throwsAsync()` must be an expectation object, `null` or `undefined`', formattedDetails: [{label: 'Called with:', formatted: /baz/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, class Bar {}), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, class Bar {}), { assertion: 't.throwsAsync()', message: 'The second argument to `t.throwsAsync()` must be an expectation object, `null` or `undefined`', formattedDetails: [{label: 'Called with:', formatted: /Bar/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, {}), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, {}), { assertion: 't.throwsAsync()', message: 'The second argument to `t.throwsAsync()` must be an expectation object, `null` or `undefined`', formattedDetails: [{label: 'Called with:', formatted: /{}/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, []), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, []), { assertion: 't.throwsAsync()', message: 'The second argument to `t.throwsAsync()` must be an expectation object, `null` or `undefined`', formattedDetails: [{label: 'Called with:', formatted: /\[]/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, {any: {}}), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, {any: {}}), { assertion: 't.throwsAsync()', message: 'The `any` property of the second argument to `t.throwsAsync()` must be a boolean', formattedDetails: [{label: 'Called with:', formatted: /any: {}/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, {code: {}}), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, {code: {}}), { assertion: 't.throwsAsync()', message: 'The `code` property of the second argument to `t.throwsAsync()` must be a string or number', formattedDetails: [{label: 'Called with:', formatted: /code: {}/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, {instanceOf: null}), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, {instanceOf: null}), { assertion: 't.throwsAsync()', message: 'The `instanceOf` property of the second argument to `t.throwsAsync()` must be a function', formattedDetails: [{label: 'Called with:', formatted: /instanceOf: null/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, {message: null}), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, {message: null}), { assertion: 't.throwsAsync()', message: 'The `message` property of the second argument to `t.throwsAsync()` must be a string, regular expression or a function', formattedDetails: [{label: 'Called with:', formatted: /message: null/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, {name: null}), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, {name: null}), { assertion: 't.throwsAsync()', message: 'The `name` property of the second argument to `t.throwsAsync()` must be a string', formattedDetails: [{label: 'Called with:', formatted: /name: null/}], - }, {expectBoolean: false}); + }); - failsWith(t, () => assertions.throwsAsync(() => {}, {is: {}, message: '', name: '', of() {}, foo: null}), { + await failsWithAsync(t, () => assertions.throwsAsync(() => {}, {is: {}, message: '', name: '', of() {}, foo: null}), { assertion: 't.throwsAsync()', message: 'The second argument to `t.throwsAsync()` contains unexpected properties', formattedDetails: [{label: 'Called with:', formatted: /foo: null/}], - }, {expectBoolean: false}); + }); t.end(); }); @@ -1278,12 +1326,10 @@ test('.throws() fails if passed null expectation', t => { t.end(); }); -test('.throwsAsync() fails if passed null', t => { +test('.throwsAsync() fails if passed null', async t => { const asserter = new AssertionsBase(); - failsWith(t, () => { - asserter.throwsAsync(() => {}, null); - }, { + await failsWithAsync(t, () => asserter.throwsAsync(() => {}, null), { assertion: 't.throwsAsync()', message: 'The second argument to `t.throwsAsync()` must be an expectation object or `undefined`', formattedDetails: [{label: 'Called with:', formatted: /null/}], @@ -1294,9 +1340,9 @@ test('.throwsAsync() fails if passed null', t => { test('.notThrows()', gather(t => { // Passes because the function doesn't throw - passes(t, () => assertions.notThrows(() => {}), {expectBoolean: false}); + passes(t, () => assertions.notThrows(() => {})); - passes(t, () => assertions.notThrows(() => {}), {expectBoolean: false}); + passes(t, () => assertions.notThrows(() => {})); // Fails because the function throws. failsWith(t, () => assertions.notThrows(() => { @@ -1305,7 +1351,7 @@ test('.notThrows()', gather(t => { assertion: 't.notThrows()', message: '', formattedDetails: [{label: 'Function threw:', formatted: /foo/}], - }, {expectBoolean: false}); + }); // Fails because the function throws. Asserts that message is used for the // assertion, not to validate the thrown error. @@ -1315,7 +1361,7 @@ test('.notThrows()', gather(t => { assertion: 't.notThrows()', message: 'my message', formattedDetails: [{label: 'Function threw:', formatted: /foo/}], - }, {expectBoolean: false}); + }); failsWith(t, () => assertions.notThrows(() => {}, null), { assertion: 't.notThrows()', @@ -1324,7 +1370,7 @@ test('.notThrows()', gather(t => { label: 'Called with:', formatted: /null/, }], - }, {expectBoolean: false}); + }); })); test('.notThrowsAsync()', gather(t => { @@ -1380,14 +1426,6 @@ test('.notThrowsAsync()', gather(t => { }); })); -test('.notThrowsAsync() returns undefined for a fulfilled promise', t => assertions.notThrowsAsync(Promise.resolve(Symbol(''))).then(actual => { - t.equal(actual, undefined); -})); - -test('.notThrowsAsync() returns undefined for a fulfilled promise returned by the function', t => assertions.notThrowsAsync(() => Promise.resolve(Symbol(''))).then(actual => { - t.equal(actual, undefined); -})); - test('.notThrows() fails if passed a bad value', t => { failsWith(t, () => assertions.notThrows('not a function'), { assertion: 't.notThrows()', @@ -1398,13 +1436,11 @@ test('.notThrows() fails if passed a bad value', t => { t.end(); }); -test('.notThrowsAsync() fails if passed a bad value', t => { - failsWith(t, () => assertions.notThrowsAsync('not a function'), { +test('.notThrowsAsync() fails if passed a bad value', async t => { + await failsWithAsync(t, () => assertions.notThrowsAsync('not a function'), { assertion: 't.notThrowsAsync()', message: '`t.notThrowsAsync()` must be called with a function or promise', formattedDetails: [{label: 'Called with:', formatted: /not a function/}], - }, { - expectBoolean: false, }); t.end(); diff --git a/test-tap/fixture/report/regular/traces-in-t-throws.cjs b/test-tap/fixture/report/regular/traces-in-t-throws.cjs index 8ed95cc73..3cccf6833 100644 --- a/test-tap/fixture/report/regular/traces-in-t-throws.cjs +++ b/test-tap/fixture/report/regular/traces-in-t-throws.cjs @@ -16,12 +16,14 @@ test('notThrows', t => { t.notThrows(() => throwError()); }); -test('notThrowsAsync', t => { - t.notThrowsAsync(() => throwError()); +test('notThrowsAsync', async t => { + await t.notThrowsAsync(() => throwError()); }); -test('throwsAsync', t => { - t.throwsAsync(() => throwError(), {instanceOf: TypeError}); +test('throwsAsync', async t => { + await t.throwsAsync(() => throwError(), {instanceOf: TypeError}); }); -test('throwsAsync different error', t => t.throwsAsync(returnRejectedPromise, {instanceOf: TypeError})); +test('throwsAsync different error', async t => { + await t.throwsAsync(returnRejectedPromise, {instanceOf: TypeError}); +}); diff --git a/test-tap/reporters/default.regular.v16.log b/test-tap/reporters/default.regular.v16.log index 77eda7ecd..49148cd43 100644 --- a/test-tap/reporters/default.regular.v16.log +++ b/test-tap/reporters/default.regular.v16.log @@ -289,9 +289,9 @@ traces-in-t-throws.cjs:20 - 19: test('notThrowsAsync', t => { -  20: t.notThrowsAsync(() => throwError()); - 21: }); + 19: test('notThrowsAsync', async t => { +  20: await t.notThrowsAsync(() => throwError()); + 21: }); Function threw: @@ -300,8 +300,8 @@ } › throwError (test-tap/fixture/report/regular/traces-in-t-throws.cjs:4:8) - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:25 - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:4 + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:31 + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:10 @@ -309,9 +309,9 @@ traces-in-t-throws.cjs:24 - 23: test('throwsAsync', t => { -  24: t.throwsAsync(() => throwError(), {instanceOf: TypeError}); - 25: }); + 23: test('throwsAsync', async t => { +  24: await t.throwsAsync(() => throwError(), {instanceOf: TypeError}); + 25: }); Function threw synchronously. Use `t.throws()` instead: @@ -320,18 +320,18 @@ } › throwError (test-tap/fixture/report/regular/traces-in-t-throws.cjs:4:8) - › t.throwsAsync.instanceOf (test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:22) - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:4 + › t.throwsAsync.instanceOf (test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:28) + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:10 traces-in-t-throws › throwsAsync different error - traces-in-t-throws.cjs:27 + traces-in-t-throws.cjs:28 - 26: -  27: test('throwsAsync different error', t => t.throwsAsync(returnRejectedPromise, {instanceOf: TypeError})); - 28: + 27: test('throwsAsync different error', async t => { +  28: await t.throwsAsync(returnRejectedPromise, {instanceOf: TypeError}); + 29: }); Returned promise rejected with unexpected exception: @@ -344,7 +344,7 @@ Function TypeError {} › returnRejectedPromise (test-tap/fixture/report/regular/traces-in-t-throws.cjs:8:24) - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:27:44 + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:28:10 ─ diff --git a/test-tap/reporters/default.regular.v18.log b/test-tap/reporters/default.regular.v18.log index 77eda7ecd..49148cd43 100644 --- a/test-tap/reporters/default.regular.v18.log +++ b/test-tap/reporters/default.regular.v18.log @@ -289,9 +289,9 @@ traces-in-t-throws.cjs:20 - 19: test('notThrowsAsync', t => { -  20: t.notThrowsAsync(() => throwError()); - 21: }); + 19: test('notThrowsAsync', async t => { +  20: await t.notThrowsAsync(() => throwError()); + 21: }); Function threw: @@ -300,8 +300,8 @@ } › throwError (test-tap/fixture/report/regular/traces-in-t-throws.cjs:4:8) - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:25 - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:4 + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:31 + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:10 @@ -309,9 +309,9 @@ traces-in-t-throws.cjs:24 - 23: test('throwsAsync', t => { -  24: t.throwsAsync(() => throwError(), {instanceOf: TypeError}); - 25: }); + 23: test('throwsAsync', async t => { +  24: await t.throwsAsync(() => throwError(), {instanceOf: TypeError}); + 25: }); Function threw synchronously. Use `t.throws()` instead: @@ -320,18 +320,18 @@ } › throwError (test-tap/fixture/report/regular/traces-in-t-throws.cjs:4:8) - › t.throwsAsync.instanceOf (test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:22) - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:4 + › t.throwsAsync.instanceOf (test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:28) + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:10 traces-in-t-throws › throwsAsync different error - traces-in-t-throws.cjs:27 + traces-in-t-throws.cjs:28 - 26: -  27: test('throwsAsync different error', t => t.throwsAsync(returnRejectedPromise, {instanceOf: TypeError})); - 28: + 27: test('throwsAsync different error', async t => { +  28: await t.throwsAsync(returnRejectedPromise, {instanceOf: TypeError}); + 29: }); Returned promise rejected with unexpected exception: @@ -344,7 +344,7 @@ Function TypeError {} › returnRejectedPromise (test-tap/fixture/report/regular/traces-in-t-throws.cjs:8:24) - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:27:44 + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:28:10 ─ diff --git a/test-tap/reporters/default.regular.v20.log b/test-tap/reporters/default.regular.v20.log index 77eda7ecd..49148cd43 100644 --- a/test-tap/reporters/default.regular.v20.log +++ b/test-tap/reporters/default.regular.v20.log @@ -289,9 +289,9 @@ traces-in-t-throws.cjs:20 - 19: test('notThrowsAsync', t => { -  20: t.notThrowsAsync(() => throwError()); - 21: }); + 19: test('notThrowsAsync', async t => { +  20: await t.notThrowsAsync(() => throwError()); + 21: }); Function threw: @@ -300,8 +300,8 @@ } › throwError (test-tap/fixture/report/regular/traces-in-t-throws.cjs:4:8) - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:25 - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:4 + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:31 + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:20:10 @@ -309,9 +309,9 @@ traces-in-t-throws.cjs:24 - 23: test('throwsAsync', t => { -  24: t.throwsAsync(() => throwError(), {instanceOf: TypeError}); - 25: }); + 23: test('throwsAsync', async t => { +  24: await t.throwsAsync(() => throwError(), {instanceOf: TypeError}); + 25: }); Function threw synchronously. Use `t.throws()` instead: @@ -320,18 +320,18 @@ } › throwError (test-tap/fixture/report/regular/traces-in-t-throws.cjs:4:8) - › t.throwsAsync.instanceOf (test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:22) - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:4 + › t.throwsAsync.instanceOf (test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:28) + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:24:10 traces-in-t-throws › throwsAsync different error - traces-in-t-throws.cjs:27 + traces-in-t-throws.cjs:28 - 26: -  27: test('throwsAsync different error', t => t.throwsAsync(returnRejectedPromise, {instanceOf: TypeError})); - 28: + 27: test('throwsAsync different error', async t => { +  28: await t.throwsAsync(returnRejectedPromise, {instanceOf: TypeError}); + 29: }); Returned promise rejected with unexpected exception: @@ -344,7 +344,7 @@ Function TypeError {} › returnRejectedPromise (test-tap/fixture/report/regular/traces-in-t-throws.cjs:8:24) - › test-tap/fixture/report/regular/traces-in-t-throws.cjs:27:44 + › test-tap/fixture/report/regular/traces-in-t-throws.cjs:28:10 ─ diff --git a/test-tap/reporters/tap.failfast.v16.log b/test-tap/reporters/tap.failfast.v16.log index f709d4a0e..890fcfe73 100644 --- a/test-tap/reporters/tap.failfast.v16.log +++ b/test-tap/reporters/tap.failfast.v16.log @@ -5,7 +5,7 @@ not ok 1 - a › fails name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator diff --git a/test-tap/reporters/tap.failfast.v18.log b/test-tap/reporters/tap.failfast.v18.log index f709d4a0e..890fcfe73 100644 --- a/test-tap/reporters/tap.failfast.v18.log +++ b/test-tap/reporters/tap.failfast.v18.log @@ -5,7 +5,7 @@ not ok 1 - a › fails name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator diff --git a/test-tap/reporters/tap.failfast.v20.log b/test-tap/reporters/tap.failfast.v20.log index f709d4a0e..890fcfe73 100644 --- a/test-tap/reporters/tap.failfast.v20.log +++ b/test-tap/reporters/tap.failfast.v20.log @@ -5,7 +5,7 @@ not ok 1 - a › fails name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator diff --git a/test-tap/reporters/tap.failfast2.v16.log b/test-tap/reporters/tap.failfast2.v16.log index ca639266e..d3212e69e 100644 --- a/test-tap/reporters/tap.failfast2.v16.log +++ b/test-tap/reporters/tap.failfast2.v16.log @@ -5,7 +5,7 @@ not ok 1 - a › fails name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator # 1 test remaining in a.cjs diff --git a/test-tap/reporters/tap.failfast2.v18.log b/test-tap/reporters/tap.failfast2.v18.log index ca639266e..d3212e69e 100644 --- a/test-tap/reporters/tap.failfast2.v18.log +++ b/test-tap/reporters/tap.failfast2.v18.log @@ -5,7 +5,7 @@ not ok 1 - a › fails name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator # 1 test remaining in a.cjs diff --git a/test-tap/reporters/tap.failfast2.v20.log b/test-tap/reporters/tap.failfast2.v20.log index ca639266e..d3212e69e 100644 --- a/test-tap/reporters/tap.failfast2.v20.log +++ b/test-tap/reporters/tap.failfast2.v20.log @@ -5,7 +5,7 @@ not ok 1 - a › fails name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator # 1 test remaining in a.cjs diff --git a/test-tap/reporters/tap.regular.v16.log b/test-tap/reporters/tap.regular.v16.log index b1ab5e641..bf22b1c7f 100644 --- a/test-tap/reporters/tap.regular.v16.log +++ b/test-tap/reporters/tap.regular.v16.log @@ -30,7 +30,7 @@ not ok 3 - nested-objects › format with max depth 4 + }, } message: '' - at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)' + at: 'ExecutionContext.deepEqual (/lib/assert.js:339:15)' ... ---tty-stream-chunk-separator not ok 4 - nested-objects › format like with max depth 4 @@ -48,7 +48,7 @@ not ok 4 - nested-objects › format like with max depth 4 }, } message: '' - at: 'ExecutionContext.like (/lib/assert.js:422:9)' + at: 'ExecutionContext.like (/lib/assert.js:391:15)' ... ---tty-stream-chunk-separator # output-in-hook › before hook @@ -72,7 +72,7 @@ not ok 6 - output-in-hook › failing test name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator # output-in-hook › afterEach hook for passing test @@ -102,7 +102,7 @@ not ok 10 - test › fails name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator ok 11 - test › known failure @@ -113,7 +113,7 @@ not ok 12 - test › no longer failing message: >- Test was expected to fail, but succeeded, you should stop marking the test as failing - at: 'Test.finish (/lib/test.js:609:28)' + at: 'Test.finish (/lib/test.js:633:28)' ... ---tty-stream-chunk-separator not ok 13 - test › logs @@ -123,7 +123,7 @@ not ok 13 - test › logs name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator not ok 14 - test › formatted @@ -135,7 +135,7 @@ not ok 14 - test › formatted - 'foo' + 'bar' message: '' - at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)' + at: 'ExecutionContext.deepEqual (/lib/assert.js:339:15)' ... ---tty-stream-chunk-separator not ok 15 - test › implementation throws non-error @@ -144,7 +144,7 @@ not ok 15 - test › implementation throws non-error details: 'Error thrown in test:': 'null' message: Error thrown in test - at: 'Test.run (/lib/test.js:526:25)' + at: 'Test.run (/lib/test.js:546:25)' ... ---tty-stream-chunk-separator not ok 16 - traces-in-t-throws › throws diff --git a/test-tap/reporters/tap.regular.v18.log b/test-tap/reporters/tap.regular.v18.log index b1ab5e641..bf22b1c7f 100644 --- a/test-tap/reporters/tap.regular.v18.log +++ b/test-tap/reporters/tap.regular.v18.log @@ -30,7 +30,7 @@ not ok 3 - nested-objects › format with max depth 4 + }, } message: '' - at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)' + at: 'ExecutionContext.deepEqual (/lib/assert.js:339:15)' ... ---tty-stream-chunk-separator not ok 4 - nested-objects › format like with max depth 4 @@ -48,7 +48,7 @@ not ok 4 - nested-objects › format like with max depth 4 }, } message: '' - at: 'ExecutionContext.like (/lib/assert.js:422:9)' + at: 'ExecutionContext.like (/lib/assert.js:391:15)' ... ---tty-stream-chunk-separator # output-in-hook › before hook @@ -72,7 +72,7 @@ not ok 6 - output-in-hook › failing test name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator # output-in-hook › afterEach hook for passing test @@ -102,7 +102,7 @@ not ok 10 - test › fails name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator ok 11 - test › known failure @@ -113,7 +113,7 @@ not ok 12 - test › no longer failing message: >- Test was expected to fail, but succeeded, you should stop marking the test as failing - at: 'Test.finish (/lib/test.js:609:28)' + at: 'Test.finish (/lib/test.js:633:28)' ... ---tty-stream-chunk-separator not ok 13 - test › logs @@ -123,7 +123,7 @@ not ok 13 - test › logs name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator not ok 14 - test › formatted @@ -135,7 +135,7 @@ not ok 14 - test › formatted - 'foo' + 'bar' message: '' - at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)' + at: 'ExecutionContext.deepEqual (/lib/assert.js:339:15)' ... ---tty-stream-chunk-separator not ok 15 - test › implementation throws non-error @@ -144,7 +144,7 @@ not ok 15 - test › implementation throws non-error details: 'Error thrown in test:': 'null' message: Error thrown in test - at: 'Test.run (/lib/test.js:526:25)' + at: 'Test.run (/lib/test.js:546:25)' ... ---tty-stream-chunk-separator not ok 16 - traces-in-t-throws › throws diff --git a/test-tap/reporters/tap.regular.v20.log b/test-tap/reporters/tap.regular.v20.log index b1ab5e641..bf22b1c7f 100644 --- a/test-tap/reporters/tap.regular.v20.log +++ b/test-tap/reporters/tap.regular.v20.log @@ -30,7 +30,7 @@ not ok 3 - nested-objects › format with max depth 4 + }, } message: '' - at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)' + at: 'ExecutionContext.deepEqual (/lib/assert.js:339:15)' ... ---tty-stream-chunk-separator not ok 4 - nested-objects › format like with max depth 4 @@ -48,7 +48,7 @@ not ok 4 - nested-objects › format like with max depth 4 }, } message: '' - at: 'ExecutionContext.like (/lib/assert.js:422:9)' + at: 'ExecutionContext.like (/lib/assert.js:391:15)' ... ---tty-stream-chunk-separator # output-in-hook › before hook @@ -72,7 +72,7 @@ not ok 6 - output-in-hook › failing test name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator # output-in-hook › afterEach hook for passing test @@ -102,7 +102,7 @@ not ok 10 - test › fails name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator ok 11 - test › known failure @@ -113,7 +113,7 @@ not ok 12 - test › no longer failing message: >- Test was expected to fail, but succeeded, you should stop marking the test as failing - at: 'Test.finish (/lib/test.js:609:28)' + at: 'Test.finish (/lib/test.js:633:28)' ... ---tty-stream-chunk-separator not ok 13 - test › logs @@ -123,7 +123,7 @@ not ok 13 - test › logs name: AssertionError assertion: t.fail() message: Test failed via `t.fail()` - at: 'ExecutionContext.fail (/lib/assert.js:294:9)' + at: 'ExecutionContext.fail (/lib/assert.js:287:15)' ... ---tty-stream-chunk-separator not ok 14 - test › formatted @@ -135,7 +135,7 @@ not ok 14 - test › formatted - 'foo' + 'bar' message: '' - at: 'ExecutionContext.deepEqual (/lib/assert.js:360:9)' + at: 'ExecutionContext.deepEqual (/lib/assert.js:339:15)' ... ---tty-stream-chunk-separator not ok 15 - test › implementation throws non-error @@ -144,7 +144,7 @@ not ok 15 - test › implementation throws non-error details: 'Error thrown in test:': 'null' message: Error thrown in test - at: 'Test.run (/lib/test.js:526:25)' + at: 'Test.run (/lib/test.js:546:25)' ... ---tty-stream-chunk-separator not ok 16 - traces-in-t-throws › throws diff --git a/test-tap/test.js b/test-tap/test.js index e76d21498..17f4bc19e 100644 --- a/test-tap/test.js +++ b/test-tap/test.js @@ -359,7 +359,10 @@ test('fails if test ends while there are pending assertions', t => ava(a => { })); test('fails if async test ends while there are pending assertions', t => ava(a => { - a.throwsAsync(Promise.reject(new Error())); + a.throwsAsync(async () => { + await delay(100); + throw new Error(); + }); return Promise.resolve(); }).run().then(result => { t.equal(result.passed, false); @@ -367,33 +370,6 @@ test('fails if async test ends while there are pending assertions', t => ava(a = t.match(result.error.message, /Test finished, but an assertion is still pending/); })); -// This behavior is incorrect, but feedback cannot be provided to the user due to -// https://github.com/avajs/ava/issues/1330 -test('no crash when adding assertions after the test has ended', t => { - t.plan(3); - - ava(a => { - a.pass(); - setImmediate(() => { - t.doesNotThrow(() => a.pass()); - }); - }).run(); - - ava(a => { - a.pass(); - setImmediate(() => { - t.doesNotThrow(() => a.fail()); - }); - }).run(); - - ava(a => { - a.pass(); - setImmediate(() => { - t.doesNotThrow(() => a.notThrowsAsync(Promise.resolve())); - }); - }).run(); -}); - test('contextRef', t => { new Test({ contextRef: { diff --git a/test-types/import-in-cts/assertions-as-type-guards.cts b/test-types/import-in-cts/assertions-as-type-guards.cts index 733e11bec..fd9a8615c 100644 --- a/test-types/import-in-cts/assertions-as-type-guards.cts +++ b/test-types/import-in-cts/assertions-as-type-guards.cts @@ -8,8 +8,6 @@ test('assert', t => { const actual = expected as Expected | undefined; if (t.assert(actual)) { expectType(actual); - } else { - expectType(actual); } }); @@ -46,8 +44,6 @@ test('falsy', t => { const actual = undefined as Actual; if (t.falsy(actual)) { expectType>(actual); - } else { - expectType(actual); } }); @@ -62,7 +58,5 @@ test('truthy', t => { const actual = expected as Expected | undefined; if (t.truthy(actual)) { expectType(actual); - } else { - expectType(actual); } }); diff --git a/test-types/import-in-cts/throws.cts b/test-types/import-in-cts/throws.cts index 606c1cc15..3cc8623cc 100644 --- a/test-types/import-in-cts/throws.cts +++ b/test-types/import-in-cts/throws.cts @@ -13,16 +13,16 @@ class CustomError extends Error { test('throws', t => { const error1 = t.throws(() => {}); - expectType(error1); - const error2: CustomError | undefined = t.throws(() => {}); - expectType(error2); - expectType(t.throws(() => {})); + expectType(error1); + const error2: CustomError = t.throws(() => {}); + expectType(error2); + expectType(t.throws(() => {})); const error3 = t.throws(() => {}, {instanceOf: CustomError}); - expectType(error3); + expectType(error3); const error4 = t.throws(() => {}, {is: new CustomError()}); - expectType(error4); + expectType(error4); const error5 = t.throws(() => {}, {instanceOf: CustomError, is: new CustomError()}); - expectType(error5); + expectType(error5); const error6 = t.throws(() => { throw 'foo' }, {any: true}); expectType(error6); // @ts-expect-error TS2769 @@ -31,17 +31,17 @@ test('throws', t => { test('throwsAsync', async t => { const error1 = await t.throwsAsync(async () => {}); - expectType(error1); - expectType(await t.throwsAsync(async () => {})); + expectType(error1); + expectType(await t.throwsAsync(async () => {})); const error2 = await t.throwsAsync(Promise.reject()); - expectType(error2); - expectType(await t.throwsAsync(Promise.reject())); + expectType(error2); + expectType(await t.throwsAsync(Promise.reject())); const error3 = await t.throwsAsync(async () => {}, {instanceOf: CustomError}); - expectType(error3); + expectType(error3); const error4 = await t.throwsAsync(async () => {}, {is: new CustomError()}); - expectType(error4); + expectType(error4); const error5 = await t.throwsAsync(async () => {}, {instanceOf: CustomError, is: new CustomError()}); - expectType(error5); + expectType(error5); const error6 = await t.throwsAsync(async () => { throw 'foo' }, {any: true}); expectType(error6); // @ts-expect-error TS2769 diff --git a/test-types/module/assertions-as-type-guards.ts b/test-types/module/assertions-as-type-guards.ts index 190c79111..15b718d79 100644 --- a/test-types/module/assertions-as-type-guards.ts +++ b/test-types/module/assertions-as-type-guards.ts @@ -9,8 +9,6 @@ test('assert', t => { const actual = expected as Expected | undefined; if (t.assert(actual)) { expectType(actual); - } else { - expectType(actual); } }); @@ -47,8 +45,6 @@ test('falsy', t => { const actual = undefined as Actual; if (t.falsy(actual)) { expectType>(actual); - } else { - expectType(actual); } }); @@ -63,7 +59,5 @@ test('truthy', t => { const actual = expected as Expected | undefined; if (t.truthy(actual)) { expectType(actual); - } else { - expectType(actual); } }); diff --git a/test-types/module/snapshot.ts b/test-types/module/snapshot.ts index 08b1b155a..0e678d51c 100644 --- a/test-types/module/snapshot.ts +++ b/test-types/module/snapshot.ts @@ -6,7 +6,7 @@ test('snapshot', t => { t.snapshot({foo: 'bar'}); t.snapshot(null, 'a snapshot with a message'); // @ts-expect-error TS2345 - expectError(t.snapshot('hello world', null)); // eslint-disable-line @typescript-eslint/no-confusing-void-expression + expectError(t.snapshot('hello world', null)); }); test('snapshot.skip', t => { diff --git a/test-types/module/throws.ts b/test-types/module/throws.ts index 79f3aaaa7..dd3954e46 100644 --- a/test-types/module/throws.ts +++ b/test-types/module/throws.ts @@ -14,16 +14,16 @@ class CustomError extends Error { test('throws', t => { const error1 = t.throws(() => {}); - expectType(error1); - const error2: CustomError | undefined = t.throws(() => {}); - expectType(error2); - expectType(t.throws(() => {})); + expectType(error1); + const error2: CustomError = t.throws(() => {}); + expectType(error2); + expectType(t.throws(() => {})); const error3 = t.throws(() => {}, {instanceOf: CustomError}); - expectType(error3); + expectType(error3); const error4 = t.throws(() => {}, {is: new CustomError()}); - expectType(error4); + expectType(error4); const error5 = t.throws(() => {}, {instanceOf: CustomError, is: new CustomError()}); - expectType(error5); + expectType(error5); const error6 = t.throws(() => { throw 'foo'; // eslint-disable-line @typescript-eslint/no-throw-literal }, {any: true}); @@ -36,17 +36,17 @@ test('throws', t => { test('throwsAsync', async t => { const error1 = await t.throwsAsync(async () => {}); - expectType(error1); - expectType(await t.throwsAsync(async () => {})); + expectType(error1); + expectType(await t.throwsAsync(async () => {})); const error2 = await t.throwsAsync(Promise.reject()); - expectType(error2); - expectType(await t.throwsAsync(Promise.reject())); + expectType(error2); + expectType(await t.throwsAsync(Promise.reject())); const error3 = await t.throwsAsync(async () => {}, {instanceOf: CustomError}); - expectType(error3); + expectType(error3); const error4 = await t.throwsAsync(async () => {}, {is: new CustomError()}); - expectType(error4); + expectType(error4); const error5 = await t.throwsAsync(async () => {}, {instanceOf: CustomError, is: new CustomError()}); - expectType(error5); + expectType(error5); const error6 = await t.throwsAsync(async () => { throw 'foo'; // eslint-disable-line @typescript-eslint/no-throw-literal }, {any: true}); diff --git a/types/assertions.d.cts b/types/assertions.d.cts index 8d7e9a510..38e7b4f06 100644 --- a/types/assertions.d.cts +++ b/types/assertions.d.cts @@ -148,10 +148,11 @@ type Falsy = T extends Exclude ? (T extends number | string | export type AssertAssertion = { /** - * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean - * indicating whether the assertion passed. + * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning `true` if the + * assertion passed and throwing otherwise. * - * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause. + * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will + * not give `0` or `''` as a potential value in an `else` clause. */ (actual: T, message?: string): actual is T extends Falsy ? never : T; @@ -162,19 +163,19 @@ export type AssertAssertion = { export type DeepEqualAssertion = { /** * Assert that `actual` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to - * `expected`, returning a boolean indicating whether the assertion passed. + * `expected`, returning `true` if the assertion passed and throwing otherwise. */ (actual: Actual, expected: Expected, message?: string): actual is Expected; /** * Assert that `actual` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to - * `expected`, returning a boolean indicating whether the assertion passed. + * `expected`, returning `true` if the assertion passed and throwing otherwise. */ (actual: Actual, expected: Expected, message?: string): expected is Actual; /** * Assert that `actual` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to - * `expected`, returning a boolean indicating whether the assertion passed. + * `expected`, returning `true` if the assertion passed and throwing otherwise. */ (actual: Actual, expected: Expected, message?: string): boolean; @@ -184,7 +185,7 @@ export type DeepEqualAssertion = { export type LikeAssertion = { /** - * Assert that `value` is like `selector`, returning a boolean indicating whether the assertion passed. + * Assert that `value` is like `selector`, returning `true` if the assertion passed and throwing otherwise. */ >(value: any, selector: Expected, message?: string): value is Expected; @@ -193,8 +194,8 @@ export type LikeAssertion = { }; export type FailAssertion = { - /** Fail the test, always returning `false`. */ - (message?: string): boolean; + /** Fail the test. */ + (message?: string): never; /** Skip this assertion. */ skip(message?: string): void; @@ -202,7 +203,7 @@ export type FailAssertion = { export type FalseAssertion = { /** - * Assert that `actual` is strictly false, returning a boolean indicating whether the assertion passed. + * Assert that `actual` is strictly false, returning `true` if the assertion passed and throwing otherwise. */ (actual: any, message?: string): actual is false; @@ -212,8 +213,8 @@ export type FalseAssertion = { export type FalsyAssertion = { /** - * Assert that `actual` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), returning a boolean - * indicating whether the assertion passed. + * Assert that `actual` is [falsy](https://developer.mozilla.org/en-US/docs/Glossary/Falsy), returning `true` if the + * assertion passed and throwing otherwise. */ (actual: T, message?: string): actual is Falsy; @@ -225,7 +226,7 @@ export type IsAssertion = { /** * Assert that `actual` is [the same * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, - * returning a boolean indicating whether the assertion passed. + * returning `true` if the assertion passed and throwing otherwise. */ (actual: Actual, expected: Expected, message?: string): actual is Expected; @@ -237,9 +238,9 @@ export type NotAssertion = { /** * Assert that `actual` is not [the same * value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) as `expected`, - * returning a boolean indicating whether the assertion passed. + * returning `true` if the assertion passed and throwing otherwise. */ - (actual: Actual, expected: Expected, message?: string): boolean; + (actual: Actual, expected: Expected, message?: string): true; /** Skip this assertion. */ skip(actual: any, expected: any, message?: string): void; @@ -248,9 +249,9 @@ export type NotAssertion = { export type NotDeepEqualAssertion = { /** * Assert that `actual` is not [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to - * `expected`, returning a boolean indicating whether the assertion passed. + * `expected`, returning `true` if the assertion passed and throwing otherwise. */ - (actual: Actual, expected: Expected, message?: string): boolean; + (actual: Actual, expected: Expected, message?: string): true; /** Skip this assertion. */ skip(actual: any, expected: any, message?: string): void; @@ -258,29 +259,40 @@ export type NotDeepEqualAssertion = { export type NotRegexAssertion = { /** - * Assert that `string` does not match the regular expression, returning a boolean indicating whether the assertion - * passed. + * Assert that `string` does not match the regular expression, returning `true` if the assertion passed and throwing + * otherwise. */ - (string: string, regex: RegExp, message?: string): boolean; + (string: string, regex: RegExp, message?: string): true; /** Skip this assertion. */ skip(string: string, regex: RegExp, message?: string): void; }; export type NotThrowsAssertion = { - /** Assert that the function does not throw. */ - (fn: () => any, message?: string): void; + /** + * Assert that the function does not throw, returning `true` if the assertion passed and throwing otherwise. + */ + (fn: () => any, message?: string): true; /** Skip this assertion. */ skip(fn: () => any, message?: string): void; }; export type NotThrowsAsyncAssertion = { - /** Assert that the async function does not throw. You must await the result. */ - (fn: () => PromiseLike, message?: string): Promise; + /** + * Assert that the async function does not throw, returning a promise for `true` if the assertion passesd and a + * rejected promise otherwise. + * + * You must await the result. + */ + (fn: () => PromiseLike, message?: string): Promise; - /** Assert that the promise does not reject. You must await the result. */ - (promise: PromiseLike, message?: string): Promise; + /** Assert that the promise does not reject, returning a promise for `true` if the assertion passesd and a + * rejected promise otherwise. + * + * You must await the result. + */ + (promise: PromiseLike, message?: string): Promise; /** Skip this assertion. */ skip(nonThrower: any, message?: string): void; @@ -288,7 +300,7 @@ export type NotThrowsAsyncAssertion = { export type PassAssertion = { /** Count a passing assertion, always returning `true`. */ - (message?: string): boolean; + (message?: string): true; /** Skip this assertion. */ skip(message?: string): void; @@ -296,9 +308,10 @@ export type PassAssertion = { export type RegexAssertion = { /** - * Assert that `string` matches the regular expression, returning a boolean indicating whether the assertion passed. + * Assert that `string` matches the regular expression, returning `true` if the assertion passed and throwing + * otherwise. */ - (string: string, regex: RegExp, message?: string): boolean; + (string: string, regex: RegExp, message?: string): true; /** Skip this assertion. */ skip(string: string, regex: RegExp, message?: string): void; @@ -309,8 +322,10 @@ export type SnapshotAssertion = { * Assert that `expected` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to a * previously recorded [snapshot](https://github.com/concordancejs/concordance#serialization-details), or if * necessary record a new snapshot. + * + * Returns `true` if the assertion passed and throws otherwise. */ - (expected: any, message?: string): void; + (expected: any, message?: string): true; /** Skip this assertion. */ skip(expected: any, message?: string): void; @@ -318,14 +333,14 @@ export type SnapshotAssertion = { export type ThrowsAssertion = { /** - * Assert that the function throws a native error. If so, returns the error value. - * The error must satisfy all expectations. Returns undefined when the assertion fails. + * Assert that the function throws a native error. The error must satisfy all expectations. Returns the error value if + * the assertion passes and throws otherwise. */ - (fn: () => any, expectations?: ThrowsExpectation, message?: string): ThrownError | undefined; + (fn: () => any, expectations?: ThrowsExpectation, message?: string): ThrownError; /** - * Assert that the function throws. If so, returns the error value. - * The error must satisfy all expectations. Returns undefined when the assertion fails. + * Assert that the function throws. The error must satisfy all expectations. Returns the error value if the assertion + * passes and throws otherwise. */ (fn: () => any, expectations?: ThrowsAnyExpectation, message?: string): unknown; @@ -335,30 +350,26 @@ export type ThrowsAssertion = { export type ThrowsAsyncAssertion = { /** - * Assert that the async function throws a native error. If so, returns the - * error value. Returns undefined when the assertion fails. You must await the - * result. The error must satisfy all expectations. + * Assert that the async function throws a native error. You must await the result. The error must satisfy all + * expectations. Returns a promise for the error value if the assertion passes and a rejected promise otherwise. */ - (fn: () => PromiseLike, expectations?: ThrowsExpectation, message?: string): Promise | undefined>; + (fn: () => PromiseLike, expectations?: ThrowsExpectation, message?: string): Promise>; /** - * Assert that the promise rejects with a native error. If so, returns the - * rejection reason. Returns undefined when the assertion fails. You must - * await the result. The error must satisfy all expectations. + * Assert that the promise rejects with a native error. You must await the result. The error must satisfy all + * expectations. Returns a promise for the error value if the assertion passes and a rejected promise otherwise. */ - (promise: PromiseLike, expectations?: ThrowsExpectation, message?: string): Promise | undefined>; + (promise: PromiseLike, expectations?: ThrowsExpectation, message?: string): Promise>; /** - * Assert that the async function throws. If so, returns the error value. - * Returns undefined when the assertion fails. You must await the result. The - * error must satisfy all expectations. + * Assert that the async function throws. You must await the result. The error must satisfy all expectations. Returns + * a promise for the error value if the assertion passes and a rejected promise otherwise. */ (fn: () => PromiseLike, expectations?: ThrowsAnyExpectation, message?: string): Promise; /** - * Assert that the promise rejects. If so, returns the rejection reason. - * Returns undefined when the assertion fails. You must await the result. The - * error must satisfy all expectations. + * Assert that the promise rejects. You must await the result. The error must satisfy all expectations. Returns a + * promise for the error value if the assertion passes and a rejected promise otherwise. */ (promise: PromiseLike, expectations?: ThrowsAnyExpectation, message?: string): Promise; @@ -368,7 +379,7 @@ export type ThrowsAsyncAssertion = { export type TrueAssertion = { /** - * Assert that `actual` is strictly true, returning a boolean indicating whether the assertion passed. + * Assert that `actual` is strictly true, returning `true` if the assertion passed and throwing otherwise. */ (actual: any, message?: string): actual is true; @@ -378,10 +389,11 @@ export type TrueAssertion = { export type TruthyAssertion = { /** - * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning a boolean - * indicating whether the assertion passed. + * Assert that `actual` is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy), returning `true` if the + * assertion passed and throwing otherwise. * - * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will not give `0` or `''` as a potential value in an `else` clause. + * Note: An `else` clause using this as a type guard will be subtly incorrect for `string` and `number` types and will + * not give `0` or `''` as a potential value in an `else` clause. */ (actual: T, message?: string): actual is T extends Falsy ? never : T;