@assertive-ts/core
Version:
A type-safe fluent assertion library
363 lines (333 loc) • 10.5 kB
text/typescript
import isDeepEqual from "fast-deep-equal/es6";
import { Assertion } from "./Assertion";
import { UnsupportedOperationError } from "./errors/UnsupportedOperationError";
import { type Expect } from "./expect";
import { type TypeFactory } from "./helpers/TypeFactories";
import { prettify } from "./helpers/messages";
import { AssertionError } from "assert";
/**
* Encapsulates assertion methods applicable to arrays.
*
* @param T the type of the array
*/
export class ArrayAssertion<T> extends Assertion<T[]> {
public constructor(actual: T[]) {
super(actual);
}
/**
* Check if all the array values match the predicate
*
* @example
* ```
* expect([1, 2, 3]).toMatchAll(x => x < 5);
* expect([apple, orange, pear]).toMatchAll(isFruit);
* ```
*
* @param matcher a generic matcher predicate
* @returns the assertion instance
*/
public toMatchAll(matcher: (value: T) => boolean): this {
const error = new AssertionError({
actual: this.actual,
message: "Expected all values of the array to return true on the matcher predicate",
});
const invertedError = new AssertionError({
actual: this.actual,
message: "Expected not every value of the array to return true on the matcher predicate",
});
return this.execute({
assertWhen: this.actual.every(matcher),
error,
invertedError,
});
}
/**
* Check if any of the array values match the predicate
*
* @example
* ```
* expect([dog, apple, cat]).toMatchAny(isFruit);
* ```
*
* @param matcher a matcher predicate
* @returns the assertion instance
*/
public toMatchAny(matcher: (value: T) => boolean): this {
const error = new AssertionError({
actual: this.actual,
message: "Expected any value of the array to return true on the matcher predicate",
});
const invertedError = new AssertionError({
actual: this.actual,
message: "Expected no value of the array to return true on the matcher predicate",
});
return this.execute({
assertWhen: this.actual.some(matcher),
error,
invertedError,
});
}
/**
* Check if all the values of the array satisfies a given assertion.
*
* @example
* ```
* const checkIsFruit = (x: Fruit) => expect(x).toBeInstanceOf(Fruit);
* expect([apple, pear, banana]).toSatisfyAll(checkIsFruit);
* ```
*
* @param consumer a consumer of the array to assert over each value of its
* values
* @returns the assertion instance
*/
public toSatisfyAll(consumer: (value: T) => void): this {
const tryAllValues = (): AssertionError | undefined => {
try {
this.actual.forEach(consumer);
return undefined;
} catch (error) {
if (error instanceof AssertionError) {
return error;
}
throw error;
}
};
const firstError = tryAllValues();
return this.execute({
assertWhen: firstError === undefined,
error: firstError ?? new AssertionError({ }),
invertedError: new AssertionError({
actual: this.actual,
message: "Expected not all values of the array to satisfy the given assertion",
}),
});
}
/**
* Check if any value of the array satisfies the give assertion.
*
* @example
* ```
* const checkIsFruit = (x: Fruit) => expect(x).toBeInstanceOf(Fruit);
* expect([dog, apple, cat]).toSatisfyAny(checkIsFruit);
* ```
*
* @param consumer a consumer of the array to assert over each value of its
* values
* @returns the assertion instance
*/
public toSatisfyAny(consumer: (value: T) => void): this {
const error = new AssertionError({
actual: this.actual,
message: "Expected any value of the array to satisfy the given assertion",
});
const invertedError = new AssertionError({
actual: this.actual,
message: "Expected no value of the array to satisfy the given assertion",
});
return this.execute({
assertWhen: this.actual.some(value => {
try {
consumer(value);
return true;
} catch (err) {
return false;
}
}),
error,
invertedError,
});
}
/**
* Check if the array is empty. That is, when its `length` property is zero.
*
* @example
* ```
* expect([]).toBeEmpty();
* ```
*
* @returns the assertion instance
*/
public toBeEmpty(): this {
const error = new AssertionError({
actual: this.actual,
message: "Expected array to be empty",
});
const invertedError = new AssertionError({
actual: this.actual,
message: "Expected array NOT to be empty",
});
return this.execute({
assertWhen: this.actual.length === 0,
error,
invertedError,
});
}
/**
* Check if the array has some specific number of elements.
*
* @example
* ```
* expect([0, 1, 2]).toHaveSize(3);
* ```
*
* @param size the expected number of elements in the array
* @returns the assertion instance
*/
public toHaveSize(size: number): this {
const error = new AssertionError({
actual: this.actual.length,
expected: size,
message: `Expected array to contain ${size} elements, but it has ${this.actual.length}`,
});
const invertedError = new AssertionError({
actual: this.actual.length,
message: `Expected array NOT to contain ${size} elements, but it does`,
});
return this.execute({
assertWhen: this.actual.length === size,
error,
invertedError,
});
}
/**
* Check if the array contains the same elements as another. This doesn't
* check the order of the elements.
*
* @example
* ```
* expect([1, 2, 3]).toHaveSameMembers([3, 2, 1]);
* ```
*
* @param expected the other array to compare its elements to
* @returns the assertion instance
*/
public toHaveSameMembers(expected: T[]): this {
const error = new AssertionError({
actual: this.actual,
expected,
message: `Expected array to have the same members as <${prettify(expected)}>`,
});
const invertedError = new AssertionError({
actual: this.actual,
message: `Expected array NOT to have the same members as <${prettify(expected)}>`,
});
return this.execute({
assertWhen:
this.actual.length === expected.length
&& this.actual.every(value => expected.includes(value)),
error,
invertedError,
});
}
/**
* Check if the array contains all the passed values.
*
* @example
* ```
* expect([1, 2, 3]).toContainAll(1, 3);
* ```
*
* @param values the values the array should contain
* @returns the assertion instance
*/
public toContainAll(...values: T[]): this {
const allValues = values.map(prettify).join(", ");
const error = new AssertionError({
actual: this.actual,
message: `Expected array to contain all the values <${allValues}>`,
});
const invertedError = new AssertionError({
actual: this.actual,
message: `Expected array NOT to contain all the values <${allValues}>`,
});
return this.execute({
assertWhen: values.every(value => this.actual.includes(value)),
error,
invertedError,
});
}
/**
* Check if the array contains any of the passed values.
*
* @example
* ```
* expect([1, 2, 3]).toContainAny(1, 50, 36);
* ```
*
* @param values the value the array should include (at least one)
* @returns the assertion instance
*/
public toContainAny(...values: T[]): this {
const allValues = values.map(prettify).join(", ");
const error = new AssertionError({
actual: this.actual,
message: `Expected array to contain any of the values <${allValues}>`,
});
const invertedError = new AssertionError({
actual: this.actual,
message: `Expected array NOT to contain any of the values <${allValues}>`,
});
return this.execute({
assertWhen: values.some(value => this.actual.includes(value)),
error,
invertedError,
});
}
/**
* Check if the array contains an specific value at an exact index.
*
* @param index the index of the array to find the value
* @param value the expected value of the index in the array
* @returns the assertion instance
*/
public toContainAt(index: number, value: T): this {
const error = new AssertionError({
actual: this.actual[index],
expected: value,
message: `Expected value at index <${index}> to be <${prettify(value)}>`,
});
const invertedError = new AssertionError({
actual: this.actual[index],
message: `Expected value at index <${index}> NOT to be <${prettify(value)}>`,
});
return this.execute({
assertWhen: isDeepEqual(this.actual[index], value),
error,
invertedError,
});
}
/**
* Extract the value on a specific index of the array and create an assertion
* instance of that specific type. This method uses {@link Assertion.asType}
* internally to create the new assertion instance.
*
* @example
* ```
* expect(["foo", 2, true])
* .extracting(1, TypeFactories.Number)
* .toBePositive();
* ```
*
* @typeParam S the type of the factory's value
* @typeParam A the type of the assertion factory
* @param index the index of the array to extract the value
* @param typeFactory a factory to assert the extracted value type and create
* an assertion for it
* @returns a more specific assertion based on the factory type for the value
*/
public extracting<S extends T, A extends Assertion<S>>(index: number, typeFactory: TypeFactory<S, A>): A {
if (this.inverted) {
throw new UnsupportedOperationError("The `.not` modifier is not allowed on `.extracting(..)` method");
}
if (index >= this.actual.length) {
throw new AssertionError({
actual: this.actual,
message: `Out of bounds! Cannot extract index ${index} from an array of ${this.actual.length} elements`,
});
}
// We use `require(..)` to avoid import cycles with the `./expect` module
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { expect } = require("./expect") as { expect: Expect; };
return expect(this.actual[index]).asType(typeFactory);
}
}