UNPKG

@augment-vir/assert

Version:

A collection of assertions for test and production code alike.

417 lines (416 loc) 16.2 kB
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), }, };