@argos-ci/vitest 0.4.7 → 0.6.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.mts CHANGED
@@ -1,5 +1,5 @@
1
- import "@argos-ci/core";
2
1
  import { ScreenshotMetadata } from "@argos-ci/util";
2
+ import "@argos-ci/core";
3
3
  import { ArgosAttachment } from "@argos-ci/playwright";
4
4
  import { StabilizationPluginOptions, ViewportOption } from "@argos-ci/browser";
5
5
  //#region src/metadata.d.ts
@@ -142,8 +142,8 @@ type SerializableSnapshotOptions = Omit<VitestSnapshotOptions, "serialize" | "na
142
142
  //#region src/index.d.ts
143
143
  declare module "vitest/browser" {
144
144
  interface BrowserCommands {
145
- argosScreenshot: (name: string, options?: VitestScreenshotOptions, test?: TestMetadata) => Promise<ArgosAttachment[]>;
146
- argosSnapshot: (name: string, content: string, options?: SerializableSnapshotOptions, test?: TestMetadata) => Promise<ArgosAttachment[]>;
145
+ argosScreenshot: (name: string, options?: VitestScreenshotOptions, test?: TestMetadata, captureIndex?: number | null) => Promise<ArgosAttachment[]>;
146
+ argosSnapshot: (name: string, content: string, options?: SerializableSnapshotOptions, test?: TestMetadata, captureIndex?: number | null) => Promise<ArgosAttachment[]>;
147
147
  }
148
148
  }
149
149
  /**
package/dist/index.mjs CHANGED
@@ -1,17 +1,32 @@
1
+ import { getTestRunKey, nextCaptureIndex } from "@argos-ci/util";
1
2
  //#region src/test-context.ts
2
3
  /**
4
+ * Entry point exposing `getCurrentTest` before Vitest 4.1, and removed
5
+ * altogether in Vitest 5.
6
+ *
7
+ * Held in a variable rather than written inline at the import: Vite's
8
+ * dependency pre-bundler resolves a literal specifier eagerly, and on Vitest 5
9
+ * that fails the whole optimize step over an export that no longer exists —
10
+ * even though the branch importing it cannot run there.
11
+ */
12
+ const LEGACY_SUITE_ENTRY = "vitest/suite";
13
+ /**
3
14
  * Get the current Vitest test task, or `undefined` when not inside a test.
4
15
  *
5
16
  * Vitest >= 4.1 exposes `TestRunner.getCurrentTest()` from the `vitest` entry
6
- * point; the `vitest/suite` export is deprecated. We prefer the new API and
7
- * fall back to `vitest/suite` for older 4.x. Both are imported dynamically so
8
- * importing `@argos-ci/vitest` in a non-Vitest environment does not pull Vitest
9
- * in — only call this once you know Vitest is available.
17
+ * point; the `vitest/suite` export is deprecated there and gone in Vitest 5. We
18
+ * prefer the new API and fall back to `vitest/suite` for older 4.x. Both are
19
+ * imported dynamically so importing `@argos-ci/vitest` in a non-Vitest
20
+ * environment does not pull Vitest in — only call this once you know Vitest is
21
+ * available.
10
22
  */
11
23
  async function getCurrentTest() {
12
24
  const runner = (await import("vitest")).TestRunner;
13
25
  if (runner?.getCurrentTest) return runner.getCurrentTest();
14
- return (await import("vitest/suite")).getCurrentTest();
26
+ return (await import(
27
+ /* @vite-ignore */
28
+ LEGACY_SUITE_ENTRY
29
+ )).getCurrentTest();
15
30
  }
16
31
  //#endregion
17
32
  //#region src/auto-name.ts
@@ -137,6 +152,23 @@ async function getTestMetadata() {
137
152
  if (!task) return null;
138
153
  return buildTestMetadata(task);
139
154
  }
155
+ /**
156
+ * Take the next capture index for the current Vitest test, or `null` outside a
157
+ * test. Screenshots and snapshots share the counter, so a test that mixes both
158
+ * still numbers them in the order it produced them.
159
+ *
160
+ * Runs on the test side, where the test context is available; the number then
161
+ * crosses the RPC boundary to the Node command.
162
+ */
163
+ async function takeCaptureIndex() {
164
+ const task = await getCurrentTest();
165
+ if (!task) return null;
166
+ return nextCaptureIndex(getTestRunKey({
167
+ id: task.id,
168
+ retry: task.result?.retryCount ?? void 0,
169
+ repeat: task.result?.repeatCount ?? void 0
170
+ }));
171
+ }
140
172
  //#endregion
141
173
  //#region ../../node_modules/.pnpm/tinyrainbow@3.1.1/node_modules/tinyrainbow/dist/index.js
142
174
  var b = {
@@ -221,7 +253,7 @@ function C({ force: e } = {}) {
221
253
  }
222
254
  var y = C();
223
255
  //#endregion
224
- //#region ../../node_modules/.pnpm/@vitest+pretty-format@4.1.10/node_modules/@vitest/pretty-format/dist/index.js
256
+ //#region ../../node_modules/.pnpm/@vitest+pretty-format@5.0.0/node_modules/@vitest/pretty-format/dist/index.js
225
257
  function _mergeNamespaces(n, m) {
226
258
  m.forEach(function(e) {
227
259
  e && typeof e !== "string" && !Array.isArray(e) && Object.keys(e).forEach(function(k) {
@@ -251,7 +283,7 @@ function getKeysOfEnumerableProperties(object, compareKeys) {
251
283
  * with spacing, indentation, and comma
252
284
  * without surrounding punctuation (for example, braces)
253
285
  */
254
- function printIteratorEntries(iterator, config, indentation, depth, refs, printer, separator = ": ") {
286
+ function printIteratorEntries(iterator, config, indentation, depth, refs, printer, separator = ": ", length) {
255
287
  let result = "";
256
288
  let width = 0;
257
289
  let current = iterator.next();
@@ -261,7 +293,7 @@ function printIteratorEntries(iterator, config, indentation, depth, refs, printe
261
293
  while (!current.done) {
262
294
  result += indentationNext;
263
295
  if (width++ === config.maxWidth) {
264
- result += "…";
296
+ result += typeof length === "number" ? `…(${length - width + 1})` : "…";
265
297
  break;
266
298
  }
267
299
  const name = printer(current.value[0], config, indentationNext, depth, refs);
@@ -280,7 +312,7 @@ function printIteratorEntries(iterator, config, indentation, depth, refs, printe
280
312
  * with spacing, indentation, and comma
281
313
  * without surrounding punctuation (braces or brackets)
282
314
  */
283
- function printIteratorValues(iterator, config, indentation, depth, refs, printer) {
315
+ function printIteratorValues(iterator, config, indentation, depth, refs, printer, length) {
284
316
  let result = "";
285
317
  let width = 0;
286
318
  let current = iterator.next();
@@ -290,7 +322,7 @@ function printIteratorValues(iterator, config, indentation, depth, refs, printer
290
322
  while (!current.done) {
291
323
  result += indentationNext;
292
324
  if (width++ === config.maxWidth) {
293
- result += "…";
325
+ result += typeof length === "number" ? `…(${length - width + 1})` : "…";
294
326
  break;
295
327
  }
296
328
  result += printer(current.value, config, indentationNext, depth, refs);
@@ -318,7 +350,7 @@ function printListItems(list, config, indentation, depth, refs, printer) {
318
350
  for (let i = 0; i < length; i++) {
319
351
  result += indentationNext;
320
352
  if (i === config.maxWidth) {
321
- result += "…";
353
+ result += `…(${length - i})`;
322
354
  break;
323
355
  }
324
356
  if (isDataView(list) || i in list) result += printer(isDataView(list) ? list.getInt8(i) : list[i], config, indentationNext, depth, refs);
@@ -334,17 +366,22 @@ function printListItems(list, config, indentation, depth, refs, printer) {
334
366
  * with spacing, indentation, and comma
335
367
  * without surrounding punctuation (for example, braces)
336
368
  */
337
- function printObjectProperties(val, config, indentation, depth, refs, printer) {
369
+ function printObjectProperties(val, config, indentation, depth, refs, printer, compareKeysOverride = config.compareKeys) {
338
370
  let result = "";
339
- const keys = getKeysOfEnumerableProperties(val, config.compareKeys);
371
+ const keys = getKeysOfEnumerableProperties(val, compareKeysOverride);
340
372
  if (keys.length > 0) {
341
373
  result += config.spacingOuter;
342
374
  const indentationNext = indentation + config.indent;
343
375
  for (let i = 0; i < keys.length; i++) {
376
+ result += indentationNext;
377
+ if (i === config.maxWidth) {
378
+ result += `…(${keys.length - i})`;
379
+ break;
380
+ }
344
381
  const key = keys[i];
345
- const name = printer(key, config, indentationNext, depth, refs);
382
+ const name = !config.quoteKeys && isUnquotableKey(key) ? key : printer(key, config, indentationNext, depth, refs);
346
383
  const value = printer(val[key], config, indentationNext, depth, refs);
347
- result += `${indentationNext + name}: ${value}`;
384
+ result += `${name}: ${value}`;
348
385
  if (i < keys.length - 1) result += `,${config.spacingInner}`;
349
386
  else if (!config.min) result += ",";
350
387
  }
@@ -352,6 +389,10 @@ function printObjectProperties(val, config, indentation, depth, refs, printer) {
352
389
  }
353
390
  return result;
354
391
  }
392
+ const keyStrRegExp = /^[a-z_]\w*$/i;
393
+ function isUnquotableKey(key) {
394
+ return typeof key === "string" && key !== "__proto__" && keyStrRegExp.test(key);
395
+ }
355
396
  const asymmetricMatcher = typeof Symbol === "function" && Symbol.for ? Symbol.for("jest.asymmetricMatcher") : 1267621;
356
397
  const SPACE$2 = " ";
357
398
  const serialize$5 = (val, config, indentation, depth, refs, printer) => {
@@ -410,7 +451,7 @@ function printProps(keys, props, config, indentation, depth, refs, printer) {
410
451
  const colors = config.colors;
411
452
  return keys.map((key) => {
412
453
  const value = props[key];
413
- if (typeof value === "string" && value[0] === "_" && value.startsWith("__vitest_") && value.match(/__vitest_\d+__/)) return "";
454
+ if (typeof value === "string" && value[0] === "_" && value.startsWith("__vitest_") && /__vitest_\d+__/.test(value)) return "";
414
455
  let printed = printer(value, config, indentationNext, depth, refs);
415
456
  if (typeof value !== "string") {
416
457
  if (printed.includes("\n")) printed = config.spacingOuter + indentationNext + printed + config.spacingOuter + indentation;
@@ -887,7 +928,7 @@ function printError(val) {
887
928
  * The first port of call for printing an object, handles most of the
888
929
  * data-types in JS.
889
930
  */
890
- function printBasicValue(val, printFunctionName, escapeRegex, escapeString) {
931
+ function printBasicValue(val, printFunctionName, escapeRegex, escapeString, singleQuote) {
891
932
  if (val === true || val === false) return `${val}`;
892
933
  if (val === void 0) return "undefined";
893
934
  if (val === null) return "null";
@@ -895,8 +936,12 @@ function printBasicValue(val, printFunctionName, escapeRegex, escapeString) {
895
936
  if (typeOf === "number") return printNumber(val);
896
937
  if (typeOf === "bigint") return printBigInt(val);
897
938
  if (typeOf === "string") {
898
- if (escapeString) return `"${val.replaceAll(/"|\\/g, "\\$&")}"`;
899
- return `"${val}"`;
939
+ const q = singleQuote ? "'" : "\"";
940
+ if (escapeString) {
941
+ const escapePattern = singleQuote ? /['\\]/g : /["\\]/g;
942
+ return `${q}${val.replaceAll(escapePattern, "\\$&")}${q}`;
943
+ }
944
+ return `${q}${val}${q}`;
900
945
  }
901
946
  if (typeOf === "function") return printFunction(val, printFunctionName);
902
947
  if (typeOf === "symbol") return printSymbol(val);
@@ -927,10 +972,10 @@ function printComplexValue(val, config, indentation, depth, refs, hasCalledToJSO
927
972
  if (config.callToJSON && !hitMaxDepth && val.toJSON && typeof val.toJSON === "function" && !hasCalledToJSON) return printer(val.toJSON(), config, indentation, depth, refs, true);
928
973
  const toStringed = toString.call(val);
929
974
  if (toStringed === "[object Arguments]") return hitMaxDepth ? "[Arguments]" : `${min ? "" : "Arguments "}[${printListItems(val, config, indentation, depth, refs, printer)}]`;
930
- if (isToStringedArrayType(toStringed)) return hitMaxDepth ? `[${val.constructor.name}]` : `${min ? "" : !config.printBasicPrototype && val.constructor.name === "Array" ? "" : `${val.constructor.name} `}[${printListItems(val, config, indentation, depth, refs, printer)}]`;
931
- if (toStringed === "[object Map]") return hitMaxDepth ? "[Map]" : `Map {${printIteratorEntries(val.entries(), config, indentation, depth, refs, printer, " => ")}}`;
932
- if (toStringed === "[object Set]") return hitMaxDepth ? "[Set]" : `Set {${printIteratorValues(val.values(), config, indentation, depth, refs, printer)}}`;
933
- return hitMaxDepth || isWindow(val) ? `[${getConstructorName(val)}]` : `${min ? "" : !config.printBasicPrototype && getConstructorName(val) === "Object" ? "" : `${getConstructorName(val)} `}{${printObjectProperties(val, config, indentation, depth, refs, printer)}}`;
975
+ if (isToStringedArrayType(toStringed)) return hitMaxDepth ? `[${val.constructor.name}]` : `${!config.printBasicPrototype && val.constructor.name === "Array" ? "" : `${val.constructor.name} `}[${printListItems(val, config, indentation, depth, refs, printer)}]`;
976
+ if (toStringed === "[object Map]") return hitMaxDepth ? "[Map]" : `Map {${printIteratorEntries(val.entries(), config, indentation, depth, refs, printer, " => ", val.size)}}`;
977
+ if (toStringed === "[object Set]") return hitMaxDepth ? "[Set]" : `Set {${printIteratorValues(val.values(), config, indentation, depth, refs, printer, val.size)}}`;
978
+ return hitMaxDepth || isWindow(val) ? `[${getConstructorName(val)}]` : `${!config.printBasicPrototype && getConstructorName(val) === "Object" ? "" : `${getConstructorName(val)} `}{${printObjectProperties(val, config, indentation, depth, refs, printer)}}`;
934
979
  }
935
980
  const ErrorPlugin = {
936
981
  test: (val) => val && val instanceof Error,
@@ -946,7 +991,7 @@ const ErrorPlugin = {
946
991
  ...rest
947
992
  };
948
993
  const name = val.name !== "Error" ? val.name : getConstructorName(val);
949
- return hitMaxDepth ? `[${name}]` : `${name} {${printIteratorEntries(Object.entries(entries).values(), config, indentation, depth, refs, printer)}}`;
994
+ return hitMaxDepth ? `[${name}]` : `${name} {${printObjectProperties(entries, config, indentation, depth, refs, printer, null)}}`;
950
995
  }
951
996
  };
952
997
  function isNewPlugin(plugin) {
@@ -982,7 +1027,7 @@ function printer(val, config, indentation, depth, refs, hasCalledToJSON) {
982
1027
  const plugin = findPlugin(config.plugins, val);
983
1028
  if (plugin !== null) result = printPlugin(plugin, val, config, indentation, depth, refs);
984
1029
  else {
985
- const basicResult = printBasicValue(val, config.printFunctionName, config.escapeRegex, config.escapeString);
1030
+ const basicResult = printBasicValue(val, config.printFunctionName, config.escapeRegex, config.escapeString, config.singleQuote);
986
1031
  if (basicResult !== null) result = basicResult;
987
1032
  else result = printComplexValue(val, config, indentation, depth, refs, hasCalledToJSON);
988
1033
  }
@@ -1014,7 +1059,11 @@ const DEFAULT_OPTIONS = {
1014
1059
  printBasicPrototype: true,
1015
1060
  printFunctionName: true,
1016
1061
  printShadowRoot: true,
1017
- theme: DEFAULT_THEME
1062
+ theme: DEFAULT_THEME,
1063
+ singleQuote: false,
1064
+ quoteKeys: true,
1065
+ spacingInner: "\n",
1066
+ spacingOuter: "\n"
1018
1067
  };
1019
1068
  function validateOptions(options) {
1020
1069
  for (const key of Object.keys(options)) if (!Object.hasOwn(DEFAULT_OPTIONS, key)) throw new Error(`pretty-format: Unknown option "${key}".`);
@@ -1059,11 +1108,13 @@ function getConfig(options) {
1059
1108
  maxWidth: options?.maxWidth ?? DEFAULT_OPTIONS.maxWidth,
1060
1109
  min: options?.min ?? DEFAULT_OPTIONS.min,
1061
1110
  plugins: options?.plugins ?? DEFAULT_OPTIONS.plugins,
1062
- printBasicPrototype: options?.printBasicPrototype ?? true,
1111
+ printBasicPrototype: options?.printBasicPrototype ?? !options?.min,
1063
1112
  printFunctionName: getPrintFunctionName(options),
1064
1113
  printShadowRoot: options?.printShadowRoot ?? true,
1065
- spacingInner: options?.min ? " " : "\n",
1066
- spacingOuter: options?.min ? "" : "\n",
1114
+ spacingInner: options?.spacingInner ?? (options?.min ? " " : "\n"),
1115
+ spacingOuter: options?.spacingOuter ?? (options?.min ? "" : "\n"),
1116
+ singleQuote: options?.singleQuote ?? DEFAULT_OPTIONS.singleQuote,
1117
+ quoteKeys: options?.quoteKeys ?? DEFAULT_OPTIONS.quoteKeys,
1067
1118
  maxOutputLength: options?.maxOutputLength ?? DEFAULT_OPTIONS.maxOutputLength,
1068
1119
  _outputLengthPerDepth: []
1069
1120
  };
@@ -1077,16 +1128,13 @@ function createIndent(indent) {
1077
1128
  * @param options Custom settings
1078
1129
  */
1079
1130
  function format(val, options) {
1080
- if (options) {
1081
- validateOptions(options);
1082
- if (options.plugins) {
1083
- const plugin = findPlugin(options.plugins, val);
1084
- if (plugin !== null) return printPlugin(plugin, val, getConfig(options), "", 0, []);
1085
- }
1086
- }
1087
- const basicResult = printBasicValue(val, getPrintFunctionName(options), getEscapeRegex(options), getEscapeString(options));
1131
+ if (options) validateOptions(options);
1132
+ const config = getConfig(options);
1133
+ const plugin = findPlugin(config.plugins, val);
1134
+ if (plugin !== null) return printPlugin(plugin, val, config, "", 0, []);
1135
+ const basicResult = printBasicValue(val, config.printFunctionName, config.escapeRegex, config.escapeString, config.singleQuote);
1088
1136
  if (basicResult !== null) return basicResult;
1089
- return printComplexValue(val, getConfig(options), "", 0, []);
1137
+ return printComplexValue(val, config, "", 0, []);
1090
1138
  }
1091
1139
  const plugins = {
1092
1140
  AsymmetricMatcher: plugin$5,
@@ -1142,9 +1190,13 @@ async function argosScreenshot(nameOrOptions, maybeOptions) {
1142
1190
  const name = typeof nameOrOptions === "string" ? nameOrOptions : void 0;
1143
1191
  const options = typeof nameOrOptions === "string" ? maybeOptions : nameOrOptions;
1144
1192
  if (!await checkIsVitestEnv()) return [];
1145
- const [resolvedName, test] = await Promise.all([resolveAutoName(name, { reservedLength: SCREENSHOT_NAME_RESERVED }), getTestMetadata()]);
1193
+ const [resolvedName, test, captureIndex] = await Promise.all([
1194
+ resolveAutoName(name, { reservedLength: SCREENSHOT_NAME_RESERVED }),
1195
+ getTestMetadata(),
1196
+ takeCaptureIndex()
1197
+ ]);
1146
1198
  const { server } = await import("vitest/browser");
1147
- return server.commands.argosScreenshot(resolvedName, options ?? {}, test);
1199
+ return server.commands.argosScreenshot(resolvedName, options ?? {}, test, captureIndex);
1148
1200
  }
1149
1201
  /**
1150
1202
  * Take an Argos snapshot of any serializable value, mimicking
@@ -1181,15 +1233,19 @@ async function argosSnapshot(content, options = {}) {
1181
1233
  if (!await checkIsVitestEnv()) return [];
1182
1234
  const rawExtension = options.extension ?? ".txt";
1183
1235
  const extension = rawExtension.startsWith(".") ? rawExtension : `.${rawExtension}`;
1184
- const [resolvedName, test] = await Promise.all([resolveAutoName(options.name, { reservedLength: 9 + extension.length + 11 }), getTestMetadata()]);
1236
+ const [resolvedName, test, captureIndex] = await Promise.all([
1237
+ resolveAutoName(options.name, { reservedLength: 9 + extension.length + 11 }),
1238
+ getTestMetadata(),
1239
+ takeCaptureIndex()
1240
+ ]);
1185
1241
  const serialized = serializeSnapshot(content, options);
1186
1242
  const { serialize: _serialize, name: _name, ...serializableOptions } = options;
1187
1243
  if (checkIsBrowserEnv()) {
1188
1244
  const { server } = await import("vitest/browser");
1189
- return server.commands.argosSnapshot(resolvedName, serialized, serializableOptions, test);
1245
+ return server.commands.argosSnapshot(resolvedName, serialized, serializableOptions, test, captureIndex);
1190
1246
  }
1191
- const { writeSnapshotFile } = await import("./snapshot-file-D8dL3z_T.mjs");
1192
- return writeSnapshotFile(resolvedName, serialized, serializableOptions, test);
1247
+ const { writeSnapshotFile } = await import("./snapshot-file-CZE4r9Wl.mjs");
1248
+ return writeSnapshotFile(resolvedName, serialized, serializableOptions, test, captureIndex);
1193
1249
  }
1194
1250
  /**
1195
1251
  * Check if we are running in a Vitest environment.
@@ -7,14 +7,25 @@ import { ViewportSize } from "@argos-ci/browser";
7
7
  */
8
8
  declare const VITEST_IFRAME_SELECTOR = "iframe[data-vitest=\"true\"]";
9
9
  /**
10
- * ID of the Vitest "tester" element that wraps the iframe with a `scale(...)`
11
- * transform.
10
+ * ID of the Vitest "tester" element wrapping the iframe.
12
11
  */
13
12
  declare const VITEST_TESTER_ID = "vitest-tester";
14
13
  /**
15
- * Remove the scale from the Vitest `#vitest-tester` element before taking a
16
- * screenshot to avoid ending up with small screenshots.
17
- * @returns A function to restore the scale after the screenshot.
14
+ * Undo the scale Vitest applies to the `#vitest-tester` element, so the
15
+ * screenshot is captured at full size instead of shrunk.
16
+ *
17
+ * Only some Vitest versions scale the tester. Up to Vitest 4 a viewport larger
18
+ * than the browser window is emulated by sizing the tester to the requested
19
+ * viewport and shrinking it with a CSS `transform: scale(...)`. From Vitest 5
20
+ * the real browser viewport is resized instead, so the tester carries no
21
+ * transform and there is nothing to undo — which is why an unscaled tester is
22
+ * a no-op rather than an error.
23
+ *
24
+ * Detection reads the *computed* transform, so a scale set from a stylesheet
25
+ * counts too, while the override goes on the inline style, which is the only
26
+ * layer guaranteed to win.
27
+ *
28
+ * @returns A function restoring the transform after the screenshot.
18
29
  */
19
30
  declare function resetTesterScale(ctx: BrowserCommandContext): Promise<() => Promise<void>>;
20
31
  /**
package/dist/internal.mjs CHANGED
@@ -7,11 +7,30 @@ import { readVersionFromPackage } from "@argos-ci/util";
7
7
  */
8
8
  const VITEST_IFRAME_SELECTOR = "iframe[data-vitest=\"true\"]";
9
9
  /**
10
- * ID of the Vitest "tester" element that wraps the iframe with a `scale(...)`
11
- * transform.
10
+ * ID of the Vitest "tester" element wrapping the iframe.
12
11
  */
13
12
  const VITEST_TESTER_ID = "vitest-tester";
14
13
  /**
14
+ * Dataset key holding the tester's inline `transform` from before Argos reset
15
+ * it.
16
+ *
17
+ * The presence of the key — not the value it holds — is what marks the
18
+ * transform as backed up: the original inline `transform` is usually an empty
19
+ * string, which is indistinguishable from "nothing was saved yet".
20
+ */
21
+ const TRANSFORM_BACKUP_KEY = "argosBckTransform";
22
+ /**
23
+ * Dataset key counting the screenshots currently holding the tester unscaled.
24
+ *
25
+ * Captures nest: a story calling `argosScreenshot` from its play function runs
26
+ * one inside the automatic screenshot taken after the test. Both share this one
27
+ * element, so without a count the inner restore would hand the scale back while
28
+ * the outer capture is still to come — and the outer screenshot would come out
29
+ * shrunk. Only the first reset saves and overrides the transform, and only the
30
+ * last restore puts it back.
31
+ */
32
+ const SCALE_HOLD_KEY = "argosScaleHold";
33
+ /**
15
34
  * Attribute holding the iframe's inline size from before Argos resized it, as
16
35
  * JSON.
17
36
  *
@@ -21,27 +40,79 @@ const VITEST_TESTER_ID = "vitest-tester";
21
40
  */
22
41
  const SIZE_BACKUP_ATTRIBUTE = "data-argos-size-backup";
23
42
  /**
24
- * Remove the scale from the Vitest `#vitest-tester` element before taking a
25
- * screenshot to avoid ending up with small screenshots.
26
- * @returns A function to restore the scale after the screenshot.
43
+ * Undo the scale Vitest applies to the `#vitest-tester` element, so the
44
+ * screenshot is captured at full size instead of shrunk.
45
+ *
46
+ * Only some Vitest versions scale the tester. Up to Vitest 4 a viewport larger
47
+ * than the browser window is emulated by sizing the tester to the requested
48
+ * viewport and shrinking it with a CSS `transform: scale(...)`. From Vitest 5
49
+ * the real browser viewport is resized instead, so the tester carries no
50
+ * transform and there is nothing to undo — which is why an unscaled tester is
51
+ * a no-op rather than an error.
52
+ *
53
+ * Detection reads the *computed* transform, so a scale set from a stylesheet
54
+ * counts too, while the override goes on the inline style, which is the only
55
+ * layer guaranteed to win.
56
+ *
57
+ * @returns A function restoring the transform after the screenshot.
27
58
  */
28
59
  async function resetTesterScale(ctx) {
29
- await ctx.page.evaluate((testerId) => {
30
- const tester = document.getElementById(testerId);
31
- if (!(tester instanceof HTMLElement)) return;
32
- if (!tester.getAttribute("data-scale")) throw new Error("Vitest iframe data-scale attribute not found");
33
- tester.dataset.bckTransform = tester.style.transform;
34
- tester.style.transform = `scale(1)`;
35
- }, VITEST_TESTER_ID);
60
+ await ctx.page.evaluate(resetTesterScaleInPage, {
61
+ testerId: VITEST_TESTER_ID,
62
+ backupKey: TRANSFORM_BACKUP_KEY,
63
+ holdKey: SCALE_HOLD_KEY
64
+ });
36
65
  return async () => {
37
- await ctx.page.evaluate((testerId) => {
38
- const tester = document.getElementById(testerId);
39
- if (!(tester instanceof HTMLElement)) return;
40
- tester.style.transform = tester.dataset.bckTransform ?? "";
41
- }, VITEST_TESTER_ID);
66
+ await ctx.page.evaluate(restoreTesterScaleInPage, {
67
+ testerId: VITEST_TESTER_ID,
68
+ backupKey: TRANSFORM_BACKUP_KEY,
69
+ holdKey: SCALE_HOLD_KEY
70
+ });
42
71
  };
43
72
  }
44
73
  /**
74
+ * Body of {@link resetTesterScale}, running in the page.
75
+ *
76
+ * Exported for tests only, and self-contained on purpose: it is serialized to
77
+ * the browser by `page.evaluate`, so it can reference nothing but its argument
78
+ * and page globals.
79
+ */
80
+ function resetTesterScaleInPage(args) {
81
+ const { testerId, backupKey, holdKey } = args;
82
+ const tester = document.getElementById(testerId);
83
+ if (!(tester instanceof HTMLElement)) return;
84
+ const held = Number(tester.dataset[holdKey] ?? "0");
85
+ tester.dataset[holdKey] = String(held + 1);
86
+ if (held > 0) return;
87
+ const { transform } = getComputedStyle(tester);
88
+ if (!transform || transform === "none") return;
89
+ const matrix = new DOMMatrixReadOnly(transform);
90
+ if (matrix.a === 1 && matrix.d === 1) return;
91
+ tester.dataset[backupKey] = tester.style.transform;
92
+ tester.style.transform = "scale(1)";
93
+ }
94
+ /**
95
+ * Body of the function {@link resetTesterScale} returns, running in the page.
96
+ *
97
+ * Exported for tests only, and self-contained for the same reason as
98
+ * {@link resetTesterScaleInPage}.
99
+ */
100
+ function restoreTesterScaleInPage(args) {
101
+ const { testerId, backupKey, holdKey } = args;
102
+ const tester = document.getElementById(testerId);
103
+ if (!(tester instanceof HTMLElement)) return;
104
+ const held = Math.max(0, Number(tester.dataset[holdKey] ?? "0") - 1);
105
+ if (held > 0) {
106
+ tester.dataset[holdKey] = String(held);
107
+ return;
108
+ }
109
+ delete tester.dataset[holdKey];
110
+ const backup = tester.dataset[backupKey];
111
+ if (backup === void 0) return;
112
+ tester.style.transform = backup;
113
+ delete tester.dataset[backupKey];
114
+ }
115
+ /**
45
116
  * Resize the Vitest iframe.
46
117
  *
47
118
  * The story/test renders inside an `<iframe data-vitest="true">` on the host
package/dist/plugin.d.mts CHANGED
@@ -3,8 +3,7 @@ import { StabilizationPluginOptions, ViewportOption } from "@argos-ci/browser";
3
3
  import { ScreenshotMetadata } from "@argos-ci/util";
4
4
  import { UploadParameters } from "@argos-ci/core";
5
5
  import { Plugin } from "vitest/config";
6
- import { BrowserCommand, Vitest } from "vitest/node";
7
- import { Reporter } from "vitest/reporters";
6
+ import { BrowserCommand, Reporter, Vitest } from "vitest/node";
8
7
  //#region src/options.d.ts
9
8
  /**
10
9
  * Configuration for the Argos Vitest reporter.
@@ -174,7 +173,7 @@ type TestMetadata = ScreenshotMetadata["test"];
174
173
  * Arguments of the `argosScreenshot` browser command.
175
174
  * Only serializable values cross the browser/node RPC boundary.
176
175
  */
177
- type ArgosScreenshotCommandArgs = [name: string, options?: VitestScreenshotOptions, test?: TestMetadata];
176
+ type ArgosScreenshotCommandArgs = [name: string, options?: VitestScreenshotOptions, test?: TestMetadata, captureIndex?: number | null];
178
177
  /**
179
178
  * Create the `argosScreenshot` browser command used to capture Argos
180
179
  * screenshots from Vitest browser tests.
@@ -191,7 +190,7 @@ declare const createArgosScreenshotCommand: (pluginOptions?: ArgosVitestPluginOp
191
190
  * Only serializable values cross the browser/node RPC boundary — the value is
192
191
  * already serialized to a string on the browser side.
193
192
  */
194
- type ArgosSnapshotCommandArgs = [name: string, content: string, options?: SerializableSnapshotOptions, test?: TestMetadata];
193
+ type ArgosSnapshotCommandArgs = [name: string, content: string, options?: SerializableSnapshotOptions, test?: TestMetadata, captureIndex?: number | null];
195
194
  /**
196
195
  * Create the `argosSnapshot` browser command used to write serialized snapshots
197
196
  * from Vitest browser tests. The serialized string is produced on the browser
package/dist/plugin.mjs CHANGED
@@ -11,11 +11,30 @@ import { getSnapshotMimeType, readConfig, upload } from "@argos-ci/core";
11
11
  */
12
12
  const VITEST_IFRAME_SELECTOR = "iframe[data-vitest=\"true\"]";
13
13
  /**
14
- * ID of the Vitest "tester" element that wraps the iframe with a `scale(...)`
15
- * transform.
14
+ * ID of the Vitest "tester" element wrapping the iframe.
16
15
  */
17
16
  const VITEST_TESTER_ID = "vitest-tester";
18
17
  /**
18
+ * Dataset key holding the tester's inline `transform` from before Argos reset
19
+ * it.
20
+ *
21
+ * The presence of the key — not the value it holds — is what marks the
22
+ * transform as backed up: the original inline `transform` is usually an empty
23
+ * string, which is indistinguishable from "nothing was saved yet".
24
+ */
25
+ const TRANSFORM_BACKUP_KEY = "argosBckTransform";
26
+ /**
27
+ * Dataset key counting the screenshots currently holding the tester unscaled.
28
+ *
29
+ * Captures nest: a story calling `argosScreenshot` from its play function runs
30
+ * one inside the automatic screenshot taken after the test. Both share this one
31
+ * element, so without a count the inner restore would hand the scale back while
32
+ * the outer capture is still to come — and the outer screenshot would come out
33
+ * shrunk. Only the first reset saves and overrides the transform, and only the
34
+ * last restore puts it back.
35
+ */
36
+ const SCALE_HOLD_KEY = "argosScaleHold";
37
+ /**
19
38
  * Attribute holding the iframe's inline size from before Argos resized it, as
20
39
  * JSON.
21
40
  *
@@ -25,27 +44,79 @@ const VITEST_TESTER_ID = "vitest-tester";
25
44
  */
26
45
  const SIZE_BACKUP_ATTRIBUTE = "data-argos-size-backup";
27
46
  /**
28
- * Remove the scale from the Vitest `#vitest-tester` element before taking a
29
- * screenshot to avoid ending up with small screenshots.
30
- * @returns A function to restore the scale after the screenshot.
47
+ * Undo the scale Vitest applies to the `#vitest-tester` element, so the
48
+ * screenshot is captured at full size instead of shrunk.
49
+ *
50
+ * Only some Vitest versions scale the tester. Up to Vitest 4 a viewport larger
51
+ * than the browser window is emulated by sizing the tester to the requested
52
+ * viewport and shrinking it with a CSS `transform: scale(...)`. From Vitest 5
53
+ * the real browser viewport is resized instead, so the tester carries no
54
+ * transform and there is nothing to undo — which is why an unscaled tester is
55
+ * a no-op rather than an error.
56
+ *
57
+ * Detection reads the *computed* transform, so a scale set from a stylesheet
58
+ * counts too, while the override goes on the inline style, which is the only
59
+ * layer guaranteed to win.
60
+ *
61
+ * @returns A function restoring the transform after the screenshot.
31
62
  */
32
63
  async function resetTesterScale(ctx) {
33
- await ctx.page.evaluate((testerId) => {
34
- const tester = document.getElementById(testerId);
35
- if (!(tester instanceof HTMLElement)) return;
36
- if (!tester.getAttribute("data-scale")) throw new Error("Vitest iframe data-scale attribute not found");
37
- tester.dataset.bckTransform = tester.style.transform;
38
- tester.style.transform = `scale(1)`;
39
- }, VITEST_TESTER_ID);
64
+ await ctx.page.evaluate(resetTesterScaleInPage, {
65
+ testerId: VITEST_TESTER_ID,
66
+ backupKey: TRANSFORM_BACKUP_KEY,
67
+ holdKey: SCALE_HOLD_KEY
68
+ });
40
69
  return async () => {
41
- await ctx.page.evaluate((testerId) => {
42
- const tester = document.getElementById(testerId);
43
- if (!(tester instanceof HTMLElement)) return;
44
- tester.style.transform = tester.dataset.bckTransform ?? "";
45
- }, VITEST_TESTER_ID);
70
+ await ctx.page.evaluate(restoreTesterScaleInPage, {
71
+ testerId: VITEST_TESTER_ID,
72
+ backupKey: TRANSFORM_BACKUP_KEY,
73
+ holdKey: SCALE_HOLD_KEY
74
+ });
46
75
  };
47
76
  }
48
77
  /**
78
+ * Body of {@link resetTesterScale}, running in the page.
79
+ *
80
+ * Exported for tests only, and self-contained on purpose: it is serialized to
81
+ * the browser by `page.evaluate`, so it can reference nothing but its argument
82
+ * and page globals.
83
+ */
84
+ function resetTesterScaleInPage(args) {
85
+ const { testerId, backupKey, holdKey } = args;
86
+ const tester = document.getElementById(testerId);
87
+ if (!(tester instanceof HTMLElement)) return;
88
+ const held = Number(tester.dataset[holdKey] ?? "0");
89
+ tester.dataset[holdKey] = String(held + 1);
90
+ if (held > 0) return;
91
+ const { transform } = getComputedStyle(tester);
92
+ if (!transform || transform === "none") return;
93
+ const matrix = new DOMMatrixReadOnly(transform);
94
+ if (matrix.a === 1 && matrix.d === 1) return;
95
+ tester.dataset[backupKey] = tester.style.transform;
96
+ tester.style.transform = "scale(1)";
97
+ }
98
+ /**
99
+ * Body of the function {@link resetTesterScale} returns, running in the page.
100
+ *
101
+ * Exported for tests only, and self-contained for the same reason as
102
+ * {@link resetTesterScaleInPage}.
103
+ */
104
+ function restoreTesterScaleInPage(args) {
105
+ const { testerId, backupKey, holdKey } = args;
106
+ const tester = document.getElementById(testerId);
107
+ if (!(tester instanceof HTMLElement)) return;
108
+ const held = Math.max(0, Number(tester.dataset[holdKey] ?? "0") - 1);
109
+ if (held > 0) {
110
+ tester.dataset[holdKey] = String(held);
111
+ return;
112
+ }
113
+ delete tester.dataset[holdKey];
114
+ const backup = tester.dataset[backupKey];
115
+ if (backup === void 0) return;
116
+ tester.style.transform = backup;
117
+ delete tester.dataset[backupKey];
118
+ }
119
+ /**
49
120
  * Resize the Vitest iframe.
50
121
  *
51
122
  * The story/test renders inside an `<iframe data-vitest="true">` on the host
@@ -188,7 +259,7 @@ async function getVitestVersion() {
188
259
  * and are merged with the serializable per-call options (per-call wins).
189
260
  */
190
261
  const createArgosScreenshotCommand = (pluginOptions = {}) => {
191
- return async (ctx, name, options, test) => {
262
+ return async (ctx, name, options, test, captureIndex) => {
192
263
  if (!name) throw new Error("The `name` argument is required.");
193
264
  const merged = {
194
265
  ...pluginOptions,
@@ -208,7 +279,8 @@ const createArgosScreenshotCommand = (pluginOptions = {}) => {
208
279
  },
209
280
  playwrightLibraries: ["vitest"],
210
281
  viewport,
211
- test
282
+ test,
283
+ captureIndex
212
284
  });
213
285
  };
214
286
  const attachments = [];
@@ -276,7 +348,7 @@ function normalizeExtension(extension) {
276
348
  * This is the shared Node-side primitive used by both the browser command (which
277
349
  * receives the already-serialized string over RPC) and the Node code path.
278
350
  */
279
- async function writeSnapshotFile(name, content, options = {}, test) {
351
+ async function writeSnapshotFile(name, content, options = {}, test, captureIndex) {
280
352
  if (!name) throw new Error("The `name` argument is required.");
281
353
  const root = options.root ?? "./snapshots";
282
354
  const extension = normalizeExtension(options.extension ?? DEFAULT_EXTENSION);
@@ -298,7 +370,8 @@ async function writeSnapshotFile(name, content, options = {}, test) {
298
370
  version: sdkVersion
299
371
  },
300
372
  ...tags ? { tags } : {},
301
- ...resolvedTest ? { test: resolvedTest } : {}
373
+ ...resolvedTest ? { test: resolvedTest } : {},
374
+ ...captureIndex != null ? { capture: { index: captureIndex } } : {}
302
375
  };
303
376
  await createDirectory(dirname(snapshotPath));
304
377
  await Promise.all([writeFile(snapshotPath, content, "utf-8"), writeMetadata(snapshotPath, metadata)]);
@@ -320,12 +393,12 @@ async function writeSnapshotFile(name, content, options = {}, test) {
320
393
  * side and this command writes it (and its metadata) to disk on the Node side.
321
394
  */
322
395
  const createArgosSnapshotCommand = (pluginOptions = {}) => {
323
- return async (_ctx, name, content, options, test) => {
396
+ return async (_ctx, name, content, options, test, captureIndex) => {
324
397
  if (!name) throw new Error("The `name` argument is required.");
325
398
  return writeSnapshotFile(name, content, {
326
399
  root: pluginOptions.root,
327
400
  ...options
328
- }, test);
401
+ }, test, captureIndex);
329
402
  };
330
403
  };
331
404
  //#endregion
@@ -1,8 +1,8 @@
1
1
  import { createRequire } from "node:module";
2
+ import { createDirectory, getGitRepositoryPath, getMetadataPath, getScreenshotName, readVersionFromPackage, writeMetadata } from "@argos-ci/util";
2
3
  import { writeFile } from "node:fs/promises";
3
4
  import { dirname, relative, resolve } from "node:path";
4
5
  import { getSnapshotMimeType } from "@argos-ci/core";
5
- import { createDirectory, getGitRepositoryPath, getMetadataPath, getScreenshotName, readVersionFromPackage, writeMetadata } from "@argos-ci/util";
6
6
  //#region src/version.ts
7
7
  const require = createRequire(import.meta.url);
8
8
  /**
@@ -60,7 +60,7 @@ function normalizeExtension(extension) {
60
60
  * This is the shared Node-side primitive used by both the browser command (which
61
61
  * receives the already-serialized string over RPC) and the Node code path.
62
62
  */
63
- async function writeSnapshotFile(name, content, options = {}, test) {
63
+ async function writeSnapshotFile(name, content, options = {}, test, captureIndex) {
64
64
  if (!name) throw new Error("The `name` argument is required.");
65
65
  const root = options.root ?? "./snapshots";
66
66
  const extension = normalizeExtension(options.extension ?? DEFAULT_EXTENSION);
@@ -82,7 +82,8 @@ async function writeSnapshotFile(name, content, options = {}, test) {
82
82
  version: sdkVersion
83
83
  },
84
84
  ...tags ? { tags } : {},
85
- ...resolvedTest ? { test: resolvedTest } : {}
85
+ ...resolvedTest ? { test: resolvedTest } : {},
86
+ ...captureIndex != null ? { capture: { index: captureIndex } } : {}
86
87
  };
87
88
  await createDirectory(dirname(snapshotPath));
88
89
  await Promise.all([writeFile(snapshotPath, content, "utf-8"), writeMetadata(snapshotPath, metadata)]);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@argos-ci/vitest",
3
3
  "description": "Vitest SDK for visual testing with Argos.",
4
- "version": "0.4.7",
4
+ "version": "0.6.0",
5
5
  "author": "Smooth Code",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -52,9 +52,9 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@argos-ci/browser": "6.4.5",
55
- "@argos-ci/core": "6.8.2",
56
- "@argos-ci/playwright": "7.4.7",
57
- "@argos-ci/util": "4.1.0"
55
+ "@argos-ci/core": "6.8.3",
56
+ "@argos-ci/playwright": "7.5.0",
57
+ "@argos-ci/util": "4.2.0"
58
58
  },
59
59
  "peerDependencies": {
60
60
  "@vitest/browser": ">=4",
@@ -64,9 +64,9 @@
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/node": "catalog:",
67
- "@vitest/browser": "^4.1.10",
68
- "@vitest/browser-playwright": "^4.1.10",
69
- "@vitest/pretty-format": "^4.1.10",
67
+ "@vitest/browser": "^5.0.0",
68
+ "@vitest/browser-playwright": "^5.0.0",
69
+ "@vitest/pretty-format": "^5.0.0",
70
70
  "playwright": "^1.62.1",
71
71
  "vitest": "catalog:"
72
72
  },
@@ -77,9 +77,10 @@
77
77
  "install-playwright": "playwright install chromium --with-deps",
78
78
  "build-e2e": "pnpm run install-playwright",
79
79
  "e2e": "cross-env BUILD_NAME=\"argos-vitest-e2e-node-$NODE_VERSION-$OS\" UPLOAD_TO_ARGOS=true pnpm run test-e2e",
80
+ "e2e-compat": "cross-env BUILD_NAME=\"argos-vitest4-e2e-node-$NODE_VERSION-$OS\" UPLOAD_TO_ARGOS=true pnpm run test-e2e",
80
81
  "check-types": "tsc",
81
82
  "check-format": "prettier --check --ignore-unknown --ignore-path=./.gitignore --ignore-path=../../.gitignore --ignore-path=../../.prettierignore .",
82
83
  "lint": "eslint ."
83
84
  },
84
- "gitHead": "30a374b2c0f2b67b7a90fd4d22e395b70c191e0c"
85
+ "gitHead": "4a87379645664a85d91dfbb3571bb57b682ec69e"
85
86
  }