@augment-vir/assert
Version:
A collection of assertions for test and production code alike.
417 lines (416 loc) • 16.2 kB
JavaScript
import { ensureError, extractErrorMessage, stringify, } from '@augment-vir/core';
import { AssertionError } from '../augments/assertion.error.js';
import { createWaitUntil } from '../guard-types/wait-until-function.js';
var ThrowsCheckType;
(function (ThrowsCheckType) {
ThrowsCheckType["Assert"] = "assert";
ThrowsCheckType["AssertWrap"] = "assert-wrap";
ThrowsCheckType["CheckWrap"] = "check-wrap";
ThrowsCheckType["Check"] = "check";
})(ThrowsCheckType || (ThrowsCheckType = {}));
function isError(actual, matchOptions, failureMessage) {
internalAssertError(actual, {
noError: 'No error.',
notInstance: `'${stringify(actual)}' is not an error instance.`,
}, matchOptions, failureMessage);
}
function assertThrownError(actual, matchOptions, failureMessage) {
internalAssertError(actual, {
noError: 'No Error was thrown.',
notInstance: `Thrown value '${stringify(actual)}' is not an error instance.`,
}, matchOptions, failureMessage);
}
function internalAssertError(actual, errorMessages, matchOptions, failureMessage) {
if (!actual) {
throw new AssertionError(errorMessages.noError, failureMessage);
}
else if (!(actual instanceof Error)) {
throw new AssertionError(errorMessages.notInstance, failureMessage);
}
else if (matchOptions?.matchConstructor &&
!(actual instanceof matchOptions.matchConstructor)) {
const constructorName = actual.constructor.name;
throw new AssertionError(`Error constructor '${constructorName}' did not match expected constructor '${matchOptions.matchConstructor.name}'.`, failureMessage);
}
else if (matchOptions?.matchMessage) {
const message = extractErrorMessage(actual);
if (typeof matchOptions.matchMessage === 'string') {
if (!message.includes(matchOptions.matchMessage)) {
throw new AssertionError(`Error message\n\n'${message}'\n\ndoes not contain\n\n'${matchOptions.matchMessage}'.`, failureMessage);
}
}
else if (!message.match(matchOptions.matchMessage)) {
throw new AssertionError(`Error message\n\n'${message}'\n\ndoes not match RegExp\n\n'${matchOptions.matchMessage}'.`, failureMessage);
}
}
}
function internalCheckError(actual, matchOptions) {
if (!actual) {
return false;
}
else if (!(actual instanceof Error)) {
return false;
}
else if (matchOptions?.matchConstructor &&
!(actual instanceof matchOptions.matchConstructor)) {
return false;
}
else if (matchOptions?.matchMessage) {
const message = extractErrorMessage(actual);
if (typeof matchOptions.matchMessage === 'string') {
if (!message.includes(matchOptions.matchMessage)) {
return false;
}
}
else if (!message.match(matchOptions.matchMessage)) {
return false;
}
}
return true;
}
function internalThrowsCheck(checkType, callbackOrPromise, matchOptions, failureMessage) {
let caughtError = undefined;
try {
const result = callbackOrPromise instanceof Promise ? callbackOrPromise : callbackOrPromise();
if (result instanceof Promise) {
return new Promise(async (resolve, reject) => {
try {
await result;
}
catch (error) {
caughtError = ensureError(error);
}
try {
assertThrownError(caughtError, matchOptions, failureMessage);
if (checkType === ThrowsCheckType.Assert) {
resolve();
}
else if (checkType === ThrowsCheckType.Check) {
resolve(true);
}
else {
resolve(caughtError);
}
}
catch (error) {
if (checkType === ThrowsCheckType.CheckWrap) {
resolve(undefined);
}
else if (checkType === ThrowsCheckType.Check) {
resolve(false);
}
else {
reject(ensureError(error));
}
}
});
}
}
catch (error) {
caughtError = ensureError(error);
}
try {
assertThrownError(caughtError, matchOptions, failureMessage);
if (checkType === ThrowsCheckType.Check) {
return true;
}
else if (checkType !== ThrowsCheckType.Assert) {
return caughtError;
}
return;
}
catch (error) {
if (checkType === ThrowsCheckType.CheckWrap) {
return undefined;
}
else if (checkType === ThrowsCheckType.Check) {
return false;
}
else {
throw error;
}
}
}
function throws(callbackOrPromise, matchOptions, failureMessage) {
return internalThrowsCheck(ThrowsCheckType.Assert, callbackOrPromise, matchOptions, failureMessage);
}
function throwsCheck(callbackOrPromise, matchOptions) {
return internalThrowsCheck(ThrowsCheckType.Check, callbackOrPromise, matchOptions);
}
function throwsAssertWrap(callbackOrPromise, matchOptions, failureMessage) {
return internalThrowsCheck(ThrowsCheckType.AssertWrap, callbackOrPromise, matchOptions, failureMessage);
}
function throwsCheckWrap(callbackOrPromise, matchOptions, failureMessage) {
return internalThrowsCheck(ThrowsCheckType.CheckWrap, callbackOrPromise, matchOptions, failureMessage);
}
const internalWaitUntilThrows = createWaitUntil(isError);
function throwsWaitUntil(matchOptionsOrCallback, callbackOrOptions, optionsOrFailureMessage, failureMessage) {
const matchOptions = typeof matchOptionsOrCallback === 'function' || matchOptionsOrCallback instanceof Promise
? undefined
: matchOptionsOrCallback;
const callback = (matchOptions ? callbackOrOptions : matchOptionsOrCallback);
const actualFailureMessage = typeof optionsOrFailureMessage === 'object' ? failureMessage : optionsOrFailureMessage;
const waitUntilOptions = typeof optionsOrFailureMessage === 'object'
? optionsOrFailureMessage
: callbackOrOptions;
if (typeof callback !== 'function') {
throw new TypeError(`Callback is not a function, got '${stringify(callback)}'`);
}
return internalWaitUntilThrows(matchOptions, async () => {
try {
await callback();
return undefined;
}
catch (error) {
return ensureError(error);
}
}, waitUntilOptions, actualFailureMessage);
}
const assertions = {
throws,
isError: isError,
};
export const throwGuards = {
assert: assertions,
check: {
/**
* If a function input is provided:
*
* Calls that function and checks that the function throw an error, comparing the error with
* the given {@link ErrorMatchOptions}, if provided.
*
* If a promise is provided:
*
* Awaits the promise and checks that the promise rejected with an error, comparing the
* error with the given {@link ErrorMatchOptions}, if provided.
*
* This assertion will automatically type itself as async vs async based on the input. (A
* promise or async function inputs results in async. Otherwise, sync.)
*
* Performs no type guarding.
*
* @example
*
* ```ts
* import {check} from '@augment-vir/assert';
*
* check.throws(() => {
* throw new Error();
* }); // returns `true`
* check.throws(
* () => {
* throw new Error();
* },
* {matchMessage: 'hi'},
* ); // returns `false`
* await check.throws(Promise.reject()); // returns `true`
* check.throws(() => {}); // returns `false`
* ```
*/
throws: throwsCheck,
/**
* Checks that a value is an instance of the built-in `Error` class and compares it to the
* given {@link ErrorMatchOptions}, if provided.
*
* Type guards the input.
*
* @example
*
* ```ts
* import {check} from '@augment-vir/assert';
*
* check.isError(new Error()); // returns `true`
* check.isError(new Error(), {matchMessage: 'hi'}); // returns `false`
* check.isError({message: 'not an error'}); // returns `false`
* ```
*/
isError(actual, matchOptions) {
return internalCheckError(actual, matchOptions);
},
},
assertWrap: {
/**
* If a function input is provided:
*
* Calls that function and asserts that the function throw an error, comparing the error
* with the given {@link ErrorMatchOptions}, if provided. Returns the Error if the assertion
* passes.
*
* If a promise is provided:
*
* Awaits the promise and asserts that the promise rejected with an error, comparing the
* error with the given {@link ErrorMatchOptions}, if provided. Returns the Error if the
* assertion passes.
*
* This assertion will automatically type itself as async vs async based on the input. (A
* promise or async function inputs results in async. Otherwise, sync.)
*
* Performs no type guarding.
*
* @example
*
* ```ts
* import {assertWrap} from '@augment-vir/assert';
*
* assertWrap.throws(() => {
* throw new Error();
* }); // returns the thrown error
* assertWrap.throws(
* () => {
* throw new Error();
* },
* {matchMessage: 'hi'},
* ); // throws an error
* await assertWrap.throws(Promise.reject()); // returns the rejection
* assertWrap.throws(() => {}); // throws an error
* ```
*
* @returns The Error if the assertion passes.
* @throws {@link AssertionError} If the assertion fails.
*/
throws: throwsAssertWrap,
/**
* Asserts that a value is an instance of the built-in `Error` class and compares it to the
* given {@link ErrorMatchOptions}, if provided.
*
* Type guards the input.
*
* @example
*
* ```ts
* import {assertWrap} from '@augment-vir/assert';
*
* assertWrap.isError(new Error()); // returns the error instance
* assertWrap.isError(new Error(), {matchMessage: 'hi'}); // throws an error
* assertWrap.isError({message: 'not an error'}); // throws an error
* ```
*
* @returns The value if the assertion passes.
* @throws {@link AssertionError} If the assertion fails.
*/
isError(actual, matchOptions, failureMessage) {
internalAssertError(actual, {
noError: 'No error.',
notInstance: `'${stringify(actual)}' is not an error instance.`,
}, matchOptions, failureMessage);
return actual;
},
},
checkWrap: {
/**
* If a function input is provided:
*
* Calls that function and checks that the function throw an error, comparing the error with
* the given {@link ErrorMatchOptions}, if provided. Returns the error if the check passes,
* otherwise `undefined`.
*
* If a promise is provided:
*
* Awaits the promise and checks that the promise rejected with an error, comparing the
* error with the given {@link ErrorMatchOptions}, if provided. Returns the error if the
* check passes, otherwise `undefined`.
*
* This assertion will automatically type itself as async vs async based on the input. (A
* promise or async function inputs results in async. Otherwise, sync.)
*
* Performs no type guarding.
*
* @example
*
* ```ts
* import {checkWrap} from '@augment-vir/assert';
*
* checkWrap.throws(() => {
* throw new Error();
* }); // returns the thrown error
* await checkWrap.throws(Promise.reject()); // returns the rejection
* checkWrap.throws(() => {}); // returns `undefined`
* ```
*
* @returns The Error if the check passes, otherwise `undefined`.
*/
throws: throwsCheckWrap,
/**
* Checks that a value is an instance of the built-in `Error` class and compares it to the
* given {@link ErrorMatchOptions}, if provided. Returns the error if the check passes,
* otherwise `undefined`.
*
* Type guards the input.
*
* @example
*
* ```ts
* import {checkWrap} from '@augment-vir/assert';
*
* checkWrap.isError(new Error()); // returns the Error
* checkWrap.isError(new Error(), {matchMessage: 'hi'}); // returns `undefined`
* checkWrap.isError({message: 'not an error'}); // returns `undefined`
* ```
*
* @returns The Error if the check passes, otherwise `undefined`.
*/
isError(actual, matchOptions) {
if (internalCheckError(actual, matchOptions)) {
return actual;
}
else {
return undefined;
}
},
},
waitUntil: {
/**
* Repeatedly calls a callback until it throws an error, comparing the error with the given
* {@link ErrorMatchOptions}, if provided (as the first input). Once the callback throws an
* Error, that Error is returned. If the attempts time out, an error is thrown.
*
* This assertion will automatically type itself as async vs async based on the input. (A
* promise or async function inputs results in async. Otherwise, sync.)
*
* Unlike the other `.throws` guards, `waitUntil.throws` does not allow a Promise input,
* only a callback input.
*
* Performs no type guarding.
*
* @example
*
* ```ts
* import {waitUntil} from '@augment-vir/assert';
*
* await waitUntil.throws(() => {
* throw new Error();
* }); // returns the thrown error
* await waitUntil.throws(Promise.reject()); // not allowed
* await waitUntil.throws(() => {}); // throws an error
* await waitUntil.throws({matchMessage: 'hi'}, () => {
* throw new Error('bye');
* }); // throws an error
* ```
*
* @returns The Error once it passes.
* @throws {@link AssertionError} On timeout.
*/
throws: throwsWaitUntil,
/**
* Repeatedly calls a callback until is output is an instance of the built-in `Error` class
* and compares it to the given {@link ErrorMatchOptions}, if provided. Once the callback
* output passes, that Error is returned. If the attempts time out, an error is thrown.
*
* Type guards the input.
*
* @example
*
* ```ts
* import {waitUntil} from '@augment-vir/assert';
*
* await waitUntil.isError(new Error()); // returns the error instance
* await waitUntil.isError(new Error(), {matchMessage: 'hi'}); // throws an error
* await waitUntil.isError({message: 'not an error'}); // throws an error
* ```
*
* @returns The callback output once it passes.
* @throws {@link AssertionError} On timeout.
*/
isError: createWaitUntil(isError),
},
};