@augment-vir/assert
Version:
A collection of assertions for test and production code alike.
373 lines (372 loc) • 14.3 kB
JavaScript
import { extractErrorMessage, } from '@augment-vir/core';
import { AssertionError } from '../../augments/assertion.error.js';
import { createWaitUntil } from '../../guard-types/wait-until-function.js';
function baseJsonEquals(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
function recursiveAssertJsonEquals(actual, expected) {
if (actual === expected || baseJsonEquals(actual, expected)) {
return;
}
if (actual != null &&
expected != null &&
typeof actual === 'object' &&
typeof expected === 'object') {
const aKeys = Object.keys(actual).sort();
const bKeys = Object.keys(expected).sort();
if (aKeys.length !== bKeys.length) {
throw new Error('Values are not JSON equal.');
}
const areKeysEqual = baseJsonEquals(aKeys, bKeys);
if (!areKeysEqual) {
throw new Error('Values are JSON equal.');
}
Object.keys(actual).forEach((key) => {
try {
recursiveAssertJsonEquals(actual[key], expected[key]);
}
catch (error) {
throw new Error(`JSON objects are not equal at key '${key}': ${extractErrorMessage(error)}`);
}
});
}
throw new Error('Values are not JSON equal.');
}
function recursiveCheckJsonEquals(actual, expected) {
if (actual === expected || baseJsonEquals(actual, expected)) {
return true;
}
if (actual != null &&
expected != null &&
typeof actual === 'object' &&
typeof expected === 'object') {
const aKeys = Object.keys(actual).sort();
const bKeys = Object.keys(expected).sort();
if (aKeys.length !== bKeys.length) {
return false;
}
const areKeysEqual = baseJsonEquals(aKeys, bKeys);
if (!areKeysEqual) {
return false;
}
return Object.keys(actual).every((key) => {
return recursiveCheckJsonEquals(actual[key], expected[key]);
});
}
return false;
}
const assertions = {
/**
* Asserts that two values are deeply equal when stringified into JSON. This will fail or may
* not make any sense if the values are not valid JSON. This internally sorts all given object
* keys so it is insensitive to object key order.
*
* Type guards the first value.
*
* @example
*
* ```ts
* import {assert} from '@augment-vir/assert';
*
* assert.jsonEquals({a: 'a'}, {a: 'a'}); // passes
*
* assert.jsonEquals({a: {b: 'b'}}, {a: {b: 'c'}}); // fails
* ```
*
* @throws {@link AssertionError} If both inputs are not equal.
* @see
* - {@link assert.notJsonEquals} : the opposite assertion.
* - {@link assert.entriesEqual} : another deep equality assertion.
* - {@link assert.deepEquals} : the most thorough (but also slow) deep equality assertion.
*/
jsonEquals(actual, expected, failureMessage) {
try {
recursiveAssertJsonEquals(actual, expected);
}
catch (error) {
throw new AssertionError(extractErrorMessage(error), failureMessage);
}
},
/**
* Asserts that two values are _not_ deeply equal when stringified into JSON. This may not make
* any sense if the values are not valid JSON. This internally sorts all given object keys so it
* is insensitive to object key order.
*
* Performs no type guarding.
*
* @example
*
* ```ts
* import {assert} from '@augment-vir/assert';
*
* assert.notJsonEquals({a: 'a'}, {a: 'a'}); // fails
*
* assert.notJsonEquals({a: {b: 'b'}}, {a: {b: 'c'}}); // passes
* ```
*
* @throws {@link AssertionError} If both inputs are not equal.
* @see
* - {@link assert.jsonEquals} : the opposite assertion.
* - {@link assert.entriesEqual} : another deep equality assertion.
* - {@link assert.deepEquals} : the most thorough (but also slow) deep equality assertion.
*/
notJsonEquals(actual, expected, failureMessage) {
try {
recursiveAssertJsonEquals(actual, expected);
}
catch {
return;
}
throw new AssertionError('Values are JSON equal.', failureMessage);
},
};
export const jsonEqualityGuards = {
assert: assertions,
check: {
/**
* Checks that two values are deeply equal when stringified into JSON. This will fail or may
* not make any sense if the values are not valid JSON. This internally sorts all given
* object keys so it is insensitive to object key order.
*
* Type guards the first value.
*
* @example
*
* ```ts
* import {check} from '@augment-vir/assert';
*
* check.jsonEquals({a: 'a'}, {a: 'a'}); // true
*
* check.jsonEquals({a: {b: 'b'}}, {a: {b: 'c'}}); // false
* ```
*
* @see
* - {@link check.notJsonEquals} : the opposite check.
* - {@link check.entriesEqual} : another deep equality check.
* - {@link check.deepEquals} : the most thorough (but also slow) deep equality check.
*/
jsonEquals(actual, expected) {
return recursiveCheckJsonEquals(actual, expected);
},
/**
* Checks that two values are _not_ deeply equal when stringified into JSON. This may not
* make any sense if the values are not valid JSON. This internally sorts all given object
* keys so it is insensitive to object key order.
*
* Performs no type guarding.
*
* @example
*
* ```ts
* import {check} from '@augment-vir/assert';
*
* check.notJsonEquals({a: 'a'}, {a: 'a'}); // false
*
* check.notJsonEquals({a: {b: 'b'}}, {a: {b: 'c'}}); // true
* ```
*
* @see
* - {@link check.jsonEquals} : the opposite check.
* - {@link check.entriesEqual} : another deep equality check.
* - {@link check.deepEquals} : the most thorough (but also slow) deep equality check.
*/
notJsonEquals(actual, expected) {
return !recursiveCheckJsonEquals(actual, expected);
},
},
assertWrap: {
/**
* Asserts that two values are deeply equal when stringified into JSON. This will fail or
* may not make any sense if the values are not valid JSON. This internally sorts all given
* object keys so it is insensitive to object key order. Returns the first value if the
* assertion passes.
*
* Type guards the first value.
*
* @example
*
* ```ts
* import {assertWrap} from '@augment-vir/assert';
*
* assertWrap.jsonEquals({a: 'a'}, {a: 'a'}); // returns `{a: 'a'}`
*
* assertWrap.jsonEquals({a: {b: 'b'}}, {a: {b: 'c'}}); // throws an error
* ```
*
* @throws {@link AssertionError} If both inputs are not equal.
* @see
* - {@link assertWrap.notJsonEquals} : the opposite assertion.
* - {@link assertWrap.entriesEqual} : another deep equality assertion.
* - {@link assertWrap.deepEquals} : the most thorough (but also slow) deep equality assertion.
*/
jsonEquals(actual, expected, failureMessage) {
try {
recursiveAssertJsonEquals(actual, expected);
return actual;
}
catch (error) {
throw new AssertionError(extractErrorMessage(error), failureMessage);
}
},
/**
* Asserts that two values are _not_ deeply equal when stringified into JSON. This may not
* make any sense if the values are not valid JSON. This internally sorts all given object
* keys so it is insensitive to object key order. Returns the first value if the assertion
* passes.
*
* Performs no type guarding.
*
* @example
*
* ```ts
* import {assertWrap} from '@augment-vir/assert';
*
* assertWrap.notJsonEquals({a: 'a'}, {a: 'a'}); // throws an error
*
* assertWrap.notJsonEquals({a: {b: 'b'}}, {a: {b: 'c'}}); // returns `{a: {b: 'b'}}`
* ```
*
* @throws {@link AssertionError} If both inputs are not equal.
* @see
* - {@link assertWrap.jsonEquals} : the opposite assertion.
* - {@link assertWrap.entriesEqual} : another deep equality assertion.
* - {@link assertWrap.deepEquals} : the most thorough (but also slow) deep equality assertion.
*/
notJsonEquals(actual, expected, failureMessage) {
try {
recursiveAssertJsonEquals(actual, expected);
}
catch {
return actual;
}
throw new AssertionError('Values are JSON equal.', failureMessage);
},
},
checkWrap: {
/**
* Checks that two values are deeply equal when stringified into JSON. This will fail or may
* not make any sense if the values are not valid JSON. This internally sorts all given
* object keys so it is insensitive to object key order. Returns the first value if the
* check passes.
*
* Type guards the first value.
*
* @example
*
* ```ts
* import {checkWrap} from '@augment-vir/assert';
*
* checkWrap.jsonEquals({a: 'a'}, {a: 'a'}); // returns `{a: 'a'}`
*
* checkWrap.jsonEquals({a: {b: 'b'}}, {a: {b: 'c'}}); // returns `undefined`
* ```
*
* @see
* - {@link checkWrap.notJsonEquals} : the opposite check.
* - {@link checkWrap.entriesEqual} : another deep equality check.
* - {@link checkWrap.deepEquals} : the most thorough (but also slow) deep equality check.
*/
jsonEquals(actual, expected) {
if (recursiveCheckJsonEquals(actual, expected)) {
return actual;
}
else {
return undefined;
}
},
/**
* Checks that two values are _not_ deeply equal when stringified into JSON. This may not
* make any sense if the values are not valid JSON. This internally sorts all given object
* keys so it is insensitive to object key order. Returns the first value if the check
* passes.
*
* Performs no type guarding.
*
* @example
*
* ```ts
* import {checkWrap} from '@augment-vir/assert';
*
* checkWrap.notJsonEquals({a: 'a'}, {a: 'a'}); // false
*
* checkWrap.notJsonEquals({a: {b: 'b'}}, {a: {b: 'c'}}); // true
* ```
*
* @see
* - {@link checkWrap.jsonEquals} : the opposite check.
* - {@link checkWrap.entriesEqual} : another deep equality check.
* - {@link checkWrap.deepEquals} : the most thorough (but also slow) deep equality check.
*/
notJsonEquals(actual, expected) {
if (recursiveCheckJsonEquals(actual, expected)) {
return undefined;
}
else {
return actual;
}
},
},
waitUntil: {
/**
* Repeatedly calls a callback until its output is deeply equal to the first input when
* stringified into JSON. This may not make any sense if the values are not valid JSON. This
* internally sorts all given object keys so it is insensitive to object key order. Once the
* callback output passes, it is returned. If the attempts time out, an error is thrown.
*
* Type guards the first value.
*
* @example
*
* ```ts
* import {waitUntil} from '@augment-vir/assert';
*
* await waitUntil.jsonEquals({a: 'a'}, () => {
* return {a: 'a'};
* }); // returns `{a: 'a'}`
*
* await waitUntil.jsonEquals({a: {b: 'c'}}, () => {
* return {a: {b: 'b'}};
* }); // throws an error
* ```
*
* @returns The callback output once it passes.
* @throws {@link AssertionError} On timeout.
* @see
* - {@link waitUntil.notJsonEquals} : the opposite assertion.
* - {@link waitUntil.entriesEqual} : another deep equality assertion.
* - {@link waitUntil.deepEquals} : the most thorough (but also slow) deep equality assertion.
*/
jsonEquals: createWaitUntil(assertions.jsonEquals),
/**
* Repeatedly calls a callback until its output is _not_ deeply equal to the first input
* when stringified into JSON. This may not make any sense if the values are not valid JSON.
* This internally sorts all given object keys so it is insensitive to object key order.
* Once the callback output passes, it is returned. If the attempts time out, an error is
* thrown.
*
* Performs no type guarding.
*
* @example
*
* ```ts
* import {waitUntil} from '@augment-vir/assert';
*
* await waitUntil.notJsonEquals({a: 'a'}, () => {
* return {a: 'a'};
* }); // throws an error
*
* await waitUntil.notJsonEquals({a: {b: 'c'}}, () => {
* return {a: {b: 'b'}};
* }); // returns `{a: {b: 'b'}}`
* ```
*
* @returns The callback output once it passes.
* @throws {@link AssertionError} On timeout.
* @see
* - {@link waitUntil.jsonEquals} : the opposite assertion.
* - {@link waitUntil.entriesEqual} : another not deep equality assertion.
* - {@link waitUntil.deepEquals} : the most thorough (but also slow) not deep equality assertion.
*/
notJsonEquals: createWaitUntil(assertions.notJsonEquals),
},
};