You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
503 lines
14 KiB
503 lines
14 KiB
'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 _utils = require('./utils'); |
|
|
|
function _interopRequireDefault(obj) { |
|
return obj && obj.__esModule ? obj : {default: obj}; |
|
} |
|
|
|
function _getRequireWildcardCache(nodeInterop) { |
|
if (typeof WeakMap !== 'function') return null; |
|
var cacheBabelInterop = new WeakMap(); |
|
var cacheNodeInterop = new WeakMap(); |
|
return (_getRequireWildcardCache = function (nodeInterop) { |
|
return nodeInterop ? cacheNodeInterop : cacheBabelInterop; |
|
})(nodeInterop); |
|
} |
|
|
|
function _interopRequireWildcard(obj, nodeInterop) { |
|
if (!nodeInterop && obj && obj.__esModule) { |
|
return obj; |
|
} |
|
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) { |
|
return {default: obj}; |
|
} |
|
var cache = _getRequireWildcardCache(nodeInterop); |
|
if (cache && cache.has(obj)) { |
|
return cache.get(obj); |
|
} |
|
var newObj = {}; |
|
var hasPropertyDescriptor = |
|
Object.defineProperty && Object.getOwnPropertyDescriptor; |
|
for (var key in obj) { |
|
if (key !== 'default' && 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 global = (function () { |
|
if (typeof globalThis !== 'undefined') { |
|
return globalThis; |
|
} else if (typeof global !== 'undefined') { |
|
return global; |
|
} else if (typeof self !== 'undefined') { |
|
return self; |
|
} else if (typeof window !== 'undefined') { |
|
return window; |
|
} else { |
|
return Function('return this')(); |
|
} |
|
})(); |
|
|
|
var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol; |
|
|
|
var global = (function () { |
|
if (typeof globalThis !== 'undefined') { |
|
return globalThis; |
|
} else if (typeof global !== 'undefined') { |
|
return global; |
|
} else if (typeof self !== 'undefined') { |
|
return self; |
|
} else if (typeof window !== 'undefined') { |
|
return window; |
|
} else { |
|
return Function('return this')(); |
|
} |
|
})(); |
|
|
|
var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol; |
|
|
|
var global = (function () { |
|
if (typeof globalThis !== 'undefined') { |
|
return globalThis; |
|
} else if (typeof global !== 'undefined') { |
|
return global; |
|
} else if (typeof self !== 'undefined') { |
|
return self; |
|
} else if (typeof window !== 'undefined') { |
|
return window; |
|
} else { |
|
return Function('return this')(); |
|
} |
|
})(); |
|
|
|
var jestWriteFile = |
|
global[Symbol.for('jest-native-write-file')] || fs.writeFileSync; |
|
|
|
var global = (function () { |
|
if (typeof globalThis !== 'undefined') { |
|
return globalThis; |
|
} else if (typeof global !== 'undefined') { |
|
return global; |
|
} else if (typeof self !== 'undefined') { |
|
return self; |
|
} else if (typeof window !== 'undefined') { |
|
return window; |
|
} else { |
|
return Function('return this')(); |
|
} |
|
})(); |
|
|
|
var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol; |
|
|
|
var global = (function () { |
|
if (typeof globalThis !== 'undefined') { |
|
return globalThis; |
|
} else if (typeof global !== 'undefined') { |
|
return global; |
|
} else if (typeof self !== 'undefined') { |
|
return self; |
|
} else if (typeof window !== 'undefined') { |
|
return window; |
|
} else { |
|
return Function('return this')(); |
|
} |
|
})(); |
|
|
|
var jestReadFile = |
|
global[Symbol.for('jest-native-read-file')] || fs.readFileSync; |
|
|
|
// prettier-ignore |
|
const babelTraverse = // @ts-expect-error requireOutside Babel transform |
|
require(require.resolve('@babel/traverse', { |
|
[(global['jest-symbol-do-not-touch'] || global.Symbol).for('jest-resolve-outside-vm-option')]: true |
|
})).default; // prettier-ignore |
|
|
|
const generate = require(require.resolve('@babel/generator', { // @ts-expect-error requireOutside Babel transform |
|
[(global['jest-symbol-do-not-touch'] || global.Symbol).for( |
|
'jest-resolve-outside-vm-option' |
|
)]: true |
|
})).default; // @ts-expect-error requireOutside Babel transform |
|
|
|
const {file, templateElement, templateLiteral} = require(require.resolve( |
|
'@babel/types', |
|
{ |
|
[(global['jest-symbol-do-not-touch'] || global.Symbol).for( |
|
'jest-resolve-outside-vm-option' |
|
)]: true |
|
} |
|
)); // @ts-expect-error requireOutside Babel transform |
|
|
|
const {parseSync} = require(require.resolve('@babel/core', { |
|
[(global['jest-symbol-do-not-touch'] || global.Symbol).for( |
|
'jest-resolve-outside-vm-option' |
|
)]: true |
|
})); |
|
|
|
function saveInlineSnapshots(snapshots, prettierPath) { |
|
let prettier = null; |
|
|
|
if (prettierPath) { |
|
try { |
|
// @ts-expect-error requireOutside Babel transform |
|
prettier = require(require.resolve(prettierPath, { |
|
[(global['jest-symbol-do-not-touch'] || global.Symbol).for( |
|
'jest-resolve-outside-vm-option' |
|
)]: true |
|
})); |
|
} catch { |
|
// Continue even if prettier is not installed. |
|
} |
|
} |
|
|
|
const snapshotsByFile = groupSnapshotsByFile(snapshots); |
|
|
|
for (const sourceFilePath of Object.keys(snapshotsByFile)) { |
|
saveSnapshotsForFile( |
|
snapshotsByFile[sourceFilePath], |
|
sourceFilePath, |
|
prettier && _semver.default.gte(prettier.version, '1.5.0') |
|
? prettier |
|
: undefined |
|
); |
|
} |
|
} |
|
|
|
const saveSnapshotsForFile = (snapshots, sourceFilePath, prettier) => { |
|
const sourceFile = jestReadFile(sourceFilePath, 'utf8'); // 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 = []; |
|
|
|
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 during traversal and pass them down one |
|
// by one to formatting parser. |
|
|
|
const snapshotMatcherNames = []; |
|
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); // 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 |
|
); |
|
const newSourceFile = prettier |
|
? runPrettier( |
|
prettier, |
|
sourceFilePath, |
|
sourceFileWithSnapshots, |
|
snapshotMatcherNames |
|
) |
|
: sourceFileWithSnapshots; |
|
|
|
if (newSourceFile !== sourceFile) { |
|
jestWriteFile(sourceFilePath, newSourceFile); |
|
} |
|
}; |
|
|
|
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 resolveAst = fileOrProgram => { |
|
// Flow uses a 'Program' parent node, babel expects a 'File'. |
|
let ast = fileOrProgram; |
|
|
|
if (ast.type !== 'File') { |
|
ast = file(ast, ast.comments, ast.tokens); |
|
delete ast.program.comments; |
|
} |
|
|
|
return ast; |
|
}; |
|
|
|
const traverseAst = (snapshots, fileOrProgram, snapshotMatcherNames) => { |
|
const ast = resolveAst(fileOrProgram); |
|
const groupedSnapshots = groupSnapshotsByFrame(snapshots); |
|
const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot)); |
|
babelTraverse(ast, { |
|
CallExpression({node}) { |
|
const {arguments: args, callee} = node; |
|
|
|
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(inlineSnapshot => { |
|
inlineSnapshot.node = node; |
|
const {snapshot} = inlineSnapshot; |
|
remainingSnapshots.delete(snapshot); |
|
return templateLiteral( |
|
[ |
|
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."); |
|
} |
|
}; |
|
|
|
const runPrettier = ( |
|
prettier, |
|
sourceFilePath, |
|
sourceFileWithSnapshots, |
|
snapshotMatcherNames |
|
) => { |
|
// 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 && 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), so we have to do two |
|
// prettier.format calls back-to-back. |
|
|
|
return prettier.format( |
|
prettier.format(sourceFileWithSnapshots, { |
|
...config, |
|
filepath: sourceFilePath |
|
}), |
|
{ |
|
...config, |
|
filepath: sourceFilePath, |
|
parser: createFormattingParser(snapshotMatcherNames, inferredParser) |
|
} |
|
); |
|
}; // This parser formats snapshots to the correct indentation. |
|
|
|
const createFormattingParser = |
|
(snapshotMatcherNames, inferredParser) => (text, parsers, options) => { |
|
// Workaround for https://github.com/prettier/prettier/issues/3150 |
|
options.parser = inferredParser; |
|
const ast = resolveAst(parsers[inferredParser](text, options)); |
|
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 = templateLiteral( |
|
[ |
|
templateElement({ |
|
raw: snapshot |
|
}) |
|
], |
|
[] |
|
); |
|
args[snapshotIndex] = replacementNode; |
|
} |
|
}); |
|
return ast; |
|
}; |
|
|
|
const simpleDetectParser = filePath => { |
|
const extname = path.extname(filePath); |
|
|
|
if (/\.tsx?$/.test(extname)) { |
|
return 'typescript'; |
|
} |
|
|
|
return 'babel'; |
|
};
|
|
|