@augment-vir/assert
Version:
A collection of assertions for test and production code alike.
261 lines (260 loc) • 11.2 kB
JavaScript
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,
},
};