// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // This module is browser compatible. // Copyright 2019 Allain Lalonde. All rights reserved. ISC License. // Copyright (c) Meta Platforms, Inc. and affiliates. // The documentation is extracted from https://github.com/jestjs/jest/blob/main/website/versioned_docs/version-29.7/ExpectAPI.md // and updated for the Deno ecosystem. import type { Expected, ExtendMatchResult, Matcher, MatcherContext, MatcherKey, Matchers, } from "./_types.ts"; import { AssertionError } from "@std/assert/assertion-error"; import { addCustomEqualityTesters, getCustomEqualityTesters, } from "./_custom_equality_tester.ts"; import { equal } from "./_equal.ts"; import { getExtendMatchers, setExtendMatchers } from "./_extend.ts"; import { toBe, toBeCloseTo, toBeDefined, toBeFalsy, toBeGreaterThan, toBeGreaterThanOrEqual, toBeInstanceOf, toBeLessThan, toBeLessThanOrEqual, toBeNaN, toBeNull, toBeTruthy, toBeUndefined, toContain, toContainEqual, toEqual, toHaveBeenCalled, toHaveBeenCalledTimes, toHaveBeenCalledWith, toHaveBeenLastCalledWith, toHaveBeenNthCalledWith, toHaveLastReturnedWith, toHaveLength, toHaveNthReturnedWith, toHaveProperty, toHaveReturned, toHaveReturnedTimes, toHaveReturnedWith, toMatch, toMatchObject, toStrictEqual, toThrow, } from "./_matchers.ts"; import { isPromiseLike } from "./_utils.ts"; import * as asymmetricMatchers from "./_asymmetric_matchers.ts"; import type { Tester } from "./_types.ts"; export type { AnyConstructor, Async, Expected } from "./_types.ts"; const matchers: Record = { lastCalledWith: toHaveBeenLastCalledWith, lastReturnedWith: toHaveLastReturnedWith, nthCalledWith: toHaveBeenNthCalledWith, nthReturnedWith: toHaveNthReturnedWith, toBeCalled: toHaveBeenCalled, toBeCalledTimes: toHaveBeenCalledTimes, toBeCalledWith: toHaveBeenCalledWith, toBeCloseTo, toBeDefined, toBeFalsy, toBeGreaterThanOrEqual, toBeGreaterThan, toBeInstanceOf, toBeLessThanOrEqual, toBeLessThan, toBeNaN, toBeNull, toBeTruthy, toBeUndefined, toBe, toContainEqual, toContain, toEqual, toHaveBeenCalledTimes, toHaveBeenCalledWith, toHaveBeenCalled, toHaveBeenLastCalledWith, toHaveBeenNthCalledWith, toHaveLength, toHaveLastReturnedWith, toHaveNthReturnedWith, toHaveProperty, toHaveReturnedTimes, toHaveReturnedWith, toHaveReturned, toMatchObject, toMatch, toReturn: toHaveReturned, toReturnTimes: toHaveReturnedTimes, toReturnWith: toHaveReturnedWith, toStrictEqual, toThrow, }; /** * **Note:** the documentation for this module is taken from [Jest](https://github.com/jestjs/jest/blob/main/website/versioned_docs/version-29.7/ExpectAPI.md) * and the examples are updated for Deno. * * The `expect` function is used to test a value. You will use `expect` along with a * "matcher" function to assert something about a value. * * @example Usage * ```ts no-assert * import { expect } from "@std/expect"; * * function bestLaCroixFlavor(): string { * return "grapefruit"; * } * * Deno.test("the best flavor is grapefruit", () => { * expect(bestLaCroixFlavor()).toBe("grapefruit"); * }); * ``` * * In this case, `toBe` is the matcher function. There are a lot of different * matcher functions, documented in the main module description. * * The argument to `expect` should be the value that your code produces, and any * argument to the matcher should be the correct value. If you mix them up, your * tests will still work, but the error messages on failing tests will look * strange. * * @param value The value to perform assertions on. * @param customMessage An optional custom message to include in the assertion error. * @returns An expected object that can be used to chain matchers. * * @typeParam T The interface used for `expect`. This is usually needed only if you want to use `expect.extend` to create custom matchers. */ export function expect( value: unknown, customMessage?: string, ): T { let isNot = false; let isPromised = false; const self: T = new Proxy( {}, { get(_, name) { if (name === "not") { isNot = !isNot; return self; } if (name === "resolves") { if (!isPromiseLike(value)) { throw new AssertionError("Expected value must be PromiseLike"); } isPromised = true; return self; } if (name === "rejects") { if (!isPromiseLike(value)) { throw new AssertionError("Expected value must be a PromiseLike"); } value = value.then( (value) => { throw new AssertionError( `Promise did not reject: resolved to ${value}`, ); }, (err) => err, ); isPromised = true; return self; } const extendMatchers: Matchers = getExtendMatchers(); const allMatchers = { ...extendMatchers, ...matchers, }; const matcher = allMatchers[name as MatcherKey] as Matcher; if (!matcher) { throw new TypeError( typeof name === "string" ? `matcher not found: ${name}` : "matcher not found", ); } return (...args: unknown[]) => { function applyMatcher(value: unknown, args: unknown[]) { const context: MatcherContext = { value, equal, isNot: false, customMessage, customTesters: getCustomEqualityTesters(), }; if (isNot) { context.isNot = true; } 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 ? (value as Promise).then((value: unknown) => applyMatcher(value, args) ) : applyMatcher(value, args); }; }, }); return self; } /** * You can use `expect.addEqualityTesters` to add your own methods to test if two * objects are equal. For example, let's say you have a class in your code that * represents volume and can determine if two volumes using different units are * equal. You may want `toEqual` (and other equality matchers) to use this custom * equality method when comparing to Volume classes. You can add a custom equality * tester to have `toEqual` detect and apply custom logic when comparing Volume * classes: * * @example * ```ts * import { expect } from "@std/expect"; * * class Volume { * amount: number; * unit: "L" | "mL"; * * constructor(amount: number, unit: "L" | "mL") { * this.amount = amount; * this.unit = unit; * } * * toString() { * return `[Volume ${this.amount}${this.unit}]`; * } * * equals(other: Volume) { * if (this.unit === other.unit) { * return this.amount === other.amount; * } else if (this.unit === "L" && other.unit === "mL") { * return this.amount * 1000 === other.amount; * } else { * return this.amount === other.amount * 1000; * } * } * } * * function areVolumesEqual(a: Volume, b: Volume) { * const isAVolume = a instanceof Volume; * const isBVolume = b instanceof Volume; * if (isAVolume && isBVolume) { * return a.equals(b); * } else if (isAVolume === isBVolume) { * return undefined; * } else { * return false; * } * } * * expect.addEqualityTesters([areVolumesEqual]); * * Deno.test("are equal with different units", () => { * expect(new Volume(1, "L")).toEqual(new Volume(1000, "mL")); * }); * ``` */ expect.addEqualityTesters = addCustomEqualityTesters as ( newTesters: Tester[], ) => void; /** * Extend `expect()` with custom provided matchers. * * To do so, you will need to extend the interface `Expected` to define the new signature of the `expect`. * * ```ts * import type { Async, Expected } from "./expect.ts"; * import { expect } from "./expect.ts"; * * // Extends the `Expected` interface with your new matchers signatures * interface ExtendedExpected extends Expected { * // Matcher that asserts value is a dinosaur * toBeDinosaur: (options?: { includeTrexs?: boolean }) => unknown; * * // NOTE: You also need to overrides the following typings to allow modifiers to correctly infer typing * not: IsAsync extends true ? Async> * : ExtendedExpected; * resolves: Async>; * rejects: Async>; * } * * // Call `expect.extend()` with your new matchers definitions * expect.extend({ * toBeDinosaur(context, options) { * const dino = `${context.value}`; * const allowed = ["🦕"]; * if (options?.includeTrexs) { * allowed.push("🦖"); * } * const pass = allowed.includes(dino); * if (context.isNot) { * // Note: when `context.isNot` is set, the test is considered successful when `pass` is false * return { * message: () => `Expected "${dino}" to NOT be a dinosaur`, * pass, * }; * } * return { message: () => `Expected "${dino}" to be a dinosaur`, pass }; * }, * }); * * // Alias expect to avoid having to pass the generic typing argument each time * // This is probably what you want to export and reuse across your tests * const myexpect = expect; * * // Perform some tests * myexpect("🦕").toBeDinosaur(); * myexpect("🦧").not.toBeDinosaur(); * await myexpect(Promise.resolve("🦕")).resolves.toBeDinosaur(); * await myexpect(Promise.resolve("🦧")).resolves.not.toBeDinosaur(); * * // Regular matchers will still be available * myexpect("foo").not.toBeNull() * myexpect.anything * ``` */ expect.extend = setExtendMatchers as (newExtendMatchers: Matchers) => void; /** * `expect.anything()` matches anything but `null` or `undefined`. You can use it * inside `toEqual` or `toHaveBeenCalledWith` instead of a literal value. * * @example * ```js * import { expect, fn } from "@std/expect"; * * Deno.test("map calls its argument with a non-null argument", () => { * const mock = fn(); * [1].map((x) => mock(x)); * expect(mock).toHaveBeenCalledWith(expect.anything()); * }); ``` */ expect.anything = asymmetricMatchers.anything as () => ReturnType< typeof asymmetricMatchers.anything >; /** * `expect.any(constructor)` matches anything that was created with the given * constructor or if it's a primitive that is of the passed type. You can use it * inside `toEqual` or `toHaveBeenCalledWith` instead of a literal value. * * @example * ```ts * import { expect } from "@std/expect"; * * class Cat {} * Deno.test("expect.any()", () => { * expect(new Cat()).toEqual(expect.any(Cat)); * expect("Hello").toEqual(expect.any(String)); * expect(1).toEqual(expect.any(Number)); * expect(() => {}).toEqual(expect.any(Function)); * expect(false).toEqual(expect.any(Boolean)); * expect(BigInt(1)).toEqual(expect.any(BigInt)); * expect(Symbol("sym")).toEqual(expect.any(Symbol)); * }); * ``` */ expect.any = asymmetricMatchers.any as ( c: unknown, ) => ReturnType; /** * `expect.arrayContaining(array)` matches a received array which contains all of * the elements in the expected array. That is, the expected array is a **subset** * of the received array. Therefore, it matches a received array which contains * elements that are **not** in the expected array. * * You can use it instead of a literal value: * * - in `toEqual` or `toHaveBeenCalledWith` * - to match a property in `objectContaining` or `toMatchObject` * * @example * ```ts * import { expect } from "@std/expect"; * * Deno.test("expect.arrayContaining() with array of numbers", () => { * const arr = [1, 2, 3]; * expect([1, 2, 3, 4]).toEqual(expect.arrayContaining(arr)); * expect([4, 5, 6]).not.toEqual(expect.arrayContaining(arr)); * expect([1, 2, 3]).toEqual(expect.arrayContaining(arr)); * }); * ``` */ expect.arrayContaining = asymmetricMatchers.arrayContaining as ( // deno-lint-ignore no-explicit-any c: any[], ) => ReturnType; /** * `expect.closeTo(number, numDigits?)` is useful when comparing floating point * numbers in object properties or array item. If you need to compare a number, * please use `.toBeCloseTo` instead. * * The optional `numDigits` argument limits the number of digits to check **after** * the decimal point. For the default value `2`, the test criterion is * `Math.abs(expected - received) < 0.005 (that is, 10 ** -2 / 2)`. * * @example * ```ts * import { expect } from "@std/expect"; * * Deno.test("compare float in object properties", () => { * expect({ * title: "0.1 + 0.2", * sum: 0.1 + 0.2, * }).toEqual({ * title: "0.1 + 0.2", * sum: expect.closeTo(0.3, 5), * }); * }); * ``` */ expect.closeTo = asymmetricMatchers.closeTo as ( num: number, numDigits?: number, ) => ReturnType; /** * `expect.stringContaining(string)` matches the received value if it is a string * that contains the exact expected string. * * @example * ```ts * import { expect } from "@std/expect"; * * Deno.test("expect.stringContaining() with strings", () => { * expect("https://deno.com/").toEqual(expect.stringContaining("deno")); * expect("function").toEqual(expect.stringContaining("func")); * * expect("Hello, World").not.toEqual(expect.stringContaining("hello")); * expect("foobar").not.toEqual(expect.stringContaining("bazz")); * }); * ``` */ expect.stringContaining = asymmetricMatchers.stringContaining as ( str: string, ) => ReturnType; /** * `expect.stringMatching(string | regexp)` matches the received value if it is a * string that matches the expected string or regular expression. * * You can use it instead of a literal value: * * - in `toEqual` or `toHaveBeenCalledWith` * - to match an element in `arrayContaining` * - to match a property in `objectContaining` (not available yet) or `toMatchObject` * * @example * ```ts * import { expect } from "@std/expect"; * * Deno.test("example", () => { * expect("deno_std").toEqual(expect.stringMatching(/std/)); * expect("0123456789").toEqual(expect.stringMatching(/\d+/)); * expect("e").not.toEqual(expect.stringMatching(/\s/)); * expect("queue").not.toEqual(expect.stringMatching(/en/)); * }); * ``` */ expect.stringMatching = asymmetricMatchers.stringMatching as ( pattern: string | RegExp, ) => ReturnType;