Skip to content

Commit bd8de10

Browse files
committed
Add toMatchNamedSnapshot
1 parent 6460335 commit bd8de10

File tree

5 files changed

+127
-7
lines changed

5 files changed

+127
-7
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525

2626
### Performance
2727

28+
## 29.6.0
29+
30+
### Features
31+
32+
- `[jest-snapshot]` Add `toMatchNamedSnapshot` to bring snapshot support to concurrent mode
2833
## 29.5.0
2934

3035
### Features

docs/ExpectAPI.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,16 @@ You can provide an optional `propertyMatchers` object argument, which has asymme
744744

745745
You can provide an optional `hint` string argument that is appended to the test name. Although Jest always appends a number at the end of a snapshot name, short descriptive hints might be more useful than numbers to differentiate **multiple** snapshots in a **single** `it` or `test` block. Jest sorts snapshots by name in the corresponding `.snap` file.
746746

747+
### `.toMatchNamedSnapshot(propertyMatchers?, snapshotName?)`
748+
749+
This ensures that a value matches the most recent snapshot. Check out [the Snapshot Testing guide](SnapshotTesting.md) for more information.
750+
751+
You can provide an optional `propertyMatchers` object argument, which has asymmetric matchers as values of a subset of expected properties, **if** the received value will be an **object** instance. It is like `toMatchObject` with flexible criteria for a subset of properties, followed by a snapshot test as exact criteria for the rest of the properties.
752+
753+
You can provide an optional `snapshotName` string argument that functions as the test name. By providing a snapshot name (as opposed to letting Jest infer the name from context) snapshot names can be guaranteed to be consistent across test runs, whatever the context at the time of evaluation. Jest always appends a number at the end of a snapshot name.
754+
755+
Jest sorts snapshots by name in the corresponding `.snap` file.
756+
747757
### `.toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot)`
748758

749759
Ensures that a value matches the most recent snapshot.

docs/SnapshotTesting.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ Now, every time the snapshot test case runs, `Date.now()` will return `148236336
264264

265265
Always strive to use descriptive test and/or snapshot names for snapshots. The best names describe the expected snapshot content. This makes it easier for reviewers to verify the snapshots during review, and for anyone to know whether or not an outdated snapshot is the correct behavior before updating.
266266

267+
The default matcher, `toMatchSnapshot`, infers the name based on the test function context when the snapshot is evaluated. This can cause snapshots in tests that are run concurrently to have different names on each test run. If you want to guarantee consistent names, you can use `toMatchNamedSnapshot` as an alternative matcher.
268+
267269
For example, compare:
268270

269271
```js

packages/jest-snapshot/src/index.ts

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,12 @@ import {
3030
printReceived,
3131
printSnapshotAndReceived,
3232
} from './printSnapshot';
33-
import type {Context, FileSystem, MatchSnapshotConfig} from './types';
33+
import type {
34+
Context,
35+
FileSystem,
36+
MatchSnapshotConfig,
37+
SnapshotNameConfig,
38+
} from './types';
3439
import {deepMerge, escapeBacktickString, serialize} from './utils';
3540

3641
export {addSerializer, getSerializers} from './plugins';
@@ -67,6 +72,19 @@ const printSnapshotName = (
6772
} ${count}\``;
6873
};
6974

75+
const getSnapshotName = (config: SnapshotNameConfig): string => {
76+
const {snapshotName, currentTestName, hint} = config;
77+
78+
if (snapshotName) {
79+
return snapshotName;
80+
}
81+
if (currentTestName && hint) {
82+
return `${currentTestName}: ${hint}`;
83+
}
84+
// future BREAKING change: || hint
85+
return currentTestName || '';
86+
};
87+
7088
function stripAddedIndentation(inlineSnapshot: string) {
7189
// Find indentation if exists.
7290
const match = inlineSnapshot.match(INDENTATION_REGEX);
@@ -209,6 +227,67 @@ export const toMatchSnapshot: MatcherFunctionWithContext<
209227
});
210228
};
211229

230+
export const toMatchNamedSnapshot: MatcherFunctionWithContext<
231+
Context,
232+
[propertiesOrSnapshot?: object | string, snapshotName?: string]
233+
> = function (received, propertiesOrSnapshotName, snapshotName) {
234+
const matcherName = 'toMatchNamedSnapshot';
235+
let properties;
236+
237+
const length = arguments.length;
238+
if (length === 2 && typeof propertiesOrSnapshotName === 'string') {
239+
snapshotName = propertiesOrSnapshotName;
240+
} else if (length >= 2) {
241+
if (
242+
Array.isArray(propertiesOrSnapshotName) ||
243+
typeof propertiesOrSnapshotName !== 'object' ||
244+
propertiesOrSnapshotName === null
245+
) {
246+
const options: MatcherHintOptions = {
247+
isNot: this.isNot,
248+
promise: this.promise,
249+
};
250+
let printedWithType = printWithType(
251+
'Expected properties',
252+
propertiesOrSnapshotName,
253+
printExpected,
254+
);
255+
256+
if (length === 3) {
257+
options.secondArgument = 'snapshotName';
258+
options.secondArgumentColor = BOLD_WEIGHT;
259+
260+
if (propertiesOrSnapshotName == null) {
261+
printedWithType +=
262+
"\n\nTo provide a snapshot name without properties: toMatchNamedSnapshot('snapshotName')";
263+
}
264+
}
265+
266+
throw new Error(
267+
matcherErrorMessage(
268+
matcherHint(matcherName, undefined, PROPERTIES_ARG, options),
269+
`Expected ${EXPECTED_COLOR('properties')} must be an object`,
270+
printedWithType,
271+
),
272+
);
273+
}
274+
275+
// Future breaking change: Snapshot hint must be a string
276+
// if (arguments.length === 3 && typeof hint !== 'string') {}
277+
278+
properties = propertiesOrSnapshotName;
279+
}
280+
281+
return _toMatchSnapshot({
282+
context: this,
283+
isInline: false,
284+
matcherName,
285+
properties,
286+
received,
287+
snapshotName,
288+
});
289+
};
290+
212291
export const toMatchInlineSnapshot: MatcherFunctionWithContext<
213292
Context,
214293
[propertiesOrSnapshot?: object | string, inlineSnapshot?: string]
@@ -273,8 +352,15 @@ export const toMatchInlineSnapshot: MatcherFunctionWithContext<
273352
};
274353

275354
const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
276-
const {context, hint, inlineSnapshot, isInline, matcherName, properties} =
277-
config;
355+
const {
356+
context,
357+
hint,
358+
inlineSnapshot,
359+
isInline,
360+
matcherName,
361+
snapshotName,
362+
properties,
363+
} = config;
278364
let {received} = config;
279365

280366
context.dontThrow && context.dontThrow();
@@ -301,10 +387,7 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
301387
);
302388
}
303389

304-
const fullTestName =
305-
currentTestName && hint
306-
? `${currentTestName}: ${hint}`
307-
: currentTestName || ''; // future BREAKING change: || hint
390+
const fullTestName = getSnapshotName({currentTestName, hint, snapshotName});
308391

309392
if (typeof properties === 'object') {
310393
if (typeof received !== 'object' || received === null) {

packages/jest-snapshot/src/types.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,17 @@ export type MatchSnapshotConfig = {
2727
inlineSnapshot?: string;
2828
isInline: boolean;
2929
matcherName: string;
30+
snapshotName?: string;
3031
properties?: object;
3132
received: any;
3233
};
3334

35+
export type SnapshotNameConfig = {
36+
hint?: string;
37+
snapshotName?: string;
38+
currentTestName?: string;
39+
};
40+
3441
export type SnapshotData = Record<string, string>;
3542

3643
export interface SnapshotMatchers<R extends void | Promise<void>, T> {
@@ -47,6 +54,19 @@ export interface SnapshotMatchers<R extends void | Promise<void>, T> {
4754
propertyMatchers: Partial<U>,
4855
hint?: string,
4956
): R;
57+
/**
58+
* This ensures that a value matches the most recent snapshot.
59+
* Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information.
60+
*/
61+
toMatchNamedSnapshot<U extends Record<keyof T, unknown>>(
62+
propertyMatchers: Partial<U>,
63+
snapshot?: string,
64+
): R;
65+
/**
66+
* This ensures that a value matches the most recent snapshot.
67+
* Check out [the Snapshot Testing guide](https://jestjs.io/docs/snapshot-testing) for more information.
68+
*/
69+
toMatchNamedSnapshot(snapshotName?: string): R;
5070
/**
5171
* This ensures that a value matches the most recent snapshot with property matchers.
5272
* Instead of writing the snapshot value to a .snap file, it will be written into the source code automatically.

0 commit comments

Comments
 (0)