Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(assertion): Add .toBeInstanceOf(Constructor) matcher #57

Merged
merged 4 commits into from
Aug 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ For a list of all matchers and extended documentation, please refer to the [API

You can find the full API reference [here](https://stackbuilders.github.io/assertive-ts/docs/build/)

## Coming Soon

- Extension mechanism ⚙️

## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
Expand Down
34 changes: 34 additions & 0 deletions src/lib/Assertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { UnsupportedOperationError } from "./errors/UnsupportedOperationError";
import { isJSObject, isKeyOf } from "./helpers/guards";
import { TypeFactory } from "./helpers/TypeFactories";

export interface Constructor<T> extends Function {
prototype: T;
}

export interface ExecuteOptions {
/**
* The condition for when the assertion should pass. The negation of this
Expand Down Expand Up @@ -217,6 +221,36 @@ export class Assertion<T> {
});
}

/**
* 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
*/
public toBeInstanceOf(Expected: Constructor<any>): this {
const error = new AssertionError({
actual: this.actual,
message: `Expected value to be an instance of <${Expected.name}>`
});
const invertedError = new 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 equal to another value.
*
Expand Down
12 changes: 4 additions & 8 deletions src/lib/FunctionAssertion.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import { AssertionError } from "assert";

import { Assertion } from "./Assertion";
import { Assertion, Constructor } from "./Assertion";
import { ErrorAssertion } from "./ErrorAssertion";
import { TypeFactory } from "./helpers/TypeFactories";

export type AnyFunction = (...args: any[]) => any;

export interface Class<T> extends Function {
prototype: T;
}

const NoThrow = Symbol("NoThrow");

/**
Expand Down Expand Up @@ -76,8 +72,8 @@ export class FunctionAssertion<T extends AnyFunction> extends Assertion<T> {
* @returns a new {@link ErrorAssertion} to assert over the error
*/
public toThrowError(): ErrorAssertion<Error>;
public toThrowError<E extends Error>(ExpectedType: Class<E>): ErrorAssertion<E>;
public toThrowError<E extends Error>(ExpectedType?: Class<E>): ErrorAssertion<E> {
public toThrowError<E extends Error>(Expected: Constructor<E>): ErrorAssertion<E>;
public toThrowError<E extends Error>(Expected?: Constructor<E>): ErrorAssertion<E> {
const captured = this.captureError();

if (captured === NoThrow) {
Expand All @@ -87,7 +83,7 @@ export class FunctionAssertion<T extends AnyFunction> extends Assertion<T> {
});
}

const ErrorType = ExpectedType ?? Error;
const ErrorType = Expected ?? Error;
const error = new AssertionError({
actual: captured,
message: `Expected the function to throw an error instance of <${ErrorType.name}>`
Expand Down
2 changes: 1 addition & 1 deletion src/lib/ObjectAssertion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isDeepStrictEqual } from "util";

import { Assertion } from "./Assertion";

export type JSObject = Record<keyof any, unknown>;
export type JSObject = Record<keyof any, any>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change fixes issues with type inference in generic constraints. The type unknown is too specific to be part of a constraint


export type Entry<T, K = keyof T> = K extends keyof T
? [K, T[K]]
Expand Down
47 changes: 47 additions & 0 deletions test/lib/Assertion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,19 @@ const BASE_DIFFS = [
["date", TODAY, new Date("2021-12-10T00:00:00.001Z")]
];

class Car {

private readonly model: string;

constructor(model: string) {
this.model = model;
}

public showModel(): string {
return this.model;
}
}

function truthyAsText(value: typeof TRUTHY_VALUES[number]): string {
if (Array.isArray(value) && value.length === 0) {
return "Empty array";
Expand Down Expand Up @@ -263,6 +276,40 @@ describe("[Unit] Assertion.test.ts", () => {
});
});

describe(".toBeInstanceOf", () => {
context("when the value is an instance of the constructor", () => {
const variants = [
[new Date(), Date],
[new Error("failed!"), Error],
[new Car("Pontiac GT-37"), Car]
] as const;

variants.forEach(([value, Constructor]) => {
it(`[Instance: ${Constructor.name}]: returns the assertion instance`, () => {
const test = new Assertion(value);

assert.deepStrictEqual(test.toBeInstanceOf(Constructor), test);
assert.throws(() => test.not.toBeInstanceOf(Constructor), {
message: `Expected value NOT to be an instance of <${Constructor.name}>`,
name: AssertionError.name
});
});
});
});

context("when the value in not an instance of the constructor", () => {
it("throws an assertion error", () => {
const test = new Assertion(new Date());

assert.throws(() => test.toBeInstanceOf(Car), {
message: "Expected value to be an instance of <Car>",
JoseLion marked this conversation as resolved.
Show resolved Hide resolved
name: AssertionError.name
});
assert.deepStrictEqual(test.not.toBeInstanceOf(Car), test);
});
});
});

describe(".toBeEqual", () => {
context("when the value is referentially, shallow, and deep equal", () => {
[
Expand Down