@vitest/snapshot 3.2.0-beta.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -20,6 +20,11 @@ declare class DefaultMap<
20
20
  }
21
21
  declare class CounterMap<K> extends DefaultMap<K, number> {
22
22
  constructor();
23
+ // compat for jest-image-snapshot https://github.com/vitest-dev/vitest/issues/7322
24
+ // `valueOf` and `Snapshot.added` setter allows
25
+ // snapshotState.added = snapshotState.added + 1
26
+ // to function as
27
+ // snapshotState.added.total_ = snapshotState.added.total() + 1
23
28
  _total: number | undefined;
24
29
  valueOf(): number;
25
30
  increment(key: K): void;
@@ -54,6 +59,8 @@ declare class SnapshotState {
54
59
  private _environment;
55
60
  private _fileExists;
56
61
  expand: boolean;
62
+ // getter/setter for jest-image-snapshot compat
63
+ // https://github.com/vitest-dev/vitest/issues/7322
57
64
  private _added;
58
65
  private _matched;
59
66
  private _unmatched;
package/dist/index.js CHANGED
@@ -656,6 +656,7 @@ const stackIgnorePatterns = [
656
656
  /\/deps\/vitest_/
657
657
  ];
658
658
  function extractLocation(urlLike) {
659
+ // Fail-fast but return locations like "(native)"
659
660
  if (!urlLike.includes(":")) {
660
661
  return [urlLike];
661
662
  }
@@ -695,6 +696,7 @@ function parseSingleFFOrSafariStack(raw) {
695
696
  if (!line.includes("@") && !line.includes(":")) {
696
697
  return null;
697
698
  }
699
+ // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/optimal-quantifier-concatenation
698
700
  const functionNameRegex = /((.*".+"[^@]*)?[^@]*)(@)/;
699
701
  const matches = line.match(functionNameRegex);
700
702
  const functionName = matches && matches[1] ? matches[1] : undefined;
@@ -709,6 +711,8 @@ function parseSingleFFOrSafariStack(raw) {
709
711
  column: Number.parseInt(columnNumber)
710
712
  };
711
713
  }
714
+ // Based on https://github.com/stacktracejs/error-stack-parser
715
+ // Credit to stacktracejs
712
716
  function parseSingleV8Stack(raw) {
713
717
  let line = raw.trim();
714
718
  if (!CHROME_IE_STACK_REGEXP.test(line)) {
@@ -718,8 +722,13 @@ function parseSingleV8Stack(raw) {
718
722
  line = line.replace(/eval code/g, "eval").replace(/(\(eval at [^()]*)|(,.*$)/g, "");
719
723
  }
720
724
  let sanitizedLine = line.replace(/^\s+/, "").replace(/\(eval code/g, "(").replace(/^.*?\s+/, "");
725
+ // capture and preserve the parenthesized location "(/foo/my bar.js:12:87)" in
726
+ // case it has spaces in it, as the string is split on \s+ later on
721
727
  const location = sanitizedLine.match(/ (\(.+\)$)/);
728
+ // remove the parenthesized location from the line, if it was matched
722
729
  sanitizedLine = location ? sanitizedLine.replace(location[0], "") : sanitizedLine;
730
+ // if a location was matched, pass it to extractLocation() otherwise pass all sanitizedLine
731
+ // because this line doesn't have function name
723
732
  const [url, lineNumber, columnNumber] = extractLocation(location ? location[1] : sanitizedLine);
724
733
  let method = location && sanitizedLine || "";
725
734
  let file = url && ["eval", "<anonymous>"].includes(url) ? undefined : url;
@@ -732,6 +741,7 @@ function parseSingleV8Stack(raw) {
732
741
  if (file.startsWith("file://")) {
733
742
  file = file.slice(7);
734
743
  }
744
+ // normalize Windows path (\ -> /)
735
745
  file = file.startsWith("node:") || file.startsWith("internal:") ? file : resolve$2(file);
736
746
  if (method) {
737
747
  method = method.replace(/__vite_ssr_import_\d+__\./g, "");
@@ -762,6 +772,10 @@ function parseStacktrace(stack, options = {}) {
762
772
  const fileUrl = stack.file.startsWith("file://") ? stack.file : `file://${stack.file}`;
763
773
  const sourceRootUrl = map.sourceRoot ? new URL(map.sourceRoot, fileUrl) : fileUrl;
764
774
  file = new URL(source, sourceRootUrl).pathname;
775
+ // if the file path is on windows, we need to remove the leading slash
776
+ if (file.match(/\/\w:\//)) {
777
+ file = file.slice(1);
778
+ }
765
779
  }
766
780
  if (shouldFilter(ignoreStackEntries, file)) {
767
781
  return null;
@@ -793,8 +807,10 @@ function parseErrorStacktrace(e, options = {}) {
793
807
  if (e.stacks) {
794
808
  return e.stacks;
795
809
  }
796
- const stackStr = e.stack || e.stackStr || "";
797
- let stackFrames = parseStacktrace(stackStr, options);
810
+ const stackStr = e.stack || "";
811
+ // if "stack" property was overwritten at runtime to be something else,
812
+ // ignore the value because we don't know how to process it
813
+ let stackFrames = typeof stackStr === "string" ? parseStacktrace(stackStr, options) : [];
798
814
  if (!stackFrames.length) {
799
815
  const e_ = e;
800
816
  if (e_.fileName != null && e_.lineNumber != null && e_.columnNumber != null) {
@@ -1447,8 +1463,10 @@ function replaceObjectSnap(code, s, index, newSnap) {
1447
1463
  const shapeEnd = getObjectShapeEndIndex(code, shapeStart);
1448
1464
  const snap = `, ${prepareSnapString(newSnap, code, index)}`;
1449
1465
  if (shapeEnd === callEnd) {
1466
+ // toMatchInlineSnapshot({ foo: expect.any(String) })
1450
1467
  s.appendLeft(callEnd, snap);
1451
1468
  } else {
1469
+ // toMatchInlineSnapshot({ foo: expect.any(String) }, ``)
1452
1470
  s.overwrite(shapeEnd, callEnd, snap);
1453
1471
  }
1454
1472
  return true;
@@ -1481,6 +1499,7 @@ function prepareSnapString(snap, source, index) {
1481
1499
  }
1482
1500
  const toMatchInlineName = "toMatchInlineSnapshot";
1483
1501
  const toThrowErrorMatchingInlineName = "toThrowErrorMatchingInlineSnapshot";
1502
+ // on webkit, the line number is at the end of the method, not at the start
1484
1503
  function getCodeStartingAtIndex(code, index) {
1485
1504
  const indexInline = index - toMatchInlineName.length;
1486
1505
  if (code.slice(indexInline, index) === toMatchInlineName) {
@@ -1527,27 +1546,37 @@ function replaceInlineSnap(code, s, currentIndex, newSnap) {
1527
1546
  }
1528
1547
  const INDENTATION_REGEX = /^([^\S\n]*)\S/m;
1529
1548
  function stripSnapshotIndentation(inlineSnapshot) {
1549
+ // Find indentation if exists.
1530
1550
  const match = inlineSnapshot.match(INDENTATION_REGEX);
1531
1551
  if (!match || !match[1]) {
1552
+ // No indentation.
1532
1553
  return inlineSnapshot;
1533
1554
  }
1534
1555
  const indentation = match[1];
1535
1556
  const lines = inlineSnapshot.split(/\n/g);
1536
1557
  if (lines.length <= 2) {
1558
+ // Must be at least 3 lines.
1537
1559
  return inlineSnapshot;
1538
1560
  }
1539
1561
  if (lines[0].trim() !== "" || lines[lines.length - 1].trim() !== "") {
1562
+ // If not blank first and last lines, abort.
1540
1563
  return inlineSnapshot;
1541
1564
  }
1542
1565
  for (let i = 1; i < lines.length - 1; i++) {
1543
1566
  if (lines[i] !== "") {
1544
1567
  if (lines[i].indexOf(indentation) !== 0) {
1568
+ // All lines except first and last should either be blank or have the same
1569
+ // indent as the first line (or more). If this isn't the case we don't
1570
+ // want to touch the snapshot at all.
1545
1571
  return inlineSnapshot;
1546
1572
  }
1547
1573
  lines[i] = lines[i].substring(indentation.length);
1548
1574
  }
1549
1575
  }
1576
+ // Last line is a special case because it won't have the same indent as others
1577
+ // but may still have been given some indent to line up.
1550
1578
  lines[lines.length - 1] = "";
1579
+ // Return inline snapshot, now at indent 0.
1551
1580
  inlineSnapshot = lines.join("\n");
1552
1581
  return inlineSnapshot;
1553
1582
  }
@@ -1628,6 +1657,7 @@ var naturalCompareExports = requireNaturalCompare();
1628
1657
  var naturalCompare = /*@__PURE__*/getDefaultExportFromCjs(naturalCompareExports);
1629
1658
 
1630
1659
  const serialize$1 = (val, config, indentation, depth, refs, printer) => {
1660
+ // Serialize a non-default name, even if config.printFunctionName is false.
1631
1661
  const name = val.getMockName();
1632
1662
  const nameString = name === "vi.fn()" ? "" : ` ${name}`;
1633
1663
  let callsString = "";
@@ -1660,6 +1690,7 @@ function getSerializers() {
1660
1690
  return PLUGINS;
1661
1691
  }
1662
1692
 
1693
+ // TODO: rewrite and clean up
1663
1694
  function testNameToKey(testName, count) {
1664
1695
  return `${testName} ${count}`;
1665
1696
  }
@@ -1677,11 +1708,15 @@ function getSnapshotData(content, options) {
1677
1708
  if (content != null) {
1678
1709
  try {
1679
1710
  snapshotContents = content;
1711
+ // eslint-disable-next-line no-new-func
1680
1712
  const populate = new Function("exports", snapshotContents);
1681
1713
  populate(data);
1682
1714
  } catch {}
1683
1715
  }
1716
+ // const validationResult = validateSnapshotVersion(snapshotContents)
1684
1717
  const isInvalid = snapshotContents;
1718
+ // if (update === 'none' && isInvalid)
1719
+ // throw validationResult
1685
1720
  if ((update === "all" || update === "new") && isInvalid) {
1686
1721
  dirty = true;
1687
1722
  }
@@ -1690,12 +1725,27 @@ function getSnapshotData(content, options) {
1690
1725
  dirty
1691
1726
  };
1692
1727
  }
1728
+ // Add extra line breaks at beginning and end of multiline snapshot
1729
+ // to make the content easier to read.
1693
1730
  function addExtraLineBreaks(string) {
1694
1731
  return string.includes("\n") ? `\n${string}\n` : string;
1695
1732
  }
1733
+ // Remove extra line breaks at beginning and end of multiline snapshot.
1734
+ // Instead of trim, which can remove additional newlines or spaces
1735
+ // at beginning or end of the content from a custom serializer.
1696
1736
  function removeExtraLineBreaks(string) {
1697
1737
  return string.length > 2 && string.startsWith("\n") && string.endsWith("\n") ? string.slice(1, -1) : string;
1698
1738
  }
1739
+ // export const removeLinesBeforeExternalMatcherTrap = (stack: string): string => {
1740
+ // const lines = stack.split('\n')
1741
+ // for (let i = 0; i < lines.length; i += 1) {
1742
+ // // It's a function name specified in `packages/expect/src/index.ts`
1743
+ // // for external custom matchers.
1744
+ // if (lines[i].includes('__EXTERNAL_MATCHER_TRAP__'))
1745
+ // return lines.slice(i + 1).join('\n')
1746
+ // }
1747
+ // return stack
1748
+ // }
1699
1749
  const escapeRegex = true;
1700
1750
  const printFunctionName = false;
1701
1751
  function serialize(val, indent = 2, formatOverrides = {}) {
@@ -1735,6 +1785,7 @@ function deepMergeArray(target = [], source = []) {
1735
1785
  } else if (isObject(targetElement)) {
1736
1786
  mergedOutput[index] = deepMergeSnapshot(target[index], sourceElement);
1737
1787
  } else {
1788
+ // Source does not exist in target or target is primitive and cannot be deep merged
1738
1789
  mergedOutput[index] = sourceElement;
1739
1790
  }
1740
1791
  });
@@ -1790,6 +1841,11 @@ class CounterMap extends DefaultMap {
1790
1841
  constructor() {
1791
1842
  super(() => 0);
1792
1843
  }
1844
+ // compat for jest-image-snapshot https://github.com/vitest-dev/vitest/issues/7322
1845
+ // `valueOf` and `Snapshot.added` setter allows
1846
+ // snapshotState.added = snapshotState.added + 1
1847
+ // to function as
1848
+ // snapshotState.added.total_ = snapshotState.added.total() + 1
1793
1849
  _total;
1794
1850
  valueOf() {
1795
1851
  return this._total = this.total();
@@ -1830,6 +1886,8 @@ class SnapshotState {
1830
1886
  _environment;
1831
1887
  _fileExists;
1832
1888
  expand;
1889
+ // getter/setter for jest-image-snapshot compat
1890
+ // https://github.com/vitest-dev/vitest/issues/7322
1833
1891
  _added = new CounterMap();
1834
1892
  _matched = new CounterMap();
1835
1893
  _unmatched = new CounterMap();
@@ -1889,14 +1947,19 @@ class SnapshotState {
1889
1947
  }
1890
1948
  markSnapshotsAsCheckedForTest(testName) {
1891
1949
  this._uncheckedKeys.forEach((uncheckedKey) => {
1950
+ // skip snapshots with following keys
1951
+ // testName n
1952
+ // testName > xxx n (this is for toMatchSnapshot("xxx") API)
1892
1953
  if (/ \d+$| > /.test(uncheckedKey.slice(testName.length))) {
1893
1954
  this._uncheckedKeys.delete(uncheckedKey);
1894
1955
  }
1895
1956
  });
1896
1957
  }
1897
1958
  clearTest(testId) {
1959
+ // clear inline
1898
1960
  this._inlineSnapshots = this._inlineSnapshots.filter((s) => s.testId !== testId);
1899
1961
  this._inlineSnapshotStacks = this._inlineSnapshotStacks.filter((s) => s.testId !== testId);
1962
+ // clear file
1900
1963
  for (const key of this._testIdToKeys.get(testId)) {
1901
1964
  const name = keyToTestName(key);
1902
1965
  const count = this._counters.get(name);
@@ -1908,16 +1971,20 @@ class SnapshotState {
1908
1971
  }
1909
1972
  }
1910
1973
  this._testIdToKeys.delete(testId);
1974
+ // clear stats
1911
1975
  this.added.delete(testId);
1912
1976
  this.updated.delete(testId);
1913
1977
  this.matched.delete(testId);
1914
1978
  this.unmatched.delete(testId);
1915
1979
  }
1916
1980
  _inferInlineSnapshotStack(stacks) {
1981
+ // if called inside resolves/rejects, stacktrace is different
1917
1982
  const promiseIndex = stacks.findIndex((i) => i.method.match(/__VITEST_(RESOLVES|REJECTS)__/));
1918
1983
  if (promiseIndex !== -1) {
1919
1984
  return stacks[promiseIndex + 3];
1920
1985
  }
1986
+ // inline snapshot function is called __INLINE_SNAPSHOT__
1987
+ // in integrations/snapshot/chai.ts
1921
1988
  const stackIndex = stacks.findIndex((i) => i.method.includes("__INLINE_SNAPSHOT__"));
1922
1989
  return stackIndex !== -1 ? stacks[stackIndex + 2] : null;
1923
1990
  }
@@ -1982,12 +2049,16 @@ class SnapshotState {
1982
2049
  }
1983
2050
  }
1984
2051
  match({ testId, testName, received, key, inlineSnapshot, isInline, error, rawSnapshot }) {
2052
+ // this also increments counter for inline snapshots. maybe we shouldn't?
1985
2053
  this._counters.increment(testName);
1986
2054
  const count = this._counters.get(testName);
1987
2055
  if (!key) {
1988
2056
  key = testNameToKey(testName, count);
1989
2057
  }
1990
2058
  this._testIdToKeys.get(testId).push(key);
2059
+ // Do not mark the snapshot as "checked" if the snapshot is inline and
2060
+ // there's an external snapshot. This way the external snapshot can be
2061
+ // removed with `--updateSnapshot`.
1991
2062
  if (!(isInline && this._snapshotData[key] !== undefined)) {
1992
2063
  this._uncheckedKeys.delete(key);
1993
2064
  }
@@ -1996,6 +2067,7 @@ class SnapshotState {
1996
2067
  receivedSerialized = addExtraLineBreaks(receivedSerialized);
1997
2068
  }
1998
2069
  if (rawSnapshot) {
2070
+ // normalize EOL when snapshot contains CRLF but received is LF
1999
2071
  if (rawSnapshot.content && rawSnapshot.content.match(/\r\n/) && !receivedSerialized.match(/\r\n/)) {
2000
2072
  rawSnapshot.content = normalizeNewlines(rawSnapshot.content);
2001
2073
  }
@@ -2006,8 +2078,15 @@ class SnapshotState {
2006
2078
  const hasSnapshot = expected !== undefined;
2007
2079
  const snapshotIsPersisted = isInline || this._fileExists || rawSnapshot && rawSnapshot.content != null;
2008
2080
  if (pass && !isInline && !rawSnapshot) {
2081
+ // Executing a snapshot file as JavaScript and writing the strings back
2082
+ // when other snapshots have changed loses the proper escaping for some
2083
+ // characters. Since we check every snapshot in every test, use the newly
2084
+ // generated formatted string.
2085
+ // Note that this is only relevant when a snapshot is added and the dirty
2086
+ // flag is set.
2009
2087
  this._snapshotData[key] = receivedSerialized;
2010
2088
  }
2089
+ // find call site of toMatchInlineSnapshot
2011
2090
  let stack;
2012
2091
  if (isInline) {
2013
2092
  var _this$environment$pro, _this$environment;
@@ -2017,9 +2096,14 @@ class SnapshotState {
2017
2096
  throw new Error(`@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${JSON.stringify(stacks)}`);
2018
2097
  }
2019
2098
  stack = ((_this$environment$pro = (_this$environment = this.environment).processStackTrace) === null || _this$environment$pro === void 0 ? void 0 : _this$environment$pro.call(_this$environment, _stack)) || _stack;
2099
+ // removing 1 column, because source map points to the wrong
2100
+ // location for js files, but `column-1` points to the same in both js/ts
2101
+ // https://github.com/vitejs/vite/issues/8657
2020
2102
  stack.column--;
2103
+ // reject multiple inline snapshots at the same location if snapshot is different
2021
2104
  const snapshotsWithSameStack = this._inlineSnapshotStacks.filter((s) => isSameStackPosition(s, stack));
2022
2105
  if (snapshotsWithSameStack.length > 0) {
2106
+ // ensure only one snapshot will be written at the same location
2023
2107
  this._inlineSnapshots = this._inlineSnapshots.filter((s) => !isSameStackPosition(s, stack));
2024
2108
  const differentSnapshot = snapshotsWithSameStack.find((s) => s.snapshot !== receivedSerialized);
2025
2109
  if (differentSnapshot) {
@@ -2035,6 +2119,13 @@ class SnapshotState {
2035
2119
  snapshot: receivedSerialized
2036
2120
  });
2037
2121
  }
2122
+ // These are the conditions on when to write snapshots:
2123
+ // * There's no snapshot file in a non-CI environment.
2124
+ // * There is a snapshot file and we decided to update the snapshot.
2125
+ // * There is a snapshot file, but it doesn't have this snapshot.
2126
+ // These are the conditions on when not to write snapshots:
2127
+ // * The update flag is set to 'none'.
2128
+ // * There's no snapshot file or a file without this snapshot on a CI environment.
2038
2129
  if (hasSnapshot && this._updateSnapshot === "all" || (!hasSnapshot || !snapshotIsPersisted) && (this._updateSnapshot === "new" || this._updateSnapshot === "all")) {
2039
2130
  if (this._updateSnapshot === "all") {
2040
2131
  if (!pass) {
@@ -2179,6 +2270,7 @@ class SnapshotClient {
2179
2270
  try {
2180
2271
  var _this$options$isEqual, _this$options;
2181
2272
  const pass = ((_this$options$isEqual = (_this$options = this.options).isEqual) === null || _this$options$isEqual === void 0 ? void 0 : _this$options$isEqual.call(_this$options, received, properties)) ?? false;
2273
+ // const pass = equals(received, properties, [iterableEquality, subsetEquality])
2182
2274
  if (!pass) {
2183
2275
  throw createMismatchError("Snapshot properties mismatched", snapshotState.expand, received, properties);
2184
2276
  } else {
@@ -2213,7 +2305,9 @@ class SnapshotClient {
2213
2305
  throw new Error("Snapshot cannot be used outside of test");
2214
2306
  }
2215
2307
  const snapshotState = this.getSnapshotState(filepath);
2308
+ // save the filepath, so it don't lose even if the await make it out-of-context
2216
2309
  options.filepath || (options.filepath = filepath);
2310
+ // resolve and read the raw snapshot file
2217
2311
  rawSnapshot.file = await snapshotState.environment.resolveRawPath(filepath, rawSnapshot.file);
2218
2312
  rawSnapshot.content = await snapshotState.environment.readSnapshotFile(rawSnapshot.file) ?? undefined;
2219
2313
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitest/snapshot",
3
3
  "type": "module",
4
- "version": "3.2.0-beta.2",
4
+ "version": "3.2.0",
5
5
  "description": "Vitest snapshot manager",
6
6
  "license": "MIT",
7
7
  "funding": "https://opencollective.com/vitest",
@@ -40,12 +40,12 @@
40
40
  "dependencies": {
41
41
  "magic-string": "^0.30.17",
42
42
  "pathe": "^2.0.3",
43
- "@vitest/pretty-format": "3.2.0-beta.2"
43
+ "@vitest/pretty-format": "3.2.0"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/natural-compare": "^1.4.3",
47
47
  "natural-compare": "^1.4.0",
48
- "@vitest/utils": "3.2.0-beta.2"
48
+ "@vitest/utils": "3.2.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "rimraf dist && rollup -c",