Skip to content

Commit

Permalink
fix(expect): implement chai inspect for AsymmetricMatcher (#4942)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
  • Loading branch information
hi-ogawa and sheremet-va committed Jan 15, 2024
1 parent 088dc05 commit 06bae4d
Show file tree
Hide file tree
Showing 3 changed files with 308 additions and 3 deletions.
10 changes: 10 additions & 0 deletions packages/expect/src/jest-asymmetric-matchers.ts
Expand Up @@ -41,6 +41,16 @@ export abstract class AsymmetricMatcher<
abstract toString(): string
getExpectedType?(): string
toAsymmetricMatcher?(): string

// implement custom chai/loupe inspect for better AssertionError.message formatting
// https://github.com/chaijs/loupe/blob/9b8a6deabcd50adc056a64fb705896194710c5c6/src/index.ts#L29
[Symbol.for('chai/inspect')](options: { depth: number; truncate: number }) {
// minimal pretty-format with simple manual truncation
const result = stringify(this, options.depth, { min: true })
if (result.length <= options.truncate)
return result
return `${this.toString()}{…}`
}
}

export class StringContaining extends AsymmetricMatcher<string> {
Expand Down
229 changes: 229 additions & 0 deletions test/core/test/__snapshots__/jest-expect.test.ts.snap
@@ -0,0 +1,229 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`asymmetric matcher error 1`] = `
{
"actual": "hello",
"diff": null,
"expected": "StringContaining "xx"",
"message": "expected 'hello' to deeply equal StringContaining "xx"",
}
`;

exports[`asymmetric matcher error 2`] = `
{
"actual": "hello",
"diff": null,
"expected": "StringNotContaining "ll"",
"message": "expected 'hello' to deeply equal StringNotContaining "ll"",
}
`;

exports[`asymmetric matcher error 3`] = `
{
"actual": "Object {
"foo": "hello",
}",
"diff": "- Expected
+ Received
Object {
- "foo": StringContaining "xx",
+ "foo": "hello",
}",
"expected": "Object {
"foo": StringContaining "xx",
}",
"message": "expected { foo: 'hello' } to deeply equal { foo: StringContaining "xx" }",
}
`;

exports[`asymmetric matcher error 4`] = `
{
"actual": "Object {
"foo": "hello",
}",
"diff": "- Expected
+ Received
Object {
- "foo": StringNotContaining "ll",
+ "foo": "hello",
}",
"expected": "Object {
"foo": StringNotContaining "ll",
}",
"message": "expected { foo: 'hello' } to deeply equal { foo: StringNotContaining "ll" }",
}
`;

exports[`asymmetric matcher error 5`] = `
{
"actual": "hello",
"diff": "- Expected:
stringContainingCustom<xx>
+ Received:
"hello"",
"expected": "stringContainingCustom<xx>",
"message": "expected 'hello' to deeply equal stringContainingCustom<xx>",
}
`;
exports[`asymmetric matcher error 6`] = `
{
"actual": "hello",
"diff": "- Expected:
not.stringContainingCustom<ll>
+ Received:
"hello"",
"expected": "not.stringContainingCustom<ll>",
"message": "expected 'hello' to deeply equal not.stringContainingCustom<ll>",
}
`;
exports[`asymmetric matcher error 7`] = `
{
"actual": "Object {
"foo": "hello",
}",
"diff": "- Expected
+ Received
Object {
- "foo": stringContainingCustom<xx>,
+ "foo": "hello",
}",
"expected": "Object {
"foo": stringContainingCustom<xx>,
}",
"message": "expected { foo: 'hello' } to deeply equal { foo: stringContainingCustom<xx> }",
}
`;
exports[`asymmetric matcher error 8`] = `
{
"actual": "Object {
"foo": "hello",
}",
"diff": "- Expected
+ Received
Object {
- "foo": not.stringContainingCustom<ll>,
+ "foo": "hello",
}",
"expected": "Object {
"foo": not.stringContainingCustom<ll>,
}",
"message": "expected { foo: 'hello' } to deeply equal { foo: not.stringContainingCustom<ll> }",
}
`;
exports[`asymmetric matcher error 9`] = `
{
"actual": "undefined",
"diff": undefined,
"expected": "undefined",
"message": "expected "hello" to contain "xx"",
}
`;
exports[`asymmetric matcher error 10`] = `
{
"actual": "undefined",
"diff": undefined,
"expected": "undefined",
"message": "expected "hello" not to contain "ll"",
}
`;
exports[`asymmetric matcher error 11`] = `
{
"actual": "hello",
"diff": "- Expected:
testComplexMatcher<[object Object]>
+ Received:
"hello"",
"expected": "testComplexMatcher<[object Object]>",
"message": "expected 'hello' to deeply equal testComplexMatcher<[object Object]>",
}
`;
exports[`asymmetric matcher error 12`] = `
{
"actual": "Object {
"k": "v",
"k2": "v2",
}",
"diff": "- Expected
+ Received
- ObjectContaining {
+ Object {
"k": "v",
- "k3": "v3",
+ "k2": "v2",
}",
"expected": "ObjectContaining {
"k": "v",
"k3": "v3",
}",
"message": "expected { k: 'v', k2: 'v2' } to deeply equal ObjectContaining {"k": "v", "k3": "v3"}",
}
`;
exports[`asymmetric matcher error 13`] = `
{
"actual": "Array [
"a",
"b",
]",
"diff": "- Expected
+ Received
- ArrayContaining [
+ Array [
"a",
- "c",
+ "b",
]",
"expected": "ArrayContaining [
"a",
"c",
]",
"message": "expected [ 'a', 'b' ] to deeply equal ArrayContaining ["a", "c"]",
}
`;
exports[`asymmetric matcher error 14`] = `
{
"actual": "hello",
"diff": null,
"expected": "StringMatching /xx/",
"message": "expected 'hello' to deeply equal StringMatching /xx/",
}
`;
exports[`asymmetric matcher error 15`] = `
{
"actual": "2.5",
"diff": "- Expected
+ Received
- NumberCloseTo 2 (1 digit)
+ 2.5",
"expected": "NumberCloseTo 2 (1 digit)",
"message": "expected 2.5 to deeply equal NumberCloseTo 2 (1 digit)",
}
`;
exports[`asymmetric matcher error 16`] = `
{
"actual": "hello",
"diff": null,
"expected": "StringContaining "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"",
"message": "expected 'hello' to deeply equal StringContaining{…}",
}
`;
72 changes: 69 additions & 3 deletions test/core/test/jest-expect.test.ts
Expand Up @@ -180,7 +180,7 @@ describe('jest-expect', () => {
}).toEqual({
sum: expect.closeTo(0.4),
})
}).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected { sum: 0.30000000000000004 } to deeply equal { sum: CloseTo{ …(4) } }]`)
}).toThrowErrorMatchingInlineSnapshot(`[AssertionError: expected { sum: 0.30000000000000004 } to deeply equal { sum: NumberCloseTo 0.4 (2 digits) }]`)
})

it('asymmetric matchers negate', () => {
Expand Down Expand Up @@ -947,7 +947,7 @@ it('toHaveProperty error diff', () => {
// non match value (with asymmetric matcher)
expect(getError(() => expect({ name: 'foo' }).toHaveProperty('name', expect.any(Number)))).toMatchInlineSnapshot(`
[
"expected { name: 'foo' } to have property "name" with value Any{ …(3) }",
"expected { name: 'foo' } to have property "name" with value Any<Number>",
"- Expected:
Any<Number>
Expand All @@ -959,7 +959,7 @@ it('toHaveProperty error diff', () => {
// non match key (with asymmetric matcher)
expect(getError(() => expect({ noName: 'foo' }).toHaveProperty('name', expect.any(Number)))).toMatchInlineSnapshot(`
[
"expected { noName: 'foo' } to have property "name" with value Any{ …(3) }",
"expected { noName: 'foo' } to have property "name" with value Any<Number>",
"- Expected:
Any<Number>
Expand Down Expand Up @@ -993,4 +993,70 @@ it('toHaveProperty error diff', () => {
`)
})

it('asymmetric matcher error', () => {
setupColors(getDefaultColors())

function snapshotError(f: () => unknown) {
try {
f()
return expect.unreachable()
}
catch (error) {
const e = processError(error)
expect({
message: e.message,
diff: e.diff,
expected: e.expected,
actual: e.actual,
}).toMatchSnapshot()
}
}

expect.extend({
stringContainingCustom(received: unknown, other: string) {
return {
pass: typeof received === 'string' && received.includes(other),
message: () => `expected ${this.utils.printReceived(received)} ${this.isNot ? 'not ' : ''}to contain ${this.utils.printExpected(other)}`,
}
},
})

// builtin: stringContaining
snapshotError(() => expect('hello').toEqual(expect.stringContaining('xx')))
snapshotError(() => expect('hello').toEqual(expect.not.stringContaining('ll')))
snapshotError(() => expect({ foo: 'hello' }).toEqual({ foo: expect.stringContaining('xx') }))
snapshotError(() => expect({ foo: 'hello' }).toEqual({ foo: expect.not.stringContaining('ll') }))

// custom
snapshotError(() => expect('hello').toEqual((expect as any).stringContainingCustom('xx')))
snapshotError(() => expect('hello').toEqual((expect as any).not.stringContainingCustom('ll')))
snapshotError(() => expect({ foo: 'hello' }).toEqual({ foo: (expect as any).stringContainingCustom('xx') }))
snapshotError(() => expect({ foo: 'hello' }).toEqual({ foo: (expect as any).not.stringContainingCustom('ll') }))

// assertion form
snapshotError(() => (expect('hello') as any).stringContainingCustom('xx'))
snapshotError(() => (expect('hello') as any).not.stringContainingCustom('ll'))

// matcher with complex argument
// (serialized by `String` so it becomes "testComplexMatcher<[object Object]>", which is same as jest's asymmetric matcher and pretty-format)
expect.extend({
testComplexMatcher(_received: unknown, _arg: unknown) {
return {
pass: false,
message: () => `NA`,
}
},
})
snapshotError(() => expect('hello').toEqual((expect as any).testComplexMatcher({ x: 'y' })))

// more builtins
snapshotError(() => expect({ k: 'v', k2: 'v2' }).toEqual(expect.objectContaining({ k: 'v', k3: 'v3' })))
snapshotError(() => expect(['a', 'b']).toEqual(expect.arrayContaining(['a', 'c'])))
snapshotError(() => expect('hello').toEqual(expect.stringMatching(/xx/)))
snapshotError(() => expect(2.5).toEqual(expect.closeTo(2, 1)))

// simple truncation if pretty-format is too long
snapshotError(() => expect('hello').toEqual(expect.stringContaining('a'.repeat(40))))
})

it('timeout', () => new Promise(resolve => setTimeout(resolve, 500)))

0 comments on commit 06bae4d

Please sign in to comment.