feat(expect): add expect.{closeTo, stringContaining, stringMatching} (#4508)

This commit is contained in:
David Luis 2024-03-26 09:57:40 +07:00 committed by GitHub
parent 4c78e13be6
commit 1c38d2c886
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 227 additions and 75 deletions

View File

@ -1,7 +1,7 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file no-explicit-any
abstract class AsymmetricMatcher<T> {
export abstract class AsymmetricMatcher<T> {
constructor(
protected value: T,
) {}
@ -21,10 +21,11 @@ export function anything(): Anything {
export class Any extends AsymmetricMatcher<any> {
constructor(value: unknown) {
if (value === undefined) {
throw TypeError("Expected a constructor function");
throw new TypeError("Expected a constructor function");
}
super(value);
}
equals(other: unknown): boolean {
if (typeof other === "object") {
return other instanceof this.value;
@ -69,11 +70,78 @@ export class ArrayContaining extends AsymmetricMatcher<any[]> {
constructor(arr: any[]) {
super(arr);
}
equals(other: any[]): boolean {
return this.value.every((e) => other.includes(e));
return Array.isArray(other) && this.value.every((e) => other.includes(e));
}
}
export function arrayContaining(c: any[]): ArrayContaining {
return new ArrayContaining(c);
}
export class CloseTo extends AsymmetricMatcher<number> {
readonly #precision: number;
constructor(num: number, precision: number = 2) {
super(num);
this.#precision = precision;
}
equals(other: number): boolean {
if (typeof other !== "number") {
return false;
}
if (
(this.value === Number.POSITIVE_INFINITY &&
other === Number.POSITIVE_INFINITY) ||
(this.value === Number.NEGATIVE_INFINITY &&
other === Number.NEGATIVE_INFINITY)
) {
return true;
}
return Math.abs(this.value - other) < Math.pow(10, -this.#precision) / 2;
}
}
export function closeTo(num: number, numDigits?: number): CloseTo {
return new CloseTo(num, numDigits);
}
export class StringContaining extends AsymmetricMatcher<string> {
constructor(str: string) {
super(str);
}
equals(other: string): boolean {
if (typeof other !== "string") {
return false;
}
return other.includes(this.value);
}
}
export function stringContaining(str: string): StringContaining {
return new StringContaining(str);
}
export class StringMatching extends AsymmetricMatcher<RegExp> {
constructor(pattern: string | RegExp) {
super(new RegExp(pattern));
}
equals(other: string): boolean {
if (typeof other !== "string") {
return false;
}
return this.value.test(other);
}
}
export function stringMatching(pattern: string | RegExp): StringMatching {
return new StringMatching(pattern);
}

21
expect/_close_to_test.ts Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { expect } from "./expect.ts";
Deno.test("expect.closeTo()", () => {
expect(0.1 + 0.2).toEqual(expect.closeTo(0.3));
expect(Math.PI).toEqual(expect.closeTo(3.14));
expect(Number.POSITIVE_INFINITY).toEqual(
expect.closeTo(Number.POSITIVE_INFINITY),
);
expect(Number.NEGATIVE_INFINITY).toEqual(
expect.closeTo(Number.NEGATIVE_INFINITY),
);
expect(0.1 + 0.2).not.toEqual(expect.closeTo(0.3, 17));
expect(0.999_999).not.toEqual(expect.closeTo(1, 10));
expect(NaN).not.toEqual(expect.closeTo(NaN));
expect(Number.POSITIVE_INFINITY).not.toEqual(
expect.closeTo(Number.NEGATIVE_INFINITY),
);
});

View File

@ -3,7 +3,7 @@
// This file is copied from `std/assert`.
import type { EqualOptions } from "./_types.ts";
import { Any, Anything, ArrayContaining } from "./_asymmetric_matchers.ts";
import { AsymmetricMatcher } from "./_asymmetric_matchers.ts";
function isKeyedCollection(x: unknown): x is Set<unknown> {
return [Symbol.iterator, "size"].every((k) => k in (x as Set<unknown>));
@ -15,6 +15,23 @@ function constructorsEqual(a: object, b: object) {
!a.constructor && b.constructor === Object;
}
function asymmetricEqual(a: unknown, b: unknown) {
const asymmetricA = a instanceof AsymmetricMatcher;
const asymmetricB = b instanceof AsymmetricMatcher;
if (asymmetricA && asymmetricB) {
return undefined;
}
if (asymmetricA) {
return a.equals(b);
}
if (asymmetricB) {
return b.equals(a);
}
}
/**
* Deep equality comparison used in assertions
* @param c actual value
@ -48,15 +65,12 @@ 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);
const asymmetric = asymmetricEqual(a, b);
if (asymmetric !== undefined) {
return asymmetric;
}
if (a instanceof Date && b instanceof Date) {
const aTime = a.getTime();
const bTime = b.getTime();

View File

@ -0,0 +1,18 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { expect } from "./expect.ts";
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"));
});
Deno.test("expect.stringContaining() with other types", () => {
expect(123).not.toEqual(expect.stringContaining("1"));
expect(true).not.toEqual(expect.stringContaining("true"));
expect(["foo", "bar"]).not.toEqual(expect.stringContaining("foo"));
expect({ foo: "bar" }).not.toEqual(expect.stringContaining(`{ foo: "bar" }`));
});

View File

@ -0,0 +1,19 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { expect } from "./expect.ts";
Deno.test("expect.stringMatching() with strings", () => {
expect("deno_std").toEqual(expect.stringMatching("std"));
expect("function").toEqual(expect.stringMatching("func"));
expect("Hello, World").not.toEqual(expect.stringMatching("hello"));
expect("foobar").not.toEqual(expect.stringMatching("bazz"));
});
Deno.test("expect.stringMatching() with RegExp", () => {
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/));
});

View File

@ -51,7 +51,14 @@ import {
toThrow,
} from "./_matchers.ts";
import { isPromiseLike } from "./_utils.ts";
import { any, anything, arrayContaining } from "./_asymmetric_matchers.ts";
import {
any,
anything,
arrayContaining,
closeTo,
stringContaining,
stringMatching,
} from "./_asymmetric_matchers.ts";
const matchers: Record<MatcherKey, Matcher> = {
lastCalledWith: toHaveBeenLastCalledWith,
@ -191,6 +198,10 @@ export function expect(value: unknown, customMessage?: string): Expected {
expect.addEqualityTesters = addCustomEqualityTesters;
expect.extend = setExtendMatchers;
expect.anything = anything;
expect.any = any;
expect.arrayContaining = arrayContaining;
expect.closeTo = closeTo;
expect.stringContaining = stringContaining;
expect.stringMatching = stringMatching;

View File

@ -3,9 +3,10 @@
// This module is browser compatible.
/**
* This module provides jest compatible expect assertion functionality.
* This module provides Jest compatible expect assertion functionality.
*
* Currently this module supports the following matchers:
* Currently this module supports the following functions:
* - Common matchers:
* - `toBe`
* - `toEqual`
* - `toStrictEqual`
@ -29,8 +30,7 @@
* - `toThrow`
* - `toHaveProperty`
* - `toHaveLength`
*
* Also this module supports the following mock related matchers:
* - Mock related matchers:
* - `toHaveBeenCalled`
* - `toHaveBeenCalledTimes`
* - `toHaveBeenCalledWith`
@ -41,32 +41,33 @@
* - `toHaveReturnedWith`
* - `toHaveLastReturnedWith`
* - `toHaveNthReturnedWith`
*
* The following matchers are not supported yet:
* - `toMatchSnapShot`
* - `toMatchInlineSnapShot`
* - `toThrowErrorMatchingSnapShot`
* - `toThrowErrorMatchingInlineSnapShot`
*
* The following asymmetric matchers are not supported yet:
* - Asymmetric matchers:
* - `expect.anything`
* - `expect.any`
* - `expect.arrayContaining`
* - `expect.not.arrayContaining`
* - `expect.closedTo`
* - `expect.objectContaining`
* - `expect.not.objectContaining`
* - `expect.closeTo`
* - `expect.stringContaining`
* - `expect.not.stringContaining`
* - `expect.stringMatching`
* - `expect.not.stringMatching`
* - Utilities:
* - `expect.addEqualityTester`
* - `expect.extend`
*
* The following uitlities are not supported yet:
* Only these functions are still not available:
* - Matchers:
* - `toMatchSnapShot`
* - `toMatchInlineSnapShot`
* - `toThrowErrorMatchingSnapShot`
* - `toThrowErrorMatchingInlineSnapShot`
* - Asymmetric matchers:
* - `expect.objectContaining`
* - `expect.not.objectContaining`
* - Utilities:
* - `expect.assertions`
* - `expect.hasAssertions`
* - `expect.addEqualityTester`
* - `expect.addSnapshotSerializer`
* - `expect.extend`
*
* This module is largely inspired by {@link https://github.com/allain/expect | x/expect} module by Allain Lalonde.
*