diff --git a/packages/utils/src/diff/index.ts b/packages/utils/src/diff/index.ts index 09f37d0e4db2..6887e55537b4 100644 --- a/packages/utils/src/diff/index.ts +++ b/packages/utils/src/diff/index.ts @@ -337,12 +337,36 @@ export function replaceAsymmetricMatcher( const actualValue = actual[key] if (isAsymmetricMatcher(expectedValue)) { if (expectedValue.asymmetricMatch(actualValue)) { - actual[key] = expectedValue + // When matcher matches, replace expected with actual value + // so they appear the same in the diff + expected[key] = actualValue + } + else if ('sample' in expectedValue && expectedValue.sample !== undefined && isReplaceable(actualValue, expectedValue.sample)) { + // For container matchers (ArrayContaining, ObjectContaining), unwrap and recursively process + // Matcher doesn't match: unwrap but keep structure to show mismatch + const replaced = replaceAsymmetricMatcher( + actualValue, + expectedValue.sample, + actualReplaced, + expectedReplaced, + ) + actual[key] = replaced.replacedActual + expected[key] = replaced.replacedExpected } } else if (isAsymmetricMatcher(actualValue)) { if (actualValue.asymmetricMatch(expectedValue)) { - expected[key] = actualValue + actual[key] = expectedValue + } + else if ('sample' in actualValue && actualValue.sample !== undefined && isReplaceable(actualValue.sample, expectedValue)) { + const replaced = replaceAsymmetricMatcher( + actualValue.sample, + expectedValue, + actualReplaced, + expectedReplaced, + ) + actual[key] = replaced.replacedActual + expected[key] = replaced.replacedExpected } } else if (isReplaceable(actualValue, expectedValue)) { diff --git a/test/core/test/diff.test.ts b/test/core/test/diff.test.ts index e037aa3d0acf..df74efc3bef0 100644 --- a/test/core/test/diff.test.ts +++ b/test/core/test/diff.test.ts @@ -142,7 +142,7 @@ test('asymmetric matcher in object', () => { { - "x": 1, + "x": 0, - "y": Anything, + "y": "foo", }" `) }) @@ -159,7 +159,7 @@ test('asymmetric matcher in object with truncated diff', () => { + Received { - "w": Anything, + "w": "foo", - "x": 1, + "x": 0, ... Diff result is truncated" @@ -174,7 +174,7 @@ test('asymmetric matcher in array', () => { [ - 1, + 0, - Anything, + "foo", ]" `) }) @@ -211,12 +211,12 @@ test('asymmetric matcher in nested', () => { { - "x": 1, + "x": 0, - "y": Anything, + "y": "foo", }, [ - 1, + 0, - Anything, + "bar", ], ]" `) @@ -237,8 +237,8 @@ test('asymmetric matcher in nested with truncated diff', () => { { - "x": 1, + "x": 0, - "y": Anything, - "z": Anything, + "y": "foo", + "z": "bar", ... Diff result is truncated" `) }) @@ -353,3 +353,136 @@ function getErrorDiff(actual: unknown, expected: unknown, options?: DiffOptions) } return expect.unreachable() } + +test('asymmetric matcher with objectContaining - simple case', () => { + const actual = { + user: { + name: 'John', + age: 25, + email: 'john@example.com', + }, + } + + const expected = { + user: expect.objectContaining({ + name: expect.stringContaining('Jane'), + age: expect.any(Number), + email: expect.stringContaining('example.com'), + }), + } + + expect(stripVTControlCharacters(getErrorDiff(actual, expected))).toMatchInlineSnapshot(` + "- Expected + + Received + + { + "user": { + "age": 25, + "email": "john@example.com", + - "name": StringContaining "Jane", + + "name": "John", + }, + }" + `) +}) + +test('asymmetric matcher with nested objectContaining and arrayContaining', () => { + const actual = { + model: 'veo-3.1-generate-preview', + instances: [ + { + prompt: 'walk', + referenceImages: [ + { + image: { + gcsUri: 'gs://example/person1.jpg', + mimeType: 'image/png', + }, + referenceType: 'asset', + }, + { + image: { + gcsUri: 'gs://example/person.jpg', + mimeType: 'image/png', + }, + referenceType: 'asset', + }, + ], + }, + ], + parameters: { + durationSeconds: '8', + aspectRatio: '16:9', + generateAudio: true, + }, + } + + const expected = { + model: expect.stringMatching(/^veo-3\.1-(fast-)?generate-preview$/), + instances: expect.arrayContaining([ + expect.objectContaining({ + prompt: expect.stringMatching(/^(?=.*walking)(?=.*together)(?=.*park).*/i), + referenceImages: expect.arrayContaining([ + expect.objectContaining({ + image: expect.objectContaining({ + gcsUri: expect.stringContaining('person1.jpg'), + mimeType: 'image/jpeg', + }), + referenceType: expect.stringMatching(/^(asset|style)$/), + }), + expect.objectContaining({ + image: expect.objectContaining({ + gcsUri: expect.stringContaining('person2.png'), + mimeType: 'image/png', + }), + referenceType: expect.stringMatching(/^(asset|style)$/), + }), + ]), + }), + ]), + parameters: expect.objectContaining({ + durationSeconds: expect.any(Number), + aspectRatio: '16:9', + generateAudio: expect.any(Boolean), + }), + } + + expect(stripVTControlCharacters(getErrorDiff(actual, expected))).toMatchInlineSnapshot(` + "- Expected + + Received + + { + "instances": [ + { + - "prompt": StringMatching /^(?=.*walking)(?=.*together)(?=.*park).*/i, + + "prompt": "walk", + "referenceImages": [ + { + "image": { + "gcsUri": "gs://example/person1.jpg", + - "mimeType": "image/jpeg", + + "mimeType": "image/png", + }, + "referenceType": "asset", + }, + { + "image": { + - "gcsUri": StringContaining "person2.png", + + "gcsUri": "gs://example/person.jpg", + "mimeType": "image/png", + }, + "referenceType": "asset", + }, + ], + }, + ], + "model": "veo-3.1-generate-preview", + "parameters": { + "aspectRatio": "16:9", + - "durationSeconds": Any, + + "durationSeconds": "8", + "generateAudio": true, + }, + }" + `) +}) diff --git a/test/core/test/expect.test.ts b/test/core/test/expect.test.ts index 7abb03cd00bb..4d4b1e92abe5 100644 --- a/test/core/test/expect.test.ts +++ b/test/core/test/expect.test.ts @@ -630,7 +630,7 @@ describe('Standard Schema', () => { - ], - }, + "age": "thirty", - "name": SchemaMatching, + "name": "John", }, }" `) diff --git a/test/core/test/jest-expect.test.ts b/test/core/test/jest-expect.test.ts index 8e3a2c9d9856..cee708112ac4 100644 --- a/test/core/test/jest-expect.test.ts +++ b/test/core/test/jest-expect.test.ts @@ -1318,7 +1318,7 @@ it('correctly prints diff with asymmetric matchers', () => { + Received { - "a": Any, + "a": 1, - "b": Any, + "b": "string", }"