@assertive-ts/core
Version:
A type-safe fluent assertion library
496 lines • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Assertion = void 0;
const tslib_1 = require("tslib");
const es6_1 = tslib_1.__importDefault(require("fast-deep-equal/es6"));
const UnsupportedOperationError_1 = require("./errors/UnsupportedOperationError");
const guards_1 = require("./helpers/guards");
const messages_1 = require("./helpers/messages");
const assert_1 = require("assert");
/**
* Base class for all assertions.
*
* @param T the type of the `actual` value
*/
class Assertion {
constructor(actual) {
this.actual = actual;
this.inverted = false;
this.not = new Proxy(this, { get: this.proxyInverter(true) });
}
/**
* A convenience method to normalize the assertion instance. If it was
* inverted with `.not`, it'll return it back to the previous non-inverted
* state. Otherwise, it returns the same instance.
*
* @returns the normalized assertion instance
*/
normalized() {
return this.inverted
? new Proxy(this, { get: this.proxyInverter(false) })
: this;
}
/**
* A convenience method to execute the assertion. The inversion logic for
* `.not` is already embedded in this method, so this should always be used
* in assertions to keep the negation system working
*
* @param options the execution options for the assertion
* @returns the Assertion instance if no error was thrown
*/
execute(options) {
const { assertWhen, error, invertedError } = options;
if (!assertWhen && !this.inverted) {
throw error;
}
if (assertWhen && this.inverted) {
throw invertedError;
}
return this.normalized();
}
/**
* Check if the value matches the given predicate.
*
* @param matcher a matcher predicate
* @returns the assertion instance
*/
toMatch(matcher) {
const error = new assert_1.AssertionError({
actual: this.actual,
message: "Expected matcher predicate to return true",
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: "Expected matcher predicate NOT to return true",
});
return this.execute({
assertWhen: matcher(this.actual),
error,
invertedError,
});
}
/**
* Check if the value exists. This means that the value should be neither
* `null` nor `undefined`.
*
* @example
* ```
* expect(planetEarth).toExist();
* ```
*
* @returns the assertion instance
*/
toExist() {
const error = new assert_1.AssertionError({
actual: this.actual,
message: `Expected value to exist, but it was <${(0, messages_1.prettify)(this.actual)}>`,
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: `Expected value to NOT exist, but it was <${(0, messages_1.prettify)(this.actual)}>`,
});
return this.execute({
assertWhen: this.actual !== undefined && this.actual !== null,
error,
invertedError,
});
}
/**
* Check if the value is `undefined`
*
* @example
* ```
* expect(myUndefinedValue).toBeUndefined()
* ```
*
* @returns the assertion instance
*/
toBeUndefined() {
const error = new assert_1.AssertionError({
actual: this.actual,
message: `Expected <${(0, messages_1.prettify)(this.actual)}> to be undefined`,
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: "Expected the value NOT to be undefined",
});
return this.execute({
assertWhen: this.actual === undefined,
error,
invertedError,
});
}
/**
* Check if the value is `null`.
*
* @example
* ```
* expect(myNullValue).toBeNull();
* ```
*
* @returns the assertion instance
*/
toBeNull() {
const error = new assert_1.AssertionError({
actual: this.actual,
message: `Expected <${(0, messages_1.prettify)(this.actual)}> to be null`,
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: "Expected the value NOT to be null",
});
return this.execute({
assertWhen: this.actual === null,
error,
invertedError,
});
}
/**
* Check if the value is present. This means that the value should not be
* `undefined`.
*
* @example
* ```
* expect(PI).toBePresent();
* ```
*
* @returns the assertion instance
*/
toBePresent() {
const error = new assert_1.AssertionError({
actual: this.actual,
message: "Expected the value to be present",
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: "Expected the value NOT to be present",
});
return this.execute({
assertWhen: this.actual !== undefined,
error,
invertedError,
});
}
/**
* Check if the value is a truthy value. There are six falsy values in
* JavaScript: `null`, `undefined`, `0`, `""`, `false`, `NaN`. Everything
* else is truthy.
*
* @example
* ```
* expect("hello world").toBeTruthy();
* expect(128).toBeTruthy();
* ```
*
* @returns the assertion instance
*/
toBeTruthy() {
const error = new assert_1.AssertionError({
actual: this.actual,
message: `Expected <${(0, messages_1.prettify)(this.actual)}> to be a truthy value`,
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: `Expected <${(0, messages_1.prettify)(this.actual)}> NOT to be a truthy value`,
});
return this.execute({
assertWhen: !!this.actual,
error,
invertedError,
});
}
/**
* Check if the value is a falsy value. There are six falsy values in
* JavaScript: `null`, `undefined`, `0`, `""`, `false`, `NaN`. Everything
* else is truthy.
*
* @example
* ```
* expect(0).toBeFalsy();
* expect("").toBeFalsy();
* ```
* @returns the assertion instance
*/
toBeFalsy() {
const error = new assert_1.AssertionError({
actual: this.actual,
message: `Expected <${(0, messages_1.prettify)(this.actual)}> to be a falsy value`,
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: `Expected <${(0, messages_1.prettify)(this.actual)}> NOT to be a falsy value`,
});
return this.execute({
assertWhen: !this.actual,
error,
invertedError,
});
}
/**
* Check if the value is an instance of the provided constructor.
*
* @example
* ```
* expect(pontiac).toBeInstanceOf(Car);
*
* expect(today).toBeInstanceOf(Date);
* ```
*
* @param Expected the constructor the value should be an instance
* @returns the assertion instance
*/
toBeInstanceOf(Expected) {
const error = new assert_1.AssertionError({
actual: this.actual,
message: `Expected value to be an instance of <${Expected.name}>`,
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: `Expected value NOT to be an instance of <${Expected.name}>`,
});
return this.execute({
assertWhen: this.actual instanceof Expected,
error,
invertedError,
});
}
/**
* Check if the value is deep strict equal to another value.
*
* @example
* ```
* expect(3 + 2).toBeEqual(5);
* expect({ a: { b: 1 } }).toBeEqual({ a: { b: 1 } });
* expect(today).toBeEqual(new Date(today.toISOString()));
* ```
*
* @param expected the value to compare for deep equality
* @returns the assertion instance
*/
toBeEqual(expected) {
const error = new assert_1.AssertionError({
actual: this.actual,
expected,
message: "Expected both values to be deep equal",
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: "Expected both values to NOT be deep equal",
});
return this.execute({
assertWhen: (0, es6_1.default)(this.actual, expected),
error,
invertedError,
});
}
/**
* Check if the value is shallow equal to another value.
*
* @example
* ```
* expect(3 + 2).toBeSimilar(5);
* expect({ a: 1 }).toBeSimilar({ a: 1 });
*
* expect({ a: { b: 1 } }).not.toBeSimilar({ a: {b: 1} });
* ```
*
* @param expected the value to compare for shallow equality
* @returns the assertion instance
*/
toBeSimilar(expected) {
const error = new assert_1.AssertionError({
actual: this.actual,
expected,
message: "Expected both values to be similar",
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: "Expected both values to NOT be similar",
});
const areShallowEqual = () => {
if (this.actual instanceof Date && expected instanceof Date) {
return this.actual.getTime() === expected.getTime();
}
if (((0, guards_1.isStruct)(this.actual) && (0, guards_1.isStruct)(expected))
|| (Array.isArray(this.actual) && Array.isArray(expected))) {
const actualKeys = Object.keys(this.actual);
const expectedKeys = Object.keys(expected);
const sizeMatch = actualKeys.length === expectedKeys.length;
const valuesMatch = actualKeys.every(key => this.actual[key] === expected[key]);
return sizeMatch && valuesMatch;
}
return Object.is(this.actual, expected);
};
const areBothNaN = typeof this.actual === "number"
&& typeof expected === "number"
&& isNaN(this.actual)
&& isNaN(expected);
return this.execute({
assertWhen: areShallowEqual() || areBothNaN || this.actual === expected,
error,
invertedError,
});
}
/**
* Check if the value is the same as another value.
*
* @example
* ```
* const x = { a: 1 };
* const y = x;
*
* expect(x).toBeSame(x);
* expect(x).toBeSame(y);
*
* expect(x).not.toBeSame({ ...x });
* ```
*
* @param expected the value to compare for referential equality
* @returns the assertion instance
*/
toBeSame(expected) {
const error = new assert_1.AssertionError({
actual: this.actual,
expected,
message: "Expected both values to be the same",
});
const invertedError = new assert_1.AssertionError({
actual: this.actual,
message: "Expected both values to NOT be the same",
});
return this.execute({
assertWhen: this.actual === expected,
error,
invertedError,
});
}
/**
* Alias of `.toBeSame(..)` assertion.
*
* @example
* ```
* const x = { a: 1 };
* const y = x;
*
* expect(x).toBeSameAs(x);
* expect(x).toBeSameAs(y);
*
* expect(x).not.toBeSameAs({ ...x });
* ```
*
* @param expected the value to compare for referential equality
* @returns the assertion instance
*/
toBeSameAs(expected) {
return this.toBeSame(expected);
}
/**
* Another alias of `.toBeSame(..)` assertion.
*
* @example
* ```
* const x = { a: 1 };
* const y = x;
*
* expect(x).toBe(x);
* expect(x).toBe(y);
*
* expect(x).not.toBe({ ...x });
* ```
*
* @param expected the value to compare for referential equality
* @returns the assertion instance
*/
toBe(expected) {
return this.toBeSame(expected);
}
/**
* Checks if the value is of a specific data type. The supported data types
* are the same as the `typeof` operator, plus an additional `array` which
* allows desabiguation between `object` (which can also be an array).
*
* @example
* ```
* const arr = [1, 2, 3];
*
* expect(arr).toBeOfType("array");
* expect(arr[0]).toBeOfType("number");
* expect(arr[9]).toBeOfType("undefined");
* ```
*
* @param expected the expected data type
* @returns the assertion instance
*/
toBeOfType(expected) {
const error = new assert_1.AssertionError({
actual: typeof this.actual,
expected,
message: `Expected <${(0, messages_1.prettify)(this.actual)}> to be of type <${expected}>`,
});
const invertedError = new assert_1.AssertionError({
actual: typeof this.actual,
message: `Expected <${(0, messages_1.prettify)(this.actual)}> NOT to be of type <${expected}>`,
});
return this.execute({
assertWhen: expected === "array"
? Array.isArray(this.actual)
: typeof this.actual === expected,
error,
invertedError,
});
}
/**
* Check first if the value is of some specific type, in which case returns
* an assertion instance for that specific type. The new assertion is built
* from a factory that should extend from the base {@link Assertion} class.
*
* We provide some basic factories in `TypeFactories`. If you need some
* other factory for a custom assertion for instance, you can easily create
* one from a Factory reference and a predicate.
*
* @example
* ```
* expect(unknownValue)
* .asType(TypeFactories.STRING)
* .toStartWith("/api/");
*
* expect(uuid)
* .asType({
* Factory: UUIDAssertion, // a custom UUID assertion
* predicate: (value): value is UUID => {
* return typeof value === "string" && UUID.PATTER.test(value);
* },
* })
* .isValid();
* ```
*
* @typeParam S the type of the factory's value
* @typeParam A the type of the assertion factory
* @param typeFactory a factory to assert the type and create an assertion
* @returns a more specific assertion based on the factory type
*/
asType(typeFactory) {
const { Factory, predicate, typeName } = typeFactory;
if (this.inverted) {
throw new UnsupportedOperationError_1.UnsupportedOperationError("The `.not` modifier is not allowed on `.asType(..)` method");
}
if (predicate(this.actual)) {
return new Factory(this.actual);
}
throw new assert_1.AssertionError({
actual: this.actual,
message: `Expected <${(0, messages_1.prettify)(this.actual)}> to be of type "${typeName}"`,
});
}
proxyInverter(isInverted) {
return (target, p) => {
const key = (0, guards_1.isKeyOf)(target, p) ? p : undefined;
if (key === "inverted") {
return isInverted;
}
return key ? target[key] : undefined;
};
}
}
exports.Assertion = Assertion;
//# sourceMappingURL=Assertion.js.map