Skip to content

Commit 1c38d2c

Browse files
authored
feat(expect): add expect.{closeTo, stringContaining, stringMatching} (denoland#4508)
1 parent 4c78e13 commit 1c38d2c

File tree

7 files changed

+227
-75
lines changed

7 files changed

+227
-75
lines changed

expect/_asymmetric_matchers.ts

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
22
// deno-lint-ignore-file no-explicit-any
33

4-
abstract class AsymmetricMatcher<T> {
4+
export abstract class AsymmetricMatcher<T> {
55
constructor(
66
protected value: T,
77
) {}
@@ -21,10 +21,11 @@ export function anything(): Anything {
2121
export class Any extends AsymmetricMatcher<any> {
2222
constructor(value: unknown) {
2323
if (value === undefined) {
24-
throw TypeError("Expected a constructor function");
24+
throw new TypeError("Expected a constructor function");
2525
}
2626
super(value);
2727
}
28+
2829
equals(other: unknown): boolean {
2930
if (typeof other === "object") {
3031
return other instanceof this.value;
@@ -69,11 +70,78 @@ export class ArrayContaining extends AsymmetricMatcher<any[]> {
6970
constructor(arr: any[]) {
7071
super(arr);
7172
}
73+
7274
equals(other: any[]): boolean {
73-
return this.value.every((e) => other.includes(e));
75+
return Array.isArray(other) && this.value.every((e) => other.includes(e));
7476
}
7577
}
7678

7779
export function arrayContaining(c: any[]): ArrayContaining {
7880
return new ArrayContaining(c);
7981
}
82+
83+
export class CloseTo extends AsymmetricMatcher<number> {
84+
readonly #precision: number;
85+
86+
constructor(num: number, precision: number = 2) {
87+
super(num);
88+
this.#precision = precision;
89+
}
90+
91+
equals(other: number): boolean {
92+
if (typeof other !== "number") {
93+
return false;
94+
}
95+
96+
if (
97+
(this.value === Number.POSITIVE_INFINITY &&
98+
other === Number.POSITIVE_INFINITY) ||
99+
(this.value === Number.NEGATIVE_INFINITY &&
100+
other === Number.NEGATIVE_INFINITY)
101+
) {
102+
return true;
103+
}
104+
105+
return Math.abs(this.value - other) < Math.pow(10, -this.#precision) / 2;
106+
}
107+
}
108+
109+
export function closeTo(num: number, numDigits?: number): CloseTo {
110+
return new CloseTo(num, numDigits);
111+
}
112+
113+
export class StringContaining extends AsymmetricMatcher<string> {
114+
constructor(str: string) {
115+
super(str);
116+
}
117+
118+
equals(other: string): boolean {
119+
if (typeof other !== "string") {
120+
return false;
121+
}
122+
123+
return other.includes(this.value);
124+
}
125+
}
126+
127+
export function stringContaining(str: string): StringContaining {
128+
return new StringContaining(str);
129+
}
130+
131+
export class StringMatching extends AsymmetricMatcher<RegExp> {
132+
constructor(pattern: string | RegExp) {
133+
super(new RegExp(pattern));
134+
}
135+
136+
equals(other: string): boolean {
137+
if (typeof other !== "string") {
138+
return false;
139+
}
140+
141+
return this.value.test(other);
142+
}
143+
}
144+
145+
export function stringMatching(pattern: string | RegExp): StringMatching {
146+
return new StringMatching(pattern);
147+
}

expect/_close_to_test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2+
3+
import { expect } from "./expect.ts";
4+
5+
Deno.test("expect.closeTo()", () => {
6+
expect(0.1 + 0.2).toEqual(expect.closeTo(0.3));
7+
expect(Math.PI).toEqual(expect.closeTo(3.14));
8+
expect(Number.POSITIVE_INFINITY).toEqual(
9+
expect.closeTo(Number.POSITIVE_INFINITY),
10+
);
11+
expect(Number.NEGATIVE_INFINITY).toEqual(
12+
expect.closeTo(Number.NEGATIVE_INFINITY),
13+
);
14+
15+
expect(0.1 + 0.2).not.toEqual(expect.closeTo(0.3, 17));
16+
expect(0.999_999).not.toEqual(expect.closeTo(1, 10));
17+
expect(NaN).not.toEqual(expect.closeTo(NaN));
18+
expect(Number.POSITIVE_INFINITY).not.toEqual(
19+
expect.closeTo(Number.NEGATIVE_INFINITY),
20+
);
21+
});

expect/_equal.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// This file is copied from `std/assert`.
44

55
import type { EqualOptions } from "./_types.ts";
6-
import { Any, Anything, ArrayContaining } from "./_asymmetric_matchers.ts";
6+
import { AsymmetricMatcher } from "./_asymmetric_matchers.ts";
77

88
function isKeyedCollection(x: unknown): x is Set<unknown> {
99
return [Symbol.iterator, "size"].every((k) => k in (x as Set<unknown>));
@@ -15,6 +15,23 @@ function constructorsEqual(a: object, b: object) {
1515
!a.constructor && b.constructor === Object;
1616
}
1717

18+
function asymmetricEqual(a: unknown, b: unknown) {
19+
const asymmetricA = a instanceof AsymmetricMatcher;
20+
const asymmetricB = b instanceof AsymmetricMatcher;
21+
22+
if (asymmetricA && asymmetricB) {
23+
return undefined;
24+
}
25+
26+
if (asymmetricA) {
27+
return a.equals(b);
28+
}
29+
30+
if (asymmetricB) {
31+
return b.equals(a);
32+
}
33+
}
34+
1835
/**
1936
* Deep equality comparison used in assertions
2037
* @param c actual value
@@ -48,15 +65,12 @@ export function equal(c: unknown, d: unknown, options?: EqualOptions): boolean {
4865
) {
4966
return String(a) === String(b);
5067
}
51-
if (b instanceof Anything) {
52-
return b.equals(a);
53-
}
54-
if (b instanceof Any) {
55-
return b.equals(a);
56-
}
57-
if (b instanceof ArrayContaining && a instanceof Array) {
58-
return b.equals(a);
68+
69+
const asymmetric = asymmetricEqual(a, b);
70+
if (asymmetric !== undefined) {
71+
return asymmetric;
5972
}
73+
6074
if (a instanceof Date && b instanceof Date) {
6175
const aTime = a.getTime();
6276
const bTime = b.getTime();

expect/_string_containing_test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2+
3+
import { expect } from "./expect.ts";
4+
5+
Deno.test("expect.stringContaining() with strings", () => {
6+
expect("https://deno.com/").toEqual(expect.stringContaining("deno"));
7+
expect("function").toEqual(expect.stringContaining("func"));
8+
9+
expect("Hello, World").not.toEqual(expect.stringContaining("hello"));
10+
expect("foobar").not.toEqual(expect.stringContaining("bazz"));
11+
});
12+
13+
Deno.test("expect.stringContaining() with other types", () => {
14+
expect(123).not.toEqual(expect.stringContaining("1"));
15+
expect(true).not.toEqual(expect.stringContaining("true"));
16+
expect(["foo", "bar"]).not.toEqual(expect.stringContaining("foo"));
17+
expect({ foo: "bar" }).not.toEqual(expect.stringContaining(`{ foo: "bar" }`));
18+
});

expect/_string_matching_test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2+
3+
import { expect } from "./expect.ts";
4+
5+
Deno.test("expect.stringMatching() with strings", () => {
6+
expect("deno_std").toEqual(expect.stringMatching("std"));
7+
expect("function").toEqual(expect.stringMatching("func"));
8+
9+
expect("Hello, World").not.toEqual(expect.stringMatching("hello"));
10+
expect("foobar").not.toEqual(expect.stringMatching("bazz"));
11+
});
12+
13+
Deno.test("expect.stringMatching() with RegExp", () => {
14+
expect("deno_std").toEqual(expect.stringMatching(/std/));
15+
expect("0123456789").toEqual(expect.stringMatching(/\d+/));
16+
17+
expect("\e").not.toEqual(expect.stringMatching(/\s/));
18+
expect("queue").not.toEqual(expect.stringMatching(/en/));
19+
});

expect/expect.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,14 @@ import {
5151
toThrow,
5252
} from "./_matchers.ts";
5353
import { isPromiseLike } from "./_utils.ts";
54-
import { any, anything, arrayContaining } from "./_asymmetric_matchers.ts";
54+
import {
55+
any,
56+
anything,
57+
arrayContaining,
58+
closeTo,
59+
stringContaining,
60+
stringMatching,
61+
} from "./_asymmetric_matchers.ts";
5562

5663
const matchers: Record<MatcherKey, Matcher> = {
5764
lastCalledWith: toHaveBeenLastCalledWith,
@@ -191,6 +198,10 @@ export function expect(value: unknown, customMessage?: string): Expected {
191198

192199
expect.addEqualityTesters = addCustomEqualityTesters;
193200
expect.extend = setExtendMatchers;
201+
194202
expect.anything = anything;
195203
expect.any = any;
196204
expect.arrayContaining = arrayContaining;
205+
expect.closeTo = closeTo;
206+
expect.stringContaining = stringContaining;
207+
expect.stringMatching = stringMatching;

expect/mod.ts

Lines changed: 63 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,70 +3,71 @@
33
// This module is browser compatible.
44

55
/**
6-
* This module provides jest compatible expect assertion functionality.
6+
* This module provides Jest compatible expect assertion functionality.
77
*
8-
* Currently this module supports the following matchers:
9-
* - `toBe`
10-
* - `toEqual`
11-
* - `toStrictEqual`
12-
* - `toMatch`
13-
* - `toMatchObject`
14-
* - `toBeDefined`
15-
* - `toBeUndefined`
16-
* - `toBeNull`
17-
* - `toBeNaN`
18-
* - `toBeTruthy`
19-
* - `toBeFalsy`
20-
* - `toContain`
21-
* - `toContainEqual`
22-
* - `toHaveLength`
23-
* - `toBeGreaterThan`
24-
* - `toBeGreaterThanOrEqual`
25-
* - `toBeLessThan`
26-
* - `toBeLessThanOrEqual`
27-
* - `toBeCloseTo`
28-
* - `toBeInstanceOf`
29-
* - `toThrow`
30-
* - `toHaveProperty`
31-
* - `toHaveLength`
8+
* Currently this module supports the following functions:
9+
* - Common matchers:
10+
* - `toBe`
11+
* - `toEqual`
12+
* - `toStrictEqual`
13+
* - `toMatch`
14+
* - `toMatchObject`
15+
* - `toBeDefined`
16+
* - `toBeUndefined`
17+
* - `toBeNull`
18+
* - `toBeNaN`
19+
* - `toBeTruthy`
20+
* - `toBeFalsy`
21+
* - `toContain`
22+
* - `toContainEqual`
23+
* - `toHaveLength`
24+
* - `toBeGreaterThan`
25+
* - `toBeGreaterThanOrEqual`
26+
* - `toBeLessThan`
27+
* - `toBeLessThanOrEqual`
28+
* - `toBeCloseTo`
29+
* - `toBeInstanceOf`
30+
* - `toThrow`
31+
* - `toHaveProperty`
32+
* - `toHaveLength`
33+
* - Mock related matchers:
34+
* - `toHaveBeenCalled`
35+
* - `toHaveBeenCalledTimes`
36+
* - `toHaveBeenCalledWith`
37+
* - `toHaveBeenLastCalledWith`
38+
* - `toHaveBeenNthCalledWith`
39+
* - `toHaveReturned`
40+
* - `toHaveReturnedTimes`
41+
* - `toHaveReturnedWith`
42+
* - `toHaveLastReturnedWith`
43+
* - `toHaveNthReturnedWith`
44+
* - Asymmetric matchers:
45+
* - `expect.anything`
46+
* - `expect.any`
47+
* - `expect.arrayContaining`
48+
* - `expect.not.arrayContaining`
49+
* - `expect.closeTo`
50+
* - `expect.stringContaining`
51+
* - `expect.not.stringContaining`
52+
* - `expect.stringMatching`
53+
* - `expect.not.stringMatching`
54+
* - Utilities:
55+
* - `expect.addEqualityTester`
56+
* - `expect.extend`
3257
*
33-
* Also this module supports the following mock related matchers:
34-
* - `toHaveBeenCalled`
35-
* - `toHaveBeenCalledTimes`
36-
* - `toHaveBeenCalledWith`
37-
* - `toHaveBeenLastCalledWith`
38-
* - `toHaveBeenNthCalledWith`
39-
* - `toHaveReturned`
40-
* - `toHaveReturnedTimes`
41-
* - `toHaveReturnedWith`
42-
* - `toHaveLastReturnedWith`
43-
* - `toHaveNthReturnedWith`
44-
*
45-
* The following matchers are not supported yet:
46-
* - `toMatchSnapShot`
47-
* - `toMatchInlineSnapShot`
48-
* - `toThrowErrorMatchingSnapShot`
49-
* - `toThrowErrorMatchingInlineSnapShot`
50-
*
51-
* The following asymmetric matchers are not supported yet:
52-
* - `expect.anything`
53-
* - `expect.any`
54-
* - `expect.arrayContaining`
55-
* - `expect.not.arrayContaining`
56-
* - `expect.closedTo`
57-
* - `expect.objectContaining`
58-
* - `expect.not.objectContaining`
59-
* - `expect.stringContaining`
60-
* - `expect.not.stringContaining`
61-
* - `expect.stringMatching`
62-
* - `expect.not.stringMatching`
63-
*
64-
* The following uitlities are not supported yet:
65-
* - `expect.assertions`
66-
* - `expect.hasAssertions`
67-
* - `expect.addEqualityTester`
68-
* - `expect.addSnapshotSerializer`
69-
* - `expect.extend`
58+
* Only these functions are still not available:
59+
* - Matchers:
60+
* - `toMatchSnapShot`
61+
* - `toMatchInlineSnapShot`
62+
* - `toThrowErrorMatchingSnapShot`
63+
* - `toThrowErrorMatchingInlineSnapShot`
64+
* - Asymmetric matchers:
65+
* - `expect.objectContaining`
66+
* - `expect.not.objectContaining`
67+
* - Utilities:
68+
* - `expect.assertions`
69+
* - `expect.hasAssertions`
70+
* - `expect.addSnapshotSerializer`
7071
*
7172
* This module is largely inspired by {@link https://github.com/allain/expect | x/expect} module by Allain Lalonde.
7273
*

0 commit comments

Comments
 (0)