Skip to content
55 changes: 51 additions & 4 deletions packages/utils/src/diff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,13 +336,60 @@ export function replaceAsymmetricMatcher(
const expectedValue = expected[key]
const actualValue = actual[key]
if (isAsymmetricMatcher(expectedValue)) {
if (expectedValue.asymmetricMatch(actualValue)) {
actual[key] = expectedValue
const matches = expectedValue.asymmetricMatch(actualValue)
// For container matchers (ArrayContaining, ObjectContaining), unwrap and recursively process
if ('sample' in expectedValue && expectedValue.sample !== undefined && isReplaceable(actualValue, expectedValue.sample)) {
if (matches) {
// Matcher matches: unwrap and recursively process to show actual structure
const replaced = replaceAsymmetricMatcher(
actualValue,
expectedValue.sample,
actualReplaced,
expectedReplaced,
)
actual[key] = replaced.replacedActual
expected[key] = replaced.replacedExpected
Comment thread
hi-ogawa marked this conversation as resolved.
Outdated
}
Comment thread
hi-ogawa marked this conversation as resolved.
Outdated
else {
// 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 {
// Simple matchers (StringContaining, Any, etc.)
if (matches) {
// When matcher matches, replace expected with actual value
// so they appear the same in the diff
expected[key] = actualValue
}
// When matcher doesn't match, keep both as-is to show the difference
}
}
else if (isAsymmetricMatcher(actualValue)) {
if (actualValue.asymmetricMatch(expectedValue)) {
expected[key] = actualValue
const matches = actualValue.asymmetricMatch(expectedValue)
// For container matchers in actual (rare case)
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 (matches) {
// When matcher matches, replace actual with expected value
actual[key] = expectedValue
}
}
}
else if (isReplaceable(actualValue, expectedValue)) {
Expand Down
151 changes: 151 additions & 0 deletions test/core/test/asymmetric-diff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { stripVTControlCharacters } from 'node:util'
import { processError } from '@vitest/utils/error'
import { describe, expect, it } from 'vitest'

describe('asymmetric matcher diff display', () => {
it('shows clear diff when simple property mismatch', () => {
const actual = {
user: {
name: 'John',
age: 25,
email: 'john@example.com',
},
}

// Test should fail - name doesn't contain "Jane"
try {
expect(actual).toMatchObject({
user: expect.objectContaining({
name: expect.stringContaining('Jane'),
age: expect.any(Number),
email: expect.stringContaining('example.com'),
}),
})
expect.unreachable()
}
catch (err) {
const error = processError(err)
expect(stripVTControlCharacters(error.diff)).toMatchInlineSnapshot(`
"- Expected
+ Received

{
"user": {
"age": 25,
"email": "john@example.com",
- "name": StringContaining "Jane",
+ "name": "John",
},
}"
`)
}
})

it('shows clear diff with nested objectContaining - complex case', () => {
// Actual data structure similar to the issue example
const actual = {
model: 'veo-3.1-generate-preview',
instances: [
{
prompt: 'walk', // This doesn't match the expected regex
referenceImages: [
{
image: {
gcsUri: 'gs://example/person1.jpg',
mimeType: 'image/png', // Mismatch: expected jpeg
},
referenceType: 'asset',
},
{
image: {
gcsUri: 'gs://example/person.jpg', // Mismatch: doesn't contain "person2.png"
mimeType: 'image/png',
},
referenceType: 'asset',
},
],
},
],
parameters: {
durationSeconds: '8', // Mismatch: string instead of number
aspectRatio: '16:9',
generateAudio: true,
},
}

// This should fail with multiple mismatches
try {
expect(actual).toMatchObject({
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.unreachable()
}
catch (err) {
const error = processError(err)
expect(stripVTControlCharacters(error.diff)).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<Number>,
+ "durationSeconds": "8",
"generateAudio": true,
},
}"
`)
}
})
})
2 changes: 1 addition & 1 deletion test/core/test/expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,7 +630,7 @@ describe('Standard Schema', () => {
- ],
- },
+ "age": "thirty",
"name": SchemaMatching,
"name": "John",
},
}"
`)
Expand Down
2 changes: 1 addition & 1 deletion test/core/test/jest-expect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1318,7 +1318,7 @@ it('correctly prints diff with asymmetric matchers', () => {
+ Received
{
"a": Any<Number>,
"a": 1,
- "b": Any<Function>,
+ "b": "string",
}"
Expand Down
Loading