@sprucelabs/test-utils
Version:
Helpful utilities to make asserting more complicated conditions quick and easy! ⚡️
246 lines (245 loc) • 9.77 kB
JavaScript
import chalk from 'chalk';
import deepEqual from 'deep-equal';
import escapeRegExp from 'lodash/escapeRegExp.js';
import isObjectLike from 'lodash/isObjectLike.js';
import { expectType } from 'ts-expect';
import diff from 'variable-diff';
import assertUtil from './assert.utility.js';
export * from './assert.utility.js';
const stringify = assertUtil.stringify.bind(assertUtil);
function isExactType(_) { }
const assert = {
areSameType() { },
isType: expectType,
isExactType,
isNumber(actual, message) {
if (typeof actual !== 'number') {
this.fail(buildErrorMessage(`${stringify(actual)} is not a number!`, message));
}
},
isEqual(actual, expected, message) {
if (actual !== expected) {
this.fail(buildErrorMessage(`${stringify(actual)}\n\n does not equal \n\n${stringify(expected)}`, message));
}
},
isNotEqual(actual, expected, message) {
if (actual === expected) {
this.fail(buildErrorMessage(`${stringify(actual)}\n\n should not equal ${stringify(expected)}\n\n`, message));
}
},
isEqualDeep(actual, expected, message, shouldAppendDelta = true) {
if (!deepEqual(actual, expected, { strict: true })) {
let result = diff(actual, expected);
this.fail(buildErrorMessage(`${`Deep equal failed.\n\nActual would need the following changes to match expected:`}${shouldAppendDelta ? `\n\n${result.text}` : ``}`, message));
}
},
isNotEqualDeep(actual, expected, message) {
this.doesThrow(() => this.isEqualDeep(actual, expected), undefined, buildErrorMessage(`The objects you passed are deep equal! They should not be!`, message));
},
isAbove(actual, floor, message) {
if (typeof actual !== 'number') {
this.fail(buildErrorMessage(`${stringify(actual)} is not a number!`, message));
}
if (actual <= floor) {
this.fail(buildErrorMessage(`${stringify(actual)} is not above ${stringify(floor)}`, message));
}
},
isBelow(actual, ceiling, message) {
if (typeof actual !== 'number') {
this.fail(buildErrorMessage(`${stringify(actual)} is not a number!`, message));
}
if (actual >= ceiling) {
this.fail(buildErrorMessage(`${stringify(actual)} is not below ${stringify(ceiling)}`, message));
}
},
isUndefined(actual, message) {
if (typeof actual !== 'undefined') {
this.fail(buildErrorMessage(`${stringify(actual)} is not undefined`, message));
}
},
isTruthy(actual, message) {
if (actual === false ||
actual === null ||
typeof actual === 'undefined' ||
actual === 0) {
this.fail(buildErrorMessage(`${stringify(actual)} is not truthy`, message));
}
},
isFalsy(actual, message) {
if (actual) {
this.fail(buildErrorMessage(`${stringify(actual)} is not falsy`, message));
}
},
isNull(actual, message) {
if (actual !== null) {
this.fail(buildErrorMessage(`${stringify(actual)} is not null`, message));
}
},
isString(actual, message) {
assertUtil.assertTypeof(actual, 'string', message);
},
isFunction(actual, message) {
assertUtil.assertTypeof(actual, 'function', message);
},
isTrue(actual, message) {
//@ts-ignore
this.isEqual(actual, true, message);
},
isFalse(actual, message) {
//@ts-ignore
this.isEqual(actual, false, message);
},
isObject(actual, message) {
if (!isObjectLike(actual)) {
throw this.fail(buildErrorMessage(`${stringify(actual)} is not an object`, message));
}
},
isArray(actual, message) {
if (!Array.isArray(actual)) {
throw this.fail(buildErrorMessage(`${stringify(actual)} is not an array`, message));
}
},
isLength(actual, expected, message) {
if (!actual) {
throw this.fail(buildErrorMessage(`Expected array of length ${expected}, but got ${stringify(actual)}`, message));
}
//@ts-ignore
this.isEqual(actual.length, expected, buildErrorMessage(`Your array is not the expected length!`, message));
},
doesNotInclude(haystack, needle, message) {
let doesInclude = false;
try {
this.doesInclude(haystack, needle, message);
doesInclude = true;
}
catch {
doesInclude = false;
}
if (doesInclude) {
this.fail(buildErrorMessage(`${stringify(haystack)} should not include ${stringify(needle)}, but it does`, message));
}
},
doesInclude(haystack, needle, message) {
let msg = `Could not find ${stringify(needle)} in ${stringify(haystack)}`;
const isNeedleString = typeof needle === 'string';
const isNeedleRegex = needle instanceof RegExp;
if (typeof haystack === 'string' &&
(isNeedleString || isNeedleRegex) &&
haystack.search(isNeedleString ? escapeRegExp(needle) : needle) > -1) {
return;
}
const isHaystackObject = isObjectLike(haystack);
const { needleHasArrayNotation, path, expected } = assertUtil.parseIncludeNeedle(needle);
if (Array.isArray(haystack)) {
let cleanedNeedle = needle;
if (path && path.substr(0, 3) === '[].') {
cleanedNeedle = { [path.substr(3)]: expected };
}
const found = assertUtil.doHaystacksPassCheck(haystack, cleanedNeedle, this.doesInclude.bind(this));
if (found) {
return;
}
}
if (isHaystackObject && isObjectLike(needle)) {
try {
//@ts-ignore
this.isEqualDeep(haystack, needle, message);
return;
}
catch { }
}
if (assertUtil.foundUsing3rdPartyIncludes(haystack, needle, isHaystackObject)) {
return;
}
if (!Array.isArray(haystack) &&
isHaystackObject &&
isObjectLike(needle) &&
Object.keys(needle).length === 1 &&
!needleHasArrayNotation &&
path) {
const actual = assertUtil.valueAtPath(haystack, path);
if (typeof actual === 'undefined') {
msg = `The path ${stringify(path)} was not found in ${stringify(haystack)}`;
}
else {
msg = `Expected:\n${chalk.green(stringify(needle[path]))}\nbut found:\n${chalk.red(stringify(actual))}\nat:\n${stringify(path)}\nin:\n${stringify(haystack)}`;
}
if (typeof expected === 'string' &&
typeof actual === 'string' &&
actual.search(expected) > -1) {
return;
}
else if (expected instanceof RegExp && expected.exec(actual)) {
return;
}
else {
//@ts-ignore
this.isEqualDeep(expected, actual, buildErrorMessage(msg, message), false);
}
return;
}
if (isHaystackObject && isObjectLike(needle) && path) {
const { actualBeforeArray, pathAfterFirstArray } = assertUtil.splitPathBasedOnArrayNotation(path, haystack);
if (!Array.isArray(actualBeforeArray)) {
this.fail(buildErrorMessage(msg, message));
}
const found = assertUtil.doHaystacksPassCheck(actualBeforeArray, {
[pathAfterFirstArray]: expected,
}, this.doesInclude.bind(this));
if (found) {
return;
}
msg = `Could not find match ${stringify(expected)} at ${stringify(pathAfterFirstArray)} in ${stringify(actualBeforeArray)}.`;
}
this.fail(buildErrorMessage(msg, message));
},
hasAllFunctions(obj, functionNames, message) {
functionNames.forEach((name) => {
if (typeof obj[name] !== 'function') {
this.fail(buildErrorMessage(`A function named "${name}" does not exist on ${stringify(obj)}`, message));
}
});
},
doesThrow(cb, matcher, msg) {
try {
cb();
}
catch (err) {
assertUtil.assertErrorIncludes(matcher, err, msg);
return err;
}
this.fail(buildErrorMessage('Expected a thrown error, but never got one!', msg));
},
async doesThrowAsync(cb, matcher, msg) {
try {
await cb();
}
catch (err) {
assertUtil.assertErrorIncludes(matcher, err, msg);
return err;
}
this.fail(buildErrorMessage('Expected a thrown error, but never got one!', msg));
},
fail: assertUtil.fail,
isInstanceOf(actual, Class, message) {
assert.isTrue(actual instanceof Class, buildErrorMessage(`${assertUtil.stringify(actual)} is not an instance of:\n\n${Class}`, message));
},
isBetween(actual, floor, ceiling, message) {
assert.isBelow(actual, ceiling, message);
assert.isAbove(actual, floor, message);
},
isBetweenInclusive(actual, floor, ceiling, message) {
//@ts-ignore
this.isNumber(actual, message);
if (actual >= floor && actual <= ceiling) {
return;
}
assert.fail(buildErrorMessage(`${actual} is not between ${floor} and ${ceiling} (inclusive)`, message));
},
};
export default assert;
function buildErrorMessage(defaultMessage, customMessage) {
return ((customMessage
? `${customMessage}\n\n${defaultMessage}`
: defaultMessage) + '\n\n');
}