inline_snapshots.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', {
  3. value: true
  4. });
  5. exports.saveInlineSnapshots = void 0;
  6. var _fs = _interopRequireDefault(require('fs'));
  7. var _path = _interopRequireDefault(require('path'));
  8. var _semver = _interopRequireDefault(require('semver'));
  9. var _types = require('@babel/types');
  10. var _utils = require('./utils');
  11. function _interopRequireDefault(obj) {
  12. return obj && obj.__esModule ? obj : {default: obj};
  13. }
  14. var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
  15. var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
  16. var jestWriteFile =
  17. global[Symbol.for('jest-native-write-file')] || _fs.default.writeFileSync;
  18. function _objectSpread(target) {
  19. for (var i = 1; i < arguments.length; i++) {
  20. var source = arguments[i] != null ? arguments[i] : {};
  21. var ownKeys = Object.keys(source);
  22. if (typeof Object.getOwnPropertySymbols === 'function') {
  23. ownKeys = ownKeys.concat(
  24. Object.getOwnPropertySymbols(source).filter(function(sym) {
  25. return Object.getOwnPropertyDescriptor(source, sym).enumerable;
  26. })
  27. );
  28. }
  29. ownKeys.forEach(function(key) {
  30. _defineProperty(target, key, source[key]);
  31. });
  32. }
  33. return target;
  34. }
  35. function _defineProperty(obj, key, value) {
  36. if (key in obj) {
  37. Object.defineProperty(obj, key, {
  38. value: value,
  39. enumerable: true,
  40. configurable: true,
  41. writable: true
  42. });
  43. } else {
  44. obj[key] = value;
  45. }
  46. return obj;
  47. }
  48. var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
  49. var jestReadFile =
  50. global[Symbol.for('jest-native-read-file')] || _fs.default.readFileSync;
  51. const saveInlineSnapshots = (snapshots, prettier, babelTraverse) => {
  52. if (!prettier) {
  53. throw new Error(
  54. `Jest: Inline Snapshots requires Prettier.\n` +
  55. `Please ensure "prettier" is installed in your project.`
  56. );
  57. } // Custom parser API was added in 1.5.0
  58. if (_semver.default.lt(prettier.version, '1.5.0')) {
  59. throw new Error(
  60. `Jest: Inline Snapshots require prettier>=1.5.0.\n` +
  61. `Please upgrade "prettier".`
  62. );
  63. }
  64. const snapshotsByFile = groupSnapshotsByFile(snapshots);
  65. var _arr = Object.keys(snapshotsByFile);
  66. for (var _i = 0; _i < _arr.length; _i++) {
  67. const sourceFilePath = _arr[_i];
  68. saveSnapshotsForFile(
  69. snapshotsByFile[sourceFilePath],
  70. sourceFilePath,
  71. prettier,
  72. babelTraverse
  73. );
  74. }
  75. };
  76. exports.saveInlineSnapshots = saveInlineSnapshots;
  77. const saveSnapshotsForFile = (
  78. snapshots,
  79. sourceFilePath,
  80. prettier,
  81. babelTraverse
  82. ) => {
  83. const sourceFile = jestReadFile(sourceFilePath, 'utf8'); // Resolve project configuration.
  84. // For older versions of Prettier, do not load configuration.
  85. const config = prettier.resolveConfig
  86. ? prettier.resolveConfig.sync(sourceFilePath, {
  87. editorconfig: true
  88. })
  89. : null; // Detect the parser for the test file.
  90. // For older versions of Prettier, fallback to a simple parser detection.
  91. const inferredParser = prettier.getFileInfo
  92. ? prettier.getFileInfo.sync(sourceFilePath).inferredParser
  93. : (config && config.parser) || simpleDetectParser(sourceFilePath); // Insert snapshots using the custom parser API. After insertion, the code is
  94. // formatted, except snapshot indentation. Snapshots cannot be formatted until
  95. // after the initial format because we don't know where the call expression
  96. // will be placed (specifically its indentation).
  97. const newSourceFile = prettier.format(
  98. sourceFile,
  99. _objectSpread({}, config, {
  100. filepath: sourceFilePath,
  101. parser: createInsertionParser(snapshots, inferredParser, babelTraverse)
  102. })
  103. ); // Format the snapshots using the custom parser API.
  104. const formattedNewSourceFile = prettier.format(
  105. newSourceFile,
  106. _objectSpread({}, config, {
  107. filepath: sourceFilePath,
  108. parser: createFormattingParser(inferredParser, babelTraverse)
  109. })
  110. );
  111. if (formattedNewSourceFile !== sourceFile) {
  112. jestWriteFile(sourceFilePath, formattedNewSourceFile);
  113. }
  114. };
  115. const groupSnapshotsBy = createKey => snapshots =>
  116. snapshots.reduce((object, inlineSnapshot) => {
  117. const key = createKey(inlineSnapshot);
  118. return _objectSpread({}, object, {
  119. [key]: (object[key] || []).concat(inlineSnapshot)
  120. });
  121. }, {});
  122. const groupSnapshotsByFrame = groupSnapshotsBy(({frame: {line, column}}) =>
  123. typeof line === 'number' && typeof column === 'number'
  124. ? `${line}:${column - 1}`
  125. : ''
  126. );
  127. const groupSnapshotsByFile = groupSnapshotsBy(({frame: {file}}) => file);
  128. const indent = (snapshot, numIndents, indentation) => {
  129. const lines = snapshot.split('\n'); // Prevent re-identation of inline snapshots.
  130. if (
  131. lines.length >= 2 &&
  132. lines[1].startsWith(indentation.repeat(numIndents + 1))
  133. ) {
  134. return snapshot;
  135. }
  136. return lines
  137. .map((line, index) => {
  138. if (index === 0) {
  139. // First line is either a 1-line snapshot or a blank line.
  140. return line;
  141. } else if (index !== lines.length - 1) {
  142. // Do not indent empty lines.
  143. if (line === '') {
  144. return line;
  145. } // Not last line, indent one level deeper than expect call.
  146. return indentation.repeat(numIndents + 1) + line;
  147. } else {
  148. // The last line should be placed on the same level as the expect call.
  149. return indentation.repeat(numIndents) + line;
  150. }
  151. })
  152. .join('\n');
  153. };
  154. const getAst = (parsers, inferredParser, text) => {
  155. // Flow uses a 'Program' parent node, babel expects a 'File'.
  156. let ast = parsers[inferredParser](text);
  157. if (ast.type !== 'File') {
  158. ast = (0, _types.file)(ast, ast.comments, ast.tokens);
  159. delete ast.program.comments;
  160. }
  161. return ast;
  162. }; // This parser inserts snapshots into the AST.
  163. const createInsertionParser = (snapshots, inferredParser, babelTraverse) => (
  164. text,
  165. parsers,
  166. options
  167. ) => {
  168. // Workaround for https://github.com/prettier/prettier/issues/3150
  169. options.parser = inferredParser;
  170. const groupedSnapshots = groupSnapshotsByFrame(snapshots);
  171. const remainingSnapshots = new Set(snapshots.map(({snapshot}) => snapshot));
  172. const ast = getAst(parsers, inferredParser, text);
  173. babelTraverse(ast, {
  174. CallExpression({node: {arguments: args, callee}}) {
  175. if (
  176. callee.type !== 'MemberExpression' ||
  177. callee.property.type !== 'Identifier'
  178. ) {
  179. return;
  180. }
  181. const _callee$property$loc$ = callee.property.loc.start,
  182. line = _callee$property$loc$.line,
  183. column = _callee$property$loc$.column;
  184. const snapshotsForFrame = groupedSnapshots[`${line}:${column}`];
  185. if (!snapshotsForFrame) {
  186. return;
  187. }
  188. if (snapshotsForFrame.length > 1) {
  189. throw new Error(
  190. 'Jest: Multiple inline snapshots for the same call are not supported.'
  191. );
  192. }
  193. const snapshotIndex = args.findIndex(
  194. ({type}) => type === 'TemplateLiteral'
  195. );
  196. const values = snapshotsForFrame.map(({snapshot}) => {
  197. remainingSnapshots.delete(snapshot);
  198. return (0, _types.templateLiteral)(
  199. [
  200. (0, _types.templateElement)({
  201. raw: (0, _utils.escapeBacktickString)(snapshot)
  202. })
  203. ],
  204. []
  205. );
  206. });
  207. const replacementNode = values[0];
  208. if (snapshotIndex > -1) {
  209. args[snapshotIndex] = replacementNode;
  210. } else {
  211. args.push(replacementNode);
  212. }
  213. }
  214. });
  215. if (remainingSnapshots.size) {
  216. throw new Error(`Jest: Couldn't locate all inline snapshots.`);
  217. }
  218. return ast;
  219. }; // This parser formats snapshots to the correct indentation.
  220. const createFormattingParser = (inferredParser, babelTraverse) => (
  221. text,
  222. parsers,
  223. options
  224. ) => {
  225. // Workaround for https://github.com/prettier/prettier/issues/3150
  226. options.parser = inferredParser;
  227. const ast = getAst(parsers, inferredParser, text);
  228. babelTraverse(ast, {
  229. CallExpression({node: {arguments: args, callee}}) {
  230. if (
  231. callee.type !== 'MemberExpression' ||
  232. callee.property.type !== 'Identifier' ||
  233. callee.property.name !== 'toMatchInlineSnapshot' ||
  234. !callee.loc ||
  235. callee.computed
  236. ) {
  237. return;
  238. }
  239. let snapshotIndex;
  240. let snapshot;
  241. for (let i = 0; i < args.length; i++) {
  242. const node = args[i];
  243. if (node.type === 'TemplateLiteral') {
  244. snapshotIndex = i;
  245. snapshot = node.quasis[0].value.raw;
  246. }
  247. }
  248. if (snapshot === undefined || snapshotIndex === undefined) {
  249. return;
  250. }
  251. const useSpaces = !options.useTabs;
  252. snapshot = indent(
  253. snapshot,
  254. Math.ceil(
  255. useSpaces
  256. ? callee.loc.start.column / options.tabWidth
  257. : callee.loc.start.column / 2 // Each tab is 2 characters.
  258. ),
  259. useSpaces ? ' '.repeat(options.tabWidth) : '\t'
  260. );
  261. const replacementNode = (0, _types.templateLiteral)(
  262. [
  263. (0, _types.templateElement)({
  264. raw: snapshot
  265. })
  266. ],
  267. []
  268. );
  269. args[snapshotIndex] = replacementNode;
  270. }
  271. });
  272. return ast;
  273. };
  274. const simpleDetectParser = filePath => {
  275. const extname = _path.default.extname(filePath);
  276. if (/tsx?$/.test(extname)) {
  277. return 'typescript';
  278. }
  279. return 'babylon';
  280. };