UNPKG

@augment-vir/assert

Version:

A collection of assertions for test and production code alike.

261 lines (260 loc) 11.2 kB
import { combineErrorMessages, ensureError, ensureErrorAndPrependMessage, extractErrorMessage, wait, } from '@augment-vir/core'; import { convertDuration } from '@date-vir/duration'; import { AssertionError } from '../augments/assertion.error.js'; import { parseWaitUntilOptions } from '../guard-types/wait-until-function.js'; import { deepEquals } from './equality/simple-equality.js'; function assertOutput(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, failureMessageOrExpectedOutput, emptyOrFailureMessage) { return innerAssertOutput(...extractOutputArgs(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, failureMessageOrExpectedOutput, emptyOrFailureMessage), false); } function extractOutputArgs(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, failureMessageOrExpectedOutput, emptyOrFailureMessage) { const usingCustomAsserter = Array.isArray(expectedOutputOrInputs); const asserter = usingCustomAsserter ? functionToCallOrAsserter : deepEquals; const functionToCall = usingCustomAsserter ? inputsOrFunctionToCall : functionToCallOrAsserter; const inputs = usingCustomAsserter ? expectedOutputOrInputs : inputsOrFunctionToCall; const expectedOutput = usingCustomAsserter ? failureMessageOrExpectedOutput : expectedOutputOrInputs; const failureMessage = usingCustomAsserter ? emptyOrFailureMessage : failureMessageOrExpectedOutput; return [ asserter, functionToCall, inputs, expectedOutput, failureMessage, ]; } function innerAssertOutput(asserter, functionToCall, inputs, expectedOutput, failureMessage, shouldReturnResult) { const result = functionToCall(...inputs); if (result instanceof Promise) { return new Promise(async (resolve, reject) => { try { const awaitedResult = await result; asserter(awaitedResult, expectedOutput); if (shouldReturnResult) { resolve(awaitedResult); } else { resolve(); } } catch (error) { reject(new AssertionError(`Output from '${functionToCall.name}' did not produce expected output. ${extractErrorMessage(error)}`, failureMessage)); } }); } else { try { asserter(result, expectedOutput); if (shouldReturnResult) { return result; } else { return; } } catch (error) { throw new AssertionError(`Output from '${functionToCall.name}' did not produce expected output. ${extractErrorMessage(error)}`, failureMessage); } } } function checkOutput(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, failureMessageOrExpectedOutput, emptyOrFailureMessage) { try { const assertionResult = innerAssertOutput(...extractOutputArgs(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, failureMessageOrExpectedOutput, emptyOrFailureMessage), false); if (assertionResult instanceof Promise) { return new Promise(async (resolve) => { try { await assertionResult; resolve(true); } catch { resolve(false); } }); } else { return true; } } catch { return false; } } function assertWrapOutput(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, failureMessageOrExpectedOutput, emptyOrFailureMessage) { return innerAssertOutput(...extractOutputArgs(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, failureMessageOrExpectedOutput, emptyOrFailureMessage), true); } function checkWrapOutput(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, failureMessageOrExpectedOutput, emptyOrFailureMessage) { try { const assertionResult = innerAssertOutput(...extractOutputArgs(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, failureMessageOrExpectedOutput, emptyOrFailureMessage), true); if (assertionResult instanceof Promise) { return new Promise(async (resolve) => { try { resolve(await assertionResult); } catch { resolve(undefined); } }); } else { return assertionResult; } } catch { return undefined; } } const notSetSymbol = Symbol('not set'); export async function waitUntilOutput(functionToCallOrAsserter, inputsOrFunctionToCall, expectedOutputOrInputs, optionsOrExpectedOutput, emptyOrFailureMessageOrOptions, emptyOrFailureMessage) { const usingCustomAsserter = Array.isArray(expectedOutputOrInputs); const asserter = usingCustomAsserter ? functionToCallOrAsserter : deepEquals; const functionToCall = usingCustomAsserter ? inputsOrFunctionToCall : functionToCallOrAsserter; const inputs = usingCustomAsserter ? expectedOutputOrInputs : inputsOrFunctionToCall; const expectedOutput = usingCustomAsserter ? optionsOrExpectedOutput : expectedOutputOrInputs; const options = parseWaitUntilOptions((usingCustomAsserter ? emptyOrFailureMessageOrOptions : optionsOrExpectedOutput)); const failureMessage = usingCustomAsserter ? emptyOrFailureMessage : emptyOrFailureMessageOrOptions; const timeout = convertDuration(options.timeout, { milliseconds: true }).milliseconds; const interval = convertDuration(options.interval, { milliseconds: true }); let lastCallbackOutput = notSetSymbol; let lastError = undefined; async function checkCondition() { try { lastCallbackOutput = await innerAssertOutput(asserter, functionToCall, inputs, expectedOutput, undefined, true); } catch (error) { lastCallbackOutput = notSetSymbol; lastError = ensureError(error); } } const startTime = Date.now(); while (lastCallbackOutput === notSetSymbol) { await checkCondition(); await wait(interval); if (Date.now() - startTime >= timeout) { throw ensureErrorAndPrependMessage(lastError, combineErrorMessages(failureMessage, `Timeout of '${timeout}' milliseconds exceeded waiting for callback value to match expectations`)); } } return lastCallbackOutput; } const assertions = { output: assertOutput, }; export const outputGuards = { assert: assertions, check: { /** * Checks that the output of the given function deeply equals expectations. A custom * asserter can optionally be provided as the first argument to change the expectation * checking from the default "deeply equals" to whatever you want. * * Performs no type guarding. * * ```ts * import {check} from '@augment-vir/assert'; * * check.output((input: number) => String(input), [5], '5'); // returns `true` * check.output((input: number) => String(input), [10], '5'); // returns `false` * * check.output(assert.isLengthAtLeast, (input: number) => String(input), [5], 2); // returns `false` * check.output(assert.isLengthAtLeast, (input: number) => String(input), [10], 2); // returns `true` * ``` */ output: checkOutput, }, assertWrap: { /** * Asserts that the output of the given function deeply equals expectations. A custom * asserter can optionally be provided as the first argument to change the expectation * checking from the default "deeply equals" to whatever you want. Returns the output if the * assertion passes. * * Performs no type guarding. * * ```ts * import {assertWrap} from '@augment-vir/assert'; * * assertWrap.output((input: number) => String(input), [5], '5'); // returns `'5'` * assertWrap.output((input: number) => String(input), [10], '5'); // throws an error * * assertWrap.output(assert.isLengthAtLeast, (input: number) => String(input), [5], 2); // throws an error * assertWrap.output(assert.isLengthAtLeast, (input: number) => String(input), [10], 2); // returns `10` * ``` * * @returns The output if the assertion passes. * @throws {@link AssertionError} If the assertion fails. */ output: assertWrapOutput, }, checkWrap: { /** * Asserts that the output of the given function deeply equals expectations. A custom * asserter can optionally be provided as the first argument to change the expectation * checking from the default "deeply equals" to whatever you want. Returns the output if the * check passes, otherwise `undefined`. * * Performs no type guarding. * * ```ts * import {checkWrap} from '@augment-vir/assert'; * * checkWrap.output((input: number) => String(input), [5], '5'); // returns `'5'` * checkWrap.output((input: number) => String(input), [10], '5'); // returns `undefined` * * checkWrap.output(assert.isLengthAtLeast, (input: number) => String(input), [5], 2); // returns `undefined` * checkWrap.output(assert.isLengthAtLeast, (input: number) => String(input), [10], 2); // returns `10` * ``` * * @returns The output if the assertion passes, otherwise `undefined`. */ output: checkWrapOutput, }, waitUntil: { /** * Repeatedly calls a callback until its output deeply equals expectations. A custom * asserter can optionally be provided as the first argument to change the expectation * checking from the default "deeply equals" to whatever you want. * * Performs no type guarding. * * ```ts * import {waitUntil} from '@augment-vir/assert'; * * await waitUntil.output((input: number) => String(input), [5], '5'); // returns `'5'` * await waitUntil.output((input: number) => String(input), [10], '5'); // throws an error * * await waitUntil.output( * assert.isLengthAtLeast, * (input: number) => String(input), * [5], * 2, * ); // throws an error * await waitUntil.output( * assert.isLengthAtLeast, * (input: number) => String(input), * [10], * 2, * ); // returns `10` * ``` * * @throws {@link AssertionError} If the assertion fails. */ output: waitUntilOutput, }, };