From fed959fd8d7795e6b259b319f7d1eab1d2a39370 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 2 Jun 2018 22:12:53 +1000 Subject: [PATCH 01/33] [RFC] Inline Snapshots --- .../legacy_code_todo_rewrite/jest_expect.js | 4 + packages/jest-jasmine2/src/jest_expect.js | 4 + packages/jest-message-util/src/index.js | 13 ++- packages/jest-snapshot/package.json | 6 +- packages/jest-snapshot/src/State.js | 59 ++++++++--- packages/jest-snapshot/src/index.js | 98 +++++++++++++++++-- packages/jest-snapshot/src/utils.js | 64 ++++++++++++ yarn.lock | 7 ++ 8 files changed, 231 insertions(+), 24 deletions(-) diff --git a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_expect.js b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_expect.js index 62204131faee..de48a4a8ea78 100644 --- a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_expect.js +++ b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_expect.js @@ -14,7 +14,9 @@ import expect from 'expect'; import { addSerializer, toMatchSnapshot, + toMatchInlineSnapshot, toThrowErrorMatchingSnapshot, + toThrowErrorMatchingInlineSnapshot, } from 'jest-snapshot'; type JasmineMatcher = { @@ -29,7 +31,9 @@ export default (config: {expand: boolean}) => { expand: config.expand, }); expect.extend({ + toMatchInlineSnapshot, toMatchSnapshot, + toThrowErrorMatchingInlineSnapshot, toThrowErrorMatchingSnapshot, }); diff --git a/packages/jest-jasmine2/src/jest_expect.js b/packages/jest-jasmine2/src/jest_expect.js index 0f3dceb3c18c..46ef61026e4c 100644 --- a/packages/jest-jasmine2/src/jest_expect.js +++ b/packages/jest-jasmine2/src/jest_expect.js @@ -13,7 +13,9 @@ import expect from 'expect'; import { addSerializer, toMatchSnapshot, + toMatchInlineSnapshot, toThrowErrorMatchingSnapshot, + toThrowErrorMatchingInlineSnapshot, } from 'jest-snapshot'; type JasmineMatcher = { @@ -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; diff --git a/packages/jest-message-util/src/index.js b/packages/jest-message-util/src/index.js index 16d46a24902d..29ef2e4fa64a 100644 --- a/packages/jest-message-util/src/index.js +++ b/packages/jest-message-util/src/index.js @@ -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; @@ -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; diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index d8821cca8f06..f753aabc93c2 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -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" } } diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index aaacad4d32f2..00c1644b006c 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -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 = {| @@ -33,6 +36,8 @@ export default class SnapshotState { _updateSnapshot: SnapshotUpdateState; _snapshotData: {[key: string]: string}; _snapshotPath: Path; + _inlineSnapshotData: {[key: string]: InlineSnapshot}; + _testPath: Path; _uncheckedKeys: Set; added: number; expand: boolean; @@ -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; @@ -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); } @@ -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); @@ -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 @@ -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') { @@ -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++; } diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index 985f9abe785d..d630ee8e99a3 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -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()`.'); @@ -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, @@ -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; @@ -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( @@ -184,7 +263,12 @@ const toThrowErrorMatchingSnapshot = function( ); } - return toMatchSnapshot.call(this, error.message, testName); + return _toMatchSnapshot({ + context, + inlineSnapshot, + received: error.message, + testName, + }); }; module.exports = { @@ -193,7 +277,9 @@ module.exports = { addSerializer, cleanup, getSerializers, + toMatchInlineSnapshot, toMatchSnapshot, + toThrowErrorMatchingInlineSnapshot, toThrowErrorMatchingSnapshot, utils, }; diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 13f907db3239..92ebb30205d3 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -16,6 +16,14 @@ import mkdirp from 'mkdirp'; import naturalCompare from 'natural-compare'; import path from 'path'; import prettyFormat from 'pretty-format'; +import prettier from 'prettier'; +import traverse from 'babel-traverse'; +import {templateElement, templateLiteral} from 'babel-types'; + +export type InlineSnapshot = {| + snapshot: string, + frame: {line: number, column: number}, +|}; export const SNAPSHOT_EXTENSION = 'snap'; export const SNAPSHOT_VERSION = '1'; @@ -179,3 +187,59 @@ export const saveSnapshotFile = ( writeSnapshotVersion() + '\n\n' + snapshots.join('\n\n') + '\n', ); }; + +export const saveInlineSnapshots = ( + snapshotData: {[key: string]: InlineSnapshot}, + sourceFilePath: Path, +) => { + const sourceFile = fs.readFileSync(sourceFilePath, 'utf8'); + const snapshots = Object.values(snapshotData); + + const config = prettier.resolveConfig.sync(sourceFilePath); + const newSourceFile = prettier.format( + sourceFile, + Object.assign({}, config, { + filepath: sourceFilePath, + parser: createParser(snapshots), + }), + ); + + if (newSourceFile !== sourceFile) { + fs.writeFileSync(sourceFilePath, newSourceFile); + } +}; + +const createParser = snapshots => (text, parsers) => { + const ast = parsers.babylon(text); + traverse(ast, { + CallExpression({node}) { + if ( + node.callee.type !== 'MemberExpression' || + node.callee.property.type !== 'Identifier' + ) { + return; + } + const matcher = node.callee.property; + for (const {snapshot, frame} of snapshots) { + if ( + matcher.loc.start.line === frame.line && + matcher.loc.start.column === frame.column - 1 + ) { + if ( + node.arguments[0] && + node.arguments[0].type === 'TemplateLiteral' + ) { + node.arguments[0].quasis[0].value.raw = snapshot; + } else { + node.arguments[0] = templateLiteral( + [templateElement({raw: snapshot})], + [], + ); + } + } + } + }, + }); + + return ast; +}; diff --git a/yarn.lock b/yarn.lock index 6e68f073a1b1..6043b472c166 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5798,6 +5798,13 @@ libqp@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8" +line-column@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2" + dependencies: + isarray "^1.0.0" + isobject "^2.0.0" + lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" From c6394845faf6c61eac19e1f4c1dd1b9f5d72a27a Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 2 Jun 2018 23:08:42 +1000 Subject: [PATCH 02/33] Fix typechecking and refactor --- packages/jest-message-util/src/index.js | 2 +- packages/jest-snapshot/src/State.js | 12 +++++----- packages/jest-snapshot/src/index.js | 32 ++++++++++++++----------- packages/jest-snapshot/src/utils.js | 3 +-- types/Matchers.js | 2 +- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/packages/jest-message-util/src/index.js b/packages/jest-message-util/src/index.js index 29ef2e4fa64a..749946f338b7 100644 --- a/packages/jest-message-util/src/index.js +++ b/packages/jest-message-util/src/index.js @@ -223,7 +223,7 @@ const formatPaths = (config: StackTraceConfig, relativeTestPath, line) => { export const getTopFrame = ( lines: string[], - options: StackTraceOptions = {}, + options: StackTraceOptions = {noStackTrace: false}, ) => { lines = removeInternalStackEntries(lines, options); diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index 00c1644b006c..c1c204ce4a3f 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -36,7 +36,7 @@ export default class SnapshotState { _updateSnapshot: SnapshotUpdateState; _snapshotData: {[key: string]: string}; _snapshotPath: Path; - _inlineSnapshotData: {[key: string]: InlineSnapshot}; + _inlineSnapshots: InlineSnapshot[]; _testPath: Path; _uncheckedKeys: Set; added: number; @@ -54,7 +54,7 @@ export default class SnapshotState { ); this._snapshotData = data; this._dirty = dirty; - this._inlineSnapshotData = Object.create(null); + this._inlineSnapshots = []; this._uncheckedKeys = new Set(Object.keys(this._snapshotData)); this._counters = new Map(); this._index = 0; @@ -82,10 +82,10 @@ export default class SnapshotState { if (!frame) { throw new Error("Jest: Couln't infer stack frame for inline snapshot."); } - this._inlineSnapshotData[key] = { + this._inlineSnapshots.push({ frame, snapshot: receivedSerialized, - }; + }); } else { this._snapshotData[key] = receivedSerialized; } @@ -93,7 +93,7 @@ export default class SnapshotState { save() { const hasExternalSnapshots = Object.keys(this._snapshotData).length; - const hasInlineSnapshots = Object.keys(this._inlineSnapshotData).length; + const hasInlineSnapshots = this._inlineSnapshots.length; const isEmpty = !hasExternalSnapshots && !hasInlineSnapshots; const status = { @@ -106,7 +106,7 @@ export default class SnapshotState { saveSnapshotFile(this._snapshotData, this._snapshotPath); } if (hasInlineSnapshots) { - saveInlineSnapshots(this._inlineSnapshotData, this._testPath); + saveInlineSnapshots(this._inlineSnapshots, this._testPath); } status.saved = true; } else if (!hasExternalSnapshots && fs.existsSync(this._snapshotPath)) { diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index d630ee8e99a3..ddc3a920dc57 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -53,7 +53,12 @@ const toMatchSnapshot = function( propertyMatchers?: any, testName?: string, ) { - return _toMatchSnapshot(received, propertyMatchers, testName); + return _toMatchSnapshot({ + context: this, + propertyMatchers, + received, + testName, + }); }; const toMatchInlineSnapshot = function( @@ -75,19 +80,19 @@ const toMatchInlineSnapshot = function( }); }; -const _toMatchSnapshot = function({ +const _toMatchSnapshot = ({ context, received, propertyMatchers, testName, inlineSnapshot, }: { - context: MatcherState, + context: MatcherState & {dontThrow?: () => any}, received: any, propertyMatchers?: any, testName?: string, inlineSnapshot?: string, -}) { +}) => { context.dontThrow && context.dontThrow(); testName = typeof propertyMatchers === 'string' ? propertyMatchers : testName; @@ -107,10 +112,9 @@ const _toMatchSnapshot = function({ : currentTestName || ''; if (typeof propertyMatchers === 'object') { - const propertyMatchers = propertyMatchers; - const propertyPass = this.equals(received, propertyMatchers, [ - this.utils.iterableEquality, - this.utils.subsetEquality, + const propertyPass = context.equals(received, propertyMatchers, [ + context.utils.iterableEquality, + context.utils.subsetEquality, ]); if (!propertyPass) { @@ -120,9 +124,9 @@ const _toMatchSnapshot = function({ `${RECEIVED_COLOR('Received value')} does not match ` + `${EXPECTED_COLOR(`snapshot properties for "${key}"`)}.\n\n` + `Expected snapshot to match properties:\n` + - ` ${this.utils.printExpected(propertyMatchers)}` + + ` ${context.utils.printExpected(propertyMatchers)}` + `\nReceived:\n` + - ` ${this.utils.printReceived(received)}`; + ` ${context.utils.printReceived(received)}`; return { message: () => @@ -219,19 +223,19 @@ const toThrowErrorMatchingInlineSnapshot = function( }); }; -const _toThrowErrorMatchingSnapshot = function({ +const _toThrowErrorMatchingSnapshot = ({ context, received, testName, fromPromise, inlineSnapshot, }: { - context: MatcherState, + context: MatcherState & {dontThrow?: () => any}, received: any, testName?: string, - fromPromise: boolean, + fromPromise?: boolean, inlineSnapshot?: string, -}) { +}) => { context.dontThrow && context.dontThrow(); const {isNot} = context; diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 92ebb30205d3..0474bb99e285 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -189,11 +189,10 @@ export const saveSnapshotFile = ( }; export const saveInlineSnapshots = ( - snapshotData: {[key: string]: InlineSnapshot}, + snapshots: InlineSnapshot[], sourceFilePath: Path, ) => { const sourceFile = fs.readFileSync(sourceFilePath, 'utf8'); - const snapshots = Object.values(snapshotData); const config = prettier.resolveConfig.sync(sourceFilePath); const newSourceFile = prettier.format( diff --git a/types/Matchers.js b/types/Matchers.js index 68a8b7ed28c5..909800a9e16e 100644 --- a/types/Matchers.js +++ b/types/Matchers.js @@ -30,7 +30,7 @@ export type PromiseMatcherFn = (actual: any) => Promise; export type MatcherState = { assertionCalls: number, currentTestName?: string, - equals: (any, any) => boolean, + equals: (any, any, ?(any[])) => boolean, expand?: boolean, expectedAssertionsNumber: ?number, isExpectingAssertions: ?boolean, From 396b20beb383b285b40560182b8bd15a089de233 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 2 Jun 2018 23:17:40 +1000 Subject: [PATCH 03/33] Refactor State#match --- packages/jest-snapshot/src/State.js | 9 +++++++-- packages/jest-snapshot/src/index.js | 9 ++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index c1c204ce4a3f..08abe263a057 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -135,12 +135,17 @@ export default class SnapshotState { } } - match( + match({ + testName, + received, + key, + inlineSnapshot, + }: { 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'; diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index ddc3a920dc57..08603420cfa6 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -142,12 +142,11 @@ const _toMatchSnapshot = ({ } } - const result = snapshotState.match( - fullTestName, - received, - /* key */ undefined, + const result = snapshotState.match({ inlineSnapshot, - ); + received, + testName: fullTestName, + }); const {pass} = result; let {actual, expected} = result; From 2816e7d2f8324c82ae671e783f15488670e31e55 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 2 Jun 2018 23:41:49 +1000 Subject: [PATCH 04/33] Fix stacktrace regression --- packages/jest-message-util/src/index.js | 16 ++++++++++++---- packages/jest-snapshot/src/State.js | 6 +++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/jest-message-util/src/index.js b/packages/jest-message-util/src/index.js index 749946f338b7..7502290813bb 100644 --- a/packages/jest-message-util/src/index.js +++ b/packages/jest-message-util/src/index.js @@ -153,7 +153,10 @@ export const formatExecError = ( return TITLE_INDENT + TITLE_BULLET + messageToUse + stack + '\n'; }; -const removeInternalStackEntries = (lines, options: StackTraceOptions) => { +const removeInternalStackEntries = ( + lines: string[], + options: StackTraceOptions, +): string[] => { let pathCounter = 0; return lines.filter(line => { @@ -221,12 +224,17 @@ const formatPaths = (config: StackTraceConfig, relativeTestPath, line) => { return STACK_TRACE_COLOR(match[1]) + filePath + STACK_TRACE_COLOR(match[3]); }; +export const getStackTraceLines = ( + stack: string, + options: StackTraceOptions = {noStackTrace: false}, +) => { + return removeInternalStackEntries(stack.split(/\n/), options); +}; + export const getTopFrame = ( lines: string[], options: StackTraceOptions = {noStackTrace: false}, ) => { - lines = removeInternalStackEntries(lines, options); - for (const line of lines) { if (line.includes(PATH_NODE_MODULES) || line.includes(PATH_JEST_PACKAGES)) { continue; @@ -248,7 +256,7 @@ export const formatStackTrace = ( options: StackTraceOptions, testPath: ?Path, ) => { - const lines = stack.split(/\n/); + const lines = getStackTraceLines(stack, options); const topFrame = getTopFrame(lines, options); let renderedCallsite = ''; const relativeTestPath = testPath diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index 08abe263a057..d7449e738826 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -10,7 +10,7 @@ import type {Path, SnapshotUpdateState} from 'types/Config'; import fs from 'fs'; -import {getTopFrame} from 'jest-message-util'; +import {getTopFrame, getStackTraceLines} from 'jest-message-util'; import { saveSnapshotFile, saveInlineSnapshots, @@ -77,8 +77,8 @@ export default class SnapshotState { _addSnapshot(key: string, receivedSerialized: string, isInline: boolean) { this._dirty = true; if (isInline) { - const stack = new Error().stack.split(/\n/); - const frame = getTopFrame(stack); + const lines = getStackTraceLines(new Error().stack); + const frame = getTopFrame(lines); if (!frame) { throw new Error("Jest: Couln't infer stack frame for inline snapshot."); } From a215e169370860847dd128a84ba574bb58f4757c Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 2 Jun 2018 23:46:40 +1000 Subject: [PATCH 05/33] Remove unused argument --- packages/jest-message-util/src/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/jest-message-util/src/index.js b/packages/jest-message-util/src/index.js index 7502290813bb..5860927401fb 100644 --- a/packages/jest-message-util/src/index.js +++ b/packages/jest-message-util/src/index.js @@ -231,10 +231,7 @@ export const getStackTraceLines = ( return removeInternalStackEntries(stack.split(/\n/), options); }; -export const getTopFrame = ( - lines: string[], - options: StackTraceOptions = {noStackTrace: false}, -) => { +export const getTopFrame = (lines: string[]) => { for (const line of lines) { if (line.includes(PATH_NODE_MODULES) || line.includes(PATH_JEST_PACKAGES)) { continue; @@ -257,7 +254,7 @@ export const formatStackTrace = ( testPath: ?Path, ) => { const lines = getStackTraceLines(stack, options); - const topFrame = getTopFrame(lines, options); + const topFrame = getTopFrame(lines); let renderedCallsite = ''; const relativeTestPath = testPath ? slash(path.relative(config.rootDir, testPath)) From 8c821a18c35e684dabe2b88979559dd091e18515 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 00:03:52 +1000 Subject: [PATCH 06/33] Fix support for property matchers --- packages/jest-snapshot/src/utils.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 0474bb99e285..96544a3077ef 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -224,16 +224,17 @@ const createParser = snapshots => (text, parsers) => { matcher.loc.start.line === frame.line && matcher.loc.start.column === frame.column - 1 ) { - if ( - node.arguments[0] && - node.arguments[0].type === 'TemplateLiteral' - ) { - node.arguments[0].quasis[0].value.raw = snapshot; + const templateIndex = node.arguments.findIndex( + arg => arg.type === 'TemplateLiteral', + ); + const element = templateElement({raw: snapshot}); + + if (templateIndex > -1) { + const template = node.arguments[templateIndex]; + template.quasis = [element]; + template.expressions = []; } else { - node.arguments[0] = templateLiteral( - [templateElement({raw: snapshot})], - [], - ); + node.arguments.push(templateLiteral([element], [])); } } } From bbe79bafcd7336ff5d94e1d0b88c31d7977f6efb Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 00:09:20 +1000 Subject: [PATCH 07/33] Fix tests after State#match refactor --- .../src/__tests__/throw_matcher.test.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/jest-snapshot/src/__tests__/throw_matcher.test.js b/packages/jest-snapshot/src/__tests__/throw_matcher.test.js index 9a23c6b1a061..83f268bf2bb5 100644 --- a/packages/jest-snapshot/src/__tests__/throw_matcher.test.js +++ b/packages/jest-snapshot/src/__tests__/throw_matcher.test.js @@ -27,7 +27,9 @@ it('throw matcher can take func', () => { throw new Error('coconut'); }); - expect(matchFn).toHaveBeenCalledWith('', 'coconut'); + expect(matchFn).toHaveBeenCalledWith( + expect.objectContaining({received: 'coconut', testName: ''}), + ); }); describe('throw matcher from promise', () => { @@ -42,7 +44,9 @@ describe('throw matcher from promise', () => { it('can take error', () => { throwMatcher(new Error('coconut'), 'testName', true); - expect(matchFn).toHaveBeenCalledWith('', 'coconut'); + expect(matchFn).toHaveBeenCalledWith( + expect.objectContaining({received: 'coconut', testName: ''}), + ); }); it('can take custom error', () => { @@ -50,6 +54,8 @@ describe('throw matcher from promise', () => { throwMatcher(new CustomError('coconut'), 'testName', true); - expect(matchFn).toHaveBeenCalledWith('', 'coconut'); + expect(matchFn).toHaveBeenCalledWith( + expect.objectContaining({received: 'coconut', testName: ''}), + ); }); }); From d5d349b5772fddd49db3e93fb1f48c1ea59e961c Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 00:27:01 +1000 Subject: [PATCH 08/33] Fix toThrowErrorMatchingInlineSnapshot --- packages/jest-snapshot/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index 08603420cfa6..69b9bc164ca3 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -212,7 +212,7 @@ const toThrowErrorMatchingInlineSnapshot = function( ) { const fromPromise = inlineSnapshot ? fromPromiseOrInlineSnapshot : undefined; if (!inlineSnapshot) { - inlineSnapshot = fromPromiseOrInlineSnapshot; + inlineSnapshot = fromPromiseOrInlineSnapshot || ''; } return _toThrowErrorMatchingSnapshot({ context: this, From 190df702f731ae069290354167f8741c49e66ca4 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 12:25:49 +1000 Subject: [PATCH 09/33] Optimize snapshot lookup --- packages/jest-snapshot/src/State.js | 2 +- packages/jest-snapshot/src/utils.js | 60 ++++++++++++++++++----------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index d7449e738826..0f0af74e594d 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -148,7 +148,7 @@ export default class SnapshotState { }) { this._counters.set(testName, (this._counters.get(testName) || 0) + 1); const count = Number(this._counters.get(testName)); - const isInline = typeof inlineSnapshot === 'string'; + const isInline = inlineSnapshot !== undefined; if (!key) { key = testNameToKey(testName, count); diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 96544a3077ef..7d732c2da08a 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -208,35 +208,49 @@ export const saveInlineSnapshots = ( } }; -const createParser = snapshots => (text, parsers) => { +const groupSnapshotsByFrame = (snapshots: InlineSnapshot[]) => { + return snapshots.reduce((object, {snapshot, frame}) => { + const key = `${frame.line}:${frame.column}`; + return Object.assign(object, { + [key]: (object[key] || []).concat(snapshot), + }); + }, {}); +}; + +const createParser = (snapshots: InlineSnapshot[]) => (text, parsers) => { + const groupedSnapshots = groupSnapshotsByFrame(snapshots); const ast = parsers.babylon(text); + traverse(ast, { - CallExpression({node}) { + CallExpression({node: {arguments: args, callee}}) { if ( - node.callee.type !== 'MemberExpression' || - node.callee.property.type !== 'Identifier' + callee.type !== 'MemberExpression' || + callee.property.type !== 'Identifier' ) { return; } - const matcher = node.callee.property; - for (const {snapshot, frame} of snapshots) { - if ( - matcher.loc.start.line === frame.line && - matcher.loc.start.column === frame.column - 1 - ) { - const templateIndex = node.arguments.findIndex( - arg => arg.type === 'TemplateLiteral', - ); - const element = templateElement({raw: snapshot}); - - if (templateIndex > -1) { - const template = node.arguments[templateIndex]; - template.quasis = [element]; - template.expressions = []; - } else { - node.arguments.push(templateLiteral([element], [])); - } - } + const {line, column} = callee.property.loc.start; + const snapshotsForFrame = groupedSnapshots[`${line}:${column + 1}`]; + if (!snapshotsForFrame) { + return; + } + if (snapshotsForFrame.length > 1) { + throw new Error( + 'Jest: Multiple inline snapshots for the same call are not supported.', + ); + } + const snapshotIndex = args.findIndex( + ({type}) => type === 'TemplateLiteral', + ); + const values = snapshotsForFrame.map(snapshot => + templateLiteral([templateElement({raw: snapshot})], []), + ); + const replacementNode = values[0]; + + if (snapshotIndex > -1) { + args[snapshotIndex] = replacementNode; + } else { + args.push(replacementNode); } }, }); From 05f94df86a2a5ce68adf43da66058b274318845b Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 13:18:34 +1000 Subject: [PATCH 10/33] Support flow parser --- packages/jest-snapshot/src/utils.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 7d732c2da08a..6c76902ada37 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -18,7 +18,7 @@ import path from 'path'; import prettyFormat from 'pretty-format'; import prettier from 'prettier'; import traverse from 'babel-traverse'; -import {templateElement, templateLiteral} from 'babel-types'; +import {templateElement, templateLiteral, file} from 'babel-types'; export type InlineSnapshot = {| snapshot: string, @@ -195,11 +195,12 @@ export const saveInlineSnapshots = ( const sourceFile = fs.readFileSync(sourceFilePath, 'utf8'); const config = prettier.resolveConfig.sync(sourceFilePath); + const {inferredParser} = prettier.getFileInfo.sync(sourceFilePath); const newSourceFile = prettier.format( sourceFile, Object.assign({}, config, { filepath: sourceFilePath, - parser: createParser(snapshots), + parser: createParser(snapshots, inferredParser), }), ); @@ -217,9 +218,17 @@ const groupSnapshotsByFrame = (snapshots: InlineSnapshot[]) => { }, {}); }; -const createParser = (snapshots: InlineSnapshot[]) => (text, parsers) => { +const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( + text: string, + parsers: {[key: string]: (string) => any}, +) => { const groupedSnapshots = groupSnapshotsByFrame(snapshots); - const ast = parsers.babylon(text); + let ast = parsers[inferredParser](text); + // flow uses a 'Program' parent node, babel expects a 'File'. + if (ast.type !== 'File') { + ast = file(ast, ast.comments, ast.tokens); + delete ast.program.comments; + } traverse(ast, { CallExpression({node: {arguments: args, callee}}) { From eb6d86df3274d28be614ccffcbe40291ad185e8d Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 13:20:28 +1000 Subject: [PATCH 11/33] Relax prettier version requirement --- packages/jest-snapshot/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index f753aabc93c2..5079b8859c9f 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -17,6 +17,6 @@ "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "pretty-format": "^23.0.1", - "prettier": "~1.13.4" + "prettier": "^1.13.4" } } From 14b8b909f1faffc9298951edbc18c23d354a2406 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 20:02:49 +1000 Subject: [PATCH 12/33] Fix TypeScript support --- packages/jest-snapshot/src/utils.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 6c76902ada37..6412ec37a7de 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -221,9 +221,14 @@ const groupSnapshotsByFrame = (snapshots: InlineSnapshot[]) => { const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( text: string, parsers: {[key: string]: (string) => any}, + options: any, ) => { + // Workaround for https://github.com/prettier/prettier/issues/3150 + options.parser = inferredParser; + const groupedSnapshots = groupSnapshotsByFrame(snapshots); let ast = parsers[inferredParser](text); + // flow uses a 'Program' parent node, babel expects a 'File'. if (ast.type !== 'File') { ast = file(ast, ast.comments, ast.tokens); From af1696e6643fbe3d4920f7223c33785ae58bacc5 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 20:41:05 +1000 Subject: [PATCH 13/33] Support toMatchInlineSnapshot from external files --- packages/jest-snapshot/src/State.js | 2 +- packages/jest-snapshot/src/utils.js | 43 +++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index 0f0af74e594d..9fe84443142d 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -106,7 +106,7 @@ export default class SnapshotState { saveSnapshotFile(this._snapshotData, this._snapshotPath); } if (hasInlineSnapshots) { - saveInlineSnapshots(this._inlineSnapshots, this._testPath); + saveInlineSnapshots(this._inlineSnapshots); } status.saved = true; } else if (!hasExternalSnapshots && fs.existsSync(this._snapshotPath)) { diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 6412ec37a7de..659f56bcf4c5 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -188,14 +188,22 @@ export const saveSnapshotFile = ( ); }; -export const saveInlineSnapshots = ( +export const saveInlineSnapshots = (snapshots: InlineSnapshot[]) => { + const snapshotsByFile = groupSnapshotsByFile(snapshots); + + for (const sourceFilePath of Object.keys(snapshotsByFile)) { + saveSnapshotsForFile(snapshotsByFile[sourceFilePath], sourceFilePath); + } +}; + +const saveSnapshotsForFile = ( snapshots: InlineSnapshot[], sourceFilePath: Path, ) => { const sourceFile = fs.readFileSync(sourceFilePath, 'utf8'); - const config = prettier.resolveConfig.sync(sourceFilePath); const {inferredParser} = prettier.getFileInfo.sync(sourceFilePath); + const newSourceFile = prettier.format( sourceFile, Object.assign({}, config, { @@ -209,14 +217,20 @@ export const saveInlineSnapshots = ( } }; -const groupSnapshotsByFrame = (snapshots: InlineSnapshot[]) => { - return snapshots.reduce((object, {snapshot, frame}) => { - const key = `${frame.line}:${frame.column}`; +const groupSnapshotsBy = (createKey: InlineSnapshot => string) => ( + snapshots: InlineSnapshot[], +) => + snapshots.reduce((object, inlineSnapshot) => { + const key = createKey(inlineSnapshot); return Object.assign(object, { - [key]: (object[key] || []).concat(snapshot), + [key]: (object[key] || []).concat(inlineSnapshot), }); }, {}); -}; + +const groupSnapshotsByFrame = groupSnapshotsBy( + ({frame: {line, column}}) => `${line}:${column - 1}`, +); +const groupSnapshotsByFile = groupSnapshotsBy(({frame: {file}}) => file); const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( text: string, @@ -227,6 +241,7 @@ const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( options.parser = inferredParser; const groupedSnapshots = groupSnapshotsByFrame(snapshots); + const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot)); let ast = parsers[inferredParser](text); // flow uses a 'Program' parent node, babel expects a 'File'. @@ -244,7 +259,7 @@ const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( return; } const {line, column} = callee.property.loc.start; - const snapshotsForFrame = groupedSnapshots[`${line}:${column + 1}`]; + const snapshotsForFrame = groupedSnapshots[`${line}:${column}`]; if (!snapshotsForFrame) { return; } @@ -256,9 +271,11 @@ const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( const snapshotIndex = args.findIndex( ({type}) => type === 'TemplateLiteral', ); - const values = snapshotsForFrame.map(snapshot => - templateLiteral([templateElement({raw: snapshot})], []), - ); + const values = snapshotsForFrame.map(({snapshot}) => { + remainingSnapshots.delete(snapshot); + + return templateLiteral([templateElement({raw: snapshot})], []); + }); const replacementNode = values[0]; if (snapshotIndex > -1) { @@ -269,5 +286,9 @@ const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( }, }); + if (remainingSnapshots.size) { + throw new Error(`Jest. Couldn't locate all inline snapshots.`); + } + return ast; }; From 91e34825e7f53be7e997149fe9904a9434ddd9d3 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 22:07:44 +1000 Subject: [PATCH 14/33] Add tests for saveInlineSnapshots() --- .../jest-snapshot/src/__mocks__/prettier.js | 18 ++++ .../jest-snapshot/src/__tests__/utils.test.js | 92 +++++++++++++++++++ packages/jest-snapshot/src/utils.js | 2 +- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 packages/jest-snapshot/src/__mocks__/prettier.js diff --git a/packages/jest-snapshot/src/__mocks__/prettier.js b/packages/jest-snapshot/src/__mocks__/prettier.js new file mode 100644 index 000000000000..abef9f92d8b9 --- /dev/null +++ b/packages/jest-snapshot/src/__mocks__/prettier.js @@ -0,0 +1,18 @@ +const prettier = require.requireActual('prettier'); + +module.exports = { + format: (text, opts) => + prettier.format( + text, + Object.assign( + { + pluginSearchDirs: [ + require('path').dirname(require.resolve('prettier')), + ], + }, + opts, + ), + ), + getFileInfo: {sync: () => ({inferredParser: 'babylon'})}, + resolveConfig: {sync: () => null}, +}; diff --git a/packages/jest-snapshot/src/__tests__/utils.test.js b/packages/jest-snapshot/src/__tests__/utils.test.js index 76cb798902f1..4414fb14a93a 100644 --- a/packages/jest-snapshot/src/__tests__/utils.test.js +++ b/packages/jest-snapshot/src/__tests__/utils.test.js @@ -6,6 +6,7 @@ */ jest.mock('fs'); +jest.mock('prettier'); const fs = require('fs'); const path = require('path'); @@ -14,6 +15,7 @@ const chalk = require('chalk'); const { getSnapshotData, getSnapshotPath, + saveInlineSnapshots, keyToTestName, saveSnapshotFile, serialize, @@ -26,15 +28,23 @@ const { const writeFileSync = fs.writeFileSync; const readFileSync = fs.readFileSync; const existsSync = fs.existsSync; +const statSync = fs.statSync; +const readdirSync = fs.readdirSync; beforeEach(() => { fs.writeFileSync = jest.fn(); fs.readFileSync = jest.fn(); fs.existsSync = jest.fn(() => true); + fs.statSync = jest.fn(filePath => ({ + isDirectory: () => !filePath.endsWith('.js'), + })); + fs.readdirSync = jest.fn(() => []); }); afterEach(() => { fs.writeFileSync = writeFileSync; fs.readFileSync = readFileSync; fs.existsSync = existsSync; + fs.statSync = statSync; + fs.readdirSync = readdirSync; }); test('keyToTestName()', () => { @@ -84,6 +94,88 @@ test('saveSnapshotFile() works with \r', () => { ); }); +test('saveInlineSnapshots() replaces empty function call with a template literal', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => `expect(1).toMatchInlineSnapshot();\n`); + + saveInlineSnapshots([ + { + frame: {column: 11, file: filename, line: 1}, + snapshot: `1`, + }, + ]); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + 'expect(1).toMatchInlineSnapshot(`1`);\n', + ); +}); + +test('saveInlineSnapshots() replaces existing template literal', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot(`2`);\n'); + + saveInlineSnapshots([ + { + frame: {column: 11, file: filename, line: 1}, + snapshot: `1`, + }, + ]); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + 'expect(1).toMatchInlineSnapshot(`1`);\n', + ); +}); + +test('saveInlineSnapshots() replaces existing template literal with property matchers', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn( + () => 'expect(1).toMatchInlineSnapshot({}, `2`);\n', + ); + + saveInlineSnapshots([ + { + frame: {column: 11, file: filename, line: 1}, + snapshot: `1`, + }, + ]); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + 'expect(1).toMatchInlineSnapshot({}, `1`);\n', + ); +}); + +test('saveInlineSnapshots() throws if frame does not match', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot();\n'); + + const save = () => + saveInlineSnapshots([ + { + frame: {column: 2 /* incorrect */, file: filename, line: 1}, + snapshot: `1`, + }, + ]); + + expect(save).toThrowError(/Couldn't locate all inline snapshots./); +}); + +test('saveInlineSnapshots() throws if multiple calls to to the same location', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot();\n'); + + const frame = {column: 11, file: filename, line: 1}; + + const save = () => + saveInlineSnapshots([{frame, snapshot: `1`}, {frame, snapshot: `2`}]); + + expect(save).toThrowError( + /Multiple inline snapshots for the same call are not supported./, + ); +}); + test('getSnapshotData() throws when no snapshot version', () => { const filename = path.join(__dirname, 'old-snapshot.snap'); fs.readFileSync = jest.fn(() => 'exports[`myKey`] = `
\n
`;\n'); diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 659f56bcf4c5..87e4daf17530 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -265,7 +265,7 @@ const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( } if (snapshotsForFrame.length > 1) { throw new Error( - 'Jest: Multiple inline snapshots for the same call are not supported.', + 'Jest. Multiple inline snapshots for the same call are not supported.', ); } const snapshotIndex = args.findIndex( From 8334b6f0ad469e7731dda7446d8f1798a9c8e2a3 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 3 Jun 2018 22:17:32 +1000 Subject: [PATCH 15/33] Fix InlineSnapshot type definition --- packages/jest-snapshot/src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 87e4daf17530..720fde9548a6 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -22,7 +22,7 @@ import {templateElement, templateLiteral, file} from 'babel-types'; export type InlineSnapshot = {| snapshot: string, - frame: {line: number, column: number}, + frame: {line: number, column: number, file: string}, |}; export const SNAPSHOT_EXTENSION = 'snap'; From 80f75eecf41bc6c90b1bf51fe1f1786f726e1cb0 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Tue, 5 Jun 2018 22:51:50 +1000 Subject: [PATCH 16/33] Escape backtick strings --- packages/jest-snapshot/src/utils.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index 720fde9548a6..a0c7312e8638 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -154,8 +154,10 @@ export const serialize = (data: any): string => { // unescape double quotes export const unescape = (data: any): string => data.replace(/\\(")/g, '$1'); +const escapeBacktickString = (str: string) => str.replace(/`|\\|\${/g, '\\$&'); + const printBacktickString = (str: string) => { - return '`' + str.replace(/`|\\|\${/g, '\\$&') + '`'; + return '`' + escapeBacktickString(str) + '`'; }; export const ensureDirectoryExists = (filePath: Path) => { @@ -274,7 +276,10 @@ const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( const values = snapshotsForFrame.map(({snapshot}) => { remainingSnapshots.delete(snapshot); - return templateLiteral([templateElement({raw: snapshot})], []); + return templateLiteral( + [templateElement({raw: escapeBacktickString(snapshot)})], + [], + ); }); const replacementNode = values[0]; From b3f462d3234fe941e5d468abf563e0b857614a39 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Wed, 6 Jun 2018 21:59:24 +1000 Subject: [PATCH 17/33] Add test for escaping backticks --- packages/jest-snapshot/src/__tests__/utils.test.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/jest-snapshot/src/__tests__/utils.test.js b/packages/jest-snapshot/src/__tests__/utils.test.js index 4414fb14a93a..10cef118f9cd 100644 --- a/packages/jest-snapshot/src/__tests__/utils.test.js +++ b/packages/jest-snapshot/src/__tests__/utils.test.js @@ -167,7 +167,6 @@ test('saveInlineSnapshots() throws if multiple calls to to the same location', ( fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot();\n'); const frame = {column: 11, file: filename, line: 1}; - const save = () => saveInlineSnapshots([{frame, snapshot: `1`}, {frame, snapshot: `2`}]); @@ -176,6 +175,19 @@ test('saveInlineSnapshots() throws if multiple calls to to the same location', ( ); }); +test('saveInlineSnapshots() uses escaped backticks', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => 'expect("`").toMatchInlineSnapshot();\n'); + + const frame = {column: 13, file: filename, line: 1}; + saveInlineSnapshots([{frame, snapshot: '`'}]); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + 'expect("`").toMatchInlineSnapshot(`\\``);\n', + ); +}); + test('getSnapshotData() throws when no snapshot version', () => { const filename = path.join(__dirname, 'old-snapshot.snap'); fs.readFileSync = jest.fn(() => 'exports[`myKey`] = `
\n
`;\n'); From 8ed6096e70a2ed86d50c849bab9fc3d2ce630570 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Fri, 8 Jun 2018 00:04:51 +1000 Subject: [PATCH 18/33] Code review fixes --- packages/jest-message-util/src/index.js | 4 +-- packages/jest-runner/src/run_test.js | 2 +- packages/jest-snapshot/src/State.js | 36 +++++++++++++------------ packages/jest-snapshot/src/utils.js | 10 +++---- types/Matchers.js | 2 +- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/jest-message-util/src/index.js b/packages/jest-message-util/src/index.js index 5860927401fb..67bd810f795d 100644 --- a/packages/jest-message-util/src/index.js +++ b/packages/jest-message-util/src/index.js @@ -227,9 +227,7 @@ const formatPaths = (config: StackTraceConfig, relativeTestPath, line) => { export const getStackTraceLines = ( stack: string, options: StackTraceOptions = {noStackTrace: false}, -) => { - return removeInternalStackEntries(stack.split(/\n/), options); -}; +) => removeInternalStackEntries(stack.split(/\n/), options); export const getTopFrame = (lines: string[]) => { for (const line of lines) { diff --git a/packages/jest-runner/src/run_test.js b/packages/jest-runner/src/run_test.js index 1efcc5ce2916..2fe785a3cc8d 100644 --- a/packages/jest-runner/src/run_test.js +++ b/packages/jest-runner/src/run_test.js @@ -211,6 +211,6 @@ export default async function runTest( // Resolve leak detector, outside the "runTestInternal" closure. result.leaks = leakDetector ? leakDetector.isLeaking() : false; - +debugger; return result; } diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index 9fe84443142d..325b82cfc77e 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -29,6 +29,13 @@ export type SnapshotStateOptions = {| expand?: boolean, |}; +export type SnapshotMatchOptions = {| + testName: string, + received: any, + key?: string, + inlineSnapshot?: string, +|}; + export default class SnapshotState { _counters: Map; _dirty: boolean; @@ -36,7 +43,7 @@ export default class SnapshotState { _updateSnapshot: SnapshotUpdateState; _snapshotData: {[key: string]: string}; _snapshotPath: Path; - _inlineSnapshots: InlineSnapshot[]; + _inlineSnapshots: Array; _testPath: Path; _uncheckedKeys: Set; added: number; @@ -74,9 +81,13 @@ export default class SnapshotState { }); } - _addSnapshot(key: string, receivedSerialized: string, isInline: boolean) { + _addSnapshot( + key: string, + receivedSerialized: string, + options: {isInline: boolean}, + ) { this._dirty = true; - if (isInline) { + if (options.isInline) { const lines = getStackTraceLines(new Error().stack); const frame = getTopFrame(lines); if (!frame) { @@ -135,17 +146,7 @@ export default class SnapshotState { } } - match({ - testName, - received, - key, - inlineSnapshot, - }: { - testName: string, - received: any, - key?: string, - inlineSnapshot?: string, - }) { + match({testName, received, key, inlineSnapshot}: SnapshotMatchOptions) { this._counters.set(testName, (this._counters.get(testName) || 0) + 1); const count = Number(this._counters.get(testName)); const isInline = inlineSnapshot !== undefined; @@ -162,6 +163,7 @@ export default class SnapshotState { const hasSnapshot = isInline ? inlineSnapshot !== '' : this._snapshotData[key] !== undefined; + const snapshotIsPersisted = isInline || fs.existsSync(this._snapshotPath); if (pass && !isInline) { // Executing a snapshot file as JavaScript and writing the strings back @@ -182,7 +184,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 || (!isInline && !fs.existsSync(this._snapshotPath))) && + ((!hasSnapshot || !snapshotIsPersisted) && (this._updateSnapshot === 'new' || this._updateSnapshot === 'all')) ) { if (this._updateSnapshot === 'all') { @@ -192,12 +194,12 @@ export default class SnapshotState { } else { this.added++; } - this._addSnapshot(key, receivedSerialized, isInline); + this._addSnapshot(key, receivedSerialized, {isInline}); } else { this.matched++; } } else { - this._addSnapshot(key, receivedSerialized, isInline); + this._addSnapshot(key, receivedSerialized, {isInline}); this.added++; } diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index a0c7312e8638..b0603bf7944b 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -199,7 +199,7 @@ export const saveInlineSnapshots = (snapshots: InlineSnapshot[]) => { }; const saveSnapshotsForFile = ( - snapshots: InlineSnapshot[], + snapshots: Array, sourceFilePath: Path, ) => { const sourceFile = fs.readFileSync(sourceFilePath, 'utf8'); @@ -220,7 +220,7 @@ const saveSnapshotsForFile = ( }; const groupSnapshotsBy = (createKey: InlineSnapshot => string) => ( - snapshots: InlineSnapshot[], + snapshots: Array, ) => snapshots.reduce((object, inlineSnapshot) => { const key = createKey(inlineSnapshot); @@ -246,7 +246,7 @@ const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot)); let ast = parsers[inferredParser](text); - // flow uses a 'Program' parent node, babel expects a 'File'. + // Flow uses a 'Program' parent node, babel expects a 'File'. if (ast.type !== 'File') { ast = file(ast, ast.comments, ast.tokens); delete ast.program.comments; @@ -267,7 +267,7 @@ const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( } if (snapshotsForFrame.length > 1) { throw new Error( - 'Jest. Multiple inline snapshots for the same call are not supported.', + 'Jest: Multiple inline snapshots for the same call are not supported.', ); } const snapshotIndex = args.findIndex( @@ -292,7 +292,7 @@ const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( }); if (remainingSnapshots.size) { - throw new Error(`Jest. Couldn't locate all inline snapshots.`); + throw new Error(`Jest: Couldn't locate all inline snapshots.`); } return ast; diff --git a/types/Matchers.js b/types/Matchers.js index 909800a9e16e..52f24984dad4 100644 --- a/types/Matchers.js +++ b/types/Matchers.js @@ -30,7 +30,7 @@ export type PromiseMatcherFn = (actual: any) => Promise; export type MatcherState = { assertionCalls: number, currentTestName?: string, - equals: (any, any, ?(any[])) => boolean, + equals: (any, any, ?Array) => boolean, expand?: boolean, expectedAssertionsNumber: ?number, isExpectingAssertions: ?boolean, From c07788ad1ce269f2e207670e8b716d582ebe1e1a Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 9 Jun 2018 15:06:13 +1000 Subject: [PATCH 19/33] Support project option for prettier Massive refactor, sorry I didn't split the commits --- TestUtils.js | 1 + .../jest_adapter_init.js | 6 +- packages/jest-cli/src/cli/args.js | 5 + packages/jest-config/src/defaults.js | 1 + packages/jest-config/src/index.js | 1 + packages/jest-config/src/normalize.js | 62 ++++--- packages/jest-config/src/utils.js | 14 +- packages/jest-config/src/valid_config.js | 1 + .../jest-jasmine2/src/setup_jest_globals.js | 6 +- packages/jest-runner/src/run_test.js | 2 +- packages/jest-snapshot/package.json | 6 +- packages/jest-snapshot/src/State.js | 9 +- .../jest-snapshot/src/__mocks__/prettier.js | 1 + .../src/__tests__/inline_snapshots.test.js | 146 +++++++++++++++ .../jest-snapshot/src/__tests__/utils.test.js | 104 ----------- .../jest-snapshot/src/inline_snapshots.js | 174 ++++++++++++++++++ packages/jest-snapshot/src/utils.js | 124 +------------ .../src/__tests__/fixtures/jest_config.js | 2 + types/Config.js | 3 + yarn.lock | 15 +- 20 files changed, 413 insertions(+), 270 deletions(-) create mode 100644 packages/jest-snapshot/src/__tests__/inline_snapshots.test.js create mode 100644 packages/jest-snapshot/src/inline_snapshots.js diff --git a/TestUtils.js b/TestUtils.js index c92b4ba9ae60..78bb0db0e1bc 100644 --- a/TestUtils.js +++ b/TestUtils.js @@ -90,6 +90,7 @@ const DEFAULT_PROJECT_CONFIG: ProjectConfig = { modulePathIgnorePatterns: [], modulePaths: [], name: 'test_name', + prettier: 'prettier', resetMocks: false, resetModules: false, resolver: null, diff --git a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js index 6a1e50f8614a..8cacd3582973 100644 --- a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js +++ b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js @@ -92,7 +92,11 @@ export const initialize = ({ }); const {expand, updateSnapshot} = globalConfig; - const snapshotState = new SnapshotState(testPath, {expand, updateSnapshot}); + const snapshotState = new SnapshotState(testPath, { + expand, + getPrettier: () => localRequire(config.prettier), + updateSnapshot, + }); setState({snapshotState, testPath}); // Return it back to the outer scope (test runner outside the VM). diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index edc17bda56a3..43ebb997c516 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -420,6 +420,11 @@ export const options = { description: "A preset that is used as a base for Jest's configuration.", type: 'string', }, + prettier: { + default: 'prettier', + description: 'The path to the "prettier" module used for inline snapshots.', + type: 'string', + }, projects: { description: 'A list of projects that use Jest to run all tests of all ' + diff --git a/packages/jest-config/src/defaults.js b/packages/jest-config/src/defaults.js index 8710f5de56bd..cb6c4ce35095 100644 --- a/packages/jest-config/src/defaults.js +++ b/packages/jest-config/src/defaults.js @@ -61,6 +61,7 @@ export default ({ notify: false, notifyMode: 'always', preset: null, + prettier: 'prettier', projects: null, resetMocks: false, resetModules: false, diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index b5392bb2b97e..dfad7b519c88 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -173,6 +173,7 @@ const getConfigs = ( modulePathIgnorePatterns: options.modulePathIgnorePatterns, modulePaths: options.modulePaths, name: options.name, + prettier: options.prettier, resetMocks: options.resetMocks, resetModules: options.resetModules, resolver: options.resolver, diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 72e3543ae42f..dd4883338c37 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -373,12 +373,11 @@ export default function normalize(options: InitialOptions, argv: Argv) { options = (options: InitialOptions); if (options.resolver) { - newOptions.resolver = resolve( - null, - options.rootDir, - 'resolver', - options.resolver, - ); + newOptions.resolver = resolve(null, { + filePath: options.resolver, + key: 'resolver', + rootDir: options.rootDir, + }); } Object.keys(options).reduce((newOptions, key) => { @@ -395,8 +394,12 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'snapshotSerializers': value = options[key] && - options[key].map( - resolve.bind(null, newOptions.resolver, options.rootDir, key), + options[key].map(filePath => + resolve(newOptions.resolver, { + filePath, + key, + rootDir: options.rootDir, + }), ); break; case 'modulePaths': @@ -429,9 +432,16 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'setupTestFrameworkScriptFile': case 'testResultsProcessor': case 'testRunner': + case 'filter': + case 'prettier': value = options[key] && - resolve(newOptions.resolver, options.rootDir, key, options[key]); + resolve(newOptions.resolver, { + filePath: options[key], + key, + optional: key === 'prettier', + rootDir: options.rootDir, + }); break; case 'moduleNameMapper': const moduleNameMapper = options[key]; @@ -448,12 +458,11 @@ export default function normalize(options: InitialOptions, argv: Argv) { transform && Object.keys(transform).map(regex => [ regex, - resolve( - newOptions.resolver, - options.rootDir, + resolve(newOptions.resolver, { + filePath: transform[regex], key, - transform[regex], - ), + rootDir: options.rootDir, + }), ]); break; case 'coveragePathIgnorePatterns': @@ -467,12 +476,14 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'haste': value = Object.assign({}, options[key]); if (value.hasteImplModulePath != null) { - value.hasteImplModulePath = resolve( - newOptions.resolver, - options.rootDir, - 'haste.hasteImplModulePath', - replaceRootDirInPath(options.rootDir, value.hasteImplModulePath), - ); + value.hasteImplModulePath = resolve(newOptions.resolver, { + filePath: replaceRootDirInPath( + options.rootDir, + value.hasteImplModulePath, + ), + key: 'haste.hasteImplModulePath', + rootDir: options.rootDir, + }); } break; case 'projects': @@ -502,11 +513,6 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'testRegex': value = options[key] && replacePathSepForRegex(options[key]); break; - case 'filter': - value = - options[key] && - resolve(newOptions.resolver, options.rootDir, key, options[key]); - break; case 'automock': case 'bail': case 'browser': @@ -564,7 +570,11 @@ export default function normalize(options: InitialOptions, argv: Argv) { break; case 'watchPlugins': value = (options[key] || []).map(watchPlugin => - resolve(newOptions.resolver, options.rootDir, key, watchPlugin), + resolve(newOptions.resolver, { + filePath: watchPlugin, + key, + rootDir: options.rootDir, + }), ); break; } diff --git a/packages/jest-config/src/utils.js b/packages/jest-config/src/utils.js index d31cc17503e3..feeeab1686bf 100644 --- a/packages/jest-config/src/utils.js +++ b/packages/jest-config/src/utils.js @@ -13,6 +13,14 @@ import path from 'path'; import {ValidationError} from 'jest-validate'; import Resolver from 'jest-resolve'; import chalk from 'chalk'; + +type ResolveOptions = {| + rootDir: string, + key: string, + filePath: Path, + optional?: boolean, +|}; + export const BULLET: string = chalk.bold('\u25cf '); export const DOCUMENTATION_NOTE = ` ${chalk.bold( 'Configuration Documentation:', @@ -30,9 +38,7 @@ const createValidationError = (message: string) => { export const resolve = ( resolver: ?string, - rootDir: string, - key: string, - filePath: Path, + {key, filePath, rootDir, optional}: ResolveOptions, ) => { const module = Resolver.findNodeModule( replaceRootDirInPath(rootDir, filePath), @@ -42,7 +48,7 @@ export const resolve = ( }, ); - if (!module) { + if (!module && !optional) { throw createValidationError( ` Module ${chalk.bold(filePath)} in the ${chalk.bold( key, diff --git a/packages/jest-config/src/valid_config.js b/packages/jest-config/src/valid_config.js index 1c5372c05b0a..00c4774b2723 100644 --- a/packages/jest-config/src/valid_config.js +++ b/packages/jest-config/src/valid_config.js @@ -65,6 +65,7 @@ export default ({ notifyMode: 'always', onlyChanged: false, preset: 'react-native', + prettier: '/node_modules/prettier', projects: ['project-a', 'project-b/'], reporters: [ 'default', diff --git a/packages/jest-jasmine2/src/setup_jest_globals.js b/packages/jest-jasmine2/src/setup_jest_globals.js index c7b3dd324888..e865e8291335 100644 --- a/packages/jest-jasmine2/src/setup_jest_globals.js +++ b/packages/jest-jasmine2/src/setup_jest_globals.js @@ -100,7 +100,11 @@ export default ({ }); patchJasmine(); const {expand, updateSnapshot} = globalConfig; - const snapshotState = new SnapshotState(testPath, {expand, updateSnapshot}); + const snapshotState = new SnapshotState(testPath, { + expand, + getPrettier: () => localRequire(config.prettier), + updateSnapshot, + }); setState({snapshotState, testPath}); // Return it back to the outer scope (test runner outside the VM). return snapshotState; diff --git a/packages/jest-runner/src/run_test.js b/packages/jest-runner/src/run_test.js index 2fe785a3cc8d..1efcc5ce2916 100644 --- a/packages/jest-runner/src/run_test.js +++ b/packages/jest-runner/src/run_test.js @@ -211,6 +211,6 @@ export default async function runTest( // Resolve leak detector, outside the "runTestInternal" closure. result.leaks = leakDetector ? leakDetector.isLeaking() : false; -debugger; + return result; } diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index 5079b8859c9f..e6a8336fa436 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -14,9 +14,13 @@ "jest-diff": "^23.0.1", "jest-matcher-utils": "^23.0.1", "jest-message-util": "^23.0.1", + "jest-resolve": "^23.0.1", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", "pretty-format": "^23.0.1", - "prettier": "^1.13.4" + "semver": "^5.5.0" + }, + "devDependencies": { + "prettier": "^1.13.4" } } diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index 325b82cfc77e..9353749aa9b7 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -13,18 +13,18 @@ import fs from 'fs'; import {getTopFrame, getStackTraceLines} from 'jest-message-util'; import { saveSnapshotFile, - saveInlineSnapshots, getSnapshotData, getSnapshotPath, keyToTestName, serialize, testNameToKey, unescape, - type InlineSnapshot, } from './utils'; +import {saveInlineSnapshots, type InlineSnapshot} from './inline_snapshots'; export type SnapshotStateOptions = {| updateSnapshot: SnapshotUpdateState, + getPrettier: () => null | any, snapshotPath?: string, expand?: boolean, |}; @@ -46,6 +46,7 @@ export default class SnapshotState { _inlineSnapshots: Array; _testPath: Path; _uncheckedKeys: Set; + _getPrettier: () => null | any; added: number; expand: boolean; matched: number; @@ -61,6 +62,7 @@ export default class SnapshotState { ); this._snapshotData = data; this._dirty = dirty; + this._getPrettier = options.getPrettier; this._inlineSnapshots = []; this._uncheckedKeys = new Set(Object.keys(this._snapshotData)); this._counters = new Map(); @@ -117,7 +119,8 @@ export default class SnapshotState { saveSnapshotFile(this._snapshotData, this._snapshotPath); } if (hasInlineSnapshots) { - saveInlineSnapshots(this._inlineSnapshots); + const prettier = this._getPrettier(); // Load lazily + saveInlineSnapshots(this._inlineSnapshots, prettier); } status.saved = true; } else if (!hasExternalSnapshots && fs.existsSync(this._snapshotPath)) { diff --git a/packages/jest-snapshot/src/__mocks__/prettier.js b/packages/jest-snapshot/src/__mocks__/prettier.js index abef9f92d8b9..4bc632d3ebdd 100644 --- a/packages/jest-snapshot/src/__mocks__/prettier.js +++ b/packages/jest-snapshot/src/__mocks__/prettier.js @@ -15,4 +15,5 @@ module.exports = { ), getFileInfo: {sync: () => ({inferredParser: 'babylon'})}, resolveConfig: {sync: () => null}, + version: prettier.version, }; diff --git a/packages/jest-snapshot/src/__tests__/inline_snapshots.test.js b/packages/jest-snapshot/src/__tests__/inline_snapshots.test.js new file mode 100644 index 000000000000..da322865e7cf --- /dev/null +++ b/packages/jest-snapshot/src/__tests__/inline_snapshots.test.js @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +jest.mock('fs'); +jest.mock('prettier'); + +const fs = require('fs'); +const path = require('path'); +const prettier = require('prettier'); + +const {saveInlineSnapshots} = require('../inline_snapshots'); + +const writeFileSync = fs.writeFileSync; +const readFileSync = fs.readFileSync; +const existsSync = fs.existsSync; +const statSync = fs.statSync; +const readdirSync = fs.readdirSync; +beforeEach(() => { + fs.writeFileSync = jest.fn(); + fs.readFileSync = jest.fn(); + fs.existsSync = jest.fn(() => true); + fs.statSync = jest.fn(filePath => ({ + isDirectory: () => !filePath.endsWith('.js'), + })); + fs.readdirSync = jest.fn(() => []); +}); +afterEach(() => { + fs.writeFileSync = writeFileSync; + fs.readFileSync = readFileSync; + fs.existsSync = existsSync; + fs.statSync = statSync; + fs.readdirSync = readdirSync; +}); + +test('saveInlineSnapshots() replaces empty function call with a template literal', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => `expect(1).toMatchInlineSnapshot();\n`); + + saveInlineSnapshots( + [ + { + frame: {column: 11, file: filename, line: 1}, + snapshot: `1`, + }, + ], + prettier, + ); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + 'expect(1).toMatchInlineSnapshot(`1`);\n', + ); +}); + +test('saveInlineSnapshots() replaces existing template literal', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot(`2`);\n'); + + saveInlineSnapshots( + [ + { + frame: {column: 11, file: filename, line: 1}, + snapshot: `1`, + }, + ], + prettier, + ); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + 'expect(1).toMatchInlineSnapshot(`1`);\n', + ); +}); + +test('saveInlineSnapshots() replaces existing template literal with property matchers', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn( + () => 'expect(1).toMatchInlineSnapshot({}, `2`);\n', + ); + + saveInlineSnapshots( + [ + { + frame: {column: 11, file: filename, line: 1}, + snapshot: `1`, + }, + ], + prettier, + ); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + 'expect(1).toMatchInlineSnapshot({}, `1`);\n', + ); +}); + +test('saveInlineSnapshots() throws if frame does not match', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot();\n'); + + const save = () => + saveInlineSnapshots( + [ + { + frame: {column: 2 /* incorrect */, file: filename, line: 1}, + snapshot: `1`, + }, + ], + prettier, + ); + + expect(save).toThrowError(/Couldn't locate all inline snapshots./); +}); + +test('saveInlineSnapshots() throws if multiple calls to to the same location', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot();\n'); + + const frame = {column: 11, file: filename, line: 1}; + const save = () => + saveInlineSnapshots( + [{frame, snapshot: `1`}, {frame, snapshot: `2`}], + prettier, + ); + + expect(save).toThrowError( + /Multiple inline snapshots for the same call are not supported./, + ); +}); + +test('saveInlineSnapshots() uses escaped backticks', () => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => 'expect("`").toMatchInlineSnapshot();\n'); + + const frame = {column: 13, file: filename, line: 1}; + saveInlineSnapshots([{frame, snapshot: '`'}], prettier); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + 'expect("`").toMatchInlineSnapshot(`\\``);\n', + ); +}); diff --git a/packages/jest-snapshot/src/__tests__/utils.test.js b/packages/jest-snapshot/src/__tests__/utils.test.js index 10cef118f9cd..76cb798902f1 100644 --- a/packages/jest-snapshot/src/__tests__/utils.test.js +++ b/packages/jest-snapshot/src/__tests__/utils.test.js @@ -6,7 +6,6 @@ */ jest.mock('fs'); -jest.mock('prettier'); const fs = require('fs'); const path = require('path'); @@ -15,7 +14,6 @@ const chalk = require('chalk'); const { getSnapshotData, getSnapshotPath, - saveInlineSnapshots, keyToTestName, saveSnapshotFile, serialize, @@ -28,23 +26,15 @@ const { const writeFileSync = fs.writeFileSync; const readFileSync = fs.readFileSync; const existsSync = fs.existsSync; -const statSync = fs.statSync; -const readdirSync = fs.readdirSync; beforeEach(() => { fs.writeFileSync = jest.fn(); fs.readFileSync = jest.fn(); fs.existsSync = jest.fn(() => true); - fs.statSync = jest.fn(filePath => ({ - isDirectory: () => !filePath.endsWith('.js'), - })); - fs.readdirSync = jest.fn(() => []); }); afterEach(() => { fs.writeFileSync = writeFileSync; fs.readFileSync = readFileSync; fs.existsSync = existsSync; - fs.statSync = statSync; - fs.readdirSync = readdirSync; }); test('keyToTestName()', () => { @@ -94,100 +84,6 @@ test('saveSnapshotFile() works with \r', () => { ); }); -test('saveInlineSnapshots() replaces empty function call with a template literal', () => { - const filename = path.join(__dirname, 'my.test.js'); - fs.readFileSync = jest.fn(() => `expect(1).toMatchInlineSnapshot();\n`); - - saveInlineSnapshots([ - { - frame: {column: 11, file: filename, line: 1}, - snapshot: `1`, - }, - ]); - - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, - 'expect(1).toMatchInlineSnapshot(`1`);\n', - ); -}); - -test('saveInlineSnapshots() replaces existing template literal', () => { - const filename = path.join(__dirname, 'my.test.js'); - fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot(`2`);\n'); - - saveInlineSnapshots([ - { - frame: {column: 11, file: filename, line: 1}, - snapshot: `1`, - }, - ]); - - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, - 'expect(1).toMatchInlineSnapshot(`1`);\n', - ); -}); - -test('saveInlineSnapshots() replaces existing template literal with property matchers', () => { - const filename = path.join(__dirname, 'my.test.js'); - fs.readFileSync = jest.fn( - () => 'expect(1).toMatchInlineSnapshot({}, `2`);\n', - ); - - saveInlineSnapshots([ - { - frame: {column: 11, file: filename, line: 1}, - snapshot: `1`, - }, - ]); - - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, - 'expect(1).toMatchInlineSnapshot({}, `1`);\n', - ); -}); - -test('saveInlineSnapshots() throws if frame does not match', () => { - const filename = path.join(__dirname, 'my.test.js'); - fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot();\n'); - - const save = () => - saveInlineSnapshots([ - { - frame: {column: 2 /* incorrect */, file: filename, line: 1}, - snapshot: `1`, - }, - ]); - - expect(save).toThrowError(/Couldn't locate all inline snapshots./); -}); - -test('saveInlineSnapshots() throws if multiple calls to to the same location', () => { - const filename = path.join(__dirname, 'my.test.js'); - fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot();\n'); - - const frame = {column: 11, file: filename, line: 1}; - const save = () => - saveInlineSnapshots([{frame, snapshot: `1`}, {frame, snapshot: `2`}]); - - expect(save).toThrowError( - /Multiple inline snapshots for the same call are not supported./, - ); -}); - -test('saveInlineSnapshots() uses escaped backticks', () => { - const filename = path.join(__dirname, 'my.test.js'); - fs.readFileSync = jest.fn(() => 'expect("`").toMatchInlineSnapshot();\n'); - - const frame = {column: 13, file: filename, line: 1}; - saveInlineSnapshots([{frame, snapshot: '`'}]); - - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, - 'expect("`").toMatchInlineSnapshot(`\\``);\n', - ); -}); - test('getSnapshotData() throws when no snapshot version', () => { const filename = path.join(__dirname, 'old-snapshot.snap'); fs.readFileSync = jest.fn(() => 'exports[`myKey`] = `
\n
`;\n'); diff --git a/packages/jest-snapshot/src/inline_snapshots.js b/packages/jest-snapshot/src/inline_snapshots.js new file mode 100644 index 000000000000..388225cc4d9b --- /dev/null +++ b/packages/jest-snapshot/src/inline_snapshots.js @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import fs from 'fs'; +import semver from 'semver'; +import path from 'path'; +import traverse from 'babel-traverse'; +import {templateElement, templateLiteral, file} from 'babel-types'; + +import type {Path} from 'types/Config'; +import {escapeBacktickString} from './utils'; + +export type InlineSnapshot = {| + snapshot: string, + frame: {line: number, column: number, file: string}, +|}; + +export const saveInlineSnapshots = ( + snapshots: InlineSnapshot[], + prettier: any, +) => { + if (!prettier) { + throw new Error( + `Jest: Inline Snapshots requires Prettier.\n` + + `Please ensure "prettier" is installed in your project.`, + ); + } + + // Custom parser API was added in 1.5.0 + if (semver.lt(prettier.version, '1.5.0')) { + throw new Error( + `Jest: Inline Snapshots require prettier>=1.5.0.\n` + + `Please upgrade "prettier".`, + ); + } + + const snapshotsByFile = groupSnapshotsByFile(snapshots); + + for (const sourceFilePath of Object.keys(snapshotsByFile)) { + saveSnapshotsForFile( + snapshotsByFile[sourceFilePath], + sourceFilePath, + prettier, + ); + } +}; + +const saveSnapshotsForFile = ( + snapshots: Array, + sourceFilePath: Path, + prettier: any, +) => { + const sourceFile = fs.readFileSync(sourceFilePath, 'utf8'); + + // Resolve project configuration. + // For older versions of Prettier, do not load configuration. + const config = prettier.resolveConfig + ? prettier.resolveConfig.sync(sourceFilePath, { + editorconfig: true, + }) + : null; + + // Detect the parser for the test file. + // For older versions of Prettier, fallback to a simple parser detection. + const inferredParser = prettier.getFileInfo + ? prettier.getFileInfo.sync(sourceFilePath).inferredParser + : (config && config.parser) || simpleDetectParser(sourceFilePath); + + // Format the source code using the custom parser API. + const newSourceFile = prettier.format( + sourceFile, + Object.assign({}, config, { + filepath: sourceFilePath, + parser: createParser(snapshots, inferredParser), + }), + ); + + if (newSourceFile !== sourceFile) { + fs.writeFileSync(sourceFilePath, newSourceFile); + } +}; + +const groupSnapshotsBy = (createKey: InlineSnapshot => string) => ( + snapshots: Array, +) => + snapshots.reduce((object, inlineSnapshot) => { + const key = createKey(inlineSnapshot); + return Object.assign(object, { + [key]: (object[key] || []).concat(inlineSnapshot), + }); + }, {}); + +const groupSnapshotsByFrame = groupSnapshotsBy( + ({frame: {line, column}}) => `${line}:${column - 1}`, +); +const groupSnapshotsByFile = groupSnapshotsBy(({frame: {file}}) => file); + +const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( + text: string, + parsers: {[key: string]: (string) => any}, + options: any, +) => { + // Workaround for https://github.com/prettier/prettier/issues/3150 + options.parser = inferredParser; + + const groupedSnapshots = groupSnapshotsByFrame(snapshots); + const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot)); + let ast = parsers[inferredParser](text); + + // Flow uses a 'Program' parent node, babel expects a 'File'. + if (ast.type !== 'File') { + ast = file(ast, ast.comments, ast.tokens); + delete ast.program.comments; + } + + traverse(ast, { + CallExpression({node: {arguments: args, callee}}) { + if ( + callee.type !== 'MemberExpression' || + callee.property.type !== 'Identifier' + ) { + return; + } + const {line, column} = callee.property.loc.start; + const snapshotsForFrame = groupedSnapshots[`${line}:${column}`]; + if (!snapshotsForFrame) { + return; + } + if (snapshotsForFrame.length > 1) { + throw new Error( + 'Jest: Multiple inline snapshots for the same call are not supported.', + ); + } + const snapshotIndex = args.findIndex( + ({type}) => type === 'TemplateLiteral', + ); + const values = snapshotsForFrame.map(({snapshot}) => { + remainingSnapshots.delete(snapshot); + + return templateLiteral( + [templateElement({raw: escapeBacktickString(snapshot)})], + [], + ); + }); + const replacementNode = values[0]; + + if (snapshotIndex > -1) { + args[snapshotIndex] = replacementNode; + } else { + args.push(replacementNode); + } + }, + }); + + if (remainingSnapshots.size) { + throw new Error(`Jest: Couldn't locate all inline snapshots.`); + } + + return ast; +}; + +const simpleDetectParser = (filePath: Path) => { + const extname = path.extname(filePath); + if (/tsx?$/.test(extname)) { + return 'typescript'; + } + return 'babylon'; +}; diff --git a/packages/jest-snapshot/src/utils.js b/packages/jest-snapshot/src/utils.js index b0603bf7944b..01a9699b4be4 100644 --- a/packages/jest-snapshot/src/utils.js +++ b/packages/jest-snapshot/src/utils.js @@ -16,14 +16,6 @@ import mkdirp from 'mkdirp'; import naturalCompare from 'natural-compare'; import path from 'path'; import prettyFormat from 'pretty-format'; -import prettier from 'prettier'; -import traverse from 'babel-traverse'; -import {templateElement, templateLiteral, file} from 'babel-types'; - -export type InlineSnapshot = {| - snapshot: string, - frame: {line: number, column: number, file: string}, -|}; export const SNAPSHOT_EXTENSION = 'snap'; export const SNAPSHOT_VERSION = '1'; @@ -154,11 +146,11 @@ export const serialize = (data: any): string => { // unescape double quotes export const unescape = (data: any): string => data.replace(/\\(")/g, '$1'); -const escapeBacktickString = (str: string) => str.replace(/`|\\|\${/g, '\\$&'); +export const escapeBacktickString = (str: string) => + str.replace(/`|\\|\${/g, '\\$&'); -const printBacktickString = (str: string) => { - return '`' + escapeBacktickString(str) + '`'; -}; +const printBacktickString = (str: string) => + '`' + escapeBacktickString(str) + '`'; export const ensureDirectoryExists = (filePath: Path) => { try { @@ -189,111 +181,3 @@ export const saveSnapshotFile = ( writeSnapshotVersion() + '\n\n' + snapshots.join('\n\n') + '\n', ); }; - -export const saveInlineSnapshots = (snapshots: InlineSnapshot[]) => { - const snapshotsByFile = groupSnapshotsByFile(snapshots); - - for (const sourceFilePath of Object.keys(snapshotsByFile)) { - saveSnapshotsForFile(snapshotsByFile[sourceFilePath], sourceFilePath); - } -}; - -const saveSnapshotsForFile = ( - snapshots: Array, - sourceFilePath: Path, -) => { - const sourceFile = fs.readFileSync(sourceFilePath, 'utf8'); - const config = prettier.resolveConfig.sync(sourceFilePath); - const {inferredParser} = prettier.getFileInfo.sync(sourceFilePath); - - const newSourceFile = prettier.format( - sourceFile, - Object.assign({}, config, { - filepath: sourceFilePath, - parser: createParser(snapshots, inferredParser), - }), - ); - - if (newSourceFile !== sourceFile) { - fs.writeFileSync(sourceFilePath, newSourceFile); - } -}; - -const groupSnapshotsBy = (createKey: InlineSnapshot => string) => ( - snapshots: Array, -) => - snapshots.reduce((object, inlineSnapshot) => { - const key = createKey(inlineSnapshot); - return Object.assign(object, { - [key]: (object[key] || []).concat(inlineSnapshot), - }); - }, {}); - -const groupSnapshotsByFrame = groupSnapshotsBy( - ({frame: {line, column}}) => `${line}:${column - 1}`, -); -const groupSnapshotsByFile = groupSnapshotsBy(({frame: {file}}) => file); - -const createParser = (snapshots: InlineSnapshot[], inferredParser: string) => ( - text: string, - parsers: {[key: string]: (string) => any}, - options: any, -) => { - // Workaround for https://github.com/prettier/prettier/issues/3150 - options.parser = inferredParser; - - const groupedSnapshots = groupSnapshotsByFrame(snapshots); - const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot)); - let ast = parsers[inferredParser](text); - - // Flow uses a 'Program' parent node, babel expects a 'File'. - if (ast.type !== 'File') { - ast = file(ast, ast.comments, ast.tokens); - delete ast.program.comments; - } - - traverse(ast, { - CallExpression({node: {arguments: args, callee}}) { - if ( - callee.type !== 'MemberExpression' || - callee.property.type !== 'Identifier' - ) { - return; - } - const {line, column} = callee.property.loc.start; - const snapshotsForFrame = groupedSnapshots[`${line}:${column}`]; - if (!snapshotsForFrame) { - return; - } - if (snapshotsForFrame.length > 1) { - throw new Error( - 'Jest: Multiple inline snapshots for the same call are not supported.', - ); - } - const snapshotIndex = args.findIndex( - ({type}) => type === 'TemplateLiteral', - ); - const values = snapshotsForFrame.map(({snapshot}) => { - remainingSnapshots.delete(snapshot); - - return templateLiteral( - [templateElement({raw: escapeBacktickString(snapshot)})], - [], - ); - }); - const replacementNode = values[0]; - - if (snapshotIndex > -1) { - args[snapshotIndex] = replacementNode; - } else { - args.push(replacementNode); - } - }, - }); - - if (remainingSnapshots.size) { - throw new Error(`Jest: Couldn't locate all inline snapshots.`); - } - - return ast; -}; diff --git a/packages/jest-validate/src/__tests__/fixtures/jest_config.js b/packages/jest-validate/src/__tests__/fixtures/jest_config.js index 263ec29aa871..705c526264bd 100644 --- a/packages/jest-validate/src/__tests__/fixtures/jest_config.js +++ b/packages/jest-validate/src/__tests__/fixtures/jest_config.js @@ -41,6 +41,7 @@ const defaultConfig = { notify: false, notifyMode: 'always', preset: null, + prettier: 'prettier', resetMocks: false, resetModules: false, restoreMocks: false, @@ -99,6 +100,7 @@ const validConfig = { notify: false, notifyMode: 'always', preset: 'react-native', + prettier: '/node_modules/prettier', resetMocks: false, resetModules: false, restoreMocks: false, diff --git a/types/Config.js b/types/Config.js index 9f2d0f317b0a..b75ad2ef893f 100644 --- a/types/Config.js +++ b/types/Config.js @@ -53,6 +53,7 @@ export type DefaultOptions = {| notify: boolean, notifyMode: string, preset: ?string, + prettier: string, projects: ?Array, resetMocks: boolean, resetModules: boolean, @@ -135,6 +136,7 @@ export type InitialOptions = { passWithNoTests?: boolean, preprocessorIgnorePatterns?: Array, preset?: ?string, + prettier?: ?string, projects?: Array, replname?: ?string, resetMocks?: boolean, @@ -258,6 +260,7 @@ export type ProjectConfig = {| modulePathIgnorePatterns: Array, modulePaths: Array, name: string, + prettier: string, resetMocks: boolean, resetModules: boolean, resolver: ?Path, diff --git a/yarn.lock b/yarn.lock index 6043b472c166..ad7499e2e21f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1221,7 +1221,7 @@ babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: babylon "^6.18.0" lodash "^4.17.4" -babel-traverse@^6.14.1, babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.25.0, babel-traverse@^6.26.0: +babel-traverse@^6.0.0, babel-traverse@^6.14.1, babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-traverse@^6.24.1, babel-traverse@^6.25.0, babel-traverse@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" dependencies: @@ -1235,7 +1235,7 @@ babel-traverse@^6.14.1, babel-traverse@^6.18.0, babel-traverse@^6.23.1, babel-tr invariant "^2.2.2" lodash "^4.17.4" -babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: +babel-types@^6.0.0, babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.23.0, babel-types@^6.24.1, babel-types@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" dependencies: @@ -5798,13 +5798,6 @@ libqp@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/libqp/-/libqp-1.1.0.tgz#f5e6e06ad74b794fb5b5b66988bf728ef1dedbe8" -line-column@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/line-column/-/line-column-1.0.2.tgz#d25af2936b6f4849172b312e4792d1d987bc34a2" - dependencies: - isarray "^1.0.0" - isobject "^2.0.0" - lines-and-columns@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" @@ -7496,6 +7489,10 @@ prettier@^1.13.3: version "1.13.3" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.3.tgz#e74c09a7df6519d472ca6febaa37cf7addb48a20" +prettier@^1.13.4: + version "1.13.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.5.tgz#7ae2076998c8edce79d63834e9b7b09fead6bfd0" + pretty-format@^22.4.0, pretty-format@^22.4.3: version "22.4.3" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f" From 657f10e59a557b664dc85a482155ecb3ca61172c Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 9 Jun 2018 15:48:21 +1000 Subject: [PATCH 20/33] Refactor configuration --- .../legacy_code_todo_rewrite/jest_adapter_init.js | 2 +- packages/jest-cli/src/cli/args.js | 1 - packages/jest-config/src/defaults.js | 2 +- packages/jest-config/src/normalize.js | 13 +++++++++++-- packages/jest-jasmine2/src/setup_jest_globals.js | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js index 8cacd3582973..45abc153fe7b 100644 --- a/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js +++ b/packages/jest-circus/src/legacy_code_todo_rewrite/jest_adapter_init.js @@ -94,7 +94,7 @@ export const initialize = ({ const {expand, updateSnapshot} = globalConfig; const snapshotState = new SnapshotState(testPath, { expand, - getPrettier: () => localRequire(config.prettier), + getPrettier: () => (config.prettier ? localRequire(config.prettier) : null), updateSnapshot, }); setState({snapshotState, testPath}); diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index 43ebb997c516..bc9e2fa62cc6 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -421,7 +421,6 @@ export const options = { type: 'string', }, prettier: { - default: 'prettier', description: 'The path to the "prettier" module used for inline snapshots.', type: 'string', }, diff --git a/packages/jest-config/src/defaults.js b/packages/jest-config/src/defaults.js index cb6c4ce35095..2d6c74526af9 100644 --- a/packages/jest-config/src/defaults.js +++ b/packages/jest-config/src/defaults.js @@ -61,7 +61,7 @@ export default ({ notify: false, notifyMode: 'always', preset: null, - prettier: 'prettier', + prettier: null, projects: null, resetMocks: false, resetModules: false, diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index dd4883338c37..ef2ec3fae646 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -433,16 +433,25 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'testResultsProcessor': case 'testRunner': case 'filter': - case 'prettier': value = options[key] && resolve(newOptions.resolver, { filePath: options[key], key, - optional: key === 'prettier', rootDir: options.rootDir, }); break; + case 'prettier': + // We only want this to throw if "prettier" is explicitly passed from + // config or CLI, and the requested path isn't found. Otherwise we set + // it to null and throw an error lazily when it is used. + value = resolve(newOptions.resolver, { + filePath: options[key] || 'prettier', + key, + optional: !options[key], + rootDir: options.rootDir, + }); + break; case 'moduleNameMapper': const moduleNameMapper = options[key]; value = diff --git a/packages/jest-jasmine2/src/setup_jest_globals.js b/packages/jest-jasmine2/src/setup_jest_globals.js index e865e8291335..54ec76415122 100644 --- a/packages/jest-jasmine2/src/setup_jest_globals.js +++ b/packages/jest-jasmine2/src/setup_jest_globals.js @@ -102,7 +102,7 @@ export default ({ const {expand, updateSnapshot} = globalConfig; const snapshotState = new SnapshotState(testPath, { expand, - getPrettier: () => localRequire(config.prettier), + getPrettier: () => (config.prettier ? localRequire(config.prettier) : null), updateSnapshot, }); setState({snapshotState, testPath}); From 97c054a9928351158b29954f256fcf3dd7082650 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 9 Jun 2018 15:50:24 +1000 Subject: [PATCH 21/33] Fix typechecking --- types/Config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/Config.js b/types/Config.js index b75ad2ef893f..6789ebfb796e 100644 --- a/types/Config.js +++ b/types/Config.js @@ -53,7 +53,7 @@ export type DefaultOptions = {| notify: boolean, notifyMode: string, preset: ?string, - prettier: string, + prettier: ?string, projects: ?Array, resetMocks: boolean, resetModules: boolean, From 6537c339e4da250df085ba9540939552c00b4371 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 9 Jun 2018 16:04:36 +1000 Subject: [PATCH 22/33] Set default value in cli/options --- packages/jest-cli/src/cli/args.js | 1 + packages/jest-config/src/defaults.js | 2 +- packages/jest-config/src/normalize.js | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index bc9e2fa62cc6..43ebb997c516 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -421,6 +421,7 @@ export const options = { type: 'string', }, prettier: { + default: 'prettier', description: 'The path to the "prettier" module used for inline snapshots.', type: 'string', }, diff --git a/packages/jest-config/src/defaults.js b/packages/jest-config/src/defaults.js index 2d6c74526af9..cb6c4ce35095 100644 --- a/packages/jest-config/src/defaults.js +++ b/packages/jest-config/src/defaults.js @@ -61,7 +61,7 @@ export default ({ notify: false, notifyMode: 'always', preset: null, - prettier: null, + prettier: 'prettier', projects: null, resetMocks: false, resetModules: false, diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index ef2ec3fae646..7fcfc203b4f6 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -446,9 +446,9 @@ export default function normalize(options: InitialOptions, argv: Argv) { // config or CLI, and the requested path isn't found. Otherwise we set // it to null and throw an error lazily when it is used. value = resolve(newOptions.resolver, { - filePath: options[key] || 'prettier', - key, - optional: !options[key], + filePath: options[key], + key: 'prettier', + optional: options[key] === DEFAULT_CONFIG[key], rootDir: options.rootDir, }); break; From 51c42ef0fd46fb98cc8954d22740f721db516eba Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sat, 9 Jun 2018 16:08:56 +1000 Subject: [PATCH 23/33] Fix typechecking --- packages/jest-config/src/normalize.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 7fcfc203b4f6..a867afdb72e0 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -445,12 +445,14 @@ export default function normalize(options: InitialOptions, argv: Argv) { // We only want this to throw if "prettier" is explicitly passed from // config or CLI, and the requested path isn't found. Otherwise we set // it to null and throw an error lazily when it is used. - value = resolve(newOptions.resolver, { - filePath: options[key], - key: 'prettier', - optional: options[key] === DEFAULT_CONFIG[key], - rootDir: options.rootDir, - }); + value = + options[key] && + resolve(newOptions.resolver, { + filePath: options[key], + key, + optional: options[key] === DEFAULT_CONFIG[key], + rootDir: options.rootDir, + }); break; case 'moduleNameMapper': const moduleNameMapper = options[key]; From cc0bcddc8b8f1f95b14e80f0066ffee0f467b914 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 10 Jun 2018 01:28:10 +1000 Subject: [PATCH 24/33] Write E2E tests for toMatchInlineSnapshot --- .../__snapshots__/show_config.test.js.snap | 1 + .../to_match_inline_snapshot.test.js.snap | 97 +++++++++++++ .../to_match_inline_snapshot.test.js | 131 ++++++++++++++++++ e2e/toMatchInlineSnapshot/package.json | 5 + packages/jest-snapshot/src/index.js | 22 +-- 5 files changed, 246 insertions(+), 10 deletions(-) create mode 100644 e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap create mode 100644 e2e/__tests__/to_match_inline_snapshot.test.js create mode 100644 e2e/toMatchInlineSnapshot/package.json diff --git a/e2e/__tests__/__snapshots__/show_config.test.js.snap b/e2e/__tests__/__snapshots__/show_config.test.js.snap index 04b1d7f17d90..9e44545bda86 100644 --- a/e2e/__tests__/__snapshots__/show_config.test.js.snap +++ b/e2e/__tests__/__snapshots__/show_config.test.js.snap @@ -33,6 +33,7 @@ exports[`--showConfig outputs config info and exits 1`] = ` \\"moduleNameMapper\\": {}, \\"modulePathIgnorePatterns\\": [], \\"name\\": \\"[md5 hash]\\", + \\"prettier\\": null, \\"resetMocks\\": false, \\"resetModules\\": false, \\"resolver\\": null, diff --git a/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap b/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap new file mode 100644 index 000000000000..6c3fdbfe970b --- /dev/null +++ b/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`basic support: initial write 1`] = ` +"test('inline snapshots', () => + expect({apple: 'original value'}).toMatchInlineSnapshot(\` +Object { + \\"apple\\": \\"original value\\", +} +\`)); +" +`; + +exports[`basic support: snapshot mismatch 1`] = ` +"test('inline snapshots', () => + expect({apple: 'updated value'}).toMatchInlineSnapshot(\` +Object { + \\"apple\\": \\"original value\\", +} +\`)); +" +`; + +exports[`basic support: snapshot passed 1`] = ` +"test('inline snapshots', () => + expect({apple: 'original value'}).toMatchInlineSnapshot(\` +Object { + \\"apple\\": \\"original value\\", +} +\`)); +" +`; + +exports[`basic support: snapshot updated 1`] = ` +"test('inline snapshots', () => + expect({apple: 'updated value'}).toMatchInlineSnapshot(\` +Object { + \\"apple\\": \\"updated value\\", +} +\`)); +" +`; + +exports[`handles property matchers: initial write 1`] = ` +"test('handles property matchers', () => { + expect({createdAt: new Date()}).toMatchInlineSnapshot( + {createdAt: expect.any(Date)}, + \` +Object { + \\"createdAt\\": Any, +} +\`, + ); +}); +" +`; + +exports[`handles property matchers: snapshot failed 1`] = ` +"test('handles property matchers', () => { + expect({createdAt: \\"string\\"}).toMatchInlineSnapshot( + {createdAt: expect.any(Date)}, + \` +Object { + \\"createdAt\\": Any, +} +\`, + ); +}); +" +`; + +exports[`handles property matchers: snapshot passed 1`] = ` +"test('handles property matchers', () => { + expect({createdAt: new Date()}).toMatchInlineSnapshot( + {createdAt: expect.any(Date)}, + \` +Object { + \\"createdAt\\": Any, +} +\`, + ); +}); +" +`; + +exports[`handles property matchers: snapshot updated 1`] = ` +"test('handles property matchers', () => { + expect({createdAt: 'string'}).toMatchInlineSnapshot( + {createdAt: expect.any(String)}, + \` +Object { + \\"createdAt\\": Any, +} +\`, + ); +}); +" +`; diff --git a/e2e/__tests__/to_match_inline_snapshot.test.js b/e2e/__tests__/to_match_inline_snapshot.test.js new file mode 100644 index 000000000000..09d937a26e65 --- /dev/null +++ b/e2e/__tests__/to_match_inline_snapshot.test.js @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const fs = require('fs'); +const path = require('path'); +const {makeTemplate, writeFiles, cleanup} = require('../Utils'); +const runJest = require('../runJest'); + +const DIR = path.resolve(__dirname, '../toMatchInlineSnapshot'); +const TESTS_DIR = path.resolve(DIR, '__tests__'); + +const readFile = filename => + fs.readFileSync(path.join(TESTS_DIR, filename), 'utf8'); + +beforeEach(() => cleanup(TESTS_DIR)); +afterAll(() => cleanup(TESTS_DIR)); + +test('basic support', () => { + const filename = 'basic-support.test.js'; + const template = makeTemplate( + `test('inline snapshots', () => expect($1).toMatchInlineSnapshot());\n`, + ); + + { + writeFiles(TESTS_DIR, { + [filename]: template(['{apple: "original value"}']), + }); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(status).toBe(0); + expect(fileAfter).toMatchSnapshot('initial write'); + } + + { + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); + expect(stderr).not.toMatch('1 snapshot written from 1 test suite.'); + expect(status).toBe(0); + expect(fileAfter).toMatchSnapshot('snapshot passed'); + } + + // This test below also covers how jest-editor-support creates terse messages + // for letting a Snapshot update, so if the wording is updated, please edit + // /packages/jest-editor-support/src/test_reconciler.js + { + writeFiles(TESTS_DIR, { + [filename]: readFile(filename).replace('original value', 'updated value'), + }); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('Received value does not match stored snapshot'); + expect(status).toBe(1); + expect(fileAfter).toMatchSnapshot('snapshot mismatch'); + } + + { + const {stderr, status} = runJest(DIR, [ + '-w=1', + '--ci=false', + filename, + '-u', + ]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot updated from 1 test suite.'); + expect(status).toBe(0); + expect(fileAfter).toMatchSnapshot('snapshot updated'); + } +}); + +test('handles property matchers', () => { + const filename = 'handle-property-matchers.test.js'; + const template = makeTemplate(`test('handles property matchers', () => { + expect({createdAt: $1}).toMatchInlineSnapshot({createdAt: expect.any(Date)}); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template(['new Date()'])}); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(status).toBe(0); + expect(fileAfter).toMatchSnapshot('initial write'); + } + + { + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('Snapshots: 1 passed, 1 total'); + expect(status).toBe(0); + expect(fileAfter).toMatchSnapshot('snapshot passed'); + } + + { + writeFiles(TESTS_DIR, { + [filename]: readFile(filename).replace('new Date()', '"string"'), + }); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch( + 'Received value does not match snapshot properties for "handles property matchers 1".', + ); + expect(stderr).toMatch('Snapshots: 1 failed, 1 total'); + expect(status).toBe(1); + expect(fileAfter).toMatchSnapshot('snapshot failed'); + } + + { + writeFiles(TESTS_DIR, { + [filename]: readFile(filename).replace('any(Date)', 'any(String)'), + }); + const {stderr, status} = runJest(DIR, [ + '-w=1', + '--ci=false', + filename, + '-u', + ]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot updated from 1 test suite.'); + expect(status).toBe(0); + expect(fileAfter).toMatchSnapshot('snapshot updated'); + } +}); diff --git a/e2e/toMatchInlineSnapshot/package.json b/e2e/toMatchInlineSnapshot/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/toMatchInlineSnapshot/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index 69b9bc164ca3..e679b54a02b2 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -66,15 +66,15 @@ const toMatchInlineSnapshot = function( propertyMatchersOrInlineSnapshot?: any, inlineSnapshot?: string, ) { - const propertyMatchers = inlineSnapshot - ? propertyMatchersOrInlineSnapshot - : undefined; - if (!inlineSnapshot) { - inlineSnapshot = propertyMatchersOrInlineSnapshot || ''; + let propertyMatchers; + if (typeof propertyMatchersOrInlineSnapshot === 'string') { + inlineSnapshot = propertyMatchersOrInlineSnapshot; + } else { + propertyMatchers = propertyMatchersOrInlineSnapshot; } return _toMatchSnapshot({ context: this, - inlineSnapshot, + inlineSnapshot: inlineSnapshot || '', propertyMatchers, received, }); @@ -210,14 +210,16 @@ const toThrowErrorMatchingInlineSnapshot = function( fromPromiseOrInlineSnapshot: any, inlineSnapshot?: string, ) { - const fromPromise = inlineSnapshot ? fromPromiseOrInlineSnapshot : undefined; - if (!inlineSnapshot) { - inlineSnapshot = fromPromiseOrInlineSnapshot || ''; + let fromPromise; + if (typeof propertyMatchersOrInlineSnapshot === 'string') { + inlineSnapshot = fromPromiseOrInlineSnapshot; + } else { + fromPromise = fromPromiseOrInlineSnapshot; } return _toThrowErrorMatchingSnapshot({ context: this, fromPromise, - inlineSnapshot, + inlineSnapshot: inlineSnapshot || '', received, }); }; From 58e6f59c9d8056dd85b2963473539096c5954869 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 10 Jun 2018 01:40:33 +1000 Subject: [PATCH 25/33] Fix bad copy/paste --- packages/jest-snapshot/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index e679b54a02b2..cf7cb01007da 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -211,7 +211,7 @@ const toThrowErrorMatchingInlineSnapshot = function( inlineSnapshot?: string, ) { let fromPromise; - if (typeof propertyMatchersOrInlineSnapshot === 'string') { + if (typeof fromPromiseOrInlineSnapshot === 'string') { inlineSnapshot = fromPromiseOrInlineSnapshot; } else { fromPromise = fromPromiseOrInlineSnapshot; From e1ffa94cfa51667133bc873210f012cb274c1791 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 10 Jun 2018 11:48:34 +1000 Subject: [PATCH 26/33] Parameterize saveInlineSnapshots test with jest-each --- .../jest-snapshot/src/__mocks__/prettier.js | 2 +- .../src/__tests__/inline_snapshots.test.js | 43 +++++++++++-------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/jest-snapshot/src/__mocks__/prettier.js b/packages/jest-snapshot/src/__mocks__/prettier.js index 4bc632d3ebdd..5c553328aa7f 100644 --- a/packages/jest-snapshot/src/__mocks__/prettier.js +++ b/packages/jest-snapshot/src/__mocks__/prettier.js @@ -14,6 +14,6 @@ module.exports = { ), ), getFileInfo: {sync: () => ({inferredParser: 'babylon'})}, - resolveConfig: {sync: () => null}, + resolveConfig: {sync: jest.fn()}, version: prettier.version, }; diff --git a/packages/jest-snapshot/src/__tests__/inline_snapshots.test.js b/packages/jest-snapshot/src/__tests__/inline_snapshots.test.js index da322865e7cf..772365774cb5 100644 --- a/packages/jest-snapshot/src/__tests__/inline_snapshots.test.js +++ b/packages/jest-snapshot/src/__tests__/inline_snapshots.test.js @@ -27,6 +27,8 @@ beforeEach(() => { isDirectory: () => !filePath.endsWith('.js'), })); fs.readdirSync = jest.fn(() => []); + + prettier.resolveConfig.sync.mockReset(); }); afterEach(() => { fs.writeFileSync = writeFileSync; @@ -56,25 +58,32 @@ test('saveInlineSnapshots() replaces empty function call with a template literal ); }); -test('saveInlineSnapshots() replaces existing template literal', () => { - const filename = path.join(__dirname, 'my.test.js'); - fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot(`2`);\n'); +test.each([['babylon'], ['flow'], ['typescript']])( + 'saveInlineSnapshots() replaces existing template literal - %s parser', + parser => { + const filename = path.join(__dirname, 'my.test.js'); + fs.readFileSync = jest.fn(() => 'expect(1).toMatchInlineSnapshot(`2`);\n'); - saveInlineSnapshots( - [ - { - frame: {column: 11, file: filename, line: 1}, - snapshot: `1`, - }, - ], - prettier, - ); + prettier.resolveConfig.sync.mockReturnValue({parser}); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, - 'expect(1).toMatchInlineSnapshot(`1`);\n', - ); -}); + saveInlineSnapshots( + [ + { + frame: {column: 11, file: filename, line: 1}, + snapshot: `1`, + }, + ], + prettier, + ); + + expect(prettier.resolveConfig.sync.mock.results[0].value).toEqual({parser}); + + expect(fs.writeFileSync).toHaveBeenCalledWith( + filename, + 'expect(1).toMatchInlineSnapshot(`1`);\n', + ); + }, +); test('saveInlineSnapshots() replaces existing template literal with property matchers', () => { const filename = path.join(__dirname, 'my.test.js'); From 408bad4f974f0e062002ef3727aab27294a1cc7d Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 10 Jun 2018 12:09:17 +1000 Subject: [PATCH 27/33] Write E2E tests for toThrowErrorMatchingInlineSnapshot --- ...rror_matching_inline_snapshot.test.js.snap | 19 ++++ ...row_error_matching_inline_snapshot.test.js | 107 ++++++++++++++++++ .../package.json | 5 + packages/jest-snapshot/src/index.js | 14 ++- 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 e2e/__tests__/__snapshots__/to_throw_error_matching_inline_snapshot.test.js.snap create mode 100644 e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js create mode 100644 e2e/toThrowErrorMatchingInlineSnapshot/package.json diff --git a/e2e/__tests__/__snapshots__/to_throw_error_matching_inline_snapshot.test.js.snap b/e2e/__tests__/__snapshots__/to_throw_error_matching_inline_snapshot.test.js.snap new file mode 100644 index 000000000000..1a909af60152 --- /dev/null +++ b/e2e/__tests__/__snapshots__/to_throw_error_matching_inline_snapshot.test.js.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`updates existing snapshot: updated snapshot 1`] = ` +"test('updates existing snapshot', () => { + expect(() => { + throw new Error('apple'); + }).toThrowErrorMatchingInlineSnapshot(\`\\"apple\\"\`); +}); +" +`; + +exports[`works fine when function throws error: initial write 1`] = ` +"test('works fine when function throws error', () => { + expect(() => { + throw new Error('apple'); + }).toThrowErrorMatchingInlineSnapshot(\`\\"apple\\"\`); +}); +" +`; diff --git a/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js b/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js new file mode 100644 index 000000000000..9fc2775a7053 --- /dev/null +++ b/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2014-present, Facebook, Inc. All rights reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const path = require('path'); +const fs = require('fs'); +const {makeTemplate, writeFiles, cleanup} = require('../Utils'); +const runJest = require('../runJest'); + +const DIR = path.resolve(__dirname, '../toThrowErrorMatchingInlineSnapshot'); +const TESTS_DIR = path.resolve(DIR, '__tests__'); + +const readFile = filename => + fs.readFileSync(path.join(TESTS_DIR, filename), 'utf8'); + +beforeEach(() => cleanup(TESTS_DIR)); +afterAll(() => cleanup(TESTS_DIR)); + +test('works fine when function throws error', () => { + const filename = 'works-fine-when-function-throws-error.test.js'; + const template = makeTemplate(` + test('works fine when function throws error', () => { + expect(() => { + throw new Error('apple'); + }) + .toThrowErrorMatchingInlineSnapshot(); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(fileAfter).toMatchSnapshot('initial write'); + expect(status).toBe(0); + } +}); + +test('updates existing snapshot', () => { + const filename = 'updates-existing-snapshot.test.js'; + const template = makeTemplate(` + test('updates existing snapshot', () => { + expect(() => { + throw new Error('apple'); + }) + .toThrowErrorMatchingInlineSnapshot(\`"banana"\`); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, status} = runJest(DIR, [ + '-w=1', + '--ci=false', + filename, + '-u', + ]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot updated from 1 test suite.'); + expect(fileAfter).toMatchSnapshot('updated snapshot'); + expect(status).toBe(0); + } +}); + +test('cannot be used with .not', () => { + const filename = 'cannot-be-used-with-not.test.js'; + const template = makeTemplate(` + test('cannot be used with .not', () => { + expect('').not.toThrowErrorMatchingInlineSnapshot(); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + expect(stderr).toMatch( + 'Jest: `.not` cannot be used with `.toThrowErrorMatchingInlineSnapshot()`.', + ); + expect(status).toBe(1); + } +}); + +// TODO: Fails because of async stack trace +test.skip('should support rejecting promises', () => { + const filename = 'should-support-rejecting-promises.test.js'; + const template = makeTemplate(` + test('should support rejecting promises', async () => { + await expect(Promise.reject(new Error('octopus'))) + .rejects.toThrowErrorMatchingInlineSnapshot(); + }); + `); + + { + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(fileAfter).toMatchSnapshot(); + expect(status).toBe(0); + } +}); diff --git a/e2e/toThrowErrorMatchingInlineSnapshot/package.json b/e2e/toThrowErrorMatchingInlineSnapshot/package.json new file mode 100644 index 000000000000..148788b25446 --- /dev/null +++ b/e2e/toThrowErrorMatchingInlineSnapshot/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "node" + } +} diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index cf7cb01007da..aa7972a64dc4 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -99,7 +99,13 @@ const _toMatchSnapshot = ({ const {currentTestName, isNot, snapshotState} = context; if (isNot) { - throw new Error('Jest: `.not` cannot be used with `.toMatchSnapshot()`.'); + const matcherName = + typeof inlineSnapshot === 'string' + ? 'toMatchInlineSnapshot' + : 'toMatchSnapshot'; + throw new Error( + `Jest: \`.not\` cannot be used with \`.${matcherName}()\`.`, + ); } if (!snapshotState) { @@ -242,8 +248,12 @@ const _toThrowErrorMatchingSnapshot = ({ const {isNot} = context; if (isNot) { + const matcherName = + typeof inlineSnapshot === 'string' + ? 'toThrowErrorMatchingInlineSnapshot' + : 'toThrowErrorMatchingSnapshot'; throw new Error( - 'Jest: `.not` cannot be used with `.toThrowErrorMatchingSnapshot()`.', + `Jest: \`.not\` cannot be used with \`.${matcherName}()\`.`, ); } From 3f6531bcf1269273cc7c8764f32ba76cc46a9910 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 10 Jun 2018 20:51:05 +1000 Subject: [PATCH 28/33] Write more aync tests --- .../to_match_inline_snapshot.test.js.snap | 8 +++++ .../to_match_inline_snapshot.test.js | 36 +++++++++++++++++++ ...row_error_matching_inline_snapshot.test.js | 16 ++++----- packages/jest-snapshot/src/index.js | 2 +- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap b/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap index 6c3fdbfe970b..8579ce32abdc 100644 --- a/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap +++ b/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap @@ -95,3 +95,11 @@ Object { }); " `; + +exports[`supports async tests 1`] = ` +"test('inline snapshots', async () => { + await 'next tick'; + expect(42).toMatchInlineSnapshot(\`42\`); +}); +" +`; diff --git a/e2e/__tests__/to_match_inline_snapshot.test.js b/e2e/__tests__/to_match_inline_snapshot.test.js index 09d937a26e65..ffce3f583dae 100644 --- a/e2e/__tests__/to_match_inline_snapshot.test.js +++ b/e2e/__tests__/to_match_inline_snapshot.test.js @@ -129,3 +129,39 @@ test('handles property matchers', () => { expect(fileAfter).toMatchSnapshot('snapshot updated'); } }); + +// TODO: Fails because of async stack trace +test.skip('supports async matchers', () => { + const filename = 'async-matchers.test.js'; + const test = ` + test('inline snapshots', async () => { + expect(Promise.resolve('success')).resolves.toMatchInlineSnapshot(); + expect(Promise.reject('fail')).rejects.toMatchInlineSnapshot(); + }); + `; + + writeFiles(TESTS_DIR, {[filename]: test}); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(status).toBe(0); + expect(fileAfter).toMatchSnapshot(); +}); + +// TODO: Fails because of async stack trace +test('supports async tests', () => { + const filename = 'async.test.js'; + const test = ` + test('inline snapshots', async () => { + await 'next tick'; + expect(42).toMatchInlineSnapshot(); + }); + `; + + writeFiles(TESTS_DIR, {[filename]: test}); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(status).toBe(0); + expect(fileAfter).toMatchSnapshot(); +}); diff --git a/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js b/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js index 9fc2775a7053..36bb7a9d995e 100644 --- a/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js +++ b/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js @@ -87,7 +87,7 @@ test('cannot be used with .not', () => { }); // TODO: Fails because of async stack trace -test.skip('should support rejecting promises', () => { +test('should support rejecting promises', () => { const filename = 'should-support-rejecting-promises.test.js'; const template = makeTemplate(` test('should support rejecting promises', async () => { @@ -96,12 +96,10 @@ test.skip('should support rejecting promises', () => { }); `); - { - writeFiles(TESTS_DIR, {[filename]: template()}); - const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); - const fileAfter = readFile(filename); - expect(stderr).toMatch('1 snapshot written from 1 test suite.'); - expect(fileAfter).toMatchSnapshot(); - expect(status).toBe(0); - } + writeFiles(TESTS_DIR, {[filename]: template()}); + const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); + const fileAfter = readFile(filename); + expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(fileAfter).toMatchSnapshot(); + expect(status).toBe(0); }); diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index aa7972a64dc4..87e03540abff 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -244,7 +244,7 @@ const _toThrowErrorMatchingSnapshot = ({ inlineSnapshot?: string, }) => { context.dontThrow && context.dontThrow(); - + fs.writeFileSync('thing.txt', JSON.stringify(context)); const {isNot} = context; if (isNot) { From 8c9a4418d2d1542ef25f2ccc45a2e50ab50f092c Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 10 Jun 2018 20:56:04 +1000 Subject: [PATCH 29/33] Skip currently failing test --- e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js b/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js index 36bb7a9d995e..6497fd4898e1 100644 --- a/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js +++ b/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js @@ -87,7 +87,7 @@ test('cannot be used with .not', () => { }); // TODO: Fails because of async stack trace -test('should support rejecting promises', () => { +test.skip('should support rejecting promises', () => { const filename = 'should-support-rejecting-promises.test.js'; const template = makeTemplate(` test('should support rejecting promises', async () => { From 52740e13fa5320b693a421717e738a0d682d0aa2 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 10 Jun 2018 21:14:44 +1000 Subject: [PATCH 30/33] Remove unused testPath property from State --- packages/jest-snapshot/src/State.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index 9353749aa9b7..a6f01101ee99 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -44,7 +44,6 @@ export default class SnapshotState { _snapshotData: {[key: string]: string}; _snapshotPath: Path; _inlineSnapshots: Array; - _testPath: Path; _uncheckedKeys: Set; _getPrettier: () => null | any; added: number; @@ -55,7 +54,6 @@ 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, From dcbaec8565f9eed2066ebe8e6175acf46aa1cf64 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Sun, 10 Jun 2018 21:49:28 +1000 Subject: [PATCH 31/33] Write documentation --- CHANGELOG.md | 4 ++++ docs/Configuration.md | 6 ++++++ docs/ExpectAPI.md | 14 +++++++++++++- docs/SnapshotTesting.md | 43 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fa07a9290d..9bab6fca17dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## master +### Features + +- `[jest-snapshot]` Introduce `toMatchInlineSnapshot` and `toThrowErrorMatchingInlineSnapshot` matchers ([#6380](https://github.com/facebook/jest/pull/6380)) + ### Fixes - `[jest-config]` Add missing options to the `defaults` object ([#6428](https://github.com/facebook/jest/pull/6428)) diff --git a/docs/Configuration.md b/docs/Configuration.md index bce49748ad85..b7985d0776b9 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -402,6 +402,12 @@ Presets may also be relative filesystem paths. } ``` +### `prettier` [string] + +Default: `'prettier'` + +Sets the path to the [`prettier`](https://prettier.io/) node module used to update inline snapshots. + ### `projects` [array] Default: `undefined` diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 8cfc199be6fa..4cffa284f97f 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -1025,12 +1025,18 @@ test('this house has my desired features', () => { This ensures that a value matches the most recent snapshot. Check out [the Snapshot Testing guide](SnapshotTesting.md) for more information. -The optional propertyMatchers argument allows you to specify asymmetric matchers which are verified instead of the exact values. +The optional `propertyMatchers` argument allows you to specify asymmetric matchers which are verified instead of the exact values. The last argument allows you option to specify a snapshot name. Otherwise, the name is inferred from the test. _Note: While snapshot testing is most commonly used with React components, any serializable value can be used as a snapshot._ +### `.toMatchInlineSnapshot(propertyMatchers, inlineSnapshot)` + +Ensures that a value matches the most recent snapshot. Unlike [`.toMatchSnapshot()`](#tomatchsnapshotpropertymatchers-snapshotname), the snapshots will be written to the current source file, inline. + +Check out the section on [Inline Snapshots](./snapshot-testing.md#inline-snapshots) for more info. + ### `.toStrictEqual(value)` Use `.toStrictEqual` to test that objects have the same types as well as structure. @@ -1134,3 +1140,9 @@ exports[`drinking flavors throws on octopus 1`] = `"yuck, octopus flavor"`; ``` Check out [React Tree Snapshot Testing](http://facebook.github.io/jest/blog/2016/07/27/jest-14.html) for more information on snapshot testing. + +### `.toThrowErrorMatchingInlineSnapshot()` + +This matcher is much like [`.toThrowErrorMatchingSnapshot`](#tothrowerrormatchingsnapshot), except instead of writing the snapshot value to a `.snap` file, it will be written into the source code automatically. + +Check out the section on [Inline Snapshots](./snapshot-testing.md#inline-snapshots) for more info. diff --git a/docs/SnapshotTesting.md b/docs/SnapshotTesting.md index dd5dbd1c4d2d..f71413788050 100644 --- a/docs/SnapshotTesting.md +++ b/docs/SnapshotTesting.md @@ -93,6 +93,49 @@ Once you're finished, Jest will give you a summary before returning back to watc ![](/jest/img/content/interactiveSnapshotDone.png) +### Inline Snapshots + +Inline snapshots behave identically to external snapshots (`.snap` files), except the snapshot values are written automatically back into the source code. This means you can get the benefits of automatically generated snapshots without having to switch to an external file to make sure the correct value was written. + +> Inline snapshots are powered by [Prettier](https://prettier.io). To use inline snapshots you must have `prettier` installed in your project. Your Prettier configuration will be respected when writing to test files. +> +> If you have `prettier` installed in a location where Jest can't find it, you can tell Jest how to find it using the [`"prettier"`](./configuration.md#prettier) configuration property. + +**Example:** + +First, you write a test, calling `.toMatchInlineSnapshot()` with no arguments: + +```javascript +it('renders correctly', () => { + const tree = renderer + .create(Prettier) + .toJSON(); + expect(tree).toMatchInlineSnapshot(); +}); +``` + +The next time you run Jest, `tree` will be evaluated, and a snapshot will be written as an argument to `toMatchInlineSnapshot`: + +```javascript +it('renders correctly', () => { + const tree = renderer + .create(Prettier) + .toJSON(); + expect(tree).toMatchInlineSnapshot(` + + Prettier + +`); +}); +``` + +That's all there is to it! You can even update the snapshots with `--updateSnapshot` or using the `u` key in `--watch` mode. + ### Property Matchers Often there are fields in the object you want to snapshot which are generated (like IDs and Dates). If you try to snapshot these objects, they will force the snapshot to fail on every run: From 436cd7f33da9f7b05f5ac72a77a0925368664b42 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Tue, 12 Jun 2018 23:57:34 +1000 Subject: [PATCH 32/33] Support async matchers --- .../to_match_inline_snapshot.test.js.snap | 10 ++++++++++ ...rror_matching_inline_snapshot.test.js.snap | 9 +++++++++ .../to_match_inline_snapshot.test.js | 6 ++---- ...row_error_matching_inline_snapshot.test.js | 3 +-- packages/expect/src/index.js | 10 +++++++--- packages/jest-snapshot/src/State.js | 18 ++++++++++++----- packages/jest-snapshot/src/index.js | 20 +++++++------------ types/Matchers.js | 1 + 8 files changed, 50 insertions(+), 27 deletions(-) diff --git a/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap b/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap index 8579ce32abdc..9045ee66b05f 100644 --- a/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap +++ b/e2e/__tests__/__snapshots__/to_match_inline_snapshot.test.js.snap @@ -96,6 +96,16 @@ Object { " `; +exports[`supports async matchers 1`] = ` +"test('inline snapshots', async () => { + expect(Promise.resolve('success')).resolves.toMatchInlineSnapshot( + \`\\"success\\"\`, + ); + expect(Promise.reject('fail')).rejects.toMatchInlineSnapshot(\`\\"fail\\"\`); +}); +" +`; + exports[`supports async tests 1`] = ` "test('inline snapshots', async () => { await 'next tick'; diff --git a/e2e/__tests__/__snapshots__/to_throw_error_matching_inline_snapshot.test.js.snap b/e2e/__tests__/__snapshots__/to_throw_error_matching_inline_snapshot.test.js.snap index 1a909af60152..0cd63ce7fd2a 100644 --- a/e2e/__tests__/__snapshots__/to_throw_error_matching_inline_snapshot.test.js.snap +++ b/e2e/__tests__/__snapshots__/to_throw_error_matching_inline_snapshot.test.js.snap @@ -1,5 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`should support rejecting promises 1`] = ` +"test('should support rejecting promises', async () => { + await expect( + Promise.reject(new Error('octopus')), + ).rejects.toThrowErrorMatchingInlineSnapshot(\`\\"octopus\\"\`); +}); +" +`; + exports[`updates existing snapshot: updated snapshot 1`] = ` "test('updates existing snapshot', () => { expect(() => { diff --git a/e2e/__tests__/to_match_inline_snapshot.test.js b/e2e/__tests__/to_match_inline_snapshot.test.js index ffce3f583dae..f02ad696c316 100644 --- a/e2e/__tests__/to_match_inline_snapshot.test.js +++ b/e2e/__tests__/to_match_inline_snapshot.test.js @@ -130,8 +130,7 @@ test('handles property matchers', () => { } }); -// TODO: Fails because of async stack trace -test.skip('supports async matchers', () => { +test('supports async matchers', () => { const filename = 'async-matchers.test.js'; const test = ` test('inline snapshots', async () => { @@ -143,12 +142,11 @@ test.skip('supports async matchers', () => { writeFiles(TESTS_DIR, {[filename]: test}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); const fileAfter = readFile(filename); - expect(stderr).toMatch('1 snapshot written from 1 test suite.'); + expect(stderr).toMatch('2 snapshots written from 1 test suite.'); expect(status).toBe(0); expect(fileAfter).toMatchSnapshot(); }); -// TODO: Fails because of async stack trace test('supports async tests', () => { const filename = 'async.test.js'; const test = ` diff --git a/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js b/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js index 6497fd4898e1..a50b01dd1dcb 100644 --- a/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js +++ b/e2e/__tests__/to_throw_error_matching_inline_snapshot.test.js @@ -86,8 +86,7 @@ test('cannot be used with .not', () => { } }); -// TODO: Fails because of async stack trace -test.skip('should support rejecting promises', () => { +test('should support rejecting promises', () => { const filename = 'should-support-rejecting-promises.test.js'; const template = makeTemplate(` test('should support rejecting promises', async () => { diff --git a/packages/expect/src/index.js b/packages/expect/src/index.js index 439c4db187fa..9939cf834272 100644 --- a/packages/expect/src/index.js +++ b/packages/expect/src/index.js @@ -62,15 +62,18 @@ const isPromise = obj => { }; const createToThrowErrorMatchingSnapshotMatcher = function(matcher) { - return function(received: any, testName?: string) { - return matcher.apply(this, [received, testName, true]); + return function(received: any, testNameOrInlineSnapshot?: string) { + return matcher.apply(this, [received, testNameOrInlineSnapshot, true]); }; }; const getPromiseMatcher = (name, matcher) => { if (name === 'toThrow' || name === 'toThrowError') { return createThrowMatcher('.' + name, true); - } else if (name === 'toThrowErrorMatchingSnapshot') { + } else if ( + name === 'toThrowErrorMatchingSnapshot' || + name === 'toThrowErrorMatchingInlineSnapshot' + ) { return createToThrowErrorMatchingSnapshotMatcher(matcher); } @@ -245,6 +248,7 @@ const makeThrowingMatcher = ( getState(), { equals, + error: err, isNot, utils, }, diff --git a/packages/jest-snapshot/src/State.js b/packages/jest-snapshot/src/State.js index a6f01101ee99..d93a35b47dcb 100644 --- a/packages/jest-snapshot/src/State.js +++ b/packages/jest-snapshot/src/State.js @@ -34,6 +34,7 @@ export type SnapshotMatchOptions = {| received: any, key?: string, inlineSnapshot?: string, + error?: Error, |}; export default class SnapshotState { @@ -84,11 +85,12 @@ export default class SnapshotState { _addSnapshot( key: string, receivedSerialized: string, - options: {isInline: boolean}, + options: {isInline: boolean, error?: Error}, ) { this._dirty = true; if (options.isInline) { - const lines = getStackTraceLines(new Error().stack); + const error = options.error || new Error(); + const lines = getStackTraceLines(error.stack); const frame = getTopFrame(lines); if (!frame) { throw new Error("Jest: Couln't infer stack frame for inline snapshot."); @@ -147,7 +149,13 @@ export default class SnapshotState { } } - match({testName, received, key, inlineSnapshot}: SnapshotMatchOptions) { + match({ + testName, + received, + key, + inlineSnapshot, + error, + }: SnapshotMatchOptions) { this._counters.set(testName, (this._counters.get(testName) || 0) + 1); const count = Number(this._counters.get(testName)); const isInline = inlineSnapshot !== undefined; @@ -195,12 +203,12 @@ export default class SnapshotState { } else { this.added++; } - this._addSnapshot(key, receivedSerialized, {isInline}); + this._addSnapshot(key, receivedSerialized, {error, isInline}); } else { this.matched++; } } else { - this._addSnapshot(key, receivedSerialized, {isInline}); + this._addSnapshot(key, receivedSerialized, {error, isInline}); this.added++; } diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index 87e03540abff..27af60fd85df 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -149,6 +149,7 @@ const _toMatchSnapshot = ({ } const result = snapshotState.match({ + error: context.error, inlineSnapshot, received, testName: fullTestName, @@ -213,15 +214,9 @@ const toThrowErrorMatchingSnapshot = function( const toThrowErrorMatchingInlineSnapshot = function( received: any, - fromPromiseOrInlineSnapshot: any, inlineSnapshot?: string, + fromPromise?: boolean, ) { - let fromPromise; - if (typeof fromPromiseOrInlineSnapshot === 'string') { - inlineSnapshot = fromPromiseOrInlineSnapshot; - } else { - fromPromise = fromPromiseOrInlineSnapshot; - } return _toThrowErrorMatchingSnapshot({ context: this, fromPromise, @@ -244,14 +239,13 @@ const _toThrowErrorMatchingSnapshot = ({ inlineSnapshot?: string, }) => { context.dontThrow && context.dontThrow(); - fs.writeFileSync('thing.txt', JSON.stringify(context)); const {isNot} = context; + const matcherName = + typeof inlineSnapshot === 'string' + ? 'toThrowErrorMatchingInlineSnapshot' + : 'toThrowErrorMatchingSnapshot'; if (isNot) { - const matcherName = - typeof inlineSnapshot === 'string' - ? 'toThrowErrorMatchingInlineSnapshot' - : 'toThrowErrorMatchingSnapshot'; throw new Error( `Jest: \`.not\` cannot be used with \`.${matcherName}()\`.`, ); @@ -271,7 +265,7 @@ const _toThrowErrorMatchingSnapshot = ({ if (error === undefined) { throw new Error( - matcherHint('.toThrowErrorMatchingSnapshot', '() => {}', '') + + matcherHint(`.${matcherName}`, '() => {}', '') + '\n\n' + `Expected the function to throw an error.\n` + `But it didn't throw anything.`, diff --git a/types/Matchers.js b/types/Matchers.js index 52f24984dad4..70be4f1a50ab 100644 --- a/types/Matchers.js +++ b/types/Matchers.js @@ -30,6 +30,7 @@ export type PromiseMatcherFn = (actual: any) => Promise; export type MatcherState = { assertionCalls: number, currentTestName?: string, + error?: Error, equals: (any, any, ?Array) => boolean, expand?: boolean, expectedAssertionsNumber: ?number, From f653fdf526c09491508248fd832c1eb741895a69 Mon Sep 17 00:00:00 2001 From: Lucas Azzola Date: Wed, 13 Jun 2018 00:17:26 +1000 Subject: [PATCH 33/33] Fix links in docs --- docs/ExpectAPI.md | 4 ++-- docs/SnapshotTesting.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 4cffa284f97f..7cd3c1abde86 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -1035,7 +1035,7 @@ _Note: While snapshot testing is most commonly used with React components, any s Ensures that a value matches the most recent snapshot. Unlike [`.toMatchSnapshot()`](#tomatchsnapshotpropertymatchers-snapshotname), the snapshots will be written to the current source file, inline. -Check out the section on [Inline Snapshots](./snapshot-testing.md#inline-snapshots) for more info. +Check out the section on [Inline Snapshots](./SnapshotTesting.md#inline-snapshots) for more info. ### `.toStrictEqual(value)` @@ -1145,4 +1145,4 @@ Check out [React Tree Snapshot Testing](http://facebook.github.io/jest/blog/2016 This matcher is much like [`.toThrowErrorMatchingSnapshot`](#tothrowerrormatchingsnapshot), except instead of writing the snapshot value to a `.snap` file, it will be written into the source code automatically. -Check out the section on [Inline Snapshots](./snapshot-testing.md#inline-snapshots) for more info. +Check out the section on [Inline Snapshots](./SnapshotTesting.md#inline-snapshots) for more info. diff --git a/docs/SnapshotTesting.md b/docs/SnapshotTesting.md index f71413788050..cec6a7f14164 100644 --- a/docs/SnapshotTesting.md +++ b/docs/SnapshotTesting.md @@ -99,7 +99,7 @@ Inline snapshots behave identically to external snapshots (`.snap` files), excep > Inline snapshots are powered by [Prettier](https://prettier.io). To use inline snapshots you must have `prettier` installed in your project. Your Prettier configuration will be respected when writing to test files. > -> If you have `prettier` installed in a location where Jest can't find it, you can tell Jest how to find it using the [`"prettier"`](./configuration.md#prettier) configuration property. +> If you have `prettier` installed in a location where Jest can't find it, you can tell Jest how to find it using the [`"prettier"`](./Configuration.md#prettier-string) configuration property. **Example:**