diff --git a/expect/_equal.ts b/expect/_equal.ts index af6c7fa01392..ec022203486c 100644 --- a/expect/_equal.ts +++ b/expect/_equal.ts @@ -19,15 +19,7 @@ function constructorsEqual(a: object, b: object) { * Deep equality comparison used in assertions * @param c actual value * @param d expected value - * @param strictCheck check value in strictMode - * - * @example - * ```ts - * import { equal } from "https://deno.land/std@$STD_VERSION/assert/equal.ts"; - * - * equal({ foo: "bar" }, { foo: "bar" }); // Returns `true` - * equal({ foo: "bar" }, { foo: "baz" }); // Returns `false - * ``` + * @param options for the equality check */ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean { const { customTesters = [], strictCheck } = options || {}; @@ -36,7 +28,10 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean { return (function compare(a: unknown, b: unknown): boolean { if (customTesters?.length) { for (const customTester of customTesters) { - const pass = customTester.call(undefined, a, b, customTesters); + const testContext = { + equal, + }; + const pass = customTester.call(testContext, a, b, customTesters); if (pass !== undefined) { return pass; } diff --git a/expect/_extend.ts b/expect/_extend.ts new file mode 100644 index 000000000000..b5bc42d14792 --- /dev/null +++ b/expect/_extend.ts @@ -0,0 +1,16 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { Matchers } from "./_types.ts"; + +let extendMatchers = {}; + +export function getExtendMatchers() { + return extendMatchers; +} + +export function setExtendMatchers(newExtendMatchers: Matchers) { + extendMatchers = { + ...extendMatchers, + ...newExtendMatchers, + }; +} diff --git a/expect/_extend_test.ts b/expect/_extend_test.ts new file mode 100644 index 000000000000..8689773c79d2 --- /dev/null +++ b/expect/_extend_test.ts @@ -0,0 +1,100 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { MatcherContext, Tester } from "./_types.ts"; + +declare module "./_types.ts" { + interface Expected { + toEqualBook: (expected: unknown) => ExtendMatchResult; + } +} + +class Author { + public name: string; + + constructor(name: string) { + this.name = name; + } +} + +class Book { + public name: string; + public authors: Array; + + constructor(name: string, authors: Array) { + this.name = name; + this.authors = authors; + } +} + +const areAuthorsEqual: Tester = (a: unknown, b: unknown) => { + const isAAuthor = a instanceof Author; + const isBAuthor = b instanceof Author; + + if (isAAuthor && isBAuthor) { + return a.name === b.name; + } else if (isAAuthor === isBAuthor) { + return undefined; + } else { + return false; + } +}; + +const areBooksEqual: Tester = function ( + this: MatcherContext, + a: unknown, + b: unknown, + customTesters: Tester[], +) { + const isABook = a instanceof Book; + const isBBook = b instanceof Book; + + if (isABook && isBBook) { + return (a.name === b.name && + this.equal(a.authors, b.authors, { customTesters: customTesters })); + } else if (isABook === isBBook) { + return undefined; + } else { + return false; + } +}; + +expect.addEqualityTesters([ + areAuthorsEqual, + areBooksEqual, +]); + +expect.extend({ + toEqualBook(context, expected) { + const actual = context.value as Book; + const result = context.equal(expected, actual, { + customTesters: context.customTesters, + }); + + return { + message: () => + `Expected Book object: ${expected.name}. Actual Book object: ${actual.name}`, + pass: result, + }; + }, +}); + +Deno.test("expect.extend() api test case", () => { + const book1a = new Book("Book 1", [ + new Author("Author 1"), + new Author("Author 2"), + ]); + const book1b = new Book("Book 1", [ + new Author("Author 1"), + new Author("Author 2"), + ]); + const book2 = new Book("Book 2", [ + new Author("Author 1"), + new Author("Author 2"), + ]); + + expect(book1a).toEqualBook(book1b); + expect(book1a).not.toEqualBook(book2); + expect(book1a).not.toEqualBook(1); + expect(book1a).not.toEqualBook(null); +}); diff --git a/expect/_types.ts b/expect/_types.ts index 9a001d06d0a8..6af7997ac40f 100644 --- a/expect/_types.ts +++ b/expect/_types.ts @@ -4,6 +4,7 @@ export interface MatcherContext { value: unknown; isNot: boolean; + equal: (a: unknown, b: unknown, options?: EqualOptions) => boolean; customTesters: Tester[]; customMessage: string | undefined; } @@ -11,12 +12,16 @@ export interface MatcherContext { export type Matcher = ( context: MatcherContext, ...args: any[] -) => MatchResult; +) => MatchResult | ExtendMatchResult; export type Matchers = { [key: string]: Matcher; }; export type MatchResult = void | Promise | boolean; +export type ExtendMatchResult = { + message: () => string; + pass: boolean; +}; export type AnyConstructor = new (...args: any[]) => any; export type Tester = ( @@ -88,6 +93,9 @@ export interface Expected { not: Expected; resolves: Async; rejects: Async; + // This declaration prepares for the `expect.extend` and just only let + // compiler pass, the more concrete type definition is defined by user + [name: string]: unknown; } export type MatcherKey = keyof Omit; diff --git a/expect/expect.ts b/expect/expect.ts index c5bc4af67725..89090612fbbb 100644 --- a/expect/expect.ts +++ b/expect/expect.ts @@ -3,15 +3,19 @@ import type { Expected, + ExtendMatchResult, Matcher, MatcherContext, MatcherKey, + Matchers, } from "./_types.ts"; import { AssertionError } from "../assert/assertion_error.ts"; import { addCustomEqualityTesters, getCustomEqualityTesters, } from "./_custom_equality_tester.ts"; +import { equal } from "./_equal.ts"; +import { getExtendMatchers, setExtendMatchers } from "./_extend.ts"; import { toBe, toBeCloseTo, @@ -132,7 +136,12 @@ export function expect(value: unknown, customMessage?: string): Expected { return self; } - const matcher: Matcher = matchers[name as MatcherKey]; + const extendMatchers: Matchers = getExtendMatchers(); + const allMatchers = { + ...extendMatchers, + ...matchers, + }; + const matcher: Matcher = allMatchers[name as MatcherKey]; if (!matcher) { throw new TypeError( typeof name === "string" @@ -145,6 +154,7 @@ export function expect(value: unknown, customMessage?: string): Expected { function applyMatcher(value: unknown, args: unknown[]) { const context: MatcherContext = { value, + equal, isNot: false, customMessage, customTesters: getCustomEqualityTesters(), @@ -152,7 +162,18 @@ export function expect(value: unknown, customMessage?: string): Expected { if (isNot) { context.isNot = true; } - matcher(context, ...args); + if (name in extendMatchers) { + const result = matcher(context, ...args) as ExtendMatchResult; + if (context.isNot) { + if (result.pass) { + throw new AssertionError(result.message()); + } + } else if (!result.pass) { + throw new AssertionError(result.message()); + } + } else { + matcher(context, ...args); + } } return isPromised @@ -169,6 +190,7 @@ export function expect(value: unknown, customMessage?: string): Expected { } expect.addEqualityTesters = addCustomEqualityTesters; +expect.extend = setExtendMatchers; expect.anything = anything; expect.any = any; expect.arrayContaining = arrayContaining;