Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(expect): implement chai inspect for AsymmetricMatcher #4942

Merged
10 changes: 10 additions & 0 deletions packages/expect/src/jest-asymmetric-matchers.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,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) }]`)

// TODO: support set
// expect(new Set(['bar'])).not.toEqual(new Set([expect.stringContaining('zoo')]))
Expand Down Expand Up @@ -949,7 +949,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 @@ -961,7 +961,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 @@ -995,4 +995,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)))