'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); exports.saveInlineSnapshots = saveInlineSnapshots; var fs = _interopRequireWildcard(require('fs')); var path = _interopRequireWildcard(require('path')); var _semver = _interopRequireDefault(require('semver')); var _types = require('@babel/types'); var _utils = require('./utils'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : {default: obj}; } function _getRequireWildcardCache() { if (typeof WeakMap !== 'function') return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) { return {default: obj}; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol; var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol; var jestWriteFile = global[Symbol.for('jest-native-write-file')] || fs.writeFileSync; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty( target, key, Object.getOwnPropertyDescriptor(source, key) ); }); } } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol; var jestReadFile = global[Symbol.for('jest-native-read-file')] || fs.readFileSync; function saveInlineSnapshots(snapshots, prettier, babelTraverse) { 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.default.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, babelTraverse ); } } const saveSnapshotsForFile = ( snapshots, sourceFilePath, prettier, babelTraverse ) => { const sourceFile = jestReadFile(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); // Record the matcher names seen in insertion parser and pass them down one // by one to formatting parser. const snapshotMatcherNames = []; // 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, _objectSpread({}, config, { filepath: sourceFilePath, parser: createInsertionParser( snapshots, snapshotMatcherNames, inferredParser, babelTraverse ) }) ); // Format the snapshots using the custom parser API. const formattedNewSourceFile = prettier.format( newSourceFile, _objectSpread({}, config, { filepath: sourceFilePath, parser: createFormattingParser( snapshotMatcherNames, inferredParser, babelTraverse ) }) ); if (formattedNewSourceFile !== sourceFile) { jestWriteFile(sourceFilePath, formattedNewSourceFile); } }; const groupSnapshotsBy = createKey => snapshots => snapshots.reduce((object, inlineSnapshot) => { const key = createKey(inlineSnapshot); return _objectSpread({}, object, { [key]: (object[key] || []).concat(inlineSnapshot) }); }, {}); const groupSnapshotsByFrame = groupSnapshotsBy(({frame: {line, column}}) => typeof line === 'number' && typeof column === 'number' ? `${line}:${column - 1}` : '' ); const groupSnapshotsByFile = groupSnapshotsBy(({frame: {file}}) => file); const indent = (snapshot, numIndents, indentation) => { const lines = snapshot.split('\n'); // Prevent re-indentation of inline snapshots. if ( lines.length >= 2 && lines[1].startsWith(indentation.repeat(numIndents + 1)) ) { return snapshot; } return lines .map((line, index) => { if (index === 0) { // First line is either a 1-line snapshot or a blank line. return line; } else if (index !== lines.length - 1) { // Do not indent empty lines. if (line === '') { return line; } // Not last line, indent one level deeper than expect call. return indentation.repeat(numIndents + 1) + line; } else { // The last line should be placed on the same level as the expect call. return indentation.repeat(numIndents) + line; } }) .join('\n'); }; const getAst = (parsers, inferredParser, text) => { // Flow uses a 'Program' parent node, babel expects a 'File'. let ast = parsers[inferredParser](text); if (ast.type !== 'File') { ast = (0, _types.file)(ast, ast.comments, ast.tokens); delete ast.program.comments; } return ast; }; // This parser inserts snapshots into the AST. const createInsertionParser = ( snapshots, snapshotMatcherNames, inferredParser, babelTraverse ) => (text, parsers, options) => { // Workaround for https://github.com/prettier/prettier/issues/3150 options.parser = inferredParser; 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}}) { 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.' ); } snapshotMatcherNames.push(callee.property.name); const snapshotIndex = args.findIndex( ({type}) => type === 'TemplateLiteral' ); const values = snapshotsForFrame.map(({snapshot}) => { remainingSnapshots.delete(snapshot); return (0, _types.templateLiteral)( [ (0, _types.templateElement)({ raw: (0, _utils.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; }; // This parser formats snapshots to the correct indentation. const createFormattingParser = ( snapshotMatcherNames, inferredParser, babelTraverse ) => (text, parsers, options) => { // Workaround for https://github.com/prettier/prettier/issues/3150 options.parser = inferredParser; const ast = getAst(parsers, inferredParser, text); babelTraverse(ast, { CallExpression({node: {arguments: args, callee}}) { if ( callee.type !== 'MemberExpression' || callee.property.type !== 'Identifier' || !snapshotMatcherNames.includes(callee.property.name) || !callee.loc || callee.computed ) { return; } let snapshotIndex; let snapshot; for (let i = 0; i < args.length; i++) { const node = args[i]; if (node.type === 'TemplateLiteral') { snapshotIndex = i; snapshot = node.quasis[0].value.raw; } } if (snapshot === undefined || snapshotIndex === undefined) { return; } const useSpaces = !options.useTabs; snapshot = indent( snapshot, Math.ceil( useSpaces ? callee.loc.start.column / options.tabWidth : callee.loc.start.column / 2 // Each tab is 2 characters. ), useSpaces ? ' '.repeat(options.tabWidth) : '\t' ); const replacementNode = (0, _types.templateLiteral)( [ (0, _types.templateElement)({ raw: snapshot }) ], [] ); args[snapshotIndex] = replacementNode; } }); return ast; }; const simpleDetectParser = filePath => { const extname = path.extname(filePath); if (/tsx?$/.test(extname)) { return 'typescript'; } return 'babylon'; };