Skip to content

Commit 9c04b50

Browse files
committed
[RFC] Inline Snapshots
1 parent ddfa099 commit 9c04b50

File tree

8 files changed

+234
-24
lines changed

8 files changed

+234
-24
lines changed

packages/jest-circus/src/legacy_code_todo_rewrite/jest_expect.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import expect from 'expect';
1414
import {
1515
addSerializer,
1616
toMatchSnapshot,
17+
toMatchInlineSnapshot,
1718
toThrowErrorMatchingSnapshot,
19+
toThrowErrorMatchingInlineSnapshot,
1820
} from 'jest-snapshot';
1921

2022
type JasmineMatcher = {
@@ -29,7 +31,9 @@ export default (config: {expand: boolean}) => {
2931
expand: config.expand,
3032
});
3133
expect.extend({
34+
toMatchInlineSnapshot,
3235
toMatchSnapshot,
36+
toThrowErrorMatchingInlineSnapshot,
3337
toThrowErrorMatchingSnapshot,
3438
});
3539

packages/jest-jasmine2/src/jest_expect.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import expect from 'expect';
1313
import {
1414
addSerializer,
1515
toMatchSnapshot,
16+
toMatchInlineSnapshot,
1617
toThrowErrorMatchingSnapshot,
18+
toThrowErrorMatchingInlineSnapshot,
1719
} from 'jest-snapshot';
1820

1921
type JasmineMatcher = {
@@ -27,7 +29,9 @@ export default (config: {expand: boolean}) => {
2729
global.expect = expect;
2830
expect.setState({expand: config.expand});
2931
expect.extend({
32+
toMatchInlineSnapshot,
3033
toMatchSnapshot,
34+
toThrowErrorMatchingInlineSnapshot,
3135
toThrowErrorMatchingSnapshot,
3236
});
3337
(expect: Object).addSnapshotSerializer = addSerializer;

packages/jest-message-util/src/index.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,12 @@ const formatPaths = (config: StackTraceConfig, relativeTestPath, line) => {
221221
return STACK_TRACE_COLOR(match[1]) + filePath + STACK_TRACE_COLOR(match[3]);
222222
};
223223

224-
const getTopFrame = (lines: string[]) => {
224+
export const getTopFrame = (
225+
lines: string[],
226+
options: StackTraceOptions = {},
227+
) => {
228+
lines = removeInternalStackEntries(lines, options);
229+
225230
for (const line of lines) {
226231
if (line.includes(PATH_NODE_MODULES) || line.includes(PATH_JEST_PACKAGES)) {
227232
continue;
@@ -243,14 +248,12 @@ export const formatStackTrace = (
243248
options: StackTraceOptions,
244249
testPath: ?Path,
245250
) => {
246-
let lines = stack.split(/\n/);
251+
const lines = stack.split(/\n/);
252+
const topFrame = getTopFrame(lines, options);
247253
let renderedCallsite = '';
248254
const relativeTestPath = testPath
249255
? slash(path.relative(config.rootDir, testPath))
250256
: null;
251-
lines = removeInternalStackEntries(lines, options);
252-
253-
const topFrame = getTopFrame(lines);
254257

255258
if (topFrame) {
256259
const filename = topFrame.file;

packages/jest-snapshot/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,15 @@
88
"license": "MIT",
99
"main": "build/index.js",
1010
"dependencies": {
11+
"babel-traverse": "^6.0.0",
12+
"babel-types": "^6.0.0",
1113
"chalk": "^2.0.1",
1214
"jest-diff": "^23.0.1",
1315
"jest-matcher-utils": "^23.0.1",
16+
"jest-message-util": "^23.0.1",
1417
"mkdirp": "^0.5.1",
1518
"natural-compare": "^1.4.0",
16-
"pretty-format": "^23.0.1"
19+
"pretty-format": "^23.0.1",
20+
"prettier": "~1.13.4"
1721
}
1822
}

packages/jest-snapshot/src/State.js

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@
1010
import type {Path, SnapshotUpdateState} from 'types/Config';
1111

1212
import fs from 'fs';
13+
import {getTopFrame} from 'jest-message-util';
1314
import {
1415
saveSnapshotFile,
16+
saveInlineSnapshots,
1517
getSnapshotData,
1618
getSnapshotPath,
1719
keyToTestName,
1820
serialize,
1921
testNameToKey,
2022
unescape,
23+
type InlineSnapshot,
2124
} from './utils';
2225

2326
export type SnapshotStateOptions = {|
@@ -33,6 +36,8 @@ export default class SnapshotState {
3336
_updateSnapshot: SnapshotUpdateState;
3437
_snapshotData: {[key: string]: string};
3538
_snapshotPath: Path;
39+
_inlineSnapshotData: {[key: string]: InlineSnapshot};
40+
_testPath: Path;
3641
_uncheckedKeys: Set<string>;
3742
added: number;
3843
expand: boolean;
@@ -42,12 +47,14 @@ export default class SnapshotState {
4247

4348
constructor(testPath: Path, options: SnapshotStateOptions) {
4449
this._snapshotPath = options.snapshotPath || getSnapshotPath(testPath);
50+
this._testPath = testPath;
4551
const {data, dirty} = getSnapshotData(
4652
this._snapshotPath,
4753
options.updateSnapshot,
4854
);
4955
this._snapshotData = data;
5056
this._dirty = dirty;
57+
this._inlineSnapshotData = Object.create(null);
5158
this._uncheckedKeys = new Set(Object.keys(this._snapshotData));
5259
this._counters = new Map();
5360
this._index = 0;
@@ -67,22 +74,42 @@ export default class SnapshotState {
6774
});
6875
}
6976

70-
_addSnapshot(key: string, receivedSerialized: string) {
77+
_addSnapshot(key: string, receivedSerialized: string, isInline: boolean) {
7178
this._dirty = true;
72-
this._snapshotData[key] = receivedSerialized;
79+
if (isInline) {
80+
const stack = new Error().stack.split(/\n/);
81+
const frame = getTopFrame(stack);
82+
if (!frame) {
83+
throw new Error("Jest: Couln't infer stack frame for inline snapshot.");
84+
}
85+
this._inlineSnapshotData[key] = {
86+
frame,
87+
snapshot: receivedSerialized,
88+
};
89+
} else {
90+
this._snapshotData[key] = receivedSerialized;
91+
}
7392
}
7493

7594
save() {
76-
const isEmpty = Object.keys(this._snapshotData).length === 0;
95+
const hasExternalSnapshots = Object.keys(this._snapshotData).length;
96+
const hasInlineSnapshots = Object.keys(this._inlineSnapshotData).length;
97+
const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots;
98+
7799
const status = {
78100
deleted: false,
79101
saved: false,
80102
};
81103

82104
if ((this._dirty || this._uncheckedKeys.size) && !isEmpty) {
83-
saveSnapshotFile(this._snapshotData, this._snapshotPath);
105+
if (hasExternalSnapshots) {
106+
saveSnapshotFile(this._snapshotData, this._snapshotPath);
107+
}
108+
if (hasInlineSnapshots) {
109+
saveInlineSnapshots(this._inlineSnapshotData, this._testPath);
110+
}
84111
status.saved = true;
85-
} else if (isEmpty && fs.existsSync(this._snapshotPath)) {
112+
} else if (!hasExternalSnapshots && fs.existsSync(this._snapshotPath)) {
86113
if (this._updateSnapshot === 'all') {
87114
fs.unlinkSync(this._snapshotPath);
88115
}
@@ -108,9 +135,15 @@ export default class SnapshotState {
108135
}
109136
}
110137

111-
match(testName: string, received: any, key?: string) {
138+
match(
139+
testName: string,
140+
received: any,
141+
key?: string,
142+
inlineSnapshot?: string,
143+
) {
112144
this._counters.set(testName, (this._counters.get(testName) || 0) + 1);
113145
const count = Number(this._counters.get(testName));
146+
const isInline = typeof inlineSnapshot === 'string';
114147

115148
if (!key) {
116149
key = testNameToKey(testName, count);
@@ -119,11 +152,13 @@ export default class SnapshotState {
119152
this._uncheckedKeys.delete(key);
120153

121154
const receivedSerialized = serialize(received);
122-
const expected = this._snapshotData[key];
155+
const expected = isInline ? inlineSnapshot : this._snapshotData[key];
123156
const pass = expected === receivedSerialized;
124-
const hasSnapshot = this._snapshotData[key] !== undefined;
157+
const hasSnapshot = isInline
158+
? inlineSnapshot !== ''
159+
: this._snapshotData[key] !== undefined;
125160

126-
if (pass) {
161+
if (pass && !isInline) {
127162
// Executing a snapshot file as JavaScript and writing the strings back
128163
// when other snapshots have changed loses the proper escaping for some
129164
// characters. Since we check every snapshot in every test, use the newly
@@ -142,7 +177,7 @@ export default class SnapshotState {
142177
// * There's no snapshot file or a file without this snapshot on a CI environment.
143178
if (
144179
(hasSnapshot && this._updateSnapshot === 'all') ||
145-
((!hasSnapshot || !fs.existsSync(this._snapshotPath)) &&
180+
((!hasSnapshot || (!isInline && !fs.existsSync(this._snapshotPath))) &&
146181
(this._updateSnapshot === 'new' || this._updateSnapshot === 'all'))
147182
) {
148183
if (this._updateSnapshot === 'all') {
@@ -152,12 +187,12 @@ export default class SnapshotState {
152187
} else {
153188
this.added++;
154189
}
155-
this._addSnapshot(key, receivedSerialized);
190+
this._addSnapshot(key, receivedSerialized, isInline);
156191
} else {
157192
this.matched++;
158193
}
159194
} else {
160-
this._addSnapshot(key, receivedSerialized);
195+
this._addSnapshot(key, receivedSerialized, isInline);
161196
this.added++;
162197
}
163198

packages/jest-snapshot/src/index.js

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,45 @@ const toMatchSnapshot = function(
5353
propertyMatchers?: any,
5454
testName?: string,
5555
) {
56-
this.dontThrow && this.dontThrow();
56+
return _toMatchSnapshot(received, propertyMatchers, testName);
57+
};
58+
59+
const toMatchInlineSnapshot = function(
60+
received: any,
61+
propertyMatchersOrInlineSnapshot?: any,
62+
inlineSnapshot?: string,
63+
) {
64+
const propertyMatchers = inlineSnapshot
65+
? propertyMatchersOrInlineSnapshot
66+
: undefined;
67+
if (!inlineSnapshot) {
68+
inlineSnapshot = propertyMatchersOrInlineSnapshot || '';
69+
}
70+
return _toMatchSnapshot({
71+
context: this,
72+
inlineSnapshot,
73+
propertyMatchers,
74+
received,
75+
});
76+
};
77+
78+
const _toMatchSnapshot = function({
79+
context,
80+
received,
81+
propertyMatchers,
82+
testName,
83+
inlineSnapshot,
84+
}: {
85+
context: MatcherState,
86+
received: any,
87+
propertyMatchers?: any,
88+
testName?: string,
89+
inlineSnapshot?: string,
90+
}) {
91+
context.dontThrow && context.dontThrow();
5792
testName = typeof propertyMatchers === 'string' ? propertyMatchers : testName;
5893

59-
const {currentTestName, isNot, snapshotState}: MatcherState = this;
94+
const {currentTestName, isNot, snapshotState} = context;
6095

6196
if (isNot) {
6297
throw new Error('Jest: `.not` cannot be used with `.toMatchSnapshot()`.');
@@ -72,6 +107,7 @@ const toMatchSnapshot = function(
72107
: currentTestName || '';
73108

74109
if (typeof propertyMatchers === 'object') {
110+
const propertyMatchers = propertyMatchers;
75111
const propertyPass = this.equals(received, propertyMatchers, [
76112
this.utils.iterableEquality,
77113
this.utils.subsetEquality,
@@ -102,7 +138,12 @@ const toMatchSnapshot = function(
102138
}
103139
}
104140

105-
const result = snapshotState.match(fullTestName, received);
141+
const result = snapshotState.match(
142+
fullTestName,
143+
received,
144+
/* key */ undefined,
145+
inlineSnapshot,
146+
);
106147
const {pass} = result;
107148
let {actual, expected} = result;
108149

@@ -153,9 +194,47 @@ const toThrowErrorMatchingSnapshot = function(
153194
testName?: string,
154195
fromPromise: boolean,
155196
) {
156-
this.dontThrow && this.dontThrow();
197+
return _toThrowErrorMatchingSnapshot({
198+
context: this,
199+
fromPromise,
200+
received,
201+
testName,
202+
});
203+
};
204+
205+
const toThrowErrorMatchingInlineSnapshot = function(
206+
received: any,
207+
fromPromiseOrInlineSnapshot: any,
208+
inlineSnapshot?: string,
209+
) {
210+
const fromPromise = inlineSnapshot ? fromPromiseOrInlineSnapshot : undefined;
211+
if (!inlineSnapshot) {
212+
inlineSnapshot = fromPromiseOrInlineSnapshot;
213+
}
214+
return _toThrowErrorMatchingSnapshot({
215+
context: this,
216+
fromPromise,
217+
inlineSnapshot,
218+
received,
219+
});
220+
};
221+
222+
const _toThrowErrorMatchingSnapshot = function({
223+
context,
224+
received,
225+
testName,
226+
fromPromise,
227+
inlineSnapshot,
228+
}: {
229+
context: MatcherState,
230+
received: any,
231+
testName?: string,
232+
fromPromise: boolean,
233+
inlineSnapshot?: string,
234+
}) {
235+
context.dontThrow && context.dontThrow();
157236

158-
const {isNot} = this;
237+
const {isNot} = context;
159238

160239
if (isNot) {
161240
throw new Error(
@@ -184,7 +263,12 @@ const toThrowErrorMatchingSnapshot = function(
184263
);
185264
}
186265

187-
return toMatchSnapshot.call(this, error.message, testName);
266+
return _toMatchSnapshot({
267+
context,
268+
inlineSnapshot,
269+
received: error.message,
270+
testName,
271+
});
188272
};
189273

190274
module.exports = {
@@ -193,7 +277,9 @@ module.exports = {
193277
addSerializer,
194278
cleanup,
195279
getSerializers,
280+
toMatchInlineSnapshot,
196281
toMatchSnapshot,
282+
toThrowErrorMatchingInlineSnapshot,
197283
toThrowErrorMatchingSnapshot,
198284
utils,
199285
};

0 commit comments

Comments
 (0)