index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. 'use strict';
  2. var fs = _interopRequireWildcard(require('graceful-fs'));
  3. var _jestMatcherUtils = require('jest-matcher-utils');
  4. var _SnapshotResolver = require('./SnapshotResolver');
  5. var _State = _interopRequireDefault(require('./State'));
  6. var _plugins = require('./plugins');
  7. var _printSnapshot = require('./printSnapshot');
  8. var utils = _interopRequireWildcard(require('./utils'));
  9. function _interopRequireDefault(obj) {
  10. return obj && obj.__esModule ? obj : {default: obj};
  11. }
  12. function _getRequireWildcardCache() {
  13. if (typeof WeakMap !== 'function') return null;
  14. var cache = new WeakMap();
  15. _getRequireWildcardCache = function () {
  16. return cache;
  17. };
  18. return cache;
  19. }
  20. function _interopRequireWildcard(obj) {
  21. if (obj && obj.__esModule) {
  22. return obj;
  23. }
  24. if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
  25. return {default: obj};
  26. }
  27. var cache = _getRequireWildcardCache();
  28. if (cache && cache.has(obj)) {
  29. return cache.get(obj);
  30. }
  31. var newObj = {};
  32. var hasPropertyDescriptor =
  33. Object.defineProperty && Object.getOwnPropertyDescriptor;
  34. for (var key in obj) {
  35. if (Object.prototype.hasOwnProperty.call(obj, key)) {
  36. var desc = hasPropertyDescriptor
  37. ? Object.getOwnPropertyDescriptor(obj, key)
  38. : null;
  39. if (desc && (desc.get || desc.set)) {
  40. Object.defineProperty(newObj, key, desc);
  41. } else {
  42. newObj[key] = obj[key];
  43. }
  44. }
  45. }
  46. newObj.default = obj;
  47. if (cache) {
  48. cache.set(obj, newObj);
  49. }
  50. return newObj;
  51. }
  52. var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
  53. var Symbol = global['jest-symbol-do-not-touch'] || global.Symbol;
  54. var jestExistsFile =
  55. global[Symbol.for('jest-native-exists-file')] || fs.existsSync;
  56. const DID_NOT_THROW = 'Received function did not throw'; // same as toThrow
  57. const NOT_SNAPSHOT_MATCHERS = `Snapshot matchers cannot be used with ${(0,
  58. _jestMatcherUtils.BOLD_WEIGHT)('not')}`;
  59. const INDENTATION_REGEX = /^([^\S\n]*)\S/m; // Display name in report when matcher fails same as in snapshot file,
  60. // but with optional hint argument in bold weight.
  61. const printSnapshotName = (concatenatedBlockNames = '', hint = '', count) => {
  62. const hasNames = concatenatedBlockNames.length !== 0;
  63. const hasHint = hint.length !== 0;
  64. return (
  65. 'Snapshot name: `' +
  66. (hasNames ? utils.escapeBacktickString(concatenatedBlockNames) : '') +
  67. (hasNames && hasHint ? ': ' : '') +
  68. (hasHint
  69. ? (0, _jestMatcherUtils.BOLD_WEIGHT)(utils.escapeBacktickString(hint))
  70. : '') +
  71. ' ' +
  72. count +
  73. '`'
  74. );
  75. };
  76. function stripAddedIndentation(inlineSnapshot) {
  77. // Find indentation if exists.
  78. const match = inlineSnapshot.match(INDENTATION_REGEX);
  79. if (!match || !match[1]) {
  80. // No indentation.
  81. return inlineSnapshot;
  82. }
  83. const indentation = match[1];
  84. const lines = inlineSnapshot.split('\n');
  85. if (lines.length <= 2) {
  86. // Must be at least 3 lines.
  87. return inlineSnapshot;
  88. }
  89. if (lines[0].trim() !== '' || lines[lines.length - 1].trim() !== '') {
  90. // If not blank first and last lines, abort.
  91. return inlineSnapshot;
  92. }
  93. for (let i = 1; i < lines.length - 1; i++) {
  94. if (lines[i] !== '') {
  95. if (lines[i].indexOf(indentation) !== 0) {
  96. // All lines except first and last should either be blank or have the same
  97. // indent as the first line (or more). If this isn't the case we don't
  98. // want to touch the snapshot at all.
  99. return inlineSnapshot;
  100. }
  101. lines[i] = lines[i].substr(indentation.length);
  102. }
  103. } // Last line is a special case because it won't have the same indent as others
  104. // but may still have been given some indent to line up.
  105. lines[lines.length - 1] = ''; // Return inline snapshot, now at indent 0.
  106. inlineSnapshot = lines.join('\n');
  107. return inlineSnapshot;
  108. }
  109. const fileExists = (filePath, hasteFS) =>
  110. hasteFS.exists(filePath) || jestExistsFile(filePath);
  111. const cleanup = (hasteFS, update, snapshotResolver, testPathIgnorePatterns) => {
  112. const pattern = '\\.' + _SnapshotResolver.EXTENSION + '$';
  113. const files = hasteFS.matchFiles(pattern);
  114. let testIgnorePatternsRegex = null;
  115. if (testPathIgnorePatterns && testPathIgnorePatterns.length > 0) {
  116. testIgnorePatternsRegex = new RegExp(testPathIgnorePatterns.join('|'));
  117. }
  118. const list = files.filter(snapshotFile => {
  119. const testPath = snapshotResolver.resolveTestPath(snapshotFile); // ignore snapshots of ignored tests
  120. if (testIgnorePatternsRegex && testIgnorePatternsRegex.test(testPath)) {
  121. return false;
  122. }
  123. if (!fileExists(testPath, hasteFS)) {
  124. if (update === 'all') {
  125. fs.unlinkSync(snapshotFile);
  126. }
  127. return true;
  128. }
  129. return false;
  130. });
  131. return {
  132. filesRemoved: list.length,
  133. filesRemovedList: list
  134. };
  135. };
  136. const toMatchSnapshot = function (received, propertiesOrHint, hint) {
  137. const matcherName = 'toMatchSnapshot';
  138. let properties;
  139. const length = arguments.length;
  140. if (length === 2 && typeof propertiesOrHint === 'string') {
  141. hint = propertiesOrHint;
  142. } else if (length >= 2) {
  143. if (typeof propertiesOrHint !== 'object' || propertiesOrHint === null) {
  144. const options = {
  145. isNot: this.isNot,
  146. promise: this.promise
  147. };
  148. let printedWithType = (0, _jestMatcherUtils.printWithType)(
  149. 'Expected properties',
  150. propertiesOrHint,
  151. _printSnapshot.printExpected
  152. );
  153. if (length === 3) {
  154. options.secondArgument = 'hint';
  155. options.secondArgumentColor = _jestMatcherUtils.BOLD_WEIGHT;
  156. if (propertiesOrHint == null) {
  157. printedWithType += `\n\nTo provide a hint without properties: toMatchSnapshot('hint')`;
  158. }
  159. }
  160. throw new Error(
  161. (0, _jestMatcherUtils.matcherErrorMessage)(
  162. (0, _jestMatcherUtils.matcherHint)(
  163. matcherName,
  164. undefined,
  165. _printSnapshot.PROPERTIES_ARG,
  166. options
  167. ),
  168. `Expected ${(0, _jestMatcherUtils.EXPECTED_COLOR)(
  169. 'properties'
  170. )} must be an object`,
  171. printedWithType
  172. )
  173. );
  174. } // Future breaking change: Snapshot hint must be a string
  175. // if (arguments.length === 3 && typeof hint !== 'string') {}
  176. properties = propertiesOrHint;
  177. }
  178. return _toMatchSnapshot({
  179. context: this,
  180. hint,
  181. isInline: false,
  182. matcherName,
  183. properties,
  184. received
  185. });
  186. };
  187. const toMatchInlineSnapshot = function (
  188. received,
  189. propertiesOrSnapshot,
  190. inlineSnapshot
  191. ) {
  192. const matcherName = 'toMatchInlineSnapshot';
  193. let properties;
  194. const length = arguments.length;
  195. if (length === 2 && typeof propertiesOrSnapshot === 'string') {
  196. inlineSnapshot = propertiesOrSnapshot;
  197. } else if (length >= 2) {
  198. const options = {
  199. isNot: this.isNot,
  200. promise: this.promise
  201. };
  202. if (length === 3) {
  203. options.secondArgument = _printSnapshot.SNAPSHOT_ARG;
  204. options.secondArgumentColor = _printSnapshot.noColor;
  205. }
  206. if (
  207. typeof propertiesOrSnapshot !== 'object' ||
  208. propertiesOrSnapshot === null
  209. ) {
  210. throw new Error(
  211. (0, _jestMatcherUtils.matcherErrorMessage)(
  212. (0, _jestMatcherUtils.matcherHint)(
  213. matcherName,
  214. undefined,
  215. _printSnapshot.PROPERTIES_ARG,
  216. options
  217. ),
  218. `Expected ${(0, _jestMatcherUtils.EXPECTED_COLOR)(
  219. 'properties'
  220. )} must be an object`,
  221. (0, _jestMatcherUtils.printWithType)(
  222. 'Expected properties',
  223. propertiesOrSnapshot,
  224. _printSnapshot.printExpected
  225. )
  226. )
  227. );
  228. }
  229. if (length === 3 && typeof inlineSnapshot !== 'string') {
  230. throw new Error(
  231. (0, _jestMatcherUtils.matcherErrorMessage)(
  232. (0, _jestMatcherUtils.matcherHint)(
  233. matcherName,
  234. undefined,
  235. _printSnapshot.PROPERTIES_ARG,
  236. options
  237. ),
  238. `Inline snapshot must be a string`,
  239. (0, _jestMatcherUtils.printWithType)(
  240. 'Inline snapshot',
  241. inlineSnapshot,
  242. utils.serialize
  243. )
  244. )
  245. );
  246. }
  247. properties = propertiesOrSnapshot;
  248. }
  249. return _toMatchSnapshot({
  250. context: this,
  251. inlineSnapshot:
  252. inlineSnapshot !== undefined
  253. ? stripAddedIndentation(inlineSnapshot)
  254. : undefined,
  255. isInline: true,
  256. matcherName,
  257. properties,
  258. received
  259. });
  260. };
  261. const _toMatchSnapshot = config => {
  262. const {
  263. context,
  264. hint,
  265. inlineSnapshot,
  266. isInline,
  267. matcherName,
  268. properties
  269. } = config;
  270. let {received} = config;
  271. context.dontThrow && context.dontThrow();
  272. const {currentTestName, isNot, snapshotState} = context;
  273. if (isNot) {
  274. throw new Error(
  275. (0, _jestMatcherUtils.matcherErrorMessage)(
  276. (0, _printSnapshot.matcherHintFromConfig)(config, false),
  277. NOT_SNAPSHOT_MATCHERS
  278. )
  279. );
  280. }
  281. if (snapshotState == null) {
  282. // Because the state is the problem, this is not a matcher error.
  283. // Call generic stringify from jest-matcher-utils package
  284. // because uninitialized snapshot state does not need snapshot serializers.
  285. throw new Error(
  286. (0, _printSnapshot.matcherHintFromConfig)(config, false) +
  287. '\n\n' +
  288. `Snapshot state must be initialized` +
  289. '\n\n' +
  290. (0, _jestMatcherUtils.printWithType)(
  291. 'Snapshot state',
  292. snapshotState,
  293. _jestMatcherUtils.stringify
  294. )
  295. );
  296. }
  297. const fullTestName =
  298. currentTestName && hint
  299. ? `${currentTestName}: ${hint}`
  300. : currentTestName || ''; // future BREAKING change: || hint
  301. if (typeof properties === 'object') {
  302. if (typeof received !== 'object' || received === null) {
  303. throw new Error(
  304. (0, _jestMatcherUtils.matcherErrorMessage)(
  305. (0, _printSnapshot.matcherHintFromConfig)(config, false),
  306. `${(0, _jestMatcherUtils.RECEIVED_COLOR)(
  307. 'received'
  308. )} value must be an object when the matcher has ${(0,
  309. _jestMatcherUtils.EXPECTED_COLOR)('properties')}`,
  310. (0, _jestMatcherUtils.printWithType)(
  311. 'Received',
  312. received,
  313. _printSnapshot.printReceived
  314. )
  315. )
  316. );
  317. }
  318. const propertyPass = context.equals(received, properties, [
  319. context.utils.iterableEquality,
  320. context.utils.subsetEquality
  321. ]);
  322. if (!propertyPass) {
  323. const key = snapshotState.fail(fullTestName, received);
  324. const matched = /(\d+)$/.exec(key);
  325. const count = matched === null ? 1 : Number(matched[1]);
  326. const message = () =>
  327. (0, _printSnapshot.matcherHintFromConfig)(config, false) +
  328. '\n\n' +
  329. printSnapshotName(currentTestName, hint, count) +
  330. '\n\n' +
  331. (0, _printSnapshot.printPropertiesAndReceived)(
  332. properties,
  333. received,
  334. snapshotState.expand
  335. );
  336. return {
  337. message,
  338. name: matcherName,
  339. pass: false
  340. };
  341. } else {
  342. received = utils.deepMerge(received, properties);
  343. }
  344. }
  345. const result = snapshotState.match({
  346. error: context.error,
  347. inlineSnapshot,
  348. isInline,
  349. received,
  350. testName: fullTestName
  351. });
  352. const {actual, count, expected, pass} = result;
  353. if (pass) {
  354. return {
  355. message: () => '',
  356. pass: true
  357. };
  358. }
  359. const message =
  360. expected === undefined
  361. ? () =>
  362. (0, _printSnapshot.matcherHintFromConfig)(config, true) +
  363. '\n\n' +
  364. printSnapshotName(currentTestName, hint, count) +
  365. '\n\n' +
  366. `New snapshot was ${(0, _jestMatcherUtils.BOLD_WEIGHT)(
  367. 'not written'
  368. )}. The update flag ` +
  369. `must be explicitly passed to write a new snapshot.\n\n` +
  370. `This is likely because this test is run in a continuous integration ` +
  371. `(CI) environment in which snapshots are not written by default.\n\n` +
  372. `Received:${actual.includes('\n') ? '\n' : ' '}${(0,
  373. _printSnapshot.bReceivedColor)(actual)}`
  374. : () =>
  375. (0, _printSnapshot.matcherHintFromConfig)(config, true) +
  376. '\n\n' +
  377. printSnapshotName(currentTestName, hint, count) +
  378. '\n\n' +
  379. (0, _printSnapshot.printSnapshotAndReceived)(
  380. expected,
  381. actual,
  382. received,
  383. snapshotState.expand
  384. ); // Passing the actual and expected objects so that a custom reporter
  385. // could access them, for example in order to display a custom visual diff,
  386. // or create a different error message
  387. return {
  388. actual,
  389. expected,
  390. message,
  391. name: matcherName,
  392. pass: false
  393. };
  394. };
  395. const toThrowErrorMatchingSnapshot = function (
  396. received,
  397. hint, // because error TS1016 for hint?: string
  398. fromPromise
  399. ) {
  400. const matcherName = 'toThrowErrorMatchingSnapshot'; // Future breaking change: Snapshot hint must be a string
  401. // if (hint !== undefined && typeof hint !== string) {}
  402. return _toThrowErrorMatchingSnapshot(
  403. {
  404. context: this,
  405. hint,
  406. isInline: false,
  407. matcherName,
  408. received
  409. },
  410. fromPromise
  411. );
  412. };
  413. const toThrowErrorMatchingInlineSnapshot = function (
  414. received,
  415. inlineSnapshot,
  416. fromPromise
  417. ) {
  418. const matcherName = 'toThrowErrorMatchingInlineSnapshot';
  419. if (inlineSnapshot !== undefined && typeof inlineSnapshot !== 'string') {
  420. const options = {
  421. expectedColor: _printSnapshot.noColor,
  422. isNot: this.isNot,
  423. promise: this.promise
  424. };
  425. throw new Error(
  426. (0, _jestMatcherUtils.matcherErrorMessage)(
  427. (0, _jestMatcherUtils.matcherHint)(
  428. matcherName,
  429. undefined,
  430. _printSnapshot.SNAPSHOT_ARG,
  431. options
  432. ),
  433. `Inline snapshot must be a string`,
  434. (0, _jestMatcherUtils.printWithType)(
  435. 'Inline snapshot',
  436. inlineSnapshot,
  437. utils.serialize
  438. )
  439. )
  440. );
  441. }
  442. return _toThrowErrorMatchingSnapshot(
  443. {
  444. context: this,
  445. inlineSnapshot:
  446. inlineSnapshot !== undefined
  447. ? stripAddedIndentation(inlineSnapshot)
  448. : undefined,
  449. isInline: true,
  450. matcherName,
  451. received
  452. },
  453. fromPromise
  454. );
  455. };
  456. const _toThrowErrorMatchingSnapshot = (config, fromPromise) => {
  457. const {
  458. context,
  459. hint,
  460. inlineSnapshot,
  461. isInline,
  462. matcherName,
  463. received
  464. } = config;
  465. context.dontThrow && context.dontThrow();
  466. const {isNot, promise} = context;
  467. if (!fromPromise) {
  468. if (typeof received !== 'function') {
  469. const options = {
  470. isNot,
  471. promise
  472. };
  473. throw new Error(
  474. (0, _jestMatcherUtils.matcherErrorMessage)(
  475. (0, _jestMatcherUtils.matcherHint)(
  476. matcherName,
  477. undefined,
  478. '',
  479. options
  480. ),
  481. `${(0, _jestMatcherUtils.RECEIVED_COLOR)(
  482. 'received'
  483. )} value must be a function`,
  484. (0, _jestMatcherUtils.printWithType)(
  485. 'Received',
  486. received,
  487. _printSnapshot.printReceived
  488. )
  489. )
  490. );
  491. }
  492. }
  493. if (isNot) {
  494. throw new Error(
  495. (0, _jestMatcherUtils.matcherErrorMessage)(
  496. (0, _printSnapshot.matcherHintFromConfig)(config, false),
  497. NOT_SNAPSHOT_MATCHERS
  498. )
  499. );
  500. }
  501. let error;
  502. if (fromPromise) {
  503. error = received;
  504. } else {
  505. try {
  506. received();
  507. } catch (e) {
  508. error = e;
  509. }
  510. }
  511. if (error === undefined) {
  512. // Because the received value is a function, this is not a matcher error.
  513. throw new Error(
  514. (0, _printSnapshot.matcherHintFromConfig)(config, false) +
  515. '\n\n' +
  516. DID_NOT_THROW
  517. );
  518. }
  519. return _toMatchSnapshot({
  520. context,
  521. hint,
  522. inlineSnapshot,
  523. isInline,
  524. matcherName,
  525. received: error.message
  526. });
  527. };
  528. const JestSnapshot = {
  529. EXTENSION: _SnapshotResolver.EXTENSION,
  530. SnapshotState: _State.default,
  531. addSerializer: _plugins.addSerializer,
  532. buildSnapshotResolver: _SnapshotResolver.buildSnapshotResolver,
  533. cleanup,
  534. getSerializers: _plugins.getSerializers,
  535. isSnapshotPath: _SnapshotResolver.isSnapshotPath,
  536. toMatchInlineSnapshot,
  537. toMatchSnapshot,
  538. toThrowErrorMatchingInlineSnapshot,
  539. toThrowErrorMatchingSnapshot,
  540. utils
  541. };
  542. module.exports = JestSnapshot;