index.js 13 KB

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