Skip to main content
Using Deno in production at your company? Earn free Deno merch.
Give us feedback
Module

std/expect/_matchers.ts

Deno standard library
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertNotStrictEquals } from "../assert/assert_not_strict_equals.ts";import { assertStrictEquals } from "../assert/assert_strict_equals.ts";import { assertInstanceOf } from "../assert/assert_instance_of.ts";import { assertIsError } from "../assert/assert_is_error.ts";import { assertNotInstanceOf } from "../assert/assert_not_instance_of.ts";import { assertMatch } from "../assert/assert_match.ts";import { assertObjectMatch } from "../assert/assert_object_match.ts";import { assertNotMatch } from "../assert/assert_not_match.ts";import { AssertionError } from "../assert/assertion_error.ts";
import { assertEquals } from "./_assert_equals.ts";import { assertNotEquals } from "./_assert_not_equals.ts";import { equal } from "./_equal.ts";import { format } from "./_format.ts";import type { AnyConstructor, MatcherContext, MatchResult } from "./_types.ts";import { getMockCalls } from "./_mock_util.ts";import { inspectArg, inspectArgs } from "./_inspect_args.ts";import { buildEqualOptions } from "./_utils.ts";
export function toBe(context: MatcherContext, expect: unknown): MatchResult { if (context.isNot) { assertNotStrictEquals(context.value, expect, context.customMessage); } else { assertStrictEquals(context.value, expect, context.customMessage); }}
export function toEqual( context: MatcherContext, expected: unknown,): MatchResult { const v = context.value; const e = expected; const equalsOptions = buildEqualOptions(context);
if (context.isNot) { assertNotEquals(v, e, equalsOptions); } else { assertEquals(v, e, equalsOptions); }}
export function toStrictEqual( context: MatcherContext, expected: unknown,): MatchResult { const equalsOptions = buildEqualOptions({ ...context, strictCheck: true, });
if (context.isNot) { assertNotEquals(context.value, expected, equalsOptions); } else { assertEquals(context.value, expected, equalsOptions); }}
export function toBeCloseTo( context: MatcherContext, expected: number, numDigits = 2,): MatchResult { if (numDigits < 0) { throw new Error( "toBeCloseTo second argument must be a non-negative integer. Got " + numDigits, ); } const tolerance = 0.5 * Math.pow(10, -numDigits); const value = Number(context.value); const pass = Math.abs(expected - value) < tolerance;
if (context.isNot) { if (pass) { throw new AssertionError( `Expected the value not to be close to ${expected} (using ${numDigits} digits), but it is`, ); } } else { if (!pass) { throw new AssertionError( `Expected the value (${value} to be close to ${expected} (using ${numDigits} digits), but it is not`, ); } }}
export function toBeDefined(context: MatcherContext): MatchResult { if (context.isNot) { assertStrictEquals(context.value, undefined, context.customMessage); } else { assertNotStrictEquals(context.value, undefined, context.customMessage); }}
export function toBeUndefined(context: MatcherContext): MatchResult { if (context.isNot) { assertNotStrictEquals( context.value, undefined, context.customMessage, ); } else { assertStrictEquals(context.value, undefined, context.customMessage); }}
export function toBeFalsy( context: MatcherContext,): MatchResult { const isFalsy = !(context.value); if (context.isNot) { if (isFalsy) { throw new AssertionError( `Expected ${context.value} to NOT be falsy`, ); } } else { if (!isFalsy) { throw new AssertionError( `Expected ${context.value} to be falsy`, ); } }}
export function toBeTruthy( context: MatcherContext,): MatchResult { const isTruthy = !!(context.value); if (context.isNot) { if (isTruthy) { throw new AssertionError( `Expected ${context.value} to NOT be truthy`, ); } } else { if (!isTruthy) { throw new AssertionError( `Expected ${context.value} to be truthy`, ); } }}
export function toBeGreaterThanOrEqual( context: MatcherContext, expected: number,): MatchResult { const isGreaterOrEqual = Number(context.value) >= Number(expected); if (context.isNot) { if (isGreaterOrEqual) { throw new AssertionError( `Expected ${context.value} to NOT be greater than or equal ${expected}`, ); } } else { if (!isGreaterOrEqual) { throw new AssertionError( `Expected ${context.value} to be greater than or equal ${expected}`, ); } }}
export function toBeGreaterThan( context: MatcherContext, expected: number,): MatchResult { const isGreater = Number(context.value) > Number(expected); if (context.isNot) { if (isGreater) { throw new AssertionError( `Expected ${context.value} to NOT be greater than ${expected}`, ); } } else { if (!isGreater) { throw new AssertionError( `Expected ${context.value} to be greater than ${expected}`, ); } }}
export function toBeInstanceOf<T extends AnyConstructor>( context: MatcherContext, expected: T,): MatchResult { if (context.isNot) { assertNotInstanceOf(context.value, expected); } else { assertInstanceOf(context.value, expected); }}export function toBeLessThanOrEqual( context: MatcherContext, expected: number,): MatchResult { const isLower = Number(context.value) <= Number(expected); if (context.isNot) { if (isLower) { throw new AssertionError( `Expected ${context.value} to NOT be lower than or equal ${expected}`, ); } } else { if (!isLower) { throw new AssertionError( `Expected ${context.value} to be lower than or equal ${expected}`, ); } }}export function toBeLessThan( context: MatcherContext, expected: number,): MatchResult { const isLower = Number(context.value) < Number(expected); if (context.isNot) { if (isLower) { throw new AssertionError( `Expected ${context.value} to NOT be lower than ${expected}`, ); } } else { if (!isLower) { throw new AssertionError( `Expected ${context.value} to be lower than ${expected}`, ); } }}export function toBeNaN(context: MatcherContext): MatchResult { const equalsOptions = buildEqualOptions(context); if (context.isNot) { assertNotEquals( isNaN(Number(context.value)), true, { ...equalsOptions, msg: equalsOptions.msg || `Expected ${context.value} to not be NaN`, }, ); } else { assertEquals( isNaN(Number(context.value)), true, { ...equalsOptions, msg: equalsOptions.msg || `Expected ${context.value} to be NaN`, }, ); }}
export function toBeNull(context: MatcherContext): MatchResult { if (context.isNot) { assertNotStrictEquals( context.value as number, null, context.customMessage || `Expected ${context.value} to not be null`, ); } else { assertStrictEquals( context.value as number, null, context.customMessage || `Expected ${context.value} to be null`, ); }}
export function toHaveLength( context: MatcherContext, expected: number,): MatchResult { const { value } = context; // deno-lint-ignore no-explicit-any const maybeLength = (value as any)?.length; const hasLength = maybeLength === expected;
if (context.isNot) { if (hasLength) { throw new AssertionError( `Expected value not to have length ${expected}, but it does`, ); } } else { if (!hasLength) { throw new AssertionError( `Expected value to have length ${expected}, but it does not. (The value has length ${maybeLength})`, ); } }}
export function toHaveProperty( context: MatcherContext, propName: string | string[], v?: unknown,): MatchResult { const { value } = context;
let propPath = [] as string[]; if (Array.isArray(propName)) { propPath = propName; } else { propPath = propName.split("."); }
// deno-lint-ignore no-explicit-any let current = value as any; while (true) { if (current === undefined || current === null) { break; } if (propPath.length === 0) { break; } const prop = propPath.shift()!; current = current[prop]; }
let hasProperty; if (v) { hasProperty = current !== undefined && propPath.length === 0 && equal(current, v, context); } else { hasProperty = current !== undefined && propPath.length === 0; }
let ofValue = ""; if (v) { ofValue = ` of the value ${inspectArg(v)}`; }
if (context.isNot) { if (hasProperty) { throw new AssertionError( `Expected the value not to have the property ${ propPath.join(".") }${ofValue}, but it does.`, ); } } else { if (!hasProperty) { throw new AssertionError( `Expected the value to have the property ${ propPath.join(".") }${ofValue}, but it does not.`, ); } }}
export function toContain( context: MatcherContext, expected: unknown,): MatchResult { // deno-lint-ignore no-explicit-any const doesContain = (context.value as any)?.includes?.(expected);
if (context.isNot) { if (doesContain) { throw new AssertionError("The value contains the expected item"); } } else { if (!doesContain) { throw new AssertionError("The value doesn't contain the expected item"); } }}
export function toContainEqual( context: MatcherContext, expected: unknown,): MatchResult { const { value } = context; assertIsIterable(value); let doesContain = false;
for (const item of value) { if (equal(item, expected, context)) { doesContain = true; break; } }
if (context.isNot) { if (doesContain) { throw new AssertionError("The value contains the expected item"); } } else { if (!doesContain) { throw new AssertionError("The value doesn't contain the expected item"); } }}
// deno-lint-ignore no-explicit-anyfunction assertIsIterable(value: any): asserts value is Iterable<unknown> { if (value == null) { throw new AssertionError("The value is null or undefined"); } if (typeof value[Symbol.iterator] !== "function") { throw new AssertionError("The value is not iterable"); }}
export function toMatch( context: MatcherContext, expected: RegExp,): MatchResult { if (context.isNot) { assertNotMatch( String(context.value), expected, context.customMessage, ); } else { assertMatch(String(context.value), expected, context.customMessage); }}
export function toMatchObject( context: MatcherContext, expected: Record<PropertyKey, unknown> | Record<PropertyKey, unknown>[],): MatchResult { if (context.isNot) { let objectMatch = false; try { assertObjectMatch( // deno-lint-ignore no-explicit-any context.value as Record<PropertyKey, any>, expected as Record<PropertyKey, unknown>, context.customMessage, ); objectMatch = true; const actualString = format(context.value); const expectedString = format(expected); throw new AssertionError( `Expected ${actualString} to NOT match ${expectedString}`, ); } catch (e) { if (objectMatch) { throw e; } return; } } else { assertObjectMatch( // deno-lint-ignore no-explicit-any context.value as Record<PropertyKey, any>, expected as Record<PropertyKey, unknown>, context.customMessage, ); }}
export function toHaveBeenCalled(context: MatcherContext): MatchResult { const calls = getMockCalls(context.value); const hasBeenCalled = calls.length > 0;
if (context.isNot) { if (hasBeenCalled) { throw new AssertionError( `Expected mock function not to be called, but it was called ${calls.length} time(s)`, ); } } else { if (!hasBeenCalled) { throw new AssertionError( `Expected mock function to be called, but it was not called`, ); } }}
export function toHaveBeenCalledTimes( context: MatcherContext, expected: number,): MatchResult { const calls = getMockCalls(context.value);
if (context.isNot) { if (calls.length === expected) { throw new AssertionError( `Expected mock function not to be called ${expected} time(s), but it was`, ); } } else { if (calls.length !== expected) { throw new AssertionError( `Expected mock function to be called ${expected} time(s), but it was called ${calls.length} time(s)`, ); } }}
export function toHaveBeenCalledWith( context: MatcherContext, ...expected: unknown[]): MatchResult { const calls = getMockCalls(context.value); const hasBeenCalled = calls.some((call) => equal(call.args, expected));
if (context.isNot) { if (hasBeenCalled) { throw new AssertionError( `Expected mock function not to be called with ${ inspectArgs(expected) }, but it was`, ); } } else { if (!hasBeenCalled) { let otherCalls = ""; if (calls.length > 0) { otherCalls = `\n Other calls:\n ${ calls.map((call) => inspectArgs(call.args)).join("\n ") }`; } throw new AssertionError( `Expected mock function to be called with ${ inspectArgs(expected) }, but it was not.${otherCalls}`, ); } }}export function toHaveBeenLastCalledWith( context: MatcherContext, ...expected: unknown[]): MatchResult { const calls = getMockCalls(context.value); const hasBeenCalled = calls.length > 0 && equal(calls.at(-1)?.args, expected);
if (context.isNot) { if (hasBeenCalled) { throw new AssertionError( `Expected mock function not to be last called with ${ inspectArgs(expected) }, but it was`, ); } } else { if (!hasBeenCalled) { const lastCall = calls.at(-1); if (!lastCall) { throw new AssertionError( `Expected mock function to be last called with ${ inspectArgs(expected) }, but it was not.`, ); } else { throw new AssertionError( `Expected mock function to be last called with ${ inspectArgs(expected) }, but it was last called with ${inspectArgs(lastCall.args)}.`, ); } } }}export function toHaveBeenNthCalledWith( context: MatcherContext, nth: number, ...expected: unknown[]): MatchResult { if (nth < 1) { new Error(`nth must be greater than 0. ${nth} was given.`); }
const calls = getMockCalls(context.value); const callIndex = nth - 1; const hasBeenCalled = calls.length > callIndex && equal(calls[callIndex]?.args, expected);
if (context.isNot) { if (hasBeenCalled) { throw new AssertionError( `Expected the n-th call (n=${nth}) of mock function is not with ${ inspectArgs(expected) }, but it was`, ); } } else { if (!hasBeenCalled) { const nthCall = calls[callIndex]; if (!nthCall) { throw new AssertionError( `Expected the n-th call (n=${nth}) of mock function is with ${ inspectArgs(expected) }, but the n-th call does not exist.`, ); } else { throw new AssertionError( `Expected the n-th call (n=${nth}) of mock function is with ${ inspectArgs(expected) }, but it was with ${inspectArgs(nthCall.args)}.`, ); } } }}
export function toHaveReturned(context: MatcherContext): MatchResult { const calls = getMockCalls(context.value); const returned = calls.filter((call) => call.returns);
if (context.isNot) { if (returned.length > 0) { throw new AssertionError( `Expected the mock function to not have returned, but it returned ${returned.length} times`, ); } } else { if (returned.length === 0) { throw new AssertionError( `Expected the mock function to have returned, but it did not return`, ); } }}
export function toHaveReturnedTimes( context: MatcherContext, expected: number,): MatchResult { const calls = getMockCalls(context.value); const returned = calls.filter((call) => call.returns);
if (context.isNot) { if (returned.length === expected) { throw new AssertionError( `Expected the mock function to not have returned ${expected} times, but it returned ${returned.length} times`, ); } } else { if (returned.length !== expected) { throw new AssertionError( `Expected the mock function to have returned ${expected} times, but it returned ${returned.length} times`, ); } }}export function toHaveReturnedWith( context: MatcherContext, expected: unknown,): MatchResult { const calls = getMockCalls(context.value); const returned = calls.filter((call) => call.returns); const returnedWithExpected = returned.some((call) => equal(call.returned, expected) );
if (context.isNot) { if (returnedWithExpected) { throw new AssertionError( `Expected the mock function to not have returned with ${ inspectArg(expected) }, but it did`, ); } } else { if (!returnedWithExpected) { throw new AssertionError( `Expected the mock function to have returned with ${ inspectArg(expected) }, but it did not`, ); } }}
export function toHaveLastReturnedWith( context: MatcherContext, expected: unknown,): MatchResult { const calls = getMockCalls(context.value); const returned = calls.filter((call) => call.returns); const lastReturnedWithExpected = returned.length > 0 && equal(returned.at(-1)?.returned, expected);
if (context.isNot) { if (lastReturnedWithExpected) { throw new AssertionError( `Expected the mock function to not have last returned with ${ inspectArg(expected) }, but it did`, ); } } else { if (!lastReturnedWithExpected) { throw new AssertionError( `Expected the mock function to have last returned with ${ inspectArg(expected) }, but it did not`, ); } }}
export function toHaveNthReturnedWith( context: MatcherContext, nth: number, expected: unknown,): MatchResult { if (nth < 1) { throw new Error(`nth(${nth}) must be greater than 0`); }
const calls = getMockCalls(context.value); const returned = calls.filter((call) => call.returns); const returnIndex = nth - 1; const maybeNthReturned = returned[returnIndex]; const nthReturnedWithExpected = maybeNthReturned && equal(maybeNthReturned.returned, expected);
if (context.isNot) { if (nthReturnedWithExpected) { throw new AssertionError( `Expected the mock function to not have n-th (n=${nth}) returned with ${ inspectArg(expected) }, but it did`, ); } } else { if (!nthReturnedWithExpected) { throw new AssertionError( `Expected the mock function to have n-th (n=${nth}) returned with ${ inspectArg(expected) }, but it did not`, ); } }}
export function toThrow<E extends Error = Error>( context: MatcherContext, // deno-lint-ignore no-explicit-any expected?: string | RegExp | E | (new (...args: any[]) => E),): MatchResult { if (typeof context.value === "function") { try { context.value = context.value(); } catch (err) { context.value = err; } }
// deno-lint-ignore no-explicit-any type ErrorClass = new (...args: any[]) => Error; let expectClass: undefined | ErrorClass = undefined; let expectMessage: undefined | string | RegExp = undefined; if (expected instanceof Error) { expectClass = expected.constructor as ErrorClass; expectMessage = expected.message; } if (expected instanceof Function) { expectClass = expected as ErrorClass; } if (typeof expected === "string" || expected instanceof RegExp) { expectMessage = expected; }
if (context.isNot) { let isError = false; try { assertIsError( context.value, expectClass, expectMessage, context.customMessage, ); isError = true; throw new AssertionError(`Expected to NOT throw ${expected}`); } catch (e) { if (isError) { throw e; } return; } } return assertIsError( context.value, expectClass, expectMessage, context.customMessage, );}