Skip to content

Commit

Permalink
[RFC] Inline Snapshots
Browse files Browse the repository at this point in the history
  • Loading branch information
azz committed Jun 2, 2018
1 parent ddfa099 commit 084b4ec
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import expect from 'expect';
import {
addSerializer,
toMatchSnapshot,
toMatchInlineSnapshot,
toThrowErrorMatchingSnapshot,
toThrowErrorMatchingInlineSnapshot,
} from 'jest-snapshot';

type JasmineMatcher = {
Expand All @@ -29,7 +31,9 @@ export default (config: {expand: boolean}) => {
expand: config.expand,
});
expect.extend({
toMatchInlineSnapshot,
toMatchSnapshot,
toThrowErrorMatchingInlineSnapshot,
toThrowErrorMatchingSnapshot,
});

Expand Down
4 changes: 4 additions & 0 deletions packages/jest-jasmine2/src/jest_expect.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import expect from 'expect';
import {
addSerializer,
toMatchSnapshot,
toMatchInlineSnapshot,
toThrowErrorMatchingSnapshot,
toThrowErrorMatchingInlineSnapshot,
} from 'jest-snapshot';

type JasmineMatcher = {
Expand All @@ -27,7 +29,9 @@ export default (config: {expand: boolean}) => {
global.expect = expect;
expect.setState({expand: config.expand});
expect.extend({
toMatchInlineSnapshot,
toMatchSnapshot,
toThrowErrorMatchingInlineSnapshot,
toThrowErrorMatchingSnapshot,
});
(expect: Object).addSnapshotSerializer = addSerializer;
Expand Down
13 changes: 8 additions & 5 deletions packages/jest-message-util/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,12 @@ const formatPaths = (config: StackTraceConfig, relativeTestPath, line) => {
return STACK_TRACE_COLOR(match[1]) + filePath + STACK_TRACE_COLOR(match[3]);
};

const getTopFrame = (lines: string[]) => {
export const getTopFrame = (
lines: string[],
options: StackTraceOptions = {},
) => {
lines = removeInternalStackEntries(lines, options);

for (const line of lines) {
if (line.includes(PATH_NODE_MODULES) || line.includes(PATH_JEST_PACKAGES)) {
continue;
Expand All @@ -243,14 +248,12 @@ export const formatStackTrace = (
options: StackTraceOptions,
testPath: ?Path,
) => {
let lines = stack.split(/\n/);
const lines = stack.split(/\n/);
const topFrame = getTopFrame(lines, options);
let renderedCallsite = '';
const relativeTestPath = testPath
? slash(path.relative(config.rootDir, testPath))
: null;
lines = removeInternalStackEntries(lines, options);

const topFrame = getTopFrame(lines);

if (topFrame) {
const filename = topFrame.file;
Expand Down
6 changes: 5 additions & 1 deletion packages/jest-snapshot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@
"license": "MIT",
"main": "build/index.js",
"dependencies": {
"babel-traverse": "^6.0.0",
"babel-types": "^6.0.0",
"chalk": "^2.0.1",
"jest-diff": "^23.0.1",
"jest-matcher-utils": "^23.0.1",
"jest-message-util": "^23.0.1",
"mkdirp": "^0.5.1",
"natural-compare": "^1.4.0",
"pretty-format": "^23.0.1"
"pretty-format": "^23.0.1",
"prettier": "~1.13.4"
}
}
59 changes: 47 additions & 12 deletions packages/jest-snapshot/src/State.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@
import type {Path, SnapshotUpdateState} from 'types/Config';

import fs from 'fs';
import {getTopFrame} from 'jest-message-util';
import {
saveSnapshotFile,
saveInlineSnapshots,
getSnapshotData,
getSnapshotPath,
keyToTestName,
serialize,
testNameToKey,
unescape,
type InlineSnapshot,
} from './utils';

export type SnapshotStateOptions = {|
Expand All @@ -33,6 +36,8 @@ export default class SnapshotState {
_updateSnapshot: SnapshotUpdateState;
_snapshotData: {[key: string]: string};
_snapshotPath: Path;
_inlineSnapshotData: {[key: string]: InlineSnapshot};
_testPath: Path;
_uncheckedKeys: Set<string>;
added: number;
expand: boolean;
Expand All @@ -42,12 +47,14 @@ export default class SnapshotState {

constructor(testPath: Path, options: SnapshotStateOptions) {
this._snapshotPath = options.snapshotPath || getSnapshotPath(testPath);
this._testPath = testPath;
const {data, dirty} = getSnapshotData(
this._snapshotPath,
options.updateSnapshot,
);
this._snapshotData = data;
this._dirty = dirty;
this._inlineSnapshotData = Object.create(null);
this._uncheckedKeys = new Set(Object.keys(this._snapshotData));
this._counters = new Map();
this._index = 0;
Expand All @@ -67,22 +74,42 @@ export default class SnapshotState {
});
}

_addSnapshot(key: string, receivedSerialized: string) {
_addSnapshot(key: string, receivedSerialized: string, isInline: boolean) {
this._dirty = true;
this._snapshotData[key] = receivedSerialized;
if (isInline) {
const stack = new Error().stack.split(/\n/);
const frame = getTopFrame(stack);
if (!frame) {
throw new Error("Jest: Couln't infer stack frame for inline snapshot.");
}
this._inlineSnapshotData[key] = {
frame,
snapshot: receivedSerialized,
};
} else {
this._snapshotData[key] = receivedSerialized;
}
}

save() {
const isEmpty = Object.keys(this._snapshotData).length === 0;
const hasExternalSnapshots = Object.keys(this._snapshotData).length;
const hasInlineSnapshots = Object.keys(this._inlineSnapshotData).length;
const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots;

const status = {
deleted: false,
saved: false,
};

if ((this._dirty || this._uncheckedKeys.size) && !isEmpty) {
saveSnapshotFile(this._snapshotData, this._snapshotPath);
if (hasExternalSnapshots) {
saveSnapshotFile(this._snapshotData, this._snapshotPath);
}
if (hasInlineSnapshots) {
saveInlineSnapshots(this._inlineSnapshotData, this._testPath);
}
status.saved = true;
} else if (isEmpty && fs.existsSync(this._snapshotPath)) {
} else if (!hasExternalSnapshots && fs.existsSync(this._snapshotPath)) {
if (this._updateSnapshot === 'all') {
fs.unlinkSync(this._snapshotPath);
}
Expand All @@ -108,9 +135,15 @@ export default class SnapshotState {
}
}

match(testName: string, received: any, key?: string) {
match(
testName: string,
received: any,
key?: string,
inlineSnapshot?: string,
) {
this._counters.set(testName, (this._counters.get(testName) || 0) + 1);
const count = Number(this._counters.get(testName));
const isInline = typeof inlineSnapshot === 'string';

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

const receivedSerialized = serialize(received);
const expected = this._snapshotData[key];
const expected = isInline ? inlineSnapshot : this._snapshotData[key];
const pass = expected === receivedSerialized;
const hasSnapshot = this._snapshotData[key] !== undefined;
const hasSnapshot = isInline
? inlineSnapshot !== ''
: this._snapshotData[key] !== undefined;

if (pass) {
if (pass && !isInline) {
// Executing a snapshot file as JavaScript and writing the strings back
// when other snapshots have changed loses the proper escaping for some
// characters. Since we check every snapshot in every test, use the newly
Expand All @@ -142,7 +177,7 @@ export default class SnapshotState {
// * There's no snapshot file or a file without this snapshot on a CI environment.
if (
(hasSnapshot && this._updateSnapshot === 'all') ||
((!hasSnapshot || !fs.existsSync(this._snapshotPath)) &&
((!hasSnapshot || (!isInline && !fs.existsSync(this._snapshotPath))) &&
(this._updateSnapshot === 'new' || this._updateSnapshot === 'all'))
) {
if (this._updateSnapshot === 'all') {
Expand All @@ -152,12 +187,12 @@ export default class SnapshotState {
} else {
this.added++;
}
this._addSnapshot(key, receivedSerialized);
this._addSnapshot(key, receivedSerialized, isInline);
} else {
this.matched++;
}
} else {
this._addSnapshot(key, receivedSerialized);
this._addSnapshot(key, receivedSerialized, isInline);
this.added++;
}

Expand Down
98 changes: 92 additions & 6 deletions packages/jest-snapshot/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,45 @@ const toMatchSnapshot = function(
propertyMatchers?: any,
testName?: string,
) {
this.dontThrow && this.dontThrow();
return _toMatchSnapshot(received, propertyMatchers, testName);
};

const toMatchInlineSnapshot = function(
received: any,
propertyMatchersOrInlineSnapshot?: any,
inlineSnapshot?: string,
) {
const propertyMatchers = inlineSnapshot
? propertyMatchersOrInlineSnapshot
: undefined;
if (!inlineSnapshot) {
inlineSnapshot = propertyMatchersOrInlineSnapshot || '';
}
return _toMatchSnapshot({
context: this,
inlineSnapshot,
propertyMatchers,
received,
});
};

const _toMatchSnapshot = function({
context,
received,
propertyMatchers,
testName,
inlineSnapshot,
}: {
context: MatcherState,
received: any,
propertyMatchers?: any,
testName?: string,
inlineSnapshot?: string,
}) {
context.dontThrow && context.dontThrow();
testName = typeof propertyMatchers === 'string' ? propertyMatchers : testName;

const {currentTestName, isNot, snapshotState}: MatcherState = this;
const {currentTestName, isNot, snapshotState} = context;

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

if (typeof propertyMatchers === 'object') {
const propertyMatchers = propertyMatchers;
const propertyPass = this.equals(received, propertyMatchers, [
this.utils.iterableEquality,
this.utils.subsetEquality,
Expand Down Expand Up @@ -102,7 +138,12 @@ const toMatchSnapshot = function(
}
}

const result = snapshotState.match(fullTestName, received);
const result = snapshotState.match(
fullTestName,
received,
/* key */ undefined,
inlineSnapshot,
);
const {pass} = result;
let {actual, expected} = result;

Expand Down Expand Up @@ -153,9 +194,47 @@ const toThrowErrorMatchingSnapshot = function(
testName?: string,
fromPromise: boolean,
) {
this.dontThrow && this.dontThrow();
return _toThrowErrorMatchingSnapshot({
context: this,
fromPromise,
received,
testName,
});
};

const toThrowErrorMatchingInlineSnapshot = function(
received: any,
fromPromiseOrInlineSnapshot: any,
inlineSnapshot?: string,
) {
const fromPromise = inlineSnapshot ? fromPromiseOrInlineSnapshot : undefined;
if (!inlineSnapshot) {
inlineSnapshot = fromPromiseOrInlineSnapshot;
}
return _toThrowErrorMatchingSnapshot({
context: this,
fromPromise,
inlineSnapshot,
received,
});
};

const _toThrowErrorMatchingSnapshot = function({
context,
received,
testName,
fromPromise,
inlineSnapshot,
}: {
context: MatcherState,
received: any,
testName?: string,
fromPromise: boolean,
inlineSnapshot?: string,
}) {
context.dontThrow && context.dontThrow();

const {isNot} = this;
const {isNot} = context;

if (isNot) {
throw new Error(
Expand Down Expand Up @@ -184,7 +263,12 @@ const toThrowErrorMatchingSnapshot = function(
);
}

return toMatchSnapshot.call(this, error.message, testName);
return _toMatchSnapshot({
context,
inlineSnapshot,
received: error.message,
testName,
});
};

module.exports = {
Expand All @@ -193,7 +277,9 @@ module.exports = {
addSerializer,
cleanup,
getSerializers,
toMatchInlineSnapshot,
toMatchSnapshot,
toThrowErrorMatchingInlineSnapshot,
toThrowErrorMatchingSnapshot,
utils,
};
Loading

0 comments on commit 084b4ec

Please sign in to comment.