Compare commits

...

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
06f2825949 [autofix.ci] apply automated fixes 2025-09-26 15:40:39 +00:00
Claude Bot
2a5a06d3ba Fix #3521: expect.any, toMatchObject, and toMatchSnapshot no longer mutate objects
Previously, when using asymmetric matchers like expect.any() with toMatchObject()
or toMatchSnapshot(), the original objects would be permanently mutated. The matcher
would replace actual property values with their string representations.

This fix disables the mutation behavior entirely by always passing false to
jestDeepMatch. This ensures comparisons work correctly but objects are never modified.

Changes:
- toMatchObject: Now passes false to jestDeepMatch
- toMatchSnapshot with property matchers: Now passes false to jestDeepMatch
- Trade-off: Error messages and snapshots now show actual values instead of
  Any<Type> representations, but user objects are never mutated

The mutation feature was originally intended to make error messages prettier
by showing Any<Date> instead of actual dates, but mutating user objects is
an unacceptable side effect. A future improvement could clone objects before
mutation for display purposes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 15:38:16 +00:00
6 changed files with 151 additions and 2 deletions

View File

@@ -799,7 +799,9 @@ pub const Expect = struct {
const prop_matchers = _prop_matchers;
if (!try value.jestDeepMatch(prop_matchers, globalThis, true)) {
// Check if it matches without mutation to avoid issue #3521
// We no longer mutate objects even for snapshots
if (!try value.jestDeepMatch(prop_matchers, globalThis, false)) {
// TODO: print diff with properties from propertyMatchers
const signature = comptime getSignature(fn_name, "<green>propertyMatchers<r>", false);
const fmt = signature ++ "\n\nExpected <green>propertyMatchers<r> to match properties from received object" ++

View File

@@ -34,7 +34,9 @@ pub fn toMatchObject(this: *Expect, globalThis: *JSGlobalObject, callFrame: *Cal
const property_matchers = args[0];
var pass = try received_object.jestDeepMatch(property_matchers, globalThis, true);
// Do the comparison without mutation to avoid mutating user's objects
// This fixes issue #3521 where expect.any() was mutating the original object
var pass = try received_object.jestDeepMatch(property_matchers, globalThis, false);
if (not) pass = !pass;
if (pass) return .js_undefined;

View File

@@ -607,3 +607,10 @@ exports[`snapshot numbering 4`] = `"snap"`;
exports[`snapshot numbering 6`] = `"hello"`;
exports[`snapshot numbering: hinted 1`] = `"hello"`;
exports[`snapshots backtick in test name 2`] = `
"// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[\`\\\` 1\`] = \`"abc"\`;
"
`;

View File

@@ -0,0 +1,27 @@
import { expect, test } from "bun:test";
test("issue #3521: toMatchSnapshot should also not mutate the original object", () => {
const obj = {
id: 123,
createdAt: new Date("2024-01-01"),
name: "Test User",
};
// Save original values
const originalId = obj.id;
const originalCreatedAt = obj.createdAt;
// Note: We're accepting the snapshot change since it now shows actual values
// instead of Any<Type>, which is a trade-off for not mutating objects
expect(obj).toMatchSnapshot();
// Verify the original object wasn't mutated (this is the important part)
expect(obj.id).toBe(originalId);
expect(obj.createdAt).toBe(originalCreatedAt);
expect(obj.id).toBe(123);
expect(obj.createdAt).toEqual(new Date("2024-01-01"));
// Confirm the object properties are still the original types
expect(typeof obj.id).toBe("number");
expect(obj.createdAt).toBeInstanceOf(Date);
});

View File

@@ -0,0 +1,102 @@
import { expect, test } from "bun:test";
test("issue #3521: expect.any and toMatchObject should not mutate the original object", () => {
const obj = {
foo: "foo",
bar: "bar",
nested: {
value: 42,
text: "hello",
},
};
// Save original values to verify no mutation
const originalBar = obj.bar;
const originalNestedText = obj.nested.text;
// Test with single property
expect(obj).toMatchObject({
bar: expect.any(String),
});
// Verify no mutation occurred
expect(obj.bar).toBe(originalBar);
expect(obj.bar).not.toContain("Any<String>");
expect(obj.bar).toBe("bar");
// Test with nested property
expect(obj).toMatchObject({
nested: {
text: expect.any(String),
},
});
// Verify nested property wasn't mutated
expect(obj.nested.text).toBe(originalNestedText);
expect(obj.nested.text).not.toContain("Any<String>");
expect(obj.nested.text).toBe("hello");
// Test with multiple matchers
const complexObj = {
str: "string",
num: 42,
bool: true,
arr: [1, 2, 3],
date: new Date(),
};
const originalComplexObj = {
...complexObj,
arr: [...complexObj.arr],
date: complexObj.date,
};
expect(complexObj).toMatchObject({
str: expect.any(String),
num: expect.any(Number),
bool: expect.any(Boolean),
arr: expect.any(Array),
date: expect.any(Date),
});
// Verify all properties remain unchanged
expect(complexObj.str).toBe(originalComplexObj.str);
expect(complexObj.num).toBe(originalComplexObj.num);
expect(complexObj.bool).toBe(originalComplexObj.bool);
expect(complexObj.arr).toEqual(originalComplexObj.arr);
expect(complexObj.date).toBe(originalComplexObj.date);
// None of the properties should have been replaced with matcher representations
expect(complexObj.str).not.toContain("Any<");
expect(String(complexObj.num)).not.toContain("Any<");
expect(String(complexObj.bool)).not.toContain("Any<");
expect(String(complexObj.arr)).not.toContain("Any<");
expect(String(complexObj.date)).not.toContain("Any<");
});
test("issue #3521: expect.objectContaining should not mutate the original object", () => {
const obj = {
meta: {
id: "123",
timestamp: Date.now(),
nested: {
deep: "value",
},
},
data: "some data",
};
const originalMeta = { ...obj.meta };
expect(obj).toMatchObject({
meta: expect.objectContaining({
id: expect.any(String),
timestamp: expect.any(Number),
}),
});
// Verify no mutations
expect(obj.meta.id).toBe(originalMeta.id);
expect(obj.meta.timestamp).toBe(originalMeta.timestamp);
expect(obj.meta.id).not.toContain("Any<");
});

View File

@@ -0,0 +1,9 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`issue #3521: toMatchSnapshot should also not mutate the original object 1`] = `
{
"createdAt": 2024-01-01T00:00:00.000Z,
"id": 123,
"name": "Test User",
}
`;