Skip to content

Commit 3c3f9f2

Browse files
swciselthymikee
authored andcommitted
Correctly merge property matchers with the rest of the snapshot in toMatchSnapshot. (#6528)
1 parent f8f966b commit 3c3f9f2

File tree

5 files changed

+105
-7
lines changed

5 files changed

+105
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
- `[jest-cli]` Don't report promises as open handles ([#6716](https://github.com/facebook/jest/pull/6716))
1919
- `[jest-each]` Add timeout support to parameterised tests ([#6660](https://github.com/facebook/jest/pull/6660))
2020
- `[jest-cli]` Improve the message when running coverage while there are no files matching global threshold ([#6334](https://github.com/facebook/jest/pull/6334))
21+
- `[jest-snapshot]` Correctly merge property matchers with the rest of the snapshot in `toMatchSnapshot`. ([#6528](https://github.com/facebook/jest/pull/6528))
22+
- `[jest-snapshot]` Add error messages for invalid property matchers. ([#6528](https://github.com/facebook/jest/pull/6528))
2123

2224
## 23.4.2
2325

e2e/__tests__/to_match_snapshot.test.js

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,49 @@ test('handles property matchers', () => {
191191
}
192192
});
193193

194+
test('handles invalid property matchers', () => {
195+
const filename = 'handle-property-matchers.test.js';
196+
{
197+
writeFiles(TESTS_DIR, {
198+
[filename]: `test('invalid property matchers', () => {
199+
expect({foo: 'bar'}).toMatchSnapshot(null);
200+
});
201+
`,
202+
});
203+
const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]);
204+
expect(stderr).toMatch('Property matchers must be an object.');
205+
expect(status).toBe(1);
206+
}
207+
{
208+
writeFiles(TESTS_DIR, {
209+
[filename]: `test('invalid property matchers', () => {
210+
expect({foo: 'bar'}).toMatchSnapshot(null, 'test-name');
211+
});
212+
`,
213+
});
214+
const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]);
215+
expect(stderr).toMatch('Property matchers must be an object.');
216+
expect(stderr).toMatch(
217+
'To provide a snapshot test name without property matchers, use: toMatchSnapshot("name")',
218+
);
219+
expect(status).toBe(1);
220+
}
221+
{
222+
writeFiles(TESTS_DIR, {
223+
[filename]: `test('invalid property matchers', () => {
224+
expect({foo: 'bar'}).toMatchSnapshot(undefined, 'test-name');
225+
});
226+
`,
227+
});
228+
const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]);
229+
expect(stderr).toMatch('Property matchers must be an object.');
230+
expect(stderr).toMatch(
231+
'To provide a snapshot test name without property matchers, use: toMatchSnapshot("name")',
232+
);
233+
expect(status).toBe(1);
234+
}
235+
});
236+
194237
test('handles property matchers with custom name', () => {
195238
const filename = 'handle-property-matchers-with-name.test.js';
196239
const template = makeTemplate(`test('handles property matchers with name', () => {
@@ -217,20 +260,21 @@ test('handles property matchers with custom name', () => {
217260
expect(stderr).toMatch(
218261
'Received value does not match snapshot properties for "handles property matchers with name: custom-name 1".',
219262
);
263+
expect(stderr).toMatch('Expected snapshot to match properties:');
220264
expect(stderr).toMatch('Snapshots: 1 failed, 1 total');
221265
expect(status).toBe(1);
222266
}
223267
});
224268

225-
test('handles property matchers with deep expect.objectContaining', () => {
269+
test('handles property matchers with deep properties', () => {
226270
const filename = 'handle-property-matchers-with-name.test.js';
227-
const template = makeTemplate(`test('handles property matchers with deep expect.objectContaining', () => {
228-
expect({ user: { createdAt: $1, name: 'Jest' }}).toMatchSnapshot({ user: expect.objectContaining({ createdAt: expect.any(Date) }) });
271+
const template = makeTemplate(`test('handles property matchers with deep properties', () => {
272+
expect({ user: { createdAt: $1, name: $2 }}).toMatchSnapshot({ user: { createdAt: expect.any(Date), name: $2 }});
229273
});
230274
`);
231275

232276
{
233-
writeFiles(TESTS_DIR, {[filename]: template(['new Date()'])});
277+
writeFiles(TESTS_DIR, {[filename]: template(['new Date()', '"Jest"'])});
234278
const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]);
235279
expect(stderr).toMatch('1 snapshot written from 1 test suite.');
236280
expect(status).toBe(0);
@@ -243,10 +287,21 @@ test('handles property matchers with deep expect.objectContaining', () => {
243287
}
244288

245289
{
246-
writeFiles(TESTS_DIR, {[filename]: template(['"string"'])});
290+
writeFiles(TESTS_DIR, {[filename]: template(['"string"', '"Jest"'])});
291+
const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]);
292+
expect(stderr).toMatch(
293+
'Received value does not match snapshot properties for "handles property matchers with deep properties 1".',
294+
);
295+
expect(stderr).toMatch('Expected snapshot to match properties:');
296+
expect(stderr).toMatch('Snapshots: 1 failed, 1 total');
297+
expect(status).toBe(1);
298+
}
299+
300+
{
301+
writeFiles(TESTS_DIR, {[filename]: template(['new Date()', '"CHANGED"'])});
247302
const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]);
248303
expect(stderr).toMatch(
249-
'Received value does not match snapshot properties for "handles property matchers with deep expect.objectContaining 1".',
304+
'Received value does not match stored snapshot "handles property matchers with deep properties 1"',
250305
);
251306
expect(stderr).toMatch('Snapshots: 1 failed, 1 total');
252307
expect(status).toBe(1);

packages/jest-snapshot/src/__tests__/utils.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const {
1818
saveSnapshotFile,
1919
serialize,
2020
testNameToKey,
21+
deepMerge,
2122
SNAPSHOT_GUIDE_LINK,
2223
SNAPSHOT_VERSION,
2324
SNAPSHOT_VERSION_WARNING,
@@ -198,3 +199,15 @@ test('serialize handles \\r\\n', () => {
198199

199200
expect(serializedData).toBe('\n"<div>\n</div>"\n');
200201
});
202+
203+
describe('DeepMerge', () => {
204+
it('Correctly merges objects with property matchers', () => {
205+
const target = {data: {bar: 'bar', foo: 'foo'}};
206+
const matcher = expect.any(String);
207+
const propertyMatchers = {data: {foo: matcher}};
208+
const mergedOutput = deepMerge(target, propertyMatchers);
209+
210+
expect(mergedOutput).toStrictEqual({data: {bar: 'bar', foo: matcher}});
211+
expect(target).toStrictEqual({data: {bar: 'bar', foo: 'foo'}});
212+
});
213+
});

packages/jest-snapshot/src/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ const toMatchSnapshot = function(
5353
propertyMatchers?: any,
5454
testName?: string,
5555
) {
56+
if (arguments.length === 3 && !propertyMatchers) {
57+
throw new Error(
58+
'Property matchers must be an object.\n\nTo provide a snapshot test name without property matchers, use: toMatchSnapshot("name")',
59+
);
60+
}
61+
5662
return _toMatchSnapshot({
5763
context: this,
5864
propertyMatchers,
@@ -118,6 +124,9 @@ const _toMatchSnapshot = ({
118124
: currentTestName || '';
119125

120126
if (typeof propertyMatchers === 'object') {
127+
if (propertyMatchers === null) {
128+
throw new Error(`Property matchers must be an object.`);
129+
}
121130
const propertyPass = context.equals(received, propertyMatchers, [
122131
context.utils.iterableEquality,
123132
context.utils.subsetEquality,
@@ -144,7 +153,7 @@ const _toMatchSnapshot = ({
144153
report,
145154
};
146155
} else {
147-
Object.assign(received, propertyMatchers);
156+
received = utils.deepMerge(received, propertyMatchers);
148157
}
149158
}
150159

packages/jest-snapshot/src/utils.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ const validateSnapshotVersion = (snapshotContents: string) => {
7777
return null;
7878
};
7979

80+
function isObject(item) {
81+
return item && typeof item === 'object' && !Array.isArray(item);
82+
}
83+
8084
export const testNameToKey = (testName: string, count: number) =>
8185
testName + ' ' + count;
8286

@@ -180,3 +184,18 @@ export const saveSnapshotFile = (
180184
writeSnapshotVersion() + '\n\n' + snapshots.join('\n\n') + '\n',
181185
);
182186
};
187+
188+
export const deepMerge = (target: any, source: any) => {
189+
const mergedOutput = Object.assign({}, target);
190+
if (isObject(target) && isObject(source)) {
191+
Object.keys(source).forEach(key => {
192+
if (isObject(source[key]) && !source[key].$$typeof) {
193+
if (!(key in target)) Object.assign(mergedOutput, {[key]: source[key]});
194+
else mergedOutput[key] = deepMerge(target[key], source[key]);
195+
} else {
196+
Object.assign(mergedOutput, {[key]: source[key]});
197+
}
198+
});
199+
}
200+
return mergedOutput;
201+
};

0 commit comments

Comments
 (0)