diff --git a/CHANGELOG.md b/CHANGELOG.md index b513ec69015e..645b594476d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[jest-config]` [**BREAKING**] Default to Node testing environment instead of browser (JSDOM) ([#9874](https://github.com/facebook/jest/pull/9874)) - `[jest-runner]` [**BREAKING**] set exit code to 1 if test logs after teardown ([#10728](https://github.com/facebook/jest/pull/10728)) +- `[jest-snapshot]`: [**BREAKING**] Make prettier optional for inline snapshots - fall back to string replacement ([#7792](https://github.com/facebook/jest/pull/7792)) ### Fixes diff --git a/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap b/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap index 09486e91ae92..b90662f2176e 100644 --- a/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap +++ b/packages/babel-plugin-jest-hoist/src/__tests__/__snapshots__/hoistPlugin.test.ts.snap @@ -12,14 +12,14 @@ _getJestObj().mock("./App", () => () => /*#__PURE__*/ _jsxDEV( "div", { - children: "Hello world" + children: "Hello world", }, void 0, false, { fileName: _jsxFileName, lineNumber: 1, - columnNumber: 32 + columnNumber: 32, }, this ) diff --git a/packages/jest-circus/package.json b/packages/jest-circus/package.json index 4d0f7aafd901..1fa02c5c7646 100644 --- a/packages/jest-circus/package.json +++ b/packages/jest-circus/package.json @@ -15,11 +15,9 @@ "./runner": "./runner.js" }, "dependencies": { - "@babel/traverse": "^7.1.0", "@jest/environment": "^26.6.2", "@jest/test-result": "^26.6.2", "@jest/types": "^26.6.2", - "@types/babel__traverse": "^7.0.4", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", @@ -41,7 +39,6 @@ "@babel/core": "^7.1.0", "@babel/register": "^7.0.0", "@jest/test-utils": "^26.6.2", - "@types/babel__traverse": "^7.0.4", "@types/co": "^4.6.0", "@types/dedent": "^0.7.0", "@types/graceful-fs": "^4.1.3", diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts index 50e5f1abe620..e16cc8a0a271 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapter.ts @@ -30,15 +30,9 @@ const jestAdapter = async ( FRAMEWORK_INITIALIZER, ); - const getPrettier = () => - config.prettierPath ? require(config.prettierPath) : null; - const getBabelTraverse = () => require('@babel/traverse').default; - const {globals, snapshotState} = await initialize({ config, environment, - getBabelTraverse, - getPrettier, globalConfig, localRequire: runtime.requireModule.bind(runtime), parentProcess: process, diff --git a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts index 46e73ade095d..bf4506a218b5 100644 --- a/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts +++ b/packages/jest-circus/src/legacy-code-todo-rewrite/jestAdapterInit.ts @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import type BabelTraverse from '@babel/traverse'; import throat from 'throat'; import type {JestEnvironment} from '@jest/environment'; import { @@ -46,8 +45,6 @@ interface JestGlobals extends Global.TestFrameworkGlobals { export const initialize = async ({ config, environment, - getPrettier, - getBabelTraverse, globalConfig, localRequire, parentProcess, @@ -57,8 +54,6 @@ export const initialize = async ({ }: { config: Config.ProjectConfig; environment: JestEnvironment; - getPrettier: () => null | any; - getBabelTraverse: () => typeof BabelTraverse; globalConfig: Config.GlobalConfig; localRequire: (path: Config.Path) => T; testPath: Config.Path; @@ -162,8 +157,7 @@ export const initialize = async ({ const snapshotPath = snapshotResolver.resolveSnapshotPath(testPath); const snapshotState = new SnapshotState(snapshotPath, { expand, - getBabelTraverse, - getPrettier, + prettierPath: config.prettierPath, updateSnapshot, }); // @ts-expect-error: snapshotState is a jest extension of `expect` diff --git a/packages/jest-jasmine2/src/setup_jest_globals.ts b/packages/jest-jasmine2/src/setup_jest_globals.ts index 1787d549e155..613fea478f73 100644 --- a/packages/jest-jasmine2/src/setup_jest_globals.ts +++ b/packages/jest-jasmine2/src/setup_jest_globals.ts @@ -106,13 +106,12 @@ export default ({ patchJasmine(); const {expand, updateSnapshot} = globalConfig; + const {prettierPath} = config; const snapshotResolver = buildSnapshotResolver(config); const snapshotPath = snapshotResolver.resolveSnapshotPath(testPath); const snapshotState = new SnapshotState(snapshotPath, { expand, - getBabelTraverse: () => require('@babel/traverse').default, - getPrettier: () => - config.prettierPath ? require(config.prettierPath) : null, + prettierPath, updateSnapshot, }); // @ts-expect-error: snapshotState is a jest extension of `expect` diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index 32391133f372..1e9c47693a92 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -14,10 +14,15 @@ "./package.json": "./package.json" }, "dependencies": { + "@babel/generator": "^7.7.2", + "@babel/parser": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", "@babel/types": "^7.0.0", "@jest/types": "^26.6.2", "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.0.0", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", "expect": "^26.6.2", "graceful-fs": "^4.2.4", @@ -32,6 +37,9 @@ "semver": "^7.3.2" }, "devDependencies": { + "@babel/core": "^7.7.2", + "@babel/preset-flow": "^7.7.2", + "@babel/preset-react": "^7.7.2", "@babel/traverse": "^7.3.4", "@jest/test-utils": "^26.6.2", "@types/graceful-fs": "^4.1.3", @@ -39,7 +47,15 @@ "@types/semver": "^7.1.0", "ansi-regex": "^5.0.0", "ansi-styles": "^4.2.0", - "prettier": "^1.19.1" + "prettier": "^2.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.7.2" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } }, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" diff --git a/packages/jest-snapshot/src/InlineSnapshots.ts b/packages/jest-snapshot/src/InlineSnapshots.ts index b8736ff89156..e7a9accaf76e 100644 --- a/packages/jest-snapshot/src/InlineSnapshots.ts +++ b/packages/jest-snapshot/src/InlineSnapshots.ts @@ -6,43 +6,51 @@ */ import * as path from 'path'; -import {file, templateElement, templateLiteral} from '@babel/types'; +import type {PluginItem} from '@babel/core'; +import type {Expression, File, Program} from '@babel/types'; import * as fs from 'graceful-fs'; import type { - BuiltInParsers as PrettierBuiltInParsers, CustomParser as PrettierCustomParser, BuiltInParserName as PrettierParserName, } from 'prettier'; import semver = require('semver'); import type {Config} from '@jest/types'; import type {Frame} from 'jest-message-util'; -import type {BabelTraverse, Prettier} from './types'; import {escapeBacktickString} from './utils'; +// @ts-expect-error requireOutside Babel transform +const babelTraverse = (requireOutside( + '@babel/traverse', +) as typeof import('@babel/traverse')).default; +// @ts-expect-error requireOutside Babel transform +const generate = (requireOutside( + '@babel/generator', +) as typeof import('@babel/generator')).default; +// @ts-expect-error requireOutside Babel transform +const {file, templateElement, templateLiteral} = requireOutside( + '@babel/types', +) as typeof import('@babel/types'); +// @ts-expect-error requireOutside Babel transform +const {parseSync} = requireOutside( + '@babel/core', +) as typeof import('@babel/core'); + +type Prettier = typeof import('prettier'); + export type InlineSnapshot = { snapshot: string; frame: Frame; + node?: Expression; }; export function saveInlineSnapshots( snapshots: Array, - prettier: Prettier | null, - babelTraverse: BabelTraverse, + prettierPath: Config.Path, ): void { - 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 prettier = prettierPath + ? // @ts-expect-error requireOutside Babel transform + (requireOutside(prettierPath) as Prettier) + : undefined; const snapshotsByFile = groupSnapshotsByFile(snapshots); @@ -50,8 +58,7 @@ export function saveInlineSnapshots( saveSnapshotsForFile( snapshotsByFile[sourceFilePath], sourceFilePath, - prettier, - babelTraverse, + prettier && semver.gte(prettier.version, '1.5.0') ? prettier : undefined, ); } } @@ -59,56 +66,67 @@ export function saveInlineSnapshots( const saveSnapshotsForFile = ( snapshots: Array, sourceFilePath: Config.Path, - prettier: Prettier, - babelTraverse: BabelTraverse, + prettier?: Prettier, ) => { 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. - // @ts-expect-error - const inferredParser: PrettierParserName = prettier.getFileInfo - ? prettier.getFileInfo.sync(sourceFilePath).inferredParser - : (config && config.parser) || simpleDetectParser(sourceFilePath); + // TypeScript projects may not have a babel config; make sure they can be parsed anyway. + const presets = [require.resolve('babel-preset-current-node-syntax')]; + const plugins: Array = []; + if (/\.tsx?$/.test(sourceFilePath)) { + plugins.push([ + require.resolve('@babel/plugin-syntax-typescript'), + {isTSX: sourceFilePath.endsWith('x')}, + // unique name to make sure Babel does not complain about a possible duplicate plugin. + 'TypeScript syntax plugin added by Jest snapshot', + ]); + } - // Record the matcher names seen in insertion parser and pass them down one + // Record the matcher names seen during traversal and pass them down one // by one to formatting parser. const snapshotMatcherNames: Array = []; - // Insert snapshots using the custom parser API. After insertion, the code is - // formatted, except snapshot indentation. Snapshots cannot be formatted until - // after the initial format because we don't know where the call expression - // will be placed (specifically its indentation). - const newSourceFile = prettier.format(sourceFile, { - ...config, - filepath: sourceFilePath, - parser: createInsertionParser( - snapshots, - snapshotMatcherNames, - inferredParser, - babelTraverse, - ), + const ast = parseSync(sourceFile, { + filename: sourceFilePath, + plugins, + presets, + root: path.dirname(sourceFilePath), }); + if (!ast) { + throw new Error(`jest-snapshot: Failed to parse ${sourceFilePath}`); + } + traverseAst(snapshots, ast, snapshotMatcherNames); - // Format the snapshots using the custom parser API. - const formattedNewSourceFile = prettier.format(newSourceFile, { - ...config, - filepath: sourceFilePath, - parser: createFormattingParser( - snapshotMatcherNames, - inferredParser, - babelTraverse, - ), - }); + // substitute in the snapshots in reverse order, so slice calculations aren't thrown off. + const sourceFileWithSnapshots = snapshots.reduceRight( + (sourceSoFar, nextSnapshot) => { + if ( + !nextSnapshot.node || + typeof nextSnapshot.node.start !== 'number' || + typeof nextSnapshot.node.end !== 'number' + ) { + throw new Error('Jest: no snapshot insert location found'); + } + return ( + sourceSoFar.slice(0, nextSnapshot.node.start) + + generate(nextSnapshot.node, {retainLines: true}).code.trim() + + sourceSoFar.slice(nextSnapshot.node.end) + ); + }, + sourceFile, + ); - if (formattedNewSourceFile !== sourceFile) { - fs.writeFileSync(sourceFilePath, formattedNewSourceFile); + const newSourceFile = prettier + ? runPrettier( + prettier, + sourceFilePath, + sourceFileWithSnapshots, + snapshotMatcherNames, + ) + : sourceFileWithSnapshots; + + if (newSourceFile !== sourceFile) { + fs.writeFileSync(sourceFilePath, newSourceFile); } }; @@ -161,13 +179,9 @@ const indent = (snapshot: string, numIndents: number, indentation: string) => { .join('\n'); }; -const getAst = ( - parsers: PrettierBuiltInParsers, - inferredParser: PrettierParserName, - text: string, -) => { +const resolveAst = (fileOrProgram: any): File => { // Flow uses a 'Program' parent node, babel expects a 'File'. - let ast = parsers[inferredParser](text); + let ast = fileOrProgram; if (ast.type !== 'File') { ast = file(ast, ast.comments, ast.tokens); delete ast.program.comments; @@ -175,22 +189,18 @@ const getAst = ( return ast; }; -// This parser inserts snapshots into the AST. -const createInsertionParser = ( +const traverseAst = ( snapshots: Array, + fileOrProgram: File | Program, snapshotMatcherNames: Array, - inferredParser: PrettierParserName, - babelTraverse: BabelTraverse, -): PrettierCustomParser => (text, parsers, options) => { - // Workaround for https://github.com/prettier/prettier/issues/3150 - options.parser = inferredParser; - +) => { + const ast = resolveAst(fileOrProgram); const groupedSnapshots = groupSnapshotsByFrame(snapshots); const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot)); - const ast = getAst(parsers, inferredParser, text); babelTraverse(ast, { - CallExpression({node: {arguments: args, callee}}) { + CallExpression({node}) { + const {arguments: args, callee} = node; if ( callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier' || @@ -214,7 +224,9 @@ const createInsertionParser = ( const snapshotIndex = args.findIndex( ({type}) => type === 'TemplateLiteral', ); - const values = snapshotsForFrame.map(({snapshot}) => { + const values = snapshotsForFrame.map(inlineSnapshot => { + inlineSnapshot.node = node; + const {snapshot} = inlineSnapshot; remainingSnapshots.delete(snapshot); return templateLiteral( @@ -235,20 +247,64 @@ const createInsertionParser = ( if (remainingSnapshots.size) { throw new Error(`Jest: Couldn't locate all inline snapshots.`); } +}; - return ast; +const runPrettier = ( + prettier: Prettier, + sourceFilePath: string, + sourceFileWithSnapshots: string, + snapshotMatcherNames: Array, +) => { + // 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. + // @ts-expect-error + const inferredParser: PrettierParserName | undefined = prettier.getFileInfo + ? prettier.getFileInfo.sync(sourceFilePath).inferredParser + : (config && typeof config.parser === 'string' && config.parser) || + simpleDetectParser(sourceFilePath); + + if (!inferredParser) { + throw new Error( + `Could not infer Prettier parser for file ${sourceFilePath}`, + ); + } + + // Snapshots have now been inserted. Run prettier to make sure that the code is + // formatted, except snapshot indentation. Snapshots cannot be formatted until + // after the initial format because we don't know where the call expression + // will be placed (specifically its indentation). + let newSourceFile = prettier.format(sourceFileWithSnapshots, { + ...config, + filepath: sourceFilePath, + }); + + if (newSourceFile !== sourceFileWithSnapshots) { + // prettier moved things around, run it again to fix snapshot indentations. + newSourceFile = prettier.format(newSourceFile, { + ...config, + filepath: sourceFilePath, + parser: createFormattingParser(snapshotMatcherNames, inferredParser), + }); + } + + return newSourceFile; }; // This parser formats snapshots to the correct indentation. const createFormattingParser = ( snapshotMatcherNames: Array, inferredParser: PrettierParserName, - babelTraverse: BabelTraverse, ): PrettierCustomParser => (text, parsers, options) => { // Workaround for https://github.com/prettier/prettier/issues/3150 options.parser = inferredParser; - const ast = getAst(parsers, inferredParser, text); + const ast = resolveAst(parsers[inferredParser](text, options)); babelTraverse(ast, { CallExpression({node: {arguments: args, callee}}) { if ( @@ -302,7 +358,7 @@ const createFormattingParser = ( const simpleDetectParser = (filePath: Config.Path): PrettierParserName => { const extname = path.extname(filePath); - if (/tsx?$/.test(extname)) { + if (/\.tsx?$/.test(extname)) { return 'typescript'; } return 'babel'; diff --git a/packages/jest-snapshot/src/State.ts b/packages/jest-snapshot/src/State.ts index 898a358c7bcf..6e143ef2d10e 100644 --- a/packages/jest-snapshot/src/State.ts +++ b/packages/jest-snapshot/src/State.ts @@ -9,7 +9,7 @@ import * as fs from 'graceful-fs'; import type {Config} from '@jest/types'; import {getStackTraceLines, getTopFrame} from 'jest-message-util'; import {InlineSnapshot, saveInlineSnapshots} from './InlineSnapshots'; -import type {BabelTraverse, Prettier, SnapshotData} from './types'; +import type {SnapshotData} from './types'; import { addExtraLineBreaks, getSnapshotData, @@ -23,8 +23,7 @@ import { export type SnapshotStateOptions = { updateSnapshot: Config.SnapshotUpdateState; - getPrettier: () => null | Prettier; - getBabelTraverse: () => BabelTraverse; + prettierPath: Config.Path; expand?: boolean; }; @@ -61,8 +60,7 @@ export default class SnapshotState { private _snapshotPath: Config.Path; private _inlineSnapshots: Array; private _uncheckedKeys: Set; - private _getBabelTraverse: SnapshotStateOptions['getBabelTraverse']; - private _getPrettier: SnapshotStateOptions['getPrettier']; + private _prettierPath: Config.Path; added: number; expand: boolean; @@ -79,8 +77,7 @@ export default class SnapshotState { this._initialData = data; this._snapshotData = data; this._dirty = dirty; - this._getBabelTraverse = options.getBabelTraverse; - this._getPrettier = options.getPrettier; + this._prettierPath = options.prettierPath; this._inlineSnapshots = []; this._uncheckedKeys = new Set(Object.keys(this._snapshotData)); this._counters = new Map(); @@ -153,9 +150,7 @@ export default class SnapshotState { saveSnapshotFile(this._snapshotData, this._snapshotPath); } if (hasInlineSnapshots) { - const prettier = this._getPrettier(); // Load lazily - const babelTraverse = this._getBabelTraverse(); // Load lazily - saveInlineSnapshots(this._inlineSnapshots, prettier, babelTraverse); + saveInlineSnapshots(this._inlineSnapshots, this._prettierPath); } 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 38a398821537..8f3880a3dc4c 100644 --- a/packages/jest-snapshot/src/__mocks__/prettier.js +++ b/packages/jest-snapshot/src/__mocks__/prettier.js @@ -13,7 +13,7 @@ module.exports = { pluginSearchDirs: [require('path').dirname(require.resolve('prettier'))], ...opts, }), - getFileInfo: {sync: () => ({inferredParser: 'babylon'})}, + getFileInfo: {sync: () => ({inferredParser: 'babel'})}, resolveConfig: {sync: jest.fn()}, version: prettier.version, }; diff --git a/packages/jest-snapshot/src/__tests__/InlineSnapshots.test.ts b/packages/jest-snapshot/src/__tests__/InlineSnapshots.test.ts index 6c5cb17f89c6..e033cf6aee1e 100644 --- a/packages/jest-snapshot/src/__tests__/InlineSnapshots.test.ts +++ b/packages/jest-snapshot/src/__tests__/InlineSnapshots.test.ts @@ -5,31 +5,28 @@ * LICENSE file in the root directory of this source tree. */ -jest.mock('graceful-fs', () => ({ - ...jest.createMockFromModule('fs'), - existsSync: jest.fn().mockReturnValue(true), - readdirSync: jest.fn().mockReturnValue([]), - statSync: jest.fn(filePath => ({ - isDirectory: () => !filePath.endsWith('.js'), - })), -})); -jest.mock('prettier'); +jest.mock(require.resolve('prettier'), () => require('../__mocks__/prettier')); +import {tmpdir} from 'os'; import * as path from 'path'; -import babelTraverse from '@babel/traverse'; +const prettier = require(require.resolve('prettier')); import * as fs from 'graceful-fs'; -import prettier from 'prettier'; import {Frame} from 'jest-message-util'; import {saveInlineSnapshots} from '../InlineSnapshots'; + +let dir; beforeEach(() => { (prettier.resolveConfig.sync as jest.Mock).mockReset(); }); +beforeEach(() => { + dir = path.join(tmpdir(), `jest-inline-snapshot-test-${Date.now()}`); + fs.mkdirSync(dir); +}); + test('saveInlineSnapshots() replaces empty function call with a template literal', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => `expect(1).toMatchInlineSnapshot();\n`, - ); + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync(filename, `expect(1).toMatchInlineSnapshot();\n`); saveInlineSnapshots( [ @@ -38,23 +35,186 @@ test('saveInlineSnapshots() replaces empty function call with a template literal snapshot: `1`, }, ], - prettier, - babelTraverse, + 'prettier', ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( 'expect(1).toMatchInlineSnapshot(`1`);\n', ); }); -test.each([['babylon'], ['flow'], ['typescript']])( +test('saveInlineSnapshots() without prettier leaves formatting outside of snapshots alone', () => { + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync( + filename, + ` +const a = [1, 2]; +expect(a).toMatchInlineSnapshot(\`an out-of-date and also multi-line +snapshot\`); +expect(a).toMatchInlineSnapshot(); +expect(a).toMatchInlineSnapshot(\`[1, 2]\`); +`.trim() + '\n', + ); + + saveInlineSnapshots( + [2, 4, 5].map(line => ({ + frame: {column: 11, file: filename, line} as Frame, + snapshot: `[1, 2]`, + })), + null, + ); + + expect(fs.readFileSync(filename, 'utf8')).toBe( + `const a = [1, 2]; +expect(a).toMatchInlineSnapshot(\`[1, 2]\`); +expect(a).toMatchInlineSnapshot(\`[1, 2]\`); +expect(a).toMatchInlineSnapshot(\`[1, 2]\`); +`, + ); +}); + +test('saveInlineSnapshots() can handle typescript without prettier', () => { + const filename = path.join(dir, 'my.test.ts'); + fs.writeFileSync( + filename, + ` +interface Foo { + foo: string +} +const a: [Foo, Foo] = [{ foo: 'one' }, { foo: 'two' }]; +expect(a).toMatchInlineSnapshot(); +`.trim() + '\n', + ); + + saveInlineSnapshots( + [ + { + frame: {column: 11, file: filename, line: 5} as Frame, + snapshot: `[{ foo: 'one' }, { foo: 'two' }]`, + }, + ], + null, + ); + + expect(fs.readFileSync(filename, 'utf8')).toBe( + ` +interface Foo { + foo: string +} +const a: [Foo, Foo] = [{ foo: 'one' }, { foo: 'two' }]; +expect(a).toMatchInlineSnapshot(\`[{ foo: 'one' }, { foo: 'two' }]\`); +`.trim() + '\n', + ); +}); + +test('saveInlineSnapshots() can handle tsx without prettier', () => { + const filename = path.join(dir, 'my.test.tsx'); + fs.writeFileSync( + filename, + ` +it('foos', async () => { + const Foo = (props: { foo: string }) =>
{props.foo}
; + const a = await Foo({ foo: "hello" }); + expect(a).toMatchInlineSnapshot(); +}) +`.trim() + '\n', + ); + + saveInlineSnapshots( + [ + { + frame: {column: 13, file: filename, line: 4} as Frame, + snapshot: `
hello
`, + }, + ], + null, + ); + + expect(fs.readFileSync(filename, 'utf-8')).toBe( + ` +it('foos', async () => { + const Foo = (props: { foo: string }) =>
{props.foo}
; + const a = await Foo({ foo: "hello" }); + expect(a).toMatchInlineSnapshot(\`
hello
\`); +}) +`.trim() + '\n', + ); +}); + +test('saveInlineSnapshots() can handle flow and jsx without prettier', () => { + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync( + filename, + ` +const Foo = (props: { foo: string }) =>
{props.foo}
; +const a = Foo({ foo: "hello" }); +expect(a).toMatchInlineSnapshot(); +`.trim() + '\n', + ); + fs.writeFileSync( + path.join(dir, '.babelrc'), + JSON.stringify({ + presets: [ + require.resolve('@babel/preset-flow'), + require.resolve('@babel/preset-react'), + ], + }), + ); + + saveInlineSnapshots( + [ + { + frame: {column: 11, file: filename, line: 3} as Frame, + snapshot: `
hello
`, + }, + ], + null, + ); + + expect(fs.readFileSync(filename, 'utf-8')).toBe( + ` +const Foo = (props: { foo: string }) =>
{props.foo}
; +const a = Foo({ foo: "hello" }); +expect(a).toMatchInlineSnapshot(\`
hello
\`); +`.trim() + '\n', + ); +}); + +test('saveInlineSnapshots() can use prettier to fix formatting for whole file', () => { + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync( + filename, + ` +const a = [1, 2]; +expect(a).toMatchInlineSnapshot(\`an out-of-date and also multi-line +snapshot\`); +expect(a).toMatchInlineSnapshot(); +expect(a).toMatchInlineSnapshot(\`[1, 2]\`); +`.trim() + '\n', + ); + + saveInlineSnapshots( + [2, 4, 5].map(line => ({ + frame: {column: 11, file: filename, line} as Frame, + snapshot: `[1, 2]`, + })), + 'prettier', + ); + + expect(fs.readFileSync(filename, 'utf-8')).toBe( + `const a = [1, 2]; +expect(a).toMatchInlineSnapshot(\`[1, 2]\`); +expect(a).toMatchInlineSnapshot(\`[1, 2]\`); +expect(a).toMatchInlineSnapshot(\`[1, 2]\`); +`, + ); +}); + +test.each([['babel'], ['flow'], ['typescript']])( 'saveInlineSnapshots() replaces existing template literal - %s parser', parser => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => 'expect(1).toMatchInlineSnapshot(`2`);\n', - ); + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync(filename, 'expect(1).toMatchInlineSnapshot(`2`);\n'); (prettier.resolveConfig.sync as jest.Mock).mockReturnValue({parser}); @@ -65,26 +225,22 @@ test.each([['babylon'], ['flow'], ['typescript']])( snapshot: `1`, }, ], - prettier, - babelTraverse, + 'prettier', ); expect( (prettier.resolveConfig.sync as jest.Mock).mock.results[0].value, ).toEqual({parser}); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( 'expect(1).toMatchInlineSnapshot(`1`);\n', ); }, ); test('saveInlineSnapshots() replaces existing template literal with property matchers', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => 'expect(1).toMatchInlineSnapshot({}, `2`);\n', - ); + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync(filename, 'expect(1).toMatchInlineSnapshot({}, `2`);\n'); saveInlineSnapshots( [ @@ -93,42 +249,61 @@ test('saveInlineSnapshots() replaces existing template literal with property mat snapshot: `1`, }, ], - prettier, - babelTraverse, + 'prettier', ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( 'expect(1).toMatchInlineSnapshot({}, `1`);\n', ); }); +test.each(['prettier', null])( + 'saveInlineSnapshots() creates template literal with property matchers', + prettierModule => { + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync(filename, 'expect(1).toMatchInlineSnapshot({});\n'); + + saveInlineSnapshots( + [ + { + frame: {column: 11, file: filename, line: 1} as Frame, + snapshot: `1`, + }, + ], + prettierModule, + ); + + expect(fs.readFileSync(filename, 'utf-8')).toBe( + 'expect(1).toMatchInlineSnapshot({}, `1`);\n', + ); + }, +); + test('saveInlineSnapshots() throws if frame does not match', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => 'expect(1).toMatchInlineSnapshot();\n', - ); + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync(filename, 'expect(1).toMatchInlineSnapshot();\n'); const save = () => saveInlineSnapshots( [ { - frame: {column: 2 /* incorrect */, file: filename, line: 1} as Frame, + frame: { + column: 2 /* incorrect */, + file: filename, + line: 1, + } as Frame, snapshot: `1`, }, ], - prettier, - babelTraverse, + '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 as jest.Mock).mockImplementation( - () => 'expect(1).toMatchInlineSnapshot();\n', - ); + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync(filename, 'expect(1).toMatchInlineSnapshot();\n'); const frame = {column: 11, file: filename, line: 1} as Frame; const save = () => @@ -137,8 +312,7 @@ test('saveInlineSnapshots() throws if multiple calls to to the same location', ( {frame, snapshot: `1`}, {frame, snapshot: `2`}, ], - prettier, - babelTraverse, + 'prettier', ); expect(save).toThrowError( @@ -147,25 +321,20 @@ 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 as jest.Mock).mockImplementation( - () => 'expect("`").toMatchInlineSnapshot();\n', - ); + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync(filename, 'expect("`").toMatchInlineSnapshot();\n'); const frame = {column: 13, file: filename, line: 1} as Frame; - saveInlineSnapshots([{frame, snapshot: '`'}], prettier, babelTraverse); + saveInlineSnapshots([{frame, snapshot: '`'}], 'prettier'); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( 'expect("`").toMatchInlineSnapshot(`\\``);\n', ); }); test('saveInlineSnapshots() works with non-literals in expect call', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => `expect({a: 'a'}).toMatchInlineSnapshot();\n`, - ); + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync(filename, `expect({a: 'a'}).toMatchInlineSnapshot();\n`); (prettier.resolveConfig.sync as jest.Mock).mockReturnValue({ bracketSpacing: false, singleQuote: true, @@ -178,21 +347,19 @@ test('saveInlineSnapshots() works with non-literals in expect call', () => { snapshot: `{a: 'a'}`, }, ], - prettier, - babelTraverse, + 'prettier', ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( "expect({a: 'a'}).toMatchInlineSnapshot(`{a: 'a'}`);\n", ); }); test('saveInlineSnapshots() indents multi-line snapshots with spaces', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => - "it('is a test', () => {\n" + + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync( + filename, + "it('is a test', () => {\n" + " expect({a: 'a'}).toMatchInlineSnapshot();\n" + '});\n', ); @@ -208,12 +375,10 @@ test('saveInlineSnapshots() indents multi-line snapshots with spaces', () => { snapshot: `\nObject {\n a: 'a'\n}\n`, }, ], - prettier, - babelTraverse, + 'prettier', ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( "it('is a test', () => {\n" + " expect({a: 'a'}).toMatchInlineSnapshot(`\n" + ' Object {\n' + @@ -225,10 +390,10 @@ test('saveInlineSnapshots() indents multi-line snapshots with spaces', () => { }); test('saveInlineSnapshots() does not re-indent error snapshots', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => - "it('is an error test', () => {\n" + + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync( + filename, + "it('is an error test', () => {\n" + ' expect(() => {\n' + " throw new Error(['a', 'b'].join('\\n'));\n" + ' }).toThrowErrorMatchingInlineSnapshot(`\n' + @@ -252,12 +417,10 @@ test('saveInlineSnapshots() does not re-indent error snapshots', () => { snapshot: `\nObject {\n a: 'a'\n}\n`, }, ], - prettier, - babelTraverse, + 'prettier', ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( "it('is an error test', () => {\n" + ' expect(() => {\n' + " throw new Error(['a', 'b'].join('\\n'));\n" + @@ -277,10 +440,10 @@ test('saveInlineSnapshots() does not re-indent error snapshots', () => { }); test('saveInlineSnapshots() does not re-indent already indented snapshots', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => - "it('is a test', () => {\n" + + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync( + filename, + "it('is a test', () => {\n" + " expect({a: 'a'}).toMatchInlineSnapshot();\n" + '});\n' + "it('is a another test', () => {\n" + @@ -303,12 +466,10 @@ test('saveInlineSnapshots() does not re-indent already indented snapshots', () = snapshot: `\nObject {\n a: 'a'\n}\n`, }, ], - prettier, - babelTraverse, + 'prettier', ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( "it('is a test', () => {\n" + " expect({a: 'a'}).toMatchInlineSnapshot(`\n" + ' Object {\n' + @@ -327,10 +488,10 @@ test('saveInlineSnapshots() does not re-indent already indented snapshots', () = }); test('saveInlineSnapshots() indents multi-line snapshots with tabs', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => - "it('is a test', () => {\n" + + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync( + filename, + "it('is a test', () => {\n" + " expect({a: 'a'}).toMatchInlineSnapshot();\n" + '});\n', ); @@ -347,12 +508,10 @@ test('saveInlineSnapshots() indents multi-line snapshots with tabs', () => { snapshot: `\nObject {\n a: 'a'\n}\n`, }, ], - prettier, - babelTraverse, + 'prettier', ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( "it('is a test', () => {\n" + "\texpect({a: 'a'}).toMatchInlineSnapshot(`\n" + '\t\tObject {\n' + @@ -364,9 +523,10 @@ test('saveInlineSnapshots() indents multi-line snapshots with tabs', () => { }); test('saveInlineSnapshots() indents snapshots after prettier reformats', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => "it('is a test', () => expect({a: 'a'}).toMatchInlineSnapshot());\n", + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync( + filename, + "it('is a test', () => expect({a: 'a'}).toMatchInlineSnapshot());\n", ); (prettier.resolveConfig.sync as jest.Mock).mockReturnValue({ bracketSpacing: false, @@ -380,12 +540,10 @@ test('saveInlineSnapshots() indents snapshots after prettier reformats', () => { snapshot: `\nObject {\n a: 'a'\n}\n`, }, ], - prettier, - babelTraverse, + 'prettier', ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( "it('is a test', () =>\n" + " expect({a: 'a'}).toMatchInlineSnapshot(`\n" + ' Object {\n' + @@ -396,10 +554,10 @@ test('saveInlineSnapshots() indents snapshots after prettier reformats', () => { }); test('saveInlineSnapshots() does not indent empty lines', () => { - const filename = path.join(__dirname, 'my.test.js'); - (fs.readFileSync as jest.Mock).mockImplementation( - () => - "it('is a test', () => expect(`hello\n\nworld`).toMatchInlineSnapshot());\n", + const filename = path.join(dir, 'my.test.js'); + fs.writeFileSync( + filename, + "it('is a test', () => expect(`hello\n\nworld`).toMatchInlineSnapshot());\n", ); (prettier.resolveConfig.sync as jest.Mock).mockReturnValue({ bracketSpacing: false, @@ -413,12 +571,10 @@ test('saveInlineSnapshots() does not indent empty lines', () => { snapshot: `\nhello\n\nworld\n`, }, ], - prettier, - babelTraverse, + 'prettier', ); - expect(fs.writeFileSync).toHaveBeenCalledWith( - filename, + expect(fs.readFileSync(filename, 'utf-8')).toBe( "it('is a test', () =>\n" + ' expect(`hello\n\nworld`).toMatchInlineSnapshot(`\n' + ' hello\n' + diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index 8dbd91c9c31a..31db0429d212 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -31,6 +31,3 @@ export type ExpectationResult = { pass: boolean; message: () => string; }; - -export type BabelTraverse = typeof import('@babel/traverse').default; -export type Prettier = typeof import('prettier'); diff --git a/yarn.lock b/yarn.lock index ab5ec7add888..a659c858ce43 100644 --- a/yarn.lock +++ b/yarn.lock @@ -107,7 +107,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:*, @babel/core@npm:^7.0.0, @babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.5, @babel/core@npm:^7.9.0": +"@babel/core@npm:*, @babel/core@npm:^7.0.0, @babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.3.4, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5, @babel/core@npm:^7.9.0": version: 7.12.3 resolution: "@babel/core@npm:7.12.3" dependencies: @@ -131,7 +131,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.1, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.5.0": +"@babel/generator@npm:^7.12.1, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.5.0, @babel/generator@npm:^7.7.2": version: 7.12.5 resolution: "@babel/generator@npm:7.12.5" dependencies: @@ -431,7 +431,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.0.0, @babel/parser@npm:^7.1.0, @babel/parser@npm:^7.10.4, @babel/parser@npm:^7.12.3, @babel/parser@npm:^7.12.5, @babel/parser@npm:^7.7.0": +"@babel/parser@npm:^7.0.0, @babel/parser@npm:^7.1.0, @babel/parser@npm:^7.10.4, @babel/parser@npm:^7.12.3, @babel/parser@npm:^7.12.5, @babel/parser@npm:^7.7.0, @babel/parser@npm:^7.7.2": version: 7.12.5 resolution: "@babel/parser@npm:7.12.5" bin: @@ -833,7 +833,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-syntax-typescript@npm:^7.12.1": +"@babel/plugin-syntax-typescript@npm:^7.12.1, @babel/plugin-syntax-typescript@npm:^7.7.2": version: 7.12.1 resolution: "@babel/plugin-syntax-typescript@npm:7.12.1" dependencies: @@ -965,7 +965,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-flow-strip-types@npm:^7.0.0": +"@babel/plugin-transform-flow-strip-types@npm:^7.0.0, @babel/plugin-transform-flow-strip-types@npm:^7.12.1": version: 7.12.1 resolution: "@babel/plugin-transform-flow-strip-types@npm:7.12.1" dependencies: @@ -1441,6 +1441,18 @@ __metadata: languageName: node linkType: hard +"@babel/preset-flow@npm:^7.7.2": + version: 7.12.1 + resolution: "@babel/preset-flow@npm:7.12.1" + dependencies: + "@babel/helper-plugin-utils": ^7.10.4 + "@babel/plugin-transform-flow-strip-types": ^7.12.1 + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 66f6b0f2af4b1d2e3d8253085bac81829917d2d8032267523d61bbad00759a08bf47792f686d1ec1aa4184f1b8179bddfe7e6bd49bdea443548dae5874f8ee6d + languageName: node + linkType: hard + "@babel/preset-modules@npm:^0.1.3": version: 0.1.4 resolution: "@babel/preset-modules@npm:0.1.4" @@ -1456,7 +1468,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:*, @babel/preset-react@npm:^7.0.0, @babel/preset-react@npm:^7.12.1, @babel/preset-react@npm:^7.9.4": +"@babel/preset-react@npm:*, @babel/preset-react@npm:^7.0.0, @babel/preset-react@npm:^7.12.1, @babel/preset-react@npm:^7.7.2, @babel/preset-react@npm:^7.9.4": version: 7.12.5 resolution: "@babel/preset-react@npm:7.12.5" dependencies: @@ -3751,7 +3763,7 @@ __metadata: languageName: node linkType: hard -"@types/prettier@npm:*, @types/prettier@npm:^2.0.0": +"@types/prettier@npm:*, @types/prettier@npm:^2.0.0, @types/prettier@npm:^2.1.5": version: 2.1.5 resolution: "@types/prettier@npm:2.1.5" checksum: bc6271235d881fa49b2f03e8e9242a7be8ecbcdcffeb7e31347afd0343f5f5e372ce65fe257e44254156737935c9249c9722cbe991dfad9aab72056c6acdbdaa @@ -11653,12 +11665,10 @@ fsevents@~2.1.2: dependencies: "@babel/core": ^7.1.0 "@babel/register": ^7.0.0 - "@babel/traverse": ^7.1.0 "@jest/environment": ^26.6.2 "@jest/test-result": ^26.6.2 "@jest/test-utils": ^26.6.2 "@jest/types": ^26.6.2 - "@types/babel__traverse": ^7.0.4 "@types/co": ^4.6.0 "@types/dedent": ^0.7.0 "@types/graceful-fs": ^4.1.3 @@ -12217,6 +12227,12 @@ fsevents@~2.1.2: version: 0.0.0-use.local resolution: "jest-snapshot@workspace:packages/jest-snapshot" dependencies: + "@babel/core": ^7.7.2 + "@babel/generator": ^7.7.2 + "@babel/parser": ^7.7.2 + "@babel/plugin-syntax-typescript": ^7.7.2 + "@babel/preset-flow": ^7.7.2 + "@babel/preset-react": ^7.7.2 "@babel/traverse": ^7.3.4 "@babel/types": ^7.0.0 "@jest/test-utils": ^26.6.2 @@ -12224,10 +12240,11 @@ fsevents@~2.1.2: "@types/babel__traverse": ^7.0.4 "@types/graceful-fs": ^4.1.3 "@types/natural-compare": ^1.4.0 - "@types/prettier": ^2.0.0 + "@types/prettier": ^2.1.5 "@types/semver": ^7.1.0 ansi-regex: ^5.0.0 ansi-styles: ^4.2.0 + babel-preset-current-node-syntax: ^1.0.0 chalk: ^4.0.0 expect: ^26.6.2 graceful-fs: ^4.2.4 @@ -12238,9 +12255,14 @@ fsevents@~2.1.2: jest-message-util: ^26.6.2 jest-resolve: ^26.6.2 natural-compare: ^1.4.0 - prettier: ^1.19.1 + prettier: ^2.0.0 pretty-format: ^26.6.2 semver: ^7.3.2 + peerDependencies: + "@babel/core": ^7.7.2 + peerDependenciesMeta: + "@babel/core": + optional: true languageName: unknown linkType: soft @@ -16165,16 +16187,7 @@ fsevents@~2.1.2: languageName: node linkType: hard -"prettier@npm:^1.19.1": - version: 1.19.1 - resolution: "prettier@npm:1.19.1" - bin: - prettier: ./bin-prettier.js - checksum: e5fcdfe5e159ef5c5480245353cf8fb5bbd1b8afff266f31f281641825238f8a645e58472f77cd75a09ccc45d44c335466e8d901ec138c2bda05e1bd6ea1077c - languageName: node - linkType: hard - -"prettier@npm:^2.0.1, prettier@npm:^2.1.1": +"prettier@npm:^2.0.0, prettier@npm:^2.0.1, prettier@npm:^2.1.1": version: 2.1.2 resolution: "prettier@npm:2.1.2" bin: