blob: f02a55bc751f59ad240c57a833615186f5aff934 [file] [log] [blame]
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
exports.saveInlineSnapshots = saveInlineSnapshots;
var path = _interopRequireWildcard(require('path'));
var fs = _interopRequireWildcard(require('graceful-fs'));
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;
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.
// @ts-expect-error
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, {
...config,
filepath: sourceFilePath,
parser: createInsertionParser(
snapshots,
snapshotMatcherNames,
inferredParser,
babelTraverse
)
}); // Format the snapshots using the custom parser API.
const formattedNewSourceFile = prettier.format(newSourceFile, {
...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 {...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' ||
callee.property.loc == null
) {
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}}) {
var _options$tabWidth, _options$tabWidth2;
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 = options.tabWidth) !== null &&
_options$tabWidth !== void 0
? _options$tabWidth
: 1)
: callee.loc.start.column / 2 // Each tab is 2 characters.
),
useSpaces
? ' '.repeat(
(_options$tabWidth2 = options.tabWidth) !== null &&
_options$tabWidth2 !== void 0
? _options$tabWidth2
: 1
)
: '\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 'babel';
};