From 5d15ab7596cfd15aa2b0fd3e785c6b12bb7981fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Hern=C3=A1ndez?= <73640929+javihernant@users.noreply.github.com> Date: Wed, 28 Feb 2024 05:07:50 +0100 Subject: [PATCH] feat(expect): add asymmetric matchers (any, anything, arrayContaining) (#4366) --- expect/_any_test.ts | 15 ++++++ expect/_anything_test.ts | 27 +++++++++++ expect/_array_containing_test.ts | 16 +++++++ expect/_asymmetric_matchers.ts | 79 ++++++++++++++++++++++++++++++++ expect/_equal.ts | 14 +++++- expect/expect.ts | 4 ++ 6 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 expect/_any_test.ts create mode 100644 expect/_anything_test.ts create mode 100644 expect/_array_containing_test.ts create mode 100644 expect/_asymmetric_matchers.ts diff --git a/expect/_any_test.ts b/expect/_any_test.ts new file mode 100644 index 000000000000..d233058f2a36 --- /dev/null +++ b/expect/_any_test.ts @@ -0,0 +1,15 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; + +class Cat {} +Deno.test("expect.any()", () => { + expect(new Cat()).toEqual(expect.any(Cat)); + expect(new Cat()).toEqual(expect.any(Object)); + 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)); +}); diff --git a/expect/_anything_test.ts b/expect/_anything_test.ts new file mode 100644 index 000000000000..813d55c81b7c --- /dev/null +++ b/expect/_anything_test.ts @@ -0,0 +1,27 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; +import { AssertionError, assertThrows } from "../assert/mod.ts"; +import { fn } from "./fn.ts"; + +Deno.test("expect.anything() as a parameter of toEqual()", () => { + expect(null).not.toEqual(expect.anything()); + expect(undefined).not.toEqual(expect.anything()); + expect(1).toEqual(expect.anything()); + + assertThrows(() => { + expect(null).toEqual(expect.anything()); + }, AssertionError); + assertThrows(() => { + expect(undefined).toEqual(expect.anything()); + }, AssertionError); + assertThrows(() => { + expect(1).not.toEqual(expect.anything()); + }, AssertionError); +}); + +Deno.test("expect.anything() as a parameter of toHaveBeenCalled()", () => { + const mockFn = fn(); + mockFn(3); + expect(mockFn).toHaveBeenCalledWith(expect.anything()); +}); diff --git a/expect/_array_containing_test.ts b/expect/_array_containing_test.ts new file mode 100644 index 000000000000..19e66eadd185 --- /dev/null +++ b/expect/_array_containing_test.ts @@ -0,0 +1,16 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +import { expect } from "./expect.ts"; + +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)); +}); + +Deno.test("expect.arrayContaining() with array of mixed types", () => { + const arr = [1, 2, "hello"]; + expect([1, 2, 3, "hello", "bye"]).toEqual(expect.arrayContaining(arr)); + expect([4, "bye"]).not.toEqual(expect.arrayContaining(arr)); +}); diff --git a/expect/_asymmetric_matchers.ts b/expect/_asymmetric_matchers.ts new file mode 100644 index 000000000000..9f93aaffcaad --- /dev/null +++ b/expect/_asymmetric_matchers.ts @@ -0,0 +1,79 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file no-explicit-any + +abstract class AsymmetricMatcher { + constructor( + protected value: T, + ) {} + abstract equals(other: unknown): boolean; +} + +export class Anything extends AsymmetricMatcher { + equals(other: unknown): boolean { + return other !== null && other !== undefined; + } +} + +export function anything(): Anything { + return new Anything(); +} + +export class Any extends AsymmetricMatcher { + constructor(value: unknown) { + if (value === undefined) { + throw TypeError("Expected a constructor function"); + } + super(value); + } + equals(other: unknown): boolean { + if (typeof other === "object") { + return other instanceof this.value; + } else { + if (this.value == Number) { + return typeof other === "number"; + } + + if (this.value == String) { + return typeof other == "string"; + } + + if (this.value == Number) { + return typeof other == "number"; + } + + if (this.value == Function) { + return typeof other == "function"; + } + + if (this.value == Boolean) { + return typeof other == "boolean"; + } + + if (this.value == BigInt) { + return typeof other == "bigint"; + } + + if (this.value == Symbol) { + return typeof other == "symbol"; + } + } + return false; + } +} + +export function any(c: unknown): Any { + return new Any(c); +} + +export class ArrayContaining extends AsymmetricMatcher { + constructor(arr: any[]) { + super(arr); + } + equals(other: any[]): boolean { + return this.value.every((e) => other.includes(e)); + } +} + +export function arrayContaining(c: any[]): ArrayContaining { + return new ArrayContaining(c); +} diff --git a/expect/_equal.ts b/expect/_equal.ts index 90bebc14eebf..af6c7fa01392 100644 --- a/expect/_equal.ts +++ b/expect/_equal.ts @@ -3,6 +3,7 @@ // This file is copied from `std/assert`. import type { EqualOptions } from "./_types.ts"; +import { Any, Anything, ArrayContaining } from "./_asymmetric_matchers.ts"; function isKeyedCollection(x: unknown): x is Set { return [Symbol.iterator, "size"].every((k) => k in (x as Set)); @@ -52,6 +53,15 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean { ) { return String(a) === String(b); } + if (b instanceof Anything) { + return b.equals(a); + } + if (b instanceof Any) { + return b.equals(a); + } + if (b instanceof ArrayContaining && a instanceof Array) { + return b.equals(a); + } if (a instanceof Date && b instanceof Date) { const aTime = a.getTime(); const bTime = b.getTime(); @@ -95,7 +105,7 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean { if (!strictCheck) { if (aLen > 0) { for (let i = 0; i < aKeys.length; i += 1) { - const key = aKeys[i]; + const key = aKeys[i]!; if ( (key in a) && (a[key as keyof typeof a] === undefined) && !(key in b) @@ -107,7 +117,7 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean { if (bLen > 0) { for (let i = 0; i < bKeys.length; i += 1) { - const key = bKeys[i]; + const key = bKeys[i]!; if ( (key in b) && (b[key as keyof typeof b] === undefined) && !(key in a) diff --git a/expect/expect.ts b/expect/expect.ts index 739776a0a332..c5bc4af67725 100644 --- a/expect/expect.ts +++ b/expect/expect.ts @@ -47,6 +47,7 @@ import { toThrow, } from "./_matchers.ts"; import { isPromiseLike } from "./_utils.ts"; +import { any, anything, arrayContaining } from "./_asymmetric_matchers.ts"; const matchers: Record = { lastCalledWith: toHaveBeenLastCalledWith, @@ -168,3 +169,6 @@ export function expect(value: unknown, customMessage?: string): Expected { } expect.addEqualityTesters = addCustomEqualityTesters; +expect.anything = anything; +expect.any = any; +expect.arrayContaining = arrayContaining;