Skip to content

Commit 06bae4d

Browse files
hi-ogawasheremet-va
andauthoredJan 15, 2024
fix(expect): implement chai inspect for AsymmetricMatcher (#4942)
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
1 parent 088dc05 commit 06bae4d

File tree

3 files changed

+308
-3
lines changed

3 files changed

+308
-3
lines changed
 

‎packages/expect/src/jest-asymmetric-matchers.ts

+10
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ export abstract class AsymmetricMatcher<
4141
abstract toString(): string
4242
getExpectedType?(): string
4343
toAsymmetricMatcher?(): string
44+
45+
// implement custom chai/loupe inspect for better AssertionError.message formatting
46+
// https://github.com/chaijs/loupe/blob/9b8a6deabcd50adc056a64fb705896194710c5c6/src/index.ts#L29
47+
[Symbol.for('chai/inspect')](options: { depth: number; truncate: number }) {
48+
// minimal pretty-format with simple manual truncation
49+
const result = stringify(this, options.depth, { min: true })
50+
if (result.length <= options.truncate)
51+
return result
52+
return `${this.toString()}{…}`
53+
}
4454
}
4555

4656
export class StringContaining extends AsymmetricMatcher<string> {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`asymmetric matcher error 1`] = `
4+
{
5+
"actual": "hello",
6+
"diff": null,
7+
"expected": "StringContaining "xx"",
8+
"message": "expected 'hello' to deeply equal StringContaining "xx"",
9+
}
10+
`;
11+
12+
exports[`asymmetric matcher error 2`] = `
13+
{
14+
"actual": "hello",
15+
"diff": null,
16+
"expected": "StringNotContaining "ll"",
17+
"message": "expected 'hello' to deeply equal StringNotContaining "ll"",
18+
}
19+
`;
20+
21+
exports[`asymmetric matcher error 3`] = `
22+
{
23+
"actual": "Object {
24+
"foo": "hello",
25+
}",
26+
"diff": "- Expected
27+
+ Received
28+
29+
Object {
30+
- "foo": StringContaining "xx",
31+
+ "foo": "hello",
32+
}",
33+
"expected": "Object {
34+
"foo": StringContaining "xx",
35+
}",
36+
"message": "expected { foo: 'hello' } to deeply equal { foo: StringContaining "xx" }",
37+
}
38+
`;
39+
40+
exports[`asymmetric matcher error 4`] = `
41+
{
42+
"actual": "Object {
43+
"foo": "hello",
44+
}",
45+
"diff": "- Expected
46+
+ Received
47+
48+
Object {
49+
- "foo": StringNotContaining "ll",
50+
+ "foo": "hello",
51+
}",
52+
"expected": "Object {
53+
"foo": StringNotContaining "ll",
54+
}",
55+
"message": "expected { foo: 'hello' } to deeply equal { foo: StringNotContaining "ll" }",
56+
}
57+
`;
58+
59+
exports[`asymmetric matcher error 5`] = `
60+
{
61+
"actual": "hello",
62+
"diff": "- Expected:
63+
stringContainingCustom<xx>
64+
65+
+ Received:
66+
"hello"",
67+
"expected": "stringContainingCustom<xx>",
68+
"message": "expected 'hello' to deeply equal stringContainingCustom<xx>",
69+
}
70+
`;
71+
72+
exports[`asymmetric matcher error 6`] = `
73+
{
74+
"actual": "hello",
75+
"diff": "- Expected:
76+
not.stringContainingCustom<ll>
77+
78+
+ Received:
79+
"hello"",
80+
"expected": "not.stringContainingCustom<ll>",
81+
"message": "expected 'hello' to deeply equal not.stringContainingCustom<ll>",
82+
}
83+
`;
84+
85+
exports[`asymmetric matcher error 7`] = `
86+
{
87+
"actual": "Object {
88+
"foo": "hello",
89+
}",
90+
"diff": "- Expected
91+
+ Received
92+
93+
Object {
94+
- "foo": stringContainingCustom<xx>,
95+
+ "foo": "hello",
96+
}",
97+
"expected": "Object {
98+
"foo": stringContainingCustom<xx>,
99+
}",
100+
"message": "expected { foo: 'hello' } to deeply equal { foo: stringContainingCustom<xx> }",
101+
}
102+
`;
103+
104+
exports[`asymmetric matcher error 8`] = `
105+
{
106+
"actual": "Object {
107+
"foo": "hello",
108+
}",
109+
"diff": "- Expected
110+
+ Received
111+
112+
Object {
113+
- "foo": not.stringContainingCustom<ll>,
114+
+ "foo": "hello",
115+
}",
116+
"expected": "Object {
117+
"foo": not.stringContainingCustom<ll>,
118+
}",
119+
"message": "expected { foo: 'hello' } to deeply equal { foo: not.stringContainingCustom<ll> }",
120+
}
121+
`;
122+
123+
exports[`asymmetric matcher error 9`] = `
124+
{
125+
"actual": "undefined",
126+
"diff": undefined,
127+
"expected": "undefined",
128+
"message": "expected "hello" to contain "xx"",
129+
}
130+
`;
131+
132+
exports[`asymmetric matcher error 10`] = `
133+
{
134+
"actual": "undefined",
135+
"diff": undefined,
136+
"expected": "undefined",
137+
"message": "expected "hello" not to contain "ll"",
138+
}
139+
`;
140+
141+
exports[`asymmetric matcher error 11`] = `
142+
{
143+
"actual": "hello",
144+
"diff": "- Expected:
145+
testComplexMatcher<[object Object]>
146+
147+
+ Received:
148+
"hello"",
149+
"expected": "testComplexMatcher<[object Object]>",
150+
"message": "expected 'hello' to deeply equal testComplexMatcher<[object Object]>",
151+
}
152+
`;
153+
154+
exports[`asymmetric matcher error 12`] = `
155+
{
156+
"actual": "Object {
157+
"k": "v",
158+
"k2": "v2",
159+
}",
160+
"diff": "- Expected
161+
+ Received
162+
163+
- ObjectContaining {
164+
+ Object {
165+
"k": "v",
166+
- "k3": "v3",
167+
+ "k2": "v2",
168+
}",
169+
"expected": "ObjectContaining {
170+
"k": "v",
171+
"k3": "v3",
172+
}",
173+
"message": "expected { k: 'v', k2: 'v2' } to deeply equal ObjectContaining {"k": "v", "k3": "v3"}",
174+
}
175+
`;
176+
177+
exports[`asymmetric matcher error 13`] = `
178+
{
179+
"actual": "Array [
180+
"a",
181+
"b",
182+
]",
183+
"diff": "- Expected
184+
+ Received
185+
186+
- ArrayContaining [
187+
+ Array [
188+
"a",
189+
- "c",
190+
+ "b",
191+
]",
192+
"expected": "ArrayContaining [
193+
"a",
194+
"c",
195+
]",
196+
"message": "expected [ 'a', 'b' ] to deeply equal ArrayContaining ["a", "c"]",
197+
}
198+
`;
199+
200+
exports[`asymmetric matcher error 14`] = `
201+
{
202+
"actual": "hello",
203+
"diff": null,
204+
"expected": "StringMatching /xx/",
205+
"message": "expected 'hello' to deeply equal StringMatching /xx/",
206+
}
207+
`;
208+
209+
exports[`asymmetric matcher error 15`] = `
210+
{
211+
"actual": "2.5",
212+
"diff": "- Expected
213+
+ Received
214+
215+
- NumberCloseTo 2 (1 digit)
216+
+ 2.5",
217+
"expected": "NumberCloseTo 2 (1 digit)",
218+
"message": "expected 2.5 to deeply equal NumberCloseTo 2 (1 digit)",
219+
}
220+
`;
221+
222+
exports[`asymmetric matcher error 16`] = `
223+
{
224+
"actual": "hello",
225+
"diff": null,
226+
"expected": "StringContaining "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"",
227+
"message": "expected 'hello' to deeply equal StringContaining{…}",
228+
}
229+
`;

‎test/core/test/jest-expect.test.ts

+69-3
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ describe('jest-expect', () => {
180180
}).toEqual({
181181
sum: expect.closeTo(0.4),
182182
})
183-
}).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected { sum: 0.30000000000000004 } to deeply equal { sum: CloseTo{ …(4) } }]`)
183+
}).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected { sum: 0.30000000000000004 } to deeply equal { sum: NumberCloseTo 0.4 (2 digits) }]`)
184184
})
185185

186186
it('asymmetric matchers negate', () => {
@@ -947,7 +947,7 @@ it('toHaveProperty error diff', () => {
947947
// non match value (with asymmetric matcher)
948948
expect(getError(() => expect({ name: 'foo' }).toHaveProperty('name', expect.any(Number)))).toMatchInlineSnapshot(`
949949
[
950-
"expected { name: 'foo' } to have property "name" with value Any{ …(3) }",
950+
"expected { name: 'foo' } to have property "name" with value Any<Number>",
951951
"- Expected:
952952
Any<Number>
953953
@@ -959,7 +959,7 @@ it('toHaveProperty error diff', () => {
959959
// non match key (with asymmetric matcher)
960960
expect(getError(() => expect({ noName: 'foo' }).toHaveProperty('name', expect.any(Number)))).toMatchInlineSnapshot(`
961961
[
962-
"expected { noName: 'foo' } to have property "name" with value Any{ …(3) }",
962+
"expected { noName: 'foo' } to have property "name" with value Any<Number>",
963963
"- Expected:
964964
Any<Number>
965965
@@ -993,4 +993,70 @@ it('toHaveProperty error diff', () => {
993993
`)
994994
})
995995

996+
it('asymmetric matcher error', () => {
997+
setupColors(getDefaultColors())
998+
999+
function snapshotError(f: () => unknown) {
1000+
try {
1001+
f()
1002+
return expect.unreachable()
1003+
}
1004+
catch (error) {
1005+
const e = processError(error)
1006+
expect({
1007+
message: e.message,
1008+
diff: e.diff,
1009+
expected: e.expected,
1010+
actual: e.actual,
1011+
}).toMatchSnapshot()
1012+
}
1013+
}
1014+
1015+
expect.extend({
1016+
stringContainingCustom(received: unknown, other: string) {
1017+
return {
1018+
pass: typeof received === 'string' && received.includes(other),
1019+
message: () => `expected ${this.utils.printReceived(received)} ${this.isNot ? 'not ' : ''}to contain ${this.utils.printExpected(other)}`,
1020+
}
1021+
},
1022+
})
1023+
1024+
// builtin: stringContaining
1025+
snapshotError(() => expect('hello').toEqual(expect.stringContaining('xx')))
1026+
snapshotError(() => expect('hello').toEqual(expect.not.stringContaining('ll')))
1027+
snapshotError(() => expect({ foo: 'hello' }).toEqual({ foo: expect.stringContaining('xx') }))
1028+
snapshotError(() => expect({ foo: 'hello' }).toEqual({ foo: expect.not.stringContaining('ll') }))
1029+
1030+
// custom
1031+
snapshotError(() => expect('hello').toEqual((expect as any).stringContainingCustom('xx')))
1032+
snapshotError(() => expect('hello').toEqual((expect as any).not.stringContainingCustom('ll')))
1033+
snapshotError(() => expect({ foo: 'hello' }).toEqual({ foo: (expect as any).stringContainingCustom('xx') }))
1034+
snapshotError(() => expect({ foo: 'hello' }).toEqual({ foo: (expect as any).not.stringContainingCustom('ll') }))
1035+
1036+
// assertion form
1037+
snapshotError(() => (expect('hello') as any).stringContainingCustom('xx'))
1038+
snapshotError(() => (expect('hello') as any).not.stringContainingCustom('ll'))
1039+
1040+
// matcher with complex argument
1041+
// (serialized by `String` so it becomes "testComplexMatcher<[object Object]>", which is same as jest's asymmetric matcher and pretty-format)
1042+
expect.extend({
1043+
testComplexMatcher(_received: unknown, _arg: unknown) {
1044+
return {
1045+
pass: false,
1046+
message: () => `NA`,
1047+
}
1048+
},
1049+
})
1050+
snapshotError(() => expect('hello').toEqual((expect as any).testComplexMatcher({ x: 'y' })))
1051+
1052+
// more builtins
1053+
snapshotError(() => expect({ k: 'v', k2: 'v2' }).toEqual(expect.objectContaining({ k: 'v', k3: 'v3' })))
1054+
snapshotError(() => expect(['a', 'b']).toEqual(expect.arrayContaining(['a', 'c'])))
1055+
snapshotError(() => expect('hello').toEqual(expect.stringMatching(/xx/)))
1056+
snapshotError(() => expect(2.5).toEqual(expect.closeTo(2, 1)))
1057+
1058+
// simple truncation if pretty-format is too long
1059+
snapshotError(() => expect('hello').toEqual(expect.stringContaining('a'.repeat(40))))
1060+
})
1061+
9961062
it('timeout', () => new Promise(resolve => setTimeout(resolve, 500)))

0 commit comments

Comments
 (0)
Please sign in to comment.