@vitest/snapshot 4.1.3 → 4.1.4

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
@@ -1,90 +1,8 @@
1
- import { S as SnapshotStateOptions, a as SnapshotMatchOptions, b as SnapshotResult, R as RawSnapshotInfo } from './rawSnapshot.d-CmdgHNLi.js';
2
- export { c as SnapshotData, d as SnapshotSerializer, e as SnapshotSummary, f as SnapshotUpdateState, U as UncheckedSnapshot } from './rawSnapshot.d-CmdgHNLi.js';
3
- import { ParsedStack } from '@vitest/utils';
4
- import { S as SnapshotEnvironment } from './environment.d-DOJxxZV9.js';
1
+ import { S as SnapshotState, a as SnapshotStateOptions, b as SnapshotResult, R as RawSnapshotInfo, D as DomainSnapshotAdapter } from './rawSnapshot.d-D_X3-62x.js';
2
+ export { c as DomainMatchResult, d as SnapshotData, e as SnapshotMatchOptions, f as SnapshotSerializer, g as SnapshotSummary, h as SnapshotUpdateState, U as UncheckedSnapshot } from './rawSnapshot.d-D_X3-62x.js';
5
3
  import { Plugin, Plugins } from '@vitest/pretty-format';
6
-
7
- /**
8
- * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
9
- *
10
- * This source code is licensed under the MIT license found in the
11
- * LICENSE file in the root directory of this source tree.
12
- */
13
-
14
- declare class DefaultMap<
15
- K,
16
- V
17
- > extends Map<K, V> {
18
- private defaultFn;
19
- constructor(defaultFn: (key: K) => V, entries?: Iterable<readonly [K, V]>);
20
- get(key: K): V;
21
- }
22
- declare class CounterMap<K> extends DefaultMap<K, number> {
23
- constructor();
24
- _total: number | undefined;
25
- valueOf(): number;
26
- increment(key: K): void;
27
- total(): number;
28
- }
29
-
30
- interface SnapshotReturnOptions {
31
- actual: string;
32
- count: number;
33
- expected?: string;
34
- key: string;
35
- pass: boolean;
36
- }
37
- interface SaveStatus {
38
- deleted: boolean;
39
- saved: boolean;
40
- }
41
- declare class SnapshotState {
42
- testFilePath: string;
43
- snapshotPath: string;
44
- private _counters;
45
- private _dirty;
46
- private _updateSnapshot;
47
- private _snapshotData;
48
- private _initialData;
49
- private _inlineSnapshots;
50
- private _inlineSnapshotStacks;
51
- private _testIdToKeys;
52
- private _rawSnapshots;
53
- private _uncheckedKeys;
54
- private _snapshotFormat;
55
- private _environment;
56
- private _fileExists;
57
- expand: boolean;
58
- private _added;
59
- private _matched;
60
- private _unmatched;
61
- private _updated;
62
- get added(): CounterMap<string>;
63
- set added(value: number);
64
- get matched(): CounterMap<string>;
65
- set matched(value: number);
66
- get unmatched(): CounterMap<string>;
67
- set unmatched(value: number);
68
- get updated(): CounterMap<string>;
69
- set updated(value: number);
70
- private constructor();
71
- static create(testFilePath: string, options: SnapshotStateOptions): Promise<SnapshotState>;
72
- get environment(): SnapshotEnvironment;
73
- markSnapshotsAsCheckedForTest(testName: string): void;
74
- clearTest(testId: string): void;
75
- protected _inferInlineSnapshotStack(stacks: ParsedStack[]): ParsedStack | null;
76
- private _addSnapshot;
77
- save(): Promise<SaveStatus>;
78
- getUncheckedCount(): number;
79
- getUncheckedKeys(): Array<string>;
80
- removeUncheckedKeys(): void;
81
- probeExpectedSnapshot(options: Pick<SnapshotMatchOptions, "testName" | "testId" | "isInline" | "inlineSnapshot">): {
82
- data?: string;
83
- markAsChecked: () => void;
84
- };
85
- match({ testId, testName, received, key, inlineSnapshot, isInline, error, rawSnapshot, assertionName }: SnapshotMatchOptions): SnapshotReturnOptions;
86
- pack(): Promise<SnapshotResult>;
87
- }
4
+ export { S as SnapshotEnvironment } from './environment.d-DOJxxZV9.js';
5
+ import '@vitest/utils';
88
6
 
89
7
  interface AssertOptions {
90
8
  received: unknown;
@@ -104,6 +22,15 @@ interface AssertOptions {
104
22
  rawSnapshot?: RawSnapshotInfo;
105
23
  assertionName?: string;
106
24
  }
25
+ interface AssertDomainOptions extends Omit<AssertOptions, "received"> {
26
+ received: unknown;
27
+ adapter: DomainSnapshotAdapter<any, any>;
28
+ }
29
+ interface AssertDomainPollOptions extends Omit<AssertDomainOptions, "received"> {
30
+ poll: () => Promise<unknown> | unknown;
31
+ timeout?: number;
32
+ interval?: number;
33
+ }
107
34
  /** Same shape as expect.extend custom matcher result (SyncExpectationResult from @vitest/expect) */
108
35
  interface MatchResult {
109
36
  pass: boolean;
@@ -125,6 +52,8 @@ declare class SnapshotClient {
125
52
  getSnapshotState(filepath: string): SnapshotState;
126
53
  match(options: AssertOptions): MatchResult;
127
54
  assert(options: AssertOptions): void;
55
+ matchDomain(options: AssertDomainOptions): MatchResult;
56
+ pollMatchDomain(options: AssertDomainPollOptions): Promise<MatchResult>;
128
57
  assertRaw(options: AssertOptions): Promise<void>;
129
58
  clear(): void;
130
59
  }
@@ -141,5 +70,5 @@ declare function stripSnapshotIndentation(inlineSnapshot: string): string;
141
70
  declare function addSerializer(plugin: Plugin): void;
142
71
  declare function getSerializers(): Plugins;
143
72
 
144
- export { SnapshotClient, SnapshotEnvironment, SnapshotMatchOptions, SnapshotResult, SnapshotState, SnapshotStateOptions, addSerializer, getSerializers, stripSnapshotIndentation };
73
+ export { DomainSnapshotAdapter, SnapshotClient, SnapshotResult, SnapshotState, SnapshotStateOptions, addSerializer, getSerializers, stripSnapshotIndentation };
145
74
  export type { MatchResult };
package/dist/index.js CHANGED
@@ -542,6 +542,9 @@ class SnapshotState {
542
542
  const content = await options.snapshotEnvironment.readSnapshotFile(snapshotPath);
543
543
  return new SnapshotState(testFilePath, snapshotPath, content, options);
544
544
  }
545
+ get snapshotUpdateState() {
546
+ return this._updateSnapshot;
547
+ }
545
548
  get environment() {
546
549
  return this._environment;
547
550
  }
@@ -583,6 +586,11 @@ class SnapshotState {
583
586
  if (promiseIndex !== -1) {
584
587
  return stacks[promiseIndex + 3];
585
588
  }
589
+ // support poll + inline snapshot
590
+ const pollChainIndex = stacks.findIndex((i) => i.method.match(/__VITEST_POLL_CHAIN__/));
591
+ if (pollChainIndex !== -1) {
592
+ return stacks[pollChainIndex + 1];
593
+ }
586
594
  // inline snapshot function can be named __INLINE_SNAPSHOT_OFFSET_<n>__
587
595
  // to specify a custom stack offset
588
596
  for (let i = 0; i < stacks.length; i++) {
@@ -620,6 +628,114 @@ class SnapshotState {
620
628
  this._snapshotData[key] = receivedSerialized;
621
629
  }
622
630
  }
631
+ _resolveKey(testId, testName, key) {
632
+ this._counters.increment(testName);
633
+ const count = this._counters.get(testName);
634
+ if (!key) {
635
+ key = testNameToKey(testName, count);
636
+ }
637
+ this._testIdToKeys.get(testId).push(key);
638
+ return {
639
+ key,
640
+ count
641
+ };
642
+ }
643
+ _resolveInlineStack(options) {
644
+ const { testId, snapshot, assertionName, error } = options;
645
+ const stacks = parseErrorStacktrace(error, { ignoreStackEntries: [] });
646
+ const _stack = this._inferInlineSnapshotStack(stacks);
647
+ if (!_stack) {
648
+ const message = stacks.map((s) => ` ${s.file}:${s.line}:${s.column}${s.method ? ` (${s.method})` : ""}`).join("\n");
649
+ throw new Error(`@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${message}`);
650
+ }
651
+ const stack = this.environment.processStackTrace?.(_stack) || _stack;
652
+ // removing 1 column, because source map points to the wrong
653
+ // location for js files, but `column-1` points to the same in both js/ts
654
+ // https://github.com/vitejs/vite/issues/8657
655
+ stack.column--;
656
+ // reject multiple inline snapshots at the same location if snapshot is different
657
+ const snapshotsWithSameStack = this._inlineSnapshotStacks.filter((s) => isSameStackPosition(s, stack));
658
+ if (snapshotsWithSameStack.length > 0) {
659
+ // ensure only one snapshot will be written at the same location
660
+ this._inlineSnapshots = this._inlineSnapshots.filter((s) => !isSameStackPosition(s, stack));
661
+ const differentSnapshot = snapshotsWithSameStack.find((s) => s.snapshot !== snapshot);
662
+ if (differentSnapshot) {
663
+ throw Object.assign(new Error(`${assertionName} with different snapshots cannot be called at the same location`), {
664
+ actual: snapshot,
665
+ expected: differentSnapshot.snapshot
666
+ });
667
+ }
668
+ }
669
+ this._inlineSnapshotStacks.push({
670
+ ...stack,
671
+ testId,
672
+ snapshot
673
+ });
674
+ return stack;
675
+ }
676
+ _reconcile(opts) {
677
+ // These are the conditions on when to write snapshots:
678
+ // * There's no snapshot file in a non-CI environment.
679
+ // * There is a snapshot file and we decided to update the snapshot.
680
+ // * There is a snapshot file, but it doesn't have this snapshot.
681
+ // These are the conditions on when not to write snapshots:
682
+ // * The update flag is set to 'none'.
683
+ // * There's no snapshot file or a file without this snapshot on a CI environment.
684
+ if (opts.hasSnapshot && this._updateSnapshot === "all" || (!opts.hasSnapshot || !opts.snapshotIsPersisted) && (this._updateSnapshot === "new" || this._updateSnapshot === "all")) {
685
+ if (this._updateSnapshot === "all") {
686
+ if (!opts.pass) {
687
+ if (opts.hasSnapshot) {
688
+ this.updated.increment(opts.testId);
689
+ } else {
690
+ this.added.increment(opts.testId);
691
+ }
692
+ this._addSnapshot(opts.key, opts.addValue, {
693
+ stack: opts.stack,
694
+ testId: opts.testId,
695
+ rawSnapshot: opts.rawSnapshot,
696
+ assertionName: opts.assertionName
697
+ });
698
+ } else {
699
+ this.matched.increment(opts.testId);
700
+ }
701
+ } else {
702
+ this._addSnapshot(opts.key, opts.addValue, {
703
+ stack: opts.stack,
704
+ testId: opts.testId,
705
+ rawSnapshot: opts.rawSnapshot,
706
+ assertionName: opts.assertionName
707
+ });
708
+ this.added.increment(opts.testId);
709
+ }
710
+ return {
711
+ actual: "",
712
+ count: opts.count,
713
+ expected: "",
714
+ key: opts.key,
715
+ pass: true
716
+ };
717
+ } else {
718
+ if (!opts.pass) {
719
+ this.unmatched.increment(opts.testId);
720
+ return {
721
+ actual: opts.actualDisplay,
722
+ count: opts.count,
723
+ expected: opts.expectedDisplay,
724
+ key: opts.key,
725
+ pass: false
726
+ };
727
+ } else {
728
+ this.matched.increment(opts.testId);
729
+ return {
730
+ actual: "",
731
+ count: opts.count,
732
+ expected: "",
733
+ key: opts.key,
734
+ pass: true
735
+ };
736
+ }
737
+ }
738
+ }
623
739
  async save() {
624
740
  const hasExternalSnapshots = Object.keys(this._snapshotData).length;
625
741
  const hasInlineSnapshots = this._inlineSnapshots.length;
@@ -667,6 +783,8 @@ class SnapshotState {
667
783
  const count = this._counters.get(options.testName) + 1;
668
784
  const key = testNameToKey(options.testName, count);
669
785
  return {
786
+ key,
787
+ count,
670
788
  data: options?.isInline ? options.inlineSnapshot : this._snapshotData[key],
671
789
  markAsChecked: () => {
672
790
  this._counters.increment(options.testName);
@@ -676,13 +794,9 @@ class SnapshotState {
676
794
  };
677
795
  }
678
796
  match({ testId, testName, received, key, inlineSnapshot, isInline, error, rawSnapshot, assertionName }) {
679
- // this also increments counter for inline snapshots. maybe we shouldn't?
680
- this._counters.increment(testName);
681
- const count = this._counters.get(testName);
682
- if (!key) {
683
- key = testNameToKey(testName, count);
684
- }
685
- this._testIdToKeys.get(testId).push(key);
797
+ const resolved = this._resolveKey(testId, testName, key);
798
+ key = resolved.key;
799
+ const count = resolved.count;
686
800
  // Do not mark the snapshot as "checked" if the snapshot is inline and
687
801
  // there's an external snapshot. This way the external snapshot can be
688
802
  // removed with `--updateSnapshot`.
@@ -705,107 +819,57 @@ class SnapshotState {
705
819
  const hasSnapshot = expected !== undefined;
706
820
  const snapshotIsPersisted = isInline || this._fileExists || rawSnapshot && rawSnapshot.content != null;
707
821
  if (pass && !isInline && !rawSnapshot) {
708
- // Executing a snapshot file as JavaScript and writing the strings back
709
- // when other snapshots have changed loses the proper escaping for some
710
- // characters. Since we check every snapshot in every test, use the newly
711
- // generated formatted string.
712
- // Note that this is only relevant when a snapshot is added and the dirty
713
- // flag is set.
822
+ // When the file is re-saved (because other snapshots changed), the JS
823
+ // round-trip can lose proper escaping. Refresh in-memory data with the
824
+ // freshly serialized string so the file is written correctly.
825
+ // _reconcile does not write _snapshotData on pass, so this is the only
826
+ // place it gets refreshed. Domain snapshots skip this because the stored
827
+ // value may contain match patterns that differ from the received output.
714
828
  this._snapshotData[key] = receivedSerialized;
715
829
  }
716
- // find call site of toMatchInlineSnapshot
717
- let stack;
718
- if (isInline) {
719
- const stacks = parseErrorStacktrace(error || new Error("snapshot"), { ignoreStackEntries: [] });
720
- const _stack = this._inferInlineSnapshotStack(stacks);
721
- if (!_stack) {
722
- throw new Error(`@vitest/snapshot: Couldn't infer stack frame for inline snapshot.\n${JSON.stringify(stacks)}`);
723
- }
724
- stack = this.environment.processStackTrace?.(_stack) || _stack;
725
- // removing 1 column, because source map points to the wrong
726
- // location for js files, but `column-1` points to the same in both js/ts
727
- // https://github.com/vitejs/vite/issues/8657
728
- stack.column--;
729
- // reject multiple inline snapshots at the same location if snapshot is different
730
- const snapshotsWithSameStack = this._inlineSnapshotStacks.filter((s) => isSameStackPosition(s, stack));
731
- if (snapshotsWithSameStack.length > 0) {
732
- // ensure only one snapshot will be written at the same location
733
- this._inlineSnapshots = this._inlineSnapshots.filter((s) => !isSameStackPosition(s, stack));
734
- const differentSnapshot = snapshotsWithSameStack.find((s) => s.snapshot !== receivedSerialized);
735
- if (differentSnapshot) {
736
- throw Object.assign(new Error("toMatchInlineSnapshot with different snapshots cannot be called at the same location"), {
737
- actual: receivedSerialized,
738
- expected: differentSnapshot.snapshot
739
- });
740
- }
741
- }
742
- this._inlineSnapshotStacks.push({
743
- ...stack,
744
- testId,
745
- snapshot: receivedSerialized
746
- });
747
- }
748
- // These are the conditions on when to write snapshots:
749
- // * There's no snapshot file in a non-CI environment.
750
- // * There is a snapshot file and we decided to update the snapshot.
751
- // * There is a snapshot file, but it doesn't have this snapshot.
752
- // These are the conditions on when not to write snapshots:
753
- // * The update flag is set to 'none'.
754
- // * There's no snapshot file or a file without this snapshot on a CI environment.
755
- if (hasSnapshot && this._updateSnapshot === "all" || (!hasSnapshot || !snapshotIsPersisted) && (this._updateSnapshot === "new" || this._updateSnapshot === "all")) {
756
- if (this._updateSnapshot === "all") {
757
- if (!pass) {
758
- if (hasSnapshot) {
759
- this.updated.increment(testId);
760
- } else {
761
- this.added.increment(testId);
762
- }
763
- this._addSnapshot(key, receivedSerialized, {
764
- stack,
765
- testId,
766
- rawSnapshot,
767
- assertionName
768
- });
769
- } else {
770
- this.matched.increment(testId);
771
- }
772
- } else {
773
- this._addSnapshot(key, receivedSerialized, {
774
- stack,
775
- testId,
776
- rawSnapshot,
777
- assertionName
778
- });
779
- this.added.increment(testId);
780
- }
781
- return {
782
- actual: "",
783
- count,
784
- expected: "",
785
- key,
786
- pass: true
787
- };
788
- } else {
789
- if (!pass) {
790
- this.unmatched.increment(testId);
791
- return {
792
- actual: rawSnapshot ? receivedSerialized : removeExtraLineBreaks(receivedSerialized),
793
- count,
794
- expected: expectedTrimmed !== undefined ? rawSnapshot ? expectedTrimmed : removeExtraLineBreaks(expectedTrimmed) : undefined,
795
- key,
796
- pass: false
797
- };
798
- } else {
799
- this.matched.increment(testId);
800
- return {
801
- actual: "",
802
- count,
803
- expected: "",
804
- key,
805
- pass: true
806
- };
807
- }
808
- }
830
+ const stack = isInline ? this._resolveInlineStack({
831
+ testId,
832
+ snapshot: receivedSerialized,
833
+ assertionName: assertionName || "toMatchInlineSnapshot",
834
+ error: error || new Error("snapshot")
835
+ }) : undefined;
836
+ return this._reconcile({
837
+ testId,
838
+ key,
839
+ count,
840
+ pass,
841
+ hasSnapshot,
842
+ snapshotIsPersisted: !!snapshotIsPersisted,
843
+ addValue: receivedSerialized,
844
+ actualDisplay: rawSnapshot ? receivedSerialized : removeExtraLineBreaks(receivedSerialized),
845
+ expectedDisplay: expectedTrimmed !== undefined ? rawSnapshot ? expectedTrimmed : removeExtraLineBreaks(expectedTrimmed) : undefined,
846
+ stack,
847
+ rawSnapshot,
848
+ assertionName
849
+ });
850
+ }
851
+ processDomainSnapshot({ testId, received, expectedSnapshot, matchResult, isInline, error, assertionName }) {
852
+ const stack = isInline ? this._resolveInlineStack({
853
+ testId,
854
+ snapshot: received,
855
+ assertionName,
856
+ error: error || new Error("STACK_TRACE_ERROR")
857
+ }) : undefined;
858
+ const actualResolved = matchResult?.resolved ?? received;
859
+ const expectedResolved = matchResult?.expected ?? expectedSnapshot.data;
860
+ return this._reconcile({
861
+ testId,
862
+ key: expectedSnapshot.key,
863
+ count: expectedSnapshot.count,
864
+ pass: matchResult?.pass ?? false,
865
+ hasSnapshot: !!expectedSnapshot.data,
866
+ snapshotIsPersisted: isInline ? true : this._fileExists,
867
+ addValue: actualResolved,
868
+ actualDisplay: removeExtraLineBreaks(actualResolved),
869
+ expectedDisplay: expectedResolved !== undefined ? removeExtraLineBreaks(expectedResolved) : undefined,
870
+ stack,
871
+ assertionName
872
+ });
809
873
  }
810
874
  async pack() {
811
875
  const snapshot = {
@@ -946,6 +1010,93 @@ class SnapshotClient {
946
1010
  throw createMismatchError(result.message(), snapshotState.expand, result.actual, result.expected);
947
1011
  }
948
1012
  }
1013
+ matchDomain(options) {
1014
+ const { received, filepath, name, testId = name, message, adapter, isInline = false, inlineSnapshot, error } = options;
1015
+ if (!filepath) {
1016
+ throw new Error("Snapshot cannot be used outside of test");
1017
+ }
1018
+ const captured = adapter.capture(received);
1019
+ const rendered = adapter.render(captured);
1020
+ const snapshotState = this.getSnapshotState(filepath);
1021
+ const testName = [name, ...message ? [message] : []].join(" > ");
1022
+ const expectedSnapshot = snapshotState.probeExpectedSnapshot({
1023
+ testName,
1024
+ testId,
1025
+ isInline,
1026
+ inlineSnapshot
1027
+ });
1028
+ expectedSnapshot.markAsChecked();
1029
+ const matchResult = expectedSnapshot.data ? adapter.match(captured, adapter.parseExpected(expectedSnapshot.data)) : undefined;
1030
+ const { actual, expected, key, pass } = snapshotState.processDomainSnapshot({
1031
+ testId,
1032
+ received: rendered,
1033
+ expectedSnapshot,
1034
+ matchResult,
1035
+ isInline,
1036
+ error,
1037
+ assertionName: options.assertionName
1038
+ });
1039
+ return {
1040
+ pass,
1041
+ message: () => `Snapshot \`${key}\` mismatched`,
1042
+ actual: actual?.trim(),
1043
+ expected: expected?.trim()
1044
+ };
1045
+ }
1046
+ async pollMatchDomain(options) {
1047
+ const { poll, filepath, name, testId = name, message, adapter, isInline = false, inlineSnapshot, error, timeout = 1e3, interval = 50 } = options;
1048
+ if (!filepath) {
1049
+ throw new Error("Snapshot cannot be used outside of test");
1050
+ }
1051
+ const snapshotState = this.getSnapshotState(filepath);
1052
+ const testName = [name, ...message ? [message] : []].join(" > ");
1053
+ const expectedSnapshot = snapshotState.probeExpectedSnapshot({
1054
+ testName,
1055
+ testId,
1056
+ isInline,
1057
+ inlineSnapshot
1058
+ });
1059
+ const reference = expectedSnapshot.data && snapshotState.snapshotUpdateState !== "all" ? adapter.parseExpected(expectedSnapshot.data) : undefined;
1060
+ const timedOut = timeout > 0 ? new Promise((r) => setTimeout(r, timeout)) : undefined;
1061
+ const stableResult = await getStableSnapshot({
1062
+ adapter,
1063
+ poll,
1064
+ interval,
1065
+ timedOut,
1066
+ match: reference ? (captured) => adapter.match(captured, reference).pass : undefined
1067
+ });
1068
+ expectedSnapshot.markAsChecked();
1069
+ if (!stableResult?.rendered) {
1070
+ // the original caller `expect.poll` later manipulates error via `throwWithCause`,
1071
+ // so here we can directly throw `lastPollError` if exists.
1072
+ if (stableResult?.lastPollError) {
1073
+ throw stableResult.lastPollError;
1074
+ }
1075
+ return {
1076
+ pass: false,
1077
+ message: () => `poll() did not produce a stable snapshot within the timeout`
1078
+ };
1079
+ }
1080
+ // TODO: should `all` mode ignore parse error?
1081
+ // Sielently hiding the error and creating snaphsot full scratch isn't good either.
1082
+ // Users can fix or purge the broken snapshot manually and that decision affects how domain snapshot gets updated.
1083
+ const matchResult = expectedSnapshot.data ? adapter.match(stableResult.captured, adapter.parseExpected(expectedSnapshot.data)) : undefined;
1084
+ const { actual, expected, key, pass } = snapshotState.processDomainSnapshot({
1085
+ testId,
1086
+ received: stableResult.rendered,
1087
+ expectedSnapshot,
1088
+ matchResult,
1089
+ isInline,
1090
+ error,
1091
+ assertionName: options.assertionName
1092
+ });
1093
+ return {
1094
+ pass,
1095
+ message: () => `Snapshot \`${key}\` mismatched`,
1096
+ actual: actual?.trim(),
1097
+ expected: expected?.trim()
1098
+ };
1099
+ }
949
1100
  async assertRaw(options) {
950
1101
  if (!options.rawSnapshot) {
951
1102
  throw new Error("Raw snapshot is required");
@@ -968,5 +1119,69 @@ class SnapshotClient {
968
1119
  this.snapshotStateMap.clear();
969
1120
  }
970
1121
  }
1122
+ /**
1123
+ * Polls repeatedly until the value reaches a stable state.
1124
+ *
1125
+ * Compares consecutive rendered outputs from the current session —
1126
+ * when two consecutive polls produce the same rendered string,
1127
+ * the value is considered stable.
1128
+ *
1129
+ * Every `await` (poll call, interval delay) races against `timedOut`
1130
+ * so that hanging polls and delays are interrupted.
1131
+ */
1132
+ async function getStableSnapshot({ adapter, poll, interval, timedOut, match }) {
1133
+ let lastRendered;
1134
+ let lastPollError;
1135
+ let lastStable;
1136
+ while (true) {
1137
+ try {
1138
+ const pollResult = await raceWith(Promise.resolve(poll()), timedOut);
1139
+ if (!pollResult.ok) {
1140
+ break;
1141
+ }
1142
+ const captured = adapter.capture(pollResult.value);
1143
+ const rendered = adapter.render(captured);
1144
+ if (lastRendered !== undefined && rendered === lastRendered) {
1145
+ lastStable = {
1146
+ captured,
1147
+ rendered
1148
+ };
1149
+ if (!match || match(captured)) {
1150
+ break;
1151
+ }
1152
+ } else {
1153
+ lastRendered = rendered;
1154
+ lastStable = undefined;
1155
+ }
1156
+ } catch (pollError) {
1157
+ // poll() threw — reset stability baseline and retry
1158
+ lastRendered = undefined;
1159
+ lastStable = undefined;
1160
+ lastPollError = pollError;
1161
+ }
1162
+ const delayed = await raceWith(new Promise((r) => setTimeout(r, interval)), timedOut);
1163
+ if (!delayed.ok) {
1164
+ break;
1165
+ }
1166
+ }
1167
+ return {
1168
+ ...lastStable,
1169
+ lastPollError
1170
+ };
1171
+ }
1172
+ /** Type-safe `Promise.race` — tells you which promise won. */
1173
+ function raceWith(promise, other) {
1174
+ const left = promise.then((value) => ({
1175
+ ok: true,
1176
+ value
1177
+ }));
1178
+ if (!other) {
1179
+ return left;
1180
+ }
1181
+ return Promise.race([left, other.then((value) => ({
1182
+ ok: false,
1183
+ value
1184
+ }))]);
1185
+ }
971
1186
 
972
1187
  export { SnapshotClient, SnapshotState, addSerializer, getSerializers, stripSnapshotIndentation };
package/dist/manager.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { S as SnapshotStateOptions, e as SnapshotSummary, b as SnapshotResult } from './rawSnapshot.d-CmdgHNLi.js';
1
+ import { a as SnapshotStateOptions, g as SnapshotSummary, b as SnapshotResult } from './rawSnapshot.d-D_X3-62x.js';
2
2
  import '@vitest/pretty-format';
3
- import './environment.d-DOJxxZV9.js';
4
3
  import '@vitest/utils';
4
+ import './environment.d-DOJxxZV9.js';
5
5
 
6
6
  declare class SnapshotManager {
7
7
  options: Omit<SnapshotStateOptions, "snapshotEnvironment">;
@@ -0,0 +1,205 @@
1
+ import { OptionsReceived, Plugin } from '@vitest/pretty-format';
2
+ import { ParsedStack } from '@vitest/utils';
3
+ import { S as SnapshotEnvironment } from './environment.d-DOJxxZV9.js';
4
+
5
+ interface DomainMatchResult {
6
+ pass: boolean;
7
+ message?: string;
8
+ /**
9
+ * The captured value viewed through the template's lens.
10
+ *
11
+ * Where the template uses patterns (e.g. regexes) or omits details,
12
+ * the resolved string adopts those patterns. Where the template doesn't
13
+ * match, the resolved string uses literal captured values instead.
14
+ *
15
+ * Used for two purposes:
16
+ * - **Diff display** (actual side): compared against `expected`
17
+ * so the diff highlights only genuine mismatches, not pattern-vs-literal noise.
18
+ * - **Snapshot update** (`--update`): written as the new snapshot content,
19
+ * preserving user-edited patterns from matched regions while incorporating
20
+ * actual values for mismatched regions.
21
+ *
22
+ * When omitted, falls back to `render(capture(received))` (the raw rendered value).
23
+ */
24
+ resolved?: string;
25
+ /**
26
+ * The stored template re-rendered as a string, representing what the user
27
+ * originally wrote or last saved.
28
+ *
29
+ * Used as the expected side in diff display.
30
+ *
31
+ * When omitted, falls back to the raw snapshot string from the snap file
32
+ * or inline snapshot.
33
+ */
34
+ expected?: string;
35
+ }
36
+ interface DomainSnapshotAdapter<
37
+ Captured = unknown,
38
+ Expected = unknown
39
+ > {
40
+ name: string;
41
+ capture: (received: unknown) => Captured;
42
+ render: (captured: Captured) => string;
43
+ parseExpected: (input: string) => Expected;
44
+ match: (captured: Captured, expected: Expected) => DomainMatchResult;
45
+ }
46
+
47
+ /**
48
+ * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
49
+ *
50
+ * This source code is licensed under the MIT license found in the
51
+ * LICENSE file in the root directory of this source tree.
52
+ */
53
+
54
+ declare class DefaultMap<
55
+ K,
56
+ V
57
+ > extends Map<K, V> {
58
+ private defaultFn;
59
+ constructor(defaultFn: (key: K) => V, entries?: Iterable<readonly [K, V]>);
60
+ get(key: K): V;
61
+ }
62
+ declare class CounterMap<K> extends DefaultMap<K, number> {
63
+ constructor();
64
+ _total: number | undefined;
65
+ valueOf(): number;
66
+ increment(key: K): void;
67
+ total(): number;
68
+ }
69
+
70
+ interface SnapshotReturnOptions {
71
+ actual: string;
72
+ count: number;
73
+ expected?: string;
74
+ key: string;
75
+ pass: boolean;
76
+ }
77
+ interface SaveStatus {
78
+ deleted: boolean;
79
+ saved: boolean;
80
+ }
81
+ interface ExpectedSnapshot {
82
+ key: string;
83
+ count: number;
84
+ data?: string;
85
+ markAsChecked: () => void;
86
+ }
87
+ declare class SnapshotState {
88
+ testFilePath: string;
89
+ snapshotPath: string;
90
+ private _counters;
91
+ private _dirty;
92
+ private _updateSnapshot;
93
+ private _snapshotData;
94
+ private _initialData;
95
+ private _inlineSnapshots;
96
+ private _inlineSnapshotStacks;
97
+ private _testIdToKeys;
98
+ private _rawSnapshots;
99
+ private _uncheckedKeys;
100
+ private _snapshotFormat;
101
+ private _environment;
102
+ private _fileExists;
103
+ expand: boolean;
104
+ private _added;
105
+ private _matched;
106
+ private _unmatched;
107
+ private _updated;
108
+ get added(): CounterMap<string>;
109
+ set added(value: number);
110
+ get matched(): CounterMap<string>;
111
+ set matched(value: number);
112
+ get unmatched(): CounterMap<string>;
113
+ set unmatched(value: number);
114
+ get updated(): CounterMap<string>;
115
+ set updated(value: number);
116
+ private constructor();
117
+ static create(testFilePath: string, options: SnapshotStateOptions): Promise<SnapshotState>;
118
+ get snapshotUpdateState(): SnapshotUpdateState;
119
+ get environment(): SnapshotEnvironment;
120
+ markSnapshotsAsCheckedForTest(testName: string): void;
121
+ clearTest(testId: string): void;
122
+ protected _inferInlineSnapshotStack(stacks: ParsedStack[]): ParsedStack | null;
123
+ private _addSnapshot;
124
+ private _resolveKey;
125
+ private _resolveInlineStack;
126
+ private _reconcile;
127
+ save(): Promise<SaveStatus>;
128
+ getUncheckedCount(): number;
129
+ getUncheckedKeys(): Array<string>;
130
+ removeUncheckedKeys(): void;
131
+ probeExpectedSnapshot(options: Pick<SnapshotMatchOptions, "testName" | "testId" | "isInline" | "inlineSnapshot">): ExpectedSnapshot;
132
+ match({ testId, testName, received, key, inlineSnapshot, isInline, error, rawSnapshot, assertionName }: SnapshotMatchOptions): SnapshotReturnOptions;
133
+ processDomainSnapshot({ testId, received, expectedSnapshot, matchResult, isInline, error, assertionName }: ProcessDomainSnapshotOptions): SnapshotReturnOptions;
134
+ pack(): Promise<SnapshotResult>;
135
+ }
136
+
137
+ type SnapshotData = Record<string, string>;
138
+ type SnapshotUpdateState = "all" | "new" | "none";
139
+ type SnapshotSerializer = Plugin;
140
+ interface SnapshotStateOptions {
141
+ updateSnapshot: SnapshotUpdateState;
142
+ snapshotEnvironment: SnapshotEnvironment;
143
+ expand?: boolean;
144
+ snapshotFormat?: OptionsReceived;
145
+ resolveSnapshotPath?: (path: string, extension: string, context?: any) => string;
146
+ }
147
+ interface SnapshotMatchOptions {
148
+ testId: string;
149
+ testName: string;
150
+ received: unknown;
151
+ key?: string;
152
+ inlineSnapshot?: string;
153
+ isInline: boolean;
154
+ error?: Error;
155
+ rawSnapshot?: RawSnapshotInfo;
156
+ assertionName?: string;
157
+ }
158
+ interface ProcessDomainSnapshotOptions {
159
+ testId: string;
160
+ received: string;
161
+ expectedSnapshot: ExpectedSnapshot;
162
+ matchResult?: DomainMatchResult;
163
+ isInline?: boolean;
164
+ assertionName?: string;
165
+ error?: Error;
166
+ }
167
+ interface SnapshotResult {
168
+ filepath: string;
169
+ added: number;
170
+ fileDeleted: boolean;
171
+ matched: number;
172
+ unchecked: number;
173
+ uncheckedKeys: Array<string>;
174
+ unmatched: number;
175
+ updated: number;
176
+ }
177
+ interface UncheckedSnapshot {
178
+ filePath: string;
179
+ keys: Array<string>;
180
+ }
181
+ interface SnapshotSummary {
182
+ added: number;
183
+ didUpdate: boolean;
184
+ failure: boolean;
185
+ filesAdded: number;
186
+ filesRemoved: number;
187
+ filesRemovedList: Array<string>;
188
+ filesUnmatched: number;
189
+ filesUpdated: number;
190
+ matched: number;
191
+ total: number;
192
+ unchecked: number;
193
+ uncheckedKeysByFile: Array<UncheckedSnapshot>;
194
+ unmatched: number;
195
+ updated: number;
196
+ }
197
+
198
+ interface RawSnapshotInfo {
199
+ file: string;
200
+ readonly?: boolean;
201
+ content?: string;
202
+ }
203
+
204
+ export { SnapshotState as S };
205
+ export type { DomainSnapshotAdapter as D, RawSnapshotInfo as R, UncheckedSnapshot as U, SnapshotStateOptions as a, SnapshotResult as b, DomainMatchResult as c, SnapshotData as d, SnapshotMatchOptions as e, SnapshotSerializer as f, SnapshotSummary as g, SnapshotUpdateState as h };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vitest/snapshot",
3
3
  "type": "module",
4
- "version": "4.1.3",
4
+ "version": "4.1.4",
5
5
  "description": "Vitest snapshot manager",
6
6
  "license": "MIT",
7
7
  "funding": "https://opencollective.com/vitest",
@@ -45,8 +45,8 @@
45
45
  "dependencies": {
46
46
  "magic-string": "^0.30.21",
47
47
  "pathe": "^2.0.3",
48
- "@vitest/utils": "4.1.3",
49
- "@vitest/pretty-format": "4.1.3"
48
+ "@vitest/pretty-format": "4.1.4",
49
+ "@vitest/utils": "4.1.4"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/natural-compare": "^1.4.3",
@@ -1,62 +0,0 @@
1
- import { OptionsReceived, Plugin } from '@vitest/pretty-format';
2
- import { S as SnapshotEnvironment } from './environment.d-DOJxxZV9.js';
3
-
4
- type SnapshotData = Record<string, string>;
5
- type SnapshotUpdateState = "all" | "new" | "none";
6
- type SnapshotSerializer = Plugin;
7
- interface SnapshotStateOptions {
8
- updateSnapshot: SnapshotUpdateState;
9
- snapshotEnvironment: SnapshotEnvironment;
10
- expand?: boolean;
11
- snapshotFormat?: OptionsReceived;
12
- resolveSnapshotPath?: (path: string, extension: string, context?: any) => string;
13
- }
14
- interface SnapshotMatchOptions {
15
- testId: string;
16
- testName: string;
17
- received: unknown;
18
- key?: string;
19
- inlineSnapshot?: string;
20
- isInline: boolean;
21
- error?: Error;
22
- rawSnapshot?: RawSnapshotInfo;
23
- assertionName?: string;
24
- }
25
- interface SnapshotResult {
26
- filepath: string;
27
- added: number;
28
- fileDeleted: boolean;
29
- matched: number;
30
- unchecked: number;
31
- uncheckedKeys: Array<string>;
32
- unmatched: number;
33
- updated: number;
34
- }
35
- interface UncheckedSnapshot {
36
- filePath: string;
37
- keys: Array<string>;
38
- }
39
- interface SnapshotSummary {
40
- added: number;
41
- didUpdate: boolean;
42
- failure: boolean;
43
- filesAdded: number;
44
- filesRemoved: number;
45
- filesRemovedList: Array<string>;
46
- filesUnmatched: number;
47
- filesUpdated: number;
48
- matched: number;
49
- total: number;
50
- unchecked: number;
51
- uncheckedKeysByFile: Array<UncheckedSnapshot>;
52
- unmatched: number;
53
- updated: number;
54
- }
55
-
56
- interface RawSnapshotInfo {
57
- file: string;
58
- readonly?: boolean;
59
- content?: string;
60
- }
61
-
62
- export type { RawSnapshotInfo as R, SnapshotStateOptions as S, UncheckedSnapshot as U, SnapshotMatchOptions as a, SnapshotResult as b, SnapshotData as c, SnapshotSerializer as d, SnapshotSummary as e, SnapshotUpdateState as f };