diff --git a/expect/_inspect_args.ts b/expect/_inspect_args.ts new file mode 100644 index 000000000000..95af662fd7d2 --- /dev/null +++ b/expect/_inspect_args.ts @@ -0,0 +1,13 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file + +export function inspectArgs(args: unknown[]): string { + return args.map(inspectArg).join(", "); +} + +export function inspectArg(arg: unknown): string { + const { Deno } = globalThis as any; + return typeof Deno !== "undefined" && Deno.inspect + ? Deno.inspect(arg) + : String(arg); +} diff --git a/expect/_matchers.ts b/expect/_matchers.ts new file mode 100644 index 000000000000..18d44b3f0ec8 --- /dev/null +++ b/expect/_matchers.ts @@ -0,0 +1,760 @@ +// Copyright 2018-2023 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 { assertNotEquals } from "../assert/assert_not_equals.ts"; +import { assertEquals } from "../assert/assert_equals.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 { equal } from "../assert/equal.ts"; +import { format } from "../assert/_format.ts"; + +import { AnyConstructor, MatcherContext, MatchResult } from "./_types.ts"; +import { getMockCalls } from "./_mock_util.ts"; +import { inspectArg, inspectArgs } from "./_inspect_args.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 { + if (context.isNot) { + assertNotEquals(context.value, expected, context.customMessage); + } else { + assertEquals(context.value, expected, context.customMessage); + } +} + +export function toStrictEqual( + context: MatcherContext, + expected: unknown, +): MatchResult { + if (context.isNot) { + assertNotStrictEquals( + context.value, + expected, + context.customMessage, + ); + } else { + assertStrictEquals(context.value, expected, context.customMessage); + } +} + +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( + 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 { + if (context.isNot) { + assertNotEquals( + isNaN(Number(context.value)), + true, + context.customMessage || `Expected ${context.value} to not be NaN`, + ); + } else { + assertEquals( + isNaN(Number(context.value)), + true, + context.customMessage || `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); + } 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)) { + 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-any +function assertIsIterable(value: any): asserts value is Iterable { + 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, +): MatchResult { + if (context.isNot) { + let objectMatch = false; + try { + assertObjectMatch( + // deno-lint-ignore no-explicit-any + context.value as Record, + expected, + 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, + expected, + 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[calls.length - 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 (!nth) { + 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[returned.length - 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( + context: MatcherContext, + // deno-lint-ignore no-explicit-any + expected: new (...args: any[]) => E, +): MatchResult { + if (typeof context.value === "function") { + try { + context.value = context.value(); + } catch (err) { + context.value = err; + } + } + if (context.isNot) { + let isError = false; + try { + assertIsError(context.value, expected, undefined, context.customMessage); + isError = true; + throw new AssertionError(`Expected to NOT throw ${expected}`); + } catch (e) { + if (isError) { + throw e; + } + return; + } + } + return assertIsError( + context.value, + expected, + undefined, + context.customMessage, + ); +} diff --git a/expect/_mock_util.ts b/expect/_mock_util.ts new file mode 100644 index 000000000000..4a998b0e318b --- /dev/null +++ b/expect/_mock_util.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file no-explicit-any + +export const MOCK_SYMBOL = Symbol.for("@MOCK"); + +export type MockCall = { + args: any[]; + returned?: any; + thrown?: any; + timestamp: number; + returns: boolean; + throws: boolean; +}; + +export function getMockCalls(f: any): MockCall[] { + const mockInfo = f[MOCK_SYMBOL]; + if (!mockInfo) { + throw new Error("Received function must be a mock or spy function"); + } + + return [...mockInfo.calls]; +} diff --git a/expect/_to_be_close_to_test.ts b/expect/_to_be_close_to_test.ts new file mode 100644 index 000000000000..538d5e986f25 --- /dev/null +++ b/expect/_to_be_close_to_test.ts @@ -0,0 +1,21 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeCloseTo()", () => { + expect(0.2 + 0.1).toBeCloseTo(0.3); + expect(0.2 + 0.1).toBeCloseTo(0.3, 5); + expect(0.2 + 0.1).toBeCloseTo(0.3, 15); + + expect(0.2 + 0.11).not.toBeCloseTo(0.3); + expect(0.2 + 0.1).not.toBeCloseTo(0.3, 16); + + assertThrows(() => { + expect(0.2 + 0.11).toBeCloseTo(0.3); + }, AssertionError); + + assertThrows(() => { + expect(0.2 + 0.1).not.toBeCloseTo(0.3); + }); +}); diff --git a/expect/_to_be_defined_test.ts b/expect/_to_be_defined_test.ts new file mode 100644 index 000000000000..c32b7467be94 --- /dev/null +++ b/expect/_to_be_defined_test.ts @@ -0,0 +1,27 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file no-explicit-any + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeDefined()", () => { + expect(1).toBeDefined(); + expect("a").toBeDefined(); + + expect(undefined).not.toBeDefined(); + expect(({} as any).foo).not.toBeDefined(); + + assertThrows(() => { + expect(undefined).toBeDefined(); + }, AssertionError); + assertThrows(() => { + expect(({} as any).foo).toBeDefined(); + }, AssertionError); + + assertThrows(() => { + expect(1).not.toBeDefined(); + }, AssertionError); + assertThrows(() => { + expect("a").not.toBeDefined(); + }, AssertionError); +}); diff --git a/expect/_to_be_falsy_test.ts b/expect/_to_be_falsy_test.ts new file mode 100644 index 000000000000..b376ba40b98b --- /dev/null +++ b/expect/_to_be_falsy_test.ts @@ -0,0 +1,34 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeFalsy()", () => { + expect(false).toBeFalsy(); + expect(0).toBeFalsy(); + expect("").toBeFalsy(); + + expect(true).not.toBeFalsy(); + expect(1).not.toBeFalsy(); + expect("hello").not.toBeFalsy(); + + assertThrows(() => { + expect(true).toBeFalsy(); + }, AssertionError); + assertThrows(() => { + expect(1).toBeFalsy(); + }, AssertionError); + assertThrows(() => { + expect("hello").toBeFalsy(); + }, AssertionError); + + assertThrows(() => { + expect(false).not.toBeFalsy(); + }, AssertionError); + assertThrows(() => { + expect(0).not.toBeFalsy(); + }, AssertionError); + assertThrows(() => { + expect("").not.toBeFalsy(); + }, AssertionError); +}); diff --git a/expect/_to_be_greater_than_or_equal_test.ts b/expect/_to_be_greater_than_or_equal_test.ts new file mode 100644 index 000000000000..95757ba6d9f2 --- /dev/null +++ b/expect/_to_be_greater_than_or_equal_test.ts @@ -0,0 +1,19 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeGreaterThanOrEqual()", () => { + expect(10).toBeGreaterThanOrEqual(10); + expect(11).toBeGreaterThanOrEqual(10); + + expect(9).not.toBeGreaterThanOrEqual(10); + + assertThrows(() => { + expect(9).toBeGreaterThanOrEqual(10); + }, AssertionError); + + assertThrows(() => { + expect(11).not.toBeGreaterThanOrEqual(10); + }, AssertionError); +}); diff --git a/expect/_to_be_greater_than_test.ts b/expect/_to_be_greater_than_test.ts new file mode 100644 index 000000000000..b52a924852a3 --- /dev/null +++ b/expect/_to_be_greater_than_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeGreaterThan()", () => { + expect(11).toBeGreaterThan(10); + + expect(10).not.toBeGreaterThan(10); + expect(9).not.toBeGreaterThan(10); + + assertThrows(() => { + expect(10).toBeGreaterThan(10); + }, AssertionError); + assertThrows(() => { + expect(9).toBeGreaterThan(10); + }); + + assertThrows(() => { + expect(11).not.toBeGreaterThan(10); + }, AssertionError); +}); diff --git a/expect/_to_be_instance_of_test.ts b/expect/_to_be_instance_of_test.ts new file mode 100644 index 000000000000..39f07bc2e568 --- /dev/null +++ b/expect/_to_be_instance_of_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeInstanceOf", () => { + expect(new Error()).toBeInstanceOf(Error); + expect(new Error()).toBeInstanceOf(Object); + + expect(new Error()).not.toBeInstanceOf(String); + + assertThrows(() => { + expect(new Error()).toBeInstanceOf(String); + }, AssertionError); + + assertThrows(() => { + expect(new Error()).not.toBeInstanceOf(Error); + }, AssertionError); + assertThrows(() => { + expect(new Error()).not.toBeInstanceOf(Object); + }, AssertionError); +}); diff --git a/expect/_to_be_less_than_or_equal_test.ts b/expect/_to_be_less_than_or_equal_test.ts new file mode 100644 index 000000000000..aa7d7d058738 --- /dev/null +++ b/expect/_to_be_less_than_or_equal_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeLessThanOrEqual", () => { + expect(10).toBeLessThanOrEqual(10); + expect(9).toBeLessThanOrEqual(10); + + expect(11).not.toBeLessThanOrEqual(10); + + assertThrows(() => { + expect(11).toBeLessThanOrEqual(10); + }, AssertionError); + + assertThrows(() => { + expect(10).not.toBeLessThanOrEqual(10); + }, AssertionError); + assertThrows(() => { + expect(9).not.toBeLessThanOrEqual(10); + }, AssertionError); +}); diff --git a/expect/_to_be_less_than_test.ts b/expect/_to_be_less_than_test.ts new file mode 100644 index 000000000000..cf6749b837d2 --- /dev/null +++ b/expect/_to_be_less_than_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeLessThan()", () => { + expect(9).toBeLessThan(10); + + expect(10).not.toBeLessThan(10); + expect(11).not.toBeLessThan(10); + + assertThrows(() => { + expect(10).toBeLessThan(10); + }, AssertionError); + assertThrows(() => { + expect(11).toBeLessThan(10); + }, AssertionError); + + assertThrows(() => { + expect(9).not.toBeLessThan(10); + }, AssertionError); +}); diff --git a/expect/_to_be_nan_test.ts b/expect/_to_be_nan_test.ts new file mode 100644 index 000000000000..603083daabf2 --- /dev/null +++ b/expect/_to_be_nan_test.ts @@ -0,0 +1,18 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeNaN()", () => { + expect(NaN).toBeNaN(); + + expect(1).not.toBeNaN(); + + assertThrows(() => { + expect(1).toBeNaN(); + }, AssertionError); + + assertThrows(() => { + expect(NaN).not.toBeNaN(); + }, AssertionError); +}); diff --git a/expect/_to_be_null_test.ts b/expect/_to_be_null_test.ts new file mode 100644 index 000000000000..3b79efab99dc --- /dev/null +++ b/expect/_to_be_null_test.ts @@ -0,0 +1,18 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeNull()", () => { + expect(null).toBeNull(); + + expect(undefined).not.toBeNull(); + + assertThrows(() => { + expect(undefined).toBeNull(); + }, AssertionError); + + assertThrows(() => { + expect(null).not.toBeNull(); + }, AssertionError); +}); diff --git a/expect/_to_be_test.ts b/expect/_to_be_test.ts new file mode 100644 index 000000000000..c90fccaedc30 --- /dev/null +++ b/expect/_to_be_test.ts @@ -0,0 +1,35 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBe()", () => { + const obj = {}; + expect(1).toBe(1); + expect("hello").toBe("hello"); + expect(obj).toBe(obj); + + expect(1).not.toBe(2); + expect("hello").not.toBe("world"); + expect(obj).not.toBe({}); + + assertThrows(() => { + expect(1).toBe(2); + }, AssertionError); + assertThrows(() => { + expect("hello").toBe("world"); + }, AssertionError); + assertThrows(() => { + expect(obj).toBe({}); + }, AssertionError); + + assertThrows(() => { + expect(1).not.toBe(1); + }, AssertionError); + assertThrows(() => { + expect("hello").not.toBe("hello"); + }, AssertionError); + assertThrows(() => { + expect(obj).not.toBe(obj); + }, AssertionError); +}); diff --git a/expect/_to_be_truthy_test.ts b/expect/_to_be_truthy_test.ts new file mode 100644 index 000000000000..8d7c28852ab4 --- /dev/null +++ b/expect/_to_be_truthy_test.ts @@ -0,0 +1,38 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeTruthy()", () => { + expect(1).toBeTruthy(); + expect("hello").toBeTruthy(); + expect({}).toBeTruthy(); + + expect(0).not.toBeTruthy(); + expect("").not.toBeTruthy(); + expect(null).not.toBeTruthy(); + expect(undefined).not.toBeTruthy(); + + assertThrows(() => { + expect(0).toBeTruthy(); + }, AssertionError); + assertThrows(() => { + expect("").toBeTruthy(); + }, AssertionError); + assertThrows(() => { + expect(null).toBeTruthy(); + }, AssertionError); + assertThrows(() => { + expect(undefined).toBeTruthy(); + }, AssertionError); + + assertThrows(() => { + expect(1).not.toBeTruthy(); + }, AssertionError); + assertThrows(() => { + expect("hello").not.toBeTruthy(); + }, AssertionError); + assertThrows(() => { + expect({}).not.toBeTruthy(); + }, AssertionError); +}); diff --git a/expect/_to_be_undefined_test.ts b/expect/_to_be_undefined_test.ts new file mode 100644 index 000000000000..d44041411567 --- /dev/null +++ b/expect/_to_be_undefined_test.ts @@ -0,0 +1,18 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toBeUndefined()", () => { + expect(undefined).toBeUndefined(); + + expect(null).not.toBeUndefined(); + + assertThrows(() => { + expect(null).toBeUndefined(); + }, AssertionError); + + assertThrows(() => { + expect(undefined).not.toBeUndefined(); + }, AssertionError); +}); diff --git a/expect/_to_contain_equal_test.ts b/expect/_to_contain_equal_test.ts new file mode 100644 index 000000000000..3db06fcb93c9 --- /dev/null +++ b/expect/_to_contain_equal_test.ts @@ -0,0 +1,19 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toContainEqual()", () => { + const arr = [{ foo: 42 }, { bar: 43 }, { baz: 44 }]; + expect(arr).toContainEqual({ bar: 43 }); + + expect(arr).not.toContainEqual({ bar: 42 }); + + assertThrows(() => { + expect(arr).toContainEqual({ bar: 42 }); + }, AssertionError); + + assertThrows(() => { + expect(arr).not.toContainEqual({ bar: 43 }); + }, AssertionError); +}); diff --git a/expect/_to_contain_test.ts b/expect/_to_contain_test.ts new file mode 100644 index 000000000000..b50e76f2d910 --- /dev/null +++ b/expect/_to_contain_test.ts @@ -0,0 +1,28 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toContain()", () => { + const arr = [1, 2, 3]; + + expect(arr).toContain(2); + expect("foobarbaz").toContain("bar"); + + expect(arr).not.toContain(4); + expect("foobarbaz").not.toContain("qux"); + + assertThrows(() => { + expect(arr).toContain(4); + }, AssertionError); + assertThrows(() => { + expect("foobarbaz").toContain("qux"); + }, AssertionError); + + assertThrows(() => { + expect(arr).not.toContain(2); + }, AssertionError); + assertThrows(() => { + expect("foobarbaz").not.toContain("bar"); + }, AssertionError); +}); diff --git a/expect/_to_equal_test.ts b/expect/_to_equal_test.ts new file mode 100644 index 000000000000..438f23576eed --- /dev/null +++ b/expect/_to_equal_test.ts @@ -0,0 +1,220 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { + bold, + gray, + green, + red, + stripAnsiCode, + yellow, +} from "../fmt/colors.ts"; +import { assertThrows } from "../assert/assert_throws.ts"; +import { AssertionError } from "../assert/assertion_error.ts"; +import { expect } from "./expect.ts"; + +const createHeader = (): string[] => [ + "", + "", + ` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${ + green( + bold("Expected"), + ) + }`, + "", + "", +]; + +const added: (s: string) => string = (s: string): string => + green(bold(stripAnsiCode(s))); +const removed: (s: string) => string = (s: string): string => + red(bold(stripAnsiCode(s))); + +Deno.test({ + name: "pass case", + fn() { + expect({ a: 10 }).toEqual({ a: 10 }); + expect(true).toEqual(true); + expect(10).toEqual(10); + expect("abc").toEqual("abc"); + expect({ a: 10, b: { c: "1" } }).toEqual({ a: 10, b: { c: "1" } }); + expect(new Date("invalid")).toEqual(new Date("invalid")); + }, +}); + +Deno.test({ + name: "failed with number", + fn() { + assertThrows( + () => expect(1).toEqual(2), + AssertionError, + [ + "Values are not equal.", + ...createHeader(), + removed(`- ${yellow("1")}`), + added(`+ ${yellow("2")}`), + "", + ].join("\n"), + ); + }, +}); + +Deno.test({ + name: "failed with number vs string", + fn() { + assertThrows( + () => expect(1).toEqual("1"), + AssertionError, + [ + "Values are not equal.", + ...createHeader(), + removed(`- ${yellow("1")}`), + added(`+ "1"`), + ].join("\n"), + ); + }, +}); + +Deno.test({ + name: "failed with array", + fn() { + assertThrows( + () => expect([1, "2", 3]).toEqual(["1", "2", 3]), + AssertionError, + ` + [ +- 1, ++ "1", + "2", + 3, + ]`, + ); + }, +}); + +Deno.test({ + name: "failed with object", + fn() { + assertThrows( + () => expect({ a: 1, b: "2", c: 3 }).toEqual({ a: 1, b: 2, c: [3] }), + AssertionError, + ` + { + a: 1, ++ b: 2, ++ c: [ ++ 3, ++ ], +- b: "2", +- c: 3, + }`, + ); + }, +}); + +Deno.test({ + name: "failed with date", + fn() { + assertThrows( + () => + expect(new Date(2019, 0, 3, 4, 20, 1, 10)).toEqual( + new Date(2019, 0, 3, 4, 20, 1, 20), + ), + AssertionError, + [ + "Values are not equal.", + ...createHeader(), + removed(`- ${new Date(2019, 0, 3, 4, 20, 1, 10).toISOString()}`), + added(`+ ${new Date(2019, 0, 3, 4, 20, 1, 20).toISOString()}`), + "", + ].join("\n"), + ); + assertThrows( + () => + expect(new Date("invalid")).toEqual(new Date(2019, 0, 3, 4, 20, 1, 20)), + AssertionError, + [ + "Values are not equal.", + ...createHeader(), + removed(`- ${new Date("invalid")}`), + added(`+ ${new Date(2019, 0, 3, 4, 20, 1, 20).toISOString()}`), + "", + ].join("\n"), + ); + }, +}); + +Deno.test({ + name: "failed with custom msg", + fn() { + assertThrows( + () => expect(1, "CUSTOM MESSAGE").toEqual(2), + AssertionError, + [ + "Values are not equal: CUSTOM MESSAGE", + ...createHeader(), + removed(`- ${yellow("1")}`), + added(`+ ${yellow("2")}`), + "", + ].join("\n"), + ); + }, +}); + +Deno.test( + "expect().toEqual compares objects structurally if one object's constructor is undefined and the other is Object", + () => { + const a = Object.create(null); + a.prop = "test"; + const b = { + prop: "test", + }; + + expect(a).toEqual(b); + expect(b).toEqual(a); + }, +); + +Deno.test("expect().toEqual diff for differently ordered objects", () => { + assertThrows( + () => { + expect({ + aaaaaaaaaaaaaaaaaaaaaaaa: 0, + bbbbbbbbbbbbbbbbbbbbbbbb: 0, + ccccccccccccccccccccccc: 0, + }).toEqual( + { + ccccccccccccccccccccccc: 1, + aaaaaaaaaaaaaaaaaaaaaaaa: 0, + bbbbbbbbbbbbbbbbbbbbbbbb: 0, + }, + ); + }, + AssertionError, + ` + { + aaaaaaaaaaaaaaaaaaaaaaaa: 0, + bbbbbbbbbbbbbbbbbbbbbbbb: 0, +- ccccccccccccccccccccccc: 0, ++ ccccccccccccccccccccccc: 1, + }`, + ); +}); + +Deno.test("expect().toEqual same Set with object keys", () => { + const data = [ + { + id: "_1p7ZED73OF98VbT1SzSkjn", + type: { id: "_ETGENUS" }, + name: "Thuja", + friendlyId: "g-thuja", + }, + { + id: "_567qzghxZmeQ9pw3q09bd3", + type: { id: "_ETGENUS" }, + name: "Pinus", + friendlyId: "g-pinus", + }, + ]; + expect(data).toEqual(data); + expect(new Set(data)).toEqual(new Set(data)); +}); diff --git a/expect/_to_have_been_called_test.ts b/expect/_to_have_been_called_test.ts new file mode 100644 index 000000000000..00d41a1cefd6 --- /dev/null +++ b/expect/_to_have_been_called_test.ts @@ -0,0 +1,10 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; + +Deno.test("expect().toHaveBeenCalled()", () => { + const mockFn = fn(); + mockFn(); + expect(mockFn).toHaveBeenCalled(); +}); diff --git a/expect/_to_have_been_called_times_test.ts b/expect/_to_have_been_called_times_test.ts new file mode 100644 index 000000000000..a6eec1cd0601 --- /dev/null +++ b/expect/_to_have_been_called_times_test.ts @@ -0,0 +1,21 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveBeenCalledTimes()", () => { + const mockFn = fn(); + mockFn(); + expect(mockFn).toHaveBeenCalledTimes(1); + + expect(mockFn).not.toHaveBeenCalledTimes(2); + + assertThrows(() => { + expect(mockFn).toHaveBeenCalledTimes(2); + }, AssertionError); + + assertThrows(() => { + expect(mockFn).not.toHaveBeenCalledTimes(1); + }, AssertionError); +}); diff --git a/expect/_to_have_been_called_with_test.ts b/expect/_to_have_been_called_with_test.ts new file mode 100644 index 000000000000..d5cfabaec0e7 --- /dev/null +++ b/expect/_to_have_been_called_with_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveBeenCalledWith()", () => { + const mockFn = fn(); + mockFn("hello", "deno"); + + expect(mockFn).toHaveBeenCalledWith("hello", "deno"); + + expect(mockFn).not.toHaveBeenCalledWith("hello", "DENO"); + + assertThrows(() => { + expect(mockFn).toHaveBeenCalledWith("hello", "DENO"); + }, AssertionError); + + assertThrows(() => { + expect(mockFn).not.toHaveBeenCalledWith("hello", "deno"); + }); +}); diff --git a/expect/_to_have_been_last_called_with_test.ts b/expect/_to_have_been_last_called_with_test.ts new file mode 100644 index 000000000000..9312f6f89983 --- /dev/null +++ b/expect/_to_have_been_last_called_with_test.ts @@ -0,0 +1,24 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveBeenLastCalledWith()", () => { + const mockFn = fn(); + + mockFn(1, 2, 3); + mockFn(4, 5, 6); + + expect(mockFn).toHaveBeenLastCalledWith(4, 5, 6); + + expect(mockFn).not.toHaveBeenLastCalledWith(1, 2, 3); + + assertThrows(() => { + expect(mockFn).toHaveBeenLastCalledWith(1, 2, 3); + }, AssertionError); + + assertThrows(() => { + expect(mockFn).not.toHaveBeenLastCalledWith(4, 5, 6); + }, AssertionError); +}); diff --git a/expect/_to_have_been_nth_called_with_test.ts b/expect/_to_have_been_nth_called_with_test.ts new file mode 100644 index 000000000000..6a268ccf18ab --- /dev/null +++ b/expect/_to_have_been_nth_called_with_test.ts @@ -0,0 +1,29 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().", () => { + const mockFn = fn(); + + mockFn(1, 2, 3); + mockFn(4, 5, 6); + mockFn(7, 8, 9); + + expect(mockFn).toHaveBeenNthCalledWith(2, 4, 5, 6); + + expect(mockFn).not.toHaveBeenNthCalledWith(2, 1, 2, 3); + expect(mockFn).not.toHaveBeenNthCalledWith(1, 4, 5, 6); + + assertThrows(() => { + expect(mockFn).toHaveBeenNthCalledWith(2, 1, 2, 3); + }, AssertionError); + assertThrows(() => { + expect(mockFn).toHaveBeenNthCalledWith(1, 4, 5, 6); + }, AssertionError); + + assertThrows(() => { + expect(mockFn).not.toHaveBeenNthCalledWith(2, 4, 5, 6); + }); +}); diff --git a/expect/_to_have_last_returned_with_test.ts b/expect/_to_have_last_returned_with_test.ts new file mode 100644 index 000000000000..9403ccef5f5c --- /dev/null +++ b/expect/_to_have_last_returned_with_test.ts @@ -0,0 +1,24 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveLastReturnedWith()", () => { + const mockFn = fn((x: number) => x + 3); + + mockFn(1); + mockFn(4); + + expect(mockFn).toHaveLastReturnedWith(7); + + expect(mockFn).not.toHaveLastReturnedWith(4); + + assertThrows(() => { + expect(mockFn).toHaveLastReturnedWith(4); + }, AssertionError); + + assertThrows(() => { + expect(mockFn).not.toHaveLastReturnedWith(7); + }, AssertionError); +}); diff --git a/expect/_to_have_lenth_test.ts b/expect/_to_have_lenth_test.ts new file mode 100644 index 000000000000..a7184010216c --- /dev/null +++ b/expect/_to_have_lenth_test.ts @@ -0,0 +1,26 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveLength()", () => { + expect([1, 2, 3]).toHaveLength(3); + expect("abc").toHaveLength(3); + + expect([1, 2, 3]).not.toHaveLength(4); + expect("abc").not.toHaveLength(4); + + assertThrows(() => { + expect([1, 2, 3]).toHaveLength(4); + }, AssertionError); + assertThrows(() => { + expect("abc").toHaveLength(4); + }, AssertionError); + + assertThrows(() => { + expect([1, 2, 3]).not.toHaveLength(3); + }, AssertionError); + assertThrows(() => { + expect("abc").not.toHaveLength(3); + }, AssertionError); +}); diff --git a/expect/_to_have_nth_returned_with_test.ts b/expect/_to_have_nth_returned_with_test.ts new file mode 100644 index 000000000000..e9f9ba5ac8e2 --- /dev/null +++ b/expect/_to_have_nth_returned_with_test.ts @@ -0,0 +1,36 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveNthReturnedWith()", () => { + const mockFn = fn((x: number) => x + 7); + + mockFn(1); + mockFn(10); + mockFn(100); + mockFn(1000); + + expect(mockFn).toHaveNthReturnedWith(1, 8); + expect(mockFn).toHaveNthReturnedWith(2, 17); + expect(mockFn).toHaveNthReturnedWith(3, 107); + expect(mockFn).toHaveNthReturnedWith(4, 1007); + + expect(mockFn).not.toHaveNthReturnedWith(1, 1); + expect(mockFn).not.toHaveNthReturnedWith(2, 10); + expect(mockFn).not.toHaveNthReturnedWith(3, 100); + expect(mockFn).not.toHaveNthReturnedWith(4, 1000); + + assertThrows(() => { + expect(mockFn).toHaveNthReturnedWith(1, 1); + }, AssertionError); + + assertThrows(() => { + expect(mockFn).not.toHaveNthReturnedWith(1, 8); + }, AssertionError); + + assertThrows(() => { + expect(mockFn).toHaveNthReturnedWith(0, 0); + }, Error); +}); diff --git a/expect/_to_have_property_test.ts b/expect/_to_have_property_test.ts new file mode 100644 index 000000000000..d8b6015a2309 --- /dev/null +++ b/expect/_to_have_property_test.ts @@ -0,0 +1,23 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveProperty()", () => { + expect({ a: 1 }).toHaveProperty("a"); + expect({ a: 1 }).toHaveProperty("a", 1); + expect({ a: { b: 1 } }).toHaveProperty("a.b", 1); + expect({ a: { b: 1 } }).toHaveProperty(["a", "b"], 1); + expect({ a: { b: { c: { d: 5 } } } }).toHaveProperty("a.b.c", { d: 5 }); + expect({ a: { b: { c: { d: 5 } } } }).toHaveProperty("a.b.c.d", 5); + + expect({ a: { b: { c: { d: 5 } } } }).not.toHaveProperty("a.b.c", { d: 6 }); + + assertThrows(() => { + expect({ a: { b: { c: { d: 5 } } } }).toHaveProperty("a.b.c", { d: 6 }); + }, AssertionError); + + assertThrows(() => { + expect({ a: { b: { c: { d: 5 } } } }).not.toHaveProperty("a.b.c", { d: 5 }); + }, AssertionError); +}); diff --git a/expect/_to_have_returned_test.ts b/expect/_to_have_returned_test.ts new file mode 100644 index 000000000000..6c12c110ae1e --- /dev/null +++ b/expect/_to_have_returned_test.ts @@ -0,0 +1,31 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveReturned()", () => { + const mockFn0 = fn(); + const mockFn1 = fn(() => { + throw new Error("foo"); + }); + + mockFn0(); + try { + mockFn1(); + } catch { + // ignore + } + + expect(mockFn0).toHaveReturned(); + + expect(mockFn1).not.toHaveReturned(); + + assertThrows(() => { + expect(mockFn1).toHaveReturned(); + }, AssertionError); + + assertThrows(() => { + expect(mockFn0).not.toHaveReturned(); + }, AssertionError); +}); diff --git a/expect/_to_have_returned_times_test.ts b/expect/_to_have_returned_times_test.ts new file mode 100644 index 000000000000..2bd0abe3d7eb --- /dev/null +++ b/expect/_to_have_returned_times_test.ts @@ -0,0 +1,24 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveReturnedTimes()", () => { + const mockFn = fn(); + + mockFn(); + mockFn(); + + expect(mockFn).toHaveReturnedTimes(2); + + expect(mockFn).not.toHaveReturnedTimes(1); + + assertThrows(() => { + expect(mockFn).toHaveReturnedTimes(1); + }, AssertionError); + + assertThrows(() => { + expect(mockFn).not.toHaveReturnedTimes(2); + }, AssertionError); +}); diff --git a/expect/_to_have_returned_with.ts b/expect/_to_have_returned_with.ts new file mode 100644 index 000000000000..d71fd210d4a9 --- /dev/null +++ b/expect/_to_have_returned_with.ts @@ -0,0 +1,36 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { MatcherContext, MatchResult } from "./_types.ts"; +import { AssertionError } from "../assert/assertion_error.ts"; +import { equal } from "../assert/equal.ts"; +import { getMockCalls } from "./_mock_util.ts"; +import { inspectArg } from "./_inspect_args.ts"; + +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`, + ); + } + } +} diff --git a/expect/_to_have_returned_with_test.ts b/expect/_to_have_returned_with_test.ts new file mode 100644 index 000000000000..29e72912f0d7 --- /dev/null +++ b/expect/_to_have_returned_with_test.ts @@ -0,0 +1,24 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { fn } from "./fn.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toHaveReturnedWith()", () => { + const mockFn = fn((x: number) => ({ foo: x + 1 })); + + mockFn(5); + mockFn(6); + + expect(mockFn).toHaveReturnedWith({ foo: 7 }); + + expect(mockFn).not.toHaveReturnedWith({ foo: 5 }); + + assertThrows(() => { + expect(mockFn).toHaveReturnedWith({ foo: 5 }); + }, AssertionError); + + assertThrows(() => { + expect(mockFn).not.toHaveReturnedWith({ foo: 7 }); + }, AssertionError); +}); diff --git a/expect/_to_match_object_test.ts b/expect/_to_match_object_test.ts new file mode 100644 index 000000000000..96a9d8f3473e --- /dev/null +++ b/expect/_to_match_object_test.ts @@ -0,0 +1,44 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toMatchObject()", () => { + const house0 = { + bath: true, + bedrooms: 4, + kitchen: { + amenities: ["oven", "stove", "washer"], + area: 20, + wallColor: "white", + }, + }; + const house1 = { + bath: true, + bedrooms: 4, + kitchen: { + amenities: ["oven", "stove"], + area: 20, + wallColor: "white", + }, + }; + const desiredHouse = { + bath: true, + kitchen: { + amenities: ["oven", "stove", "washer"], + wallColor: "white", + }, + }; + + expect(house0).toMatchObject(desiredHouse); + + expect(house1).not.toMatchObject(desiredHouse); + + assertThrows(() => { + expect(house1).toMatchObject(desiredHouse); + }, AssertionError); + + assertThrows(() => { + expect(house0).not.toMatchObject(desiredHouse); + }, AssertionError); +}); diff --git a/expect/_to_match_test.ts b/expect/_to_match_test.ts new file mode 100644 index 000000000000..7b5ad0147d75 --- /dev/null +++ b/expect/_to_match_test.ts @@ -0,0 +1,18 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toMatch()", () => { + expect("hello deno").toMatch(/deno/); + + expect("hello deno").not.toMatch(/DENO/); + + assertThrows(() => { + expect("hello deno").toMatch(/DENO/); + }, AssertionError); + + assertThrows(() => { + expect("hello deno").not.toMatch(/deno/); + }); +}); diff --git a/expect/_to_strict_equal_test.ts b/expect/_to_strict_equal_test.ts new file mode 100644 index 000000000000..11d6da6787b1 --- /dev/null +++ b/expect/_to_strict_equal_test.ts @@ -0,0 +1,27 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toStrictEqual()", () => { + const obj = { a: 1 }; + expect(1).toStrictEqual(1); + expect(obj).toStrictEqual(obj); + + expect(1).not.toStrictEqual(2); + expect(obj).not.toStrictEqual({ a: 1 }); + + assertThrows(() => { + expect(1).toStrictEqual(2); + }, AssertionError); + assertThrows(() => { + expect(obj).toStrictEqual({ a: 1 }); + }, AssertionError); + + assertThrows(() => { + expect(1).not.toStrictEqual(1); + }, AssertionError); + assertThrows(() => { + expect(obj).not.toStrictEqual(obj); + }, AssertionError); +}); diff --git a/expect/_to_throw_test.ts b/expect/_to_throw_test.ts new file mode 100644 index 000000000000..efd863d69936 --- /dev/null +++ b/expect/_to_throw_test.ts @@ -0,0 +1,22 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; + +Deno.test("expect().toThrow()", () => { + expect(() => { + throw new Error("hello world"); + }).toThrow(); + + expect(() => {}).not.toThrow(); + + assertThrows(() => { + expect(() => {}).toThrow(); + }, AssertionError); + + assertThrows(() => { + expect(() => { + throw new Error("hello world"); + }).not.toThrow(); + }, AssertionError); +}); diff --git a/expect/_types.ts b/expect/_types.ts new file mode 100644 index 000000000000..eba03d095a20 --- /dev/null +++ b/expect/_types.ts @@ -0,0 +1,19 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file no-explicit-any + +export interface MatcherContext { + value: unknown; + isNot: boolean; + customMessage: string | undefined; +} + +export type Matcher = ( + context: MatcherContext, + ...args: any[] +) => MatchResult; + +export type Matchers = { + [key: string]: Matcher; +}; +export type MatchResult = void | Promise | boolean; +export type AnyConstructor = new (...args: any[]) => any; diff --git a/expect/expect.ts b/expect/expect.ts new file mode 100644 index 000000000000..425175dca152 --- /dev/null +++ b/expect/expect.ts @@ -0,0 +1,226 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2019 Allain Lalonde. All rights reserved. ISC License. + +import type { AnyConstructor, Matcher, MatcherContext } from "./_types.ts"; +import { AssertionError } from "../assert/assertion_error.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"; +export interface Expected { + lastCalledWith(...expected: unknown[]): void; + lastReturnedWith(expected: unknown): void; + nthCalledWith(nth: number, ...expected: unknown[]): void; + nthReturnedWith(nth: number, expected: unknown): void; + toBeCalled(): void; + toBeCalledTimes(expected: number): void; + toBeCalledWith(...expected: unknown[]): void; + toBeCloseTo(candidate: number, tolerance?: number): void; + toBeDefined(): void; + toBeFalsy(): void; + toBeGreaterThan(expected: number): void; + toBeGreaterThanOrEqual(expected: number): void; + toBeInstanceOf(expected: T): void; + toBeLessThan(expected: number): void; + toBeLessThanOrEqual(expected: number): void; + toBeNaN(): void; + toBeNull(): void; + toBeTruthy(): void; + toBeUndefined(): void; + toBe(expected: unknown): void; + toContainEqual(expected: unknown): void; + toContain(expected: unknown): void; + toEqual(expected: unknown): void; + toHaveBeenCalledTimes(expected: number): void; + toHaveBeenCalledWith(...expected: unknown[]): void; + toHaveBeenCalled(): void; + toHaveBeenLastCalledWith(...expected: unknown[]): void; + toHaveBeenNthCalledWith(nth: number, ...expected: unknown[]): void; + toHaveLength(expected: number): void; + toHaveLastReturnedWith(expected: unknown): void; + toHaveNthReturnedWith(nth: number, expected: unknown): void; + toHaveProperty(propName: string | string[], value?: unknown): void; + toHaveReturnedTimes(expected: number): void; + toHaveReturnedWith(expected: unknown): void; + toHaveReturned(): void; + toMatch(expected: RegExp): void; + toMatchObject(expected: Record): void; + toReturn(): void; + toReturnTimes(expected: number): void; + toReturnWith(expected: unknown): void; + toStrictEqual(candidate: unknown): void; + // deno-lint-ignore no-explicit-any + toThrow(expected?: new (...args: any[]) => E): void; + not: Expected; + resolves: Async; + rejects: Async; +} + +type MatcherKey = keyof Omit; + +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, +}; + +export function expect(value: unknown, customMessage?: string): Expected { + let isNot = false; + let isPromised = false; + const self: Expected = 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 matcher: Matcher = matchers[name as MatcherKey]; + 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, + isNot: false, + customMessage, + }; + if (isNot) { + context.isNot = true; + } + matcher(context, ...args); + } + + return isPromised + ? (value as Promise).then((value: unknown) => + applyMatcher(value, args) + ) + : applyMatcher(value, args); + }; + }, + }, + ); + + return self; +} + +// a helper type to match any function. Used so that we only convert functions +// to return a promise and not properties. +type Fn = (...args: unknown[]) => unknown; + +// converts all the methods in an interface to be async functions +export type Async = { + [K in keyof T]: T[K] extends Fn + ? (...args: Parameters) => Promise> + : T[K]; +}; + +function isPromiseLike(value: unknown): value is PromiseLike { + if (value == null) { + return false; + } else { + return typeof ((value as Record).then) === "function"; + } +} diff --git a/expect/fn.ts b/expect/fn.ts new file mode 100644 index 000000000000..6db57e06db33 --- /dev/null +++ b/expect/fn.ts @@ -0,0 +1,45 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2019 Allain Lalonde. All rights reserved. ISC License. +// deno-lint-ignore-file no-explicit-any ban-types + +import { MOCK_SYMBOL, MockCall } from "./_mock_util.ts"; + +export function fn(...stubs: Function[]) { + const calls: MockCall[] = []; + + const f = (...args: any[]) => { + const stub = stubs.length === 1 + // keep reusing the first + ? stubs[0] + // pick the exact mock for the current call + : stubs[calls.length]; + + try { + const returned = stub ? stub(...args) : undefined; + calls.push({ + args, + returned, + timestamp: Date.now(), + returns: true, + throws: false, + }); + return returned; + } catch (err) { + calls.push({ + args, + timestamp: Date.now(), + returns: false, + thrown: err, + throws: true, + }); + throw err; + } + }; + + Object.defineProperty(f, MOCK_SYMBOL, { + value: { calls }, + writable: false, + }); + + return f; +} diff --git a/expect/mod.ts b/expect/mod.ts new file mode 100644 index 000000000000..f13c1018720a --- /dev/null +++ b/expect/mod.ts @@ -0,0 +1,88 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright 2019 Allain Lalonde. All rights reserved. ISC License. +// This module is browser compatible. + +/** + * @module + * + * This module provides jest compatible expect assertion functionality. + * + * @example + * ```ts + * import { expect } from "https://deno.land/std@$STD_VERSION/expect/mod.ts"; + * + * const x = 6 * 7; + * expect(x).toEqual(42); + * expect(x).not.toEqual(0); + * + * await expect(Promise.resolve(x)).resolves.toEqual(42); + * ``` + * + * Currently this module supports the following matchers: + * - `toBe` + * - `toEqual` + * - `toStrictEqual` + * - `toMatch` + * - `toMatchObject` + * - `toBeDefined` + * - `toBeUndefined` + * - `toBeNull` + * - `toBeNaN` + * - `toBeTruthy` + * - `toBeFalsy` + * - `toContain` + * - `toContainEqual` + * - `toHaveLength` + * - `toBeGreaterThan` + * - `toBeGreaterThanOrEqual` + * - `toBeLessThan` + * - `toBeLessThanOrEqual` + * - `toBeCloseTo` + * - `toBeInstanceOf` + * - `toThrow` + * - `toHaveProperty` + * - `toHaveLength` + * + * Also this module supports the following mock related matchers: + * - `toHaveBeenCalled` + * - `toHaveBeenCalledTimes` + * - `toHaveBeenCalledWith` + * - `toHaveBeenLastCalledWith` + * - `toHaveBeenNthCalledWith` + * - `toHaveReturned` + * - `toHaveReturnedTimes` + * - `toHaveReturnedWith` + * - `toHaveLastReturnedWith` + * - `toHaveNthReturnedWith` + * + * The following matchers are not supported yet: + * - `toMatchSnapShot` + * - `toMatchInlineSnapShot` + * - `toThrowErrorMatchingSnapShot` + * - `toThrowErrorMatchingInlineSnapShot` + * + * The following asymmetric matchers are not supported yet: + * - `expect.anything` + * - `expect.any` + * - `expect.arrayContaining` + * - `expect.not.arrayContaining` + * - `expect.closedTo` + * - `expect.objectContaining` + * - `expect.not.objectContaining` + * - `expect.stringContaining` + * - `expect.not.stringContaining` + * - `expect.stringMatching` + * - `expect.not.stringMatching` + * + * The following uitlities are not supported yet: + * - `expect.assertions` + * - `expect.hasAssertions` + * - `expect.addEqualityTester` + * - `expect.addSnapshotSerializer` + * - `expect.extend` + * + * This module is largely inspired by [x/expect](https://github.com/allain/expect) module by Allain Lalonde. + */ + +export { expect } from "./expect.ts"; +export { fn } from "./fn.ts";