InlineSnapshots.js 9.9 KB

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