Skip to content
28 changes: 26 additions & 2 deletions packages/utils/src/diff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
147 changes: 140 additions & 7 deletions test/core/test/diff.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ test('asymmetric matcher in object', () => {
{
- "x": 1,
+ "x": 0,
"y": Anything,
"y": "foo",
}"
`)
})
Expand All @@ -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"
Expand All @@ -174,7 +174,7 @@ test('asymmetric matcher in array', () => {
[
- 1,
+ 0,
Anything,
"foo",
]"
`)
})
Expand Down Expand Up @@ -211,12 +211,12 @@ test('asymmetric matcher in nested', () => {
{
- "x": 1,
+ "x": 0,
"y": Anything,
"y": "foo",
},
[
- 1,
+ 0,
Anything,
"bar",
],
]"
`)
Expand All @@ -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"
`)
})
Expand Down Expand Up @@ -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: '[email protected]',
},
}

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": "[email protected]",
- "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<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