@argos-ci/vitest 0.2.3 → 0.3.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/README.md CHANGED
@@ -80,6 +80,25 @@ test("Button", async () => {
80
80
  });
81
81
  ```
82
82
 
83
+ ### Automatic naming
84
+
85
+ The name is optional. When omitted, Argos derives one from the current test,
86
+ mimicking [Vitest snapshots](https://vitest.dev/guide/snapshot). Several unnamed
87
+ captures in the same test get an incrementing counter so they stay unique:
88
+
89
+ ```ts
90
+ test("Button", async () => {
91
+ render(<Button>Click me</Button>);
92
+ await argosScreenshot(); // -> "src/Button.test.tsx > Button 1"
93
+ await argosScreenshot(); // -> "src/Button.test.tsx > Button 2"
94
+ });
95
+ ```
96
+
97
+ Unlike Vitest — which keeps a per-file `.snap`, so its keys only need to be
98
+ unique within a file — Argos names are **global** across the build. The
99
+ generated name therefore includes the test file path, so two tests with the same
100
+ title in different files never collide.
101
+
83
102
  ## Snapshots
84
103
 
85
104
  `argosSnapshot` captures a snapshot of any value — not just a screenshot — and
@@ -87,6 +106,10 @@ uploads it to Argos to diff across builds, mimicking
87
106
  [Vitest snapshots](https://vitest.dev/guide/snapshot). Unlike `argosScreenshot`,
88
107
  it does not need a browser and works in **both** browser and Node tests.
89
108
 
109
+ The value comes first; the name is optional. Omit it to auto-name the snapshot
110
+ from the current test (like screenshots above), or pass `options.name` to set it
111
+ explicitly:
112
+
90
113
  ```ts
91
114
  import { test } from "vitest";
92
115
  import { argosSnapshot } from "@argos-ci/vitest";
@@ -95,7 +118,8 @@ test("API response", async () => {
95
118
  const user = await fetchUser();
96
119
  // Objects are serialized with `@vitest/pretty-format`, strings are written
97
120
  // verbatim.
98
- await argosSnapshot("user", user);
121
+ await argosSnapshot(user); // -> "src/user.test.ts > API response 1"
122
+ await argosSnapshot(user, { name: "user" }); // explicit name
99
123
  });
100
124
  ```
101
125
 
@@ -103,14 +127,16 @@ Use the `extension` option to control how Argos renders and diffs the snapshot,
103
127
  and `tag` to attach tags:
104
128
 
105
129
  ```ts
106
- await argosSnapshot("config", JSON.stringify(config, null, 2), {
130
+ await argosSnapshot(JSON.stringify(config, null, 2), {
131
+ name: "config",
107
132
  extension: ".json",
108
133
  tag: "config",
109
134
  });
110
135
  ```
111
136
 
112
- Snapshots are written to the same folder as screenshots and uploaded by the
113
- reporter when `uploadToArgos` is enabled.
137
+ Screenshots and snapshots are both written to the `./snapshots` directory by
138
+ default (configurable via the plugin `root` option) and uploaded by the reporter
139
+ when `uploadToArgos` is enabled.
114
140
 
115
141
  ## Links
116
142
 
package/dist/index.d.mts CHANGED
@@ -1,6 +1,11 @@
1
+ import { ScreenshotMetadata } from "@argos-ci/util";
1
2
  import { ArgosAttachment } from "@argos-ci/playwright";
2
3
  import { StabilizationPluginOptions, ViewportOption } from "@argos-ci/browser";
3
4
 
5
+ //#region src/metadata.d.ts
6
+ /** The `test` slice of {@link ScreenshotMetadata}. */
7
+ type TestMetadata = ScreenshotMetadata["test"];
8
+ //#endregion
4
9
  //#region src/options.d.ts
5
10
  /**
6
11
  * Options passed when calling `argosScreenshot` from a browser test.
@@ -76,12 +81,22 @@ interface VitestScreenshotOptions {
76
81
  * applied on the test side *before* the value is sent to Node.
77
82
  */
78
83
  interface VitestSnapshotOptions {
84
+ /**
85
+ * Unique name of the snapshot.
86
+ *
87
+ * When omitted, Argos generates one automatically from the current test,
88
+ * mimicking {@link https://vitest.dev/guide/snapshot Vitest's snapshot naming}.
89
+ * The generated name includes the test file path so names stay unique across
90
+ * files (Argos names are global across the build, unlike Vitest's per-file
91
+ * `.snap`).
92
+ */
93
+ name?: string;
79
94
  /**
80
95
  * Folder where the snapshot is written.
81
96
  *
82
- * In Node tests this defaults to `"./screenshots"`. In browser tests it
97
+ * In Node tests this defaults to `"./snapshots"`. In browser tests it
83
98
  * defaults to the plugin `root` and can be overridden per call.
84
- * @default "./screenshots"
99
+ * @default "./snapshots"
85
100
  */
86
101
  root?: string;
87
102
  /**
@@ -102,16 +117,17 @@ interface VitestSnapshotOptions {
102
117
  }
103
118
  /**
104
119
  * Subset of {@link VitestSnapshotOptions} that can cross the Vitest
105
- * browser/node RPC boundary (everything but `serialize`, which is applied
106
- * before the value is sent to Node).
120
+ * browser/node RPC boundary. Excludes `serialize` (applied before the value is
121
+ * sent to Node) and `name` (resolved on the test side and passed to the Node
122
+ * primitives as a separate argument).
107
123
  */
108
- type SerializableSnapshotOptions = Omit<VitestSnapshotOptions, "serialize">;
124
+ type SerializableSnapshotOptions = Omit<VitestSnapshotOptions, "serialize" | "name">;
109
125
  //#endregion
110
126
  //#region src/index.d.ts
111
127
  declare module "vitest/browser" {
112
128
  interface BrowserCommands {
113
- argosScreenshot: (name: string, options?: VitestScreenshotOptions) => Promise<ArgosAttachment[]>;
114
- argosSnapshot: (name: string, content: string, options?: SerializableSnapshotOptions) => Promise<ArgosAttachment[]>;
129
+ argosScreenshot: (name: string, options?: VitestScreenshotOptions, test?: TestMetadata) => Promise<ArgosAttachment[]>;
130
+ argosSnapshot: (name: string, content: string, options?: SerializableSnapshotOptions, test?: TestMetadata) => Promise<ArgosAttachment[]>;
115
131
  }
116
132
  }
117
133
  /**
@@ -120,6 +136,12 @@ declare module "vitest/browser" {
120
136
  * Requires the {@link https://www.npmjs.com/package/@argos-ci/vitest Argos Vitest plugin}
121
137
  * to be registered in your Vitest config.
122
138
  *
139
+ * The `name` is optional: when omitted, Argos generates one automatically from
140
+ * the current test, mimicking {@link https://vitest.dev/guide/snapshot Vitest's
141
+ * snapshot naming}. The name includes the test file path (Argos names are global
142
+ * across the build, unlike Vitest's per-file `.snap`), so names stay unique
143
+ * across files.
144
+ *
123
145
  * @example
124
146
  * ```ts
125
147
  * import { render } from "vitest-browser-react";
@@ -127,15 +149,18 @@ declare module "vitest/browser" {
127
149
  *
128
150
  * test("Button", async () => {
129
151
  * render(<Button>Click me</Button>);
130
- * await argosScreenshot("button");
152
+ * await argosScreenshot("button"); // explicit name
153
+ * await argosScreenshot(); // -> "src/Button.test.tsx > Button 1"
131
154
  * });
132
155
  * ```
133
156
  *
134
- * @param name - Unique name of the screenshot.
157
+ * @param name - Unique name of the screenshot. Omit to generate one from the
158
+ * current test.
135
159
  * @param options - Serializable screenshot options.
136
160
  * @returns The attachments captured, or an empty array outside of Vitest.
137
161
  */
138
162
  declare function argosScreenshot(name: string, options?: VitestScreenshotOptions): Promise<ArgosAttachment[]>;
163
+ declare function argosScreenshot(options?: VitestScreenshotOptions): Promise<ArgosAttachment[]>;
139
164
  /**
140
165
  * Take an Argos snapshot of any serializable value, mimicking
141
166
  * {@link https://vitest.dev/guide/snapshot Vitest snapshots}.
@@ -145,23 +170,29 @@ declare function argosScreenshot(name: string, options?: VitestScreenshotOptions
145
170
  * file that Argos picks up and diffs across builds. It works both in Vitest
146
171
  * browser tests and in plain Node tests.
147
172
  *
173
+ * The name is optional: pass it via `options.name`, or omit it to have Argos
174
+ * generate one automatically from the current test, mimicking
175
+ * {@link https://vitest.dev/guide/snapshot Vitest's snapshot naming}. The name
176
+ * includes the test file path (Argos names are global across the build, unlike
177
+ * Vitest's per-file `.snap`), so names stay unique across files.
178
+ *
148
179
  * @example
149
180
  * ```ts
150
181
  * import { argosSnapshot } from "@argos-ci/vitest";
151
182
  *
152
183
  * test("API response", async () => {
153
184
  * const data = await fetchUser();
154
- * await argosSnapshot("user", data);
185
+ * await argosSnapshot(data); // -> "src/user.test.ts > API response 1"
186
+ * await argosSnapshot(data, { name: "user" }); // explicit name
155
187
  * });
156
188
  * ```
157
189
  *
158
- * @param name - Unique name of the snapshot.
159
190
  * @param content - The value to snapshot. Strings are written as-is; any other
160
191
  * value is serialized.
161
- * @param options - Snapshot options.
192
+ * @param options - Snapshot options, including an optional `name`.
162
193
  * @returns The attachments written, or an empty array outside of Vitest.
163
194
  */
164
- declare function argosSnapshot(name: string, content: unknown, options?: VitestSnapshotOptions): Promise<ArgosAttachment[]>;
195
+ declare function argosSnapshot(content: unknown, options?: VitestSnapshotOptions): Promise<ArgosAttachment[]>;
165
196
  /**
166
197
  * Check if we are running in a Vitest environment.
167
198
  */
package/dist/index.mjs CHANGED
@@ -1,3 +1,143 @@
1
+ //#region src/test-context.ts
2
+ /**
3
+ * Get the current Vitest test task, or `undefined` when not inside a test.
4
+ *
5
+ * 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.
10
+ */
11
+ async function getCurrentTest() {
12
+ const runner = (await import("vitest")).TestRunner;
13
+ if (runner?.getCurrentTest) return runner.getCurrentTest();
14
+ return (await import("vitest/suite")).getCurrentTest();
15
+ }
16
+ //#endregion
17
+ //#region src/auto-name.ts
18
+ /**
19
+ * Maximum length of a single filename component on common filesystems (ext4,
20
+ * APFS, NTFS all cap at 255 bytes/chars).
21
+ */
22
+ const MAX_FILENAME_LENGTH = 255;
23
+ /**
24
+ * Per-test counter used to generate unique automatic names, keyed by the
25
+ * current test task. A `WeakMap` lets the entries be garbage-collected with the
26
+ * test tasks, so nothing leaks across files or runs.
27
+ */
28
+ const counters = /* @__PURE__ */ new WeakMap();
29
+ /**
30
+ * Truncate `text` to `length` characters, replacing the tail with an ellipsis.
31
+ */
32
+ function truncate(text, length) {
33
+ if (text.length <= length) return text;
34
+ return `${text.slice(0, Math.max(0, length - 1))}…`;
35
+ }
36
+ /**
37
+ * Resolve the name of a screenshot or snapshot.
38
+ *
39
+ * When an explicit `name` is provided, it is returned as-is. Otherwise a name
40
+ * is generated automatically from the current Vitest test, mimicking
41
+ * {@link https://vitest.dev/guide/snapshot Vitest's own snapshot naming}:
42
+ * `` `${test.fullName} ${count}` ``, where `count` increments per test so
43
+ * several auto-named captures in the same test stay unique.
44
+ *
45
+ * Unlike Vitest — which stores snapshots in a per-file `.snap`, so its keys only
46
+ * need to be unique within a file — Argos names are global across the whole
47
+ * build. The name therefore includes the test file path so two tests with the
48
+ * same title in different files do not collide. Vitest's `fullName` already
49
+ * starts with the file path; we prepend it defensively in case that changes.
50
+ *
51
+ * The name is kept short enough that the final filename — including the
52
+ * extension(s) the caller appends (e.g. `.snapshot.txt`, ` vw-800.png`) and the
53
+ * `.argos.json` metadata sidecar — fits within {@link MAX_FILENAME_LENGTH}.
54
+ * Mirroring the Playwright SDK, when the readable name would overflow we fall
55
+ * back to the test id (short and unique) so truncated names never collide, then
56
+ * keep as much of the readable name as fits.
57
+ *
58
+ * Must only be called once we know we are running inside Vitest (the caller
59
+ * checks `checkIsVitestEnv` first).
60
+ *
61
+ * @param name - Explicit name, or `undefined`/empty to auto-generate one.
62
+ * @param options.reservedLength - Number of characters the caller appends after
63
+ * the returned name when building the filename (extensions + metadata
64
+ * suffix), reserved so the whole filename stays within the limit.
65
+ * @returns The resolved, non-empty name.
66
+ */
67
+ async function resolveAutoName(name, options = {}) {
68
+ if (name) return name;
69
+ const test = await getCurrentTest();
70
+ if (!test) throw new Error("Argos could not generate an automatic name because it is not running inside a Vitest test. Pass an explicit `name` argument.");
71
+ const count = (counters.get(test) ?? 0) + 1;
72
+ counters.set(test, count);
73
+ const file = test.file?.name;
74
+ const fullName = file && !test.fullName.startsWith(file) ? `${file} > ${test.fullName}` : test.fullName;
75
+ const suffix = ` ${count}`;
76
+ const maxBase = MAX_FILENAME_LENGTH - (options.reservedLength ?? 0) - suffix.length;
77
+ return `${fullName.length > maxBase ? truncate(`${test.id} ${fullName}`, maxBase) : fullName}${suffix}`;
78
+ }
79
+ //#endregion
80
+ //#region src/metadata.ts
81
+ /**
82
+ * Build the title path of a task (`[file, ...describes, title]`), replicating
83
+ * Vitest's own `getNames` helper so it matches the framework's conventions.
84
+ */
85
+ function getTitlePath(task) {
86
+ const names = [task.name];
87
+ let current = task;
88
+ while (current.suite) {
89
+ current = current.suite;
90
+ if (current.name) names.unshift(current.name);
91
+ }
92
+ if (current !== task.file) names.unshift(task.file.name);
93
+ return names;
94
+ }
95
+ /**
96
+ * Build the Argos `test` metadata from a Vitest test task, mirroring the
97
+ * Playwright SDK.
98
+ *
99
+ * `location.file` is left absolute here; the Node side (the Playwright SDK for
100
+ * screenshots, {@link writeSnapshotFile} for snapshots) resolves it relative to
101
+ * the git repository — the same treatment the Playwright SDK applies.
102
+ */
103
+ function buildTestMetadata(task) {
104
+ return {
105
+ id: task.id,
106
+ title: task.name,
107
+ titlePath: getTitlePath(task),
108
+ tags: task.tags && task.tags.length > 0 ? task.tags : void 0,
109
+ retries: task.retry ?? void 0,
110
+ retry: task.result?.retryCount ?? void 0,
111
+ repeat: task.result?.repeatCount ?? task.repeats ?? void 0,
112
+ location: {
113
+ file: task.file.filepath,
114
+ line: task.location?.line ?? 0,
115
+ column: task.location?.column ?? 0
116
+ },
117
+ annotations: task.annotations && task.annotations.length > 0 ? task.annotations.map((annotation) => ({
118
+ type: annotation.type,
119
+ description: annotation.message,
120
+ location: annotation.location ? {
121
+ file: annotation.location.file ?? task.file.filepath,
122
+ line: annotation.location.line ?? 0,
123
+ column: annotation.location.column ?? 0
124
+ } : void 0
125
+ })) : void 0
126
+ };
127
+ }
128
+ /**
129
+ * Get the Argos `test` metadata for the current Vitest test, or `null` when not
130
+ * running inside a test.
131
+ *
132
+ * Runs on the test side (browser or Node) where the test context is available;
133
+ * the resulting plain object crosses the browser/Node RPC boundary unchanged.
134
+ */
135
+ async function getTestMetadata() {
136
+ const task = await getCurrentTest();
137
+ if (!task) return null;
138
+ return buildTestMetadata(task);
139
+ }
140
+ //#endregion
1
141
  //#region ../../node_modules/.pnpm/tinyrainbow@3.1.0/node_modules/tinyrainbow/dist/index.js
2
142
  var b = {
3
143
  reset: [0, 0],
@@ -992,30 +1132,19 @@ function serializeSnapshot(content, options = {}) {
992
1132
  //#endregion
993
1133
  //#region src/index.ts
994
1134
  /**
995
- * Take an Argos screenshot in a Vitest browser test.
996
- *
997
- * Requires the {@link https://www.npmjs.com/package/@argos-ci/vitest Argos Vitest plugin}
998
- * to be registered in your Vitest config.
999
- *
1000
- * @example
1001
- * ```ts
1002
- * import { render } from "vitest-browser-react";
1003
- * import { argosScreenshot } from "@argos-ci/vitest";
1004
- *
1005
- * test("Button", async () => {
1006
- * render(<Button>Click me</Button>);
1007
- * await argosScreenshot("button");
1008
- * });
1009
- * ```
1010
- *
1011
- * @param name - Unique name of the screenshot.
1012
- * @param options - Serializable screenshot options.
1013
- * @returns The attachments captured, or an empty array outside of Vitest.
1135
+ * Characters reserved after a screenshot name when building its filename: the
1136
+ * `` ` vw-<width>` `` viewport suffix, the largest capture extension
1137
+ * (`.aria.yml`), and the metadata sidecar. Keeps auto-generated names within
1138
+ * the filesystem limit.
1014
1139
  */
1015
- async function argosScreenshot(name, options) {
1140
+ const SCREENSHOT_NAME_RESERVED = 29;
1141
+ async function argosScreenshot(nameOrOptions, maybeOptions) {
1142
+ const name = typeof nameOrOptions === "string" ? nameOrOptions : void 0;
1143
+ const options = typeof nameOrOptions === "string" ? maybeOptions : nameOrOptions;
1016
1144
  if (!await checkIsVitestEnv()) return [];
1145
+ const [resolvedName, test] = await Promise.all([resolveAutoName(name, { reservedLength: SCREENSHOT_NAME_RESERVED }), getTestMetadata()]);
1017
1146
  const { server } = await import("vitest/browser");
1018
- return server.commands.argosScreenshot(name, options);
1147
+ return server.commands.argosScreenshot(resolvedName, options ?? {}, test);
1019
1148
  }
1020
1149
  /**
1021
1150
  * Take an Argos snapshot of any serializable value, mimicking
@@ -1026,33 +1155,41 @@ async function argosScreenshot(name, options) {
1026
1155
  * file that Argos picks up and diffs across builds. It works both in Vitest
1027
1156
  * browser tests and in plain Node tests.
1028
1157
  *
1158
+ * The name is optional: pass it via `options.name`, or omit it to have Argos
1159
+ * generate one automatically from the current test, mimicking
1160
+ * {@link https://vitest.dev/guide/snapshot Vitest's snapshot naming}. The name
1161
+ * includes the test file path (Argos names are global across the build, unlike
1162
+ * Vitest's per-file `.snap`), so names stay unique across files.
1163
+ *
1029
1164
  * @example
1030
1165
  * ```ts
1031
1166
  * import { argosSnapshot } from "@argos-ci/vitest";
1032
1167
  *
1033
1168
  * test("API response", async () => {
1034
1169
  * const data = await fetchUser();
1035
- * await argosSnapshot("user", data);
1170
+ * await argosSnapshot(data); // -> "src/user.test.ts > API response 1"
1171
+ * await argosSnapshot(data, { name: "user" }); // explicit name
1036
1172
  * });
1037
1173
  * ```
1038
1174
  *
1039
- * @param name - Unique name of the snapshot.
1040
1175
  * @param content - The value to snapshot. Strings are written as-is; any other
1041
1176
  * value is serialized.
1042
- * @param options - Snapshot options.
1177
+ * @param options - Snapshot options, including an optional `name`.
1043
1178
  * @returns The attachments written, or an empty array outside of Vitest.
1044
1179
  */
1045
- async function argosSnapshot(name, content, options = {}) {
1046
- if (!name) throw new Error("The `name` argument is required.");
1180
+ async function argosSnapshot(content, options = {}) {
1047
1181
  if (!await checkIsVitestEnv()) return [];
1182
+ const rawExtension = options.extension ?? ".txt";
1183
+ const extension = rawExtension.startsWith(".") ? rawExtension : `.${rawExtension}`;
1184
+ const [resolvedName, test] = await Promise.all([resolveAutoName(options.name, { reservedLength: 9 + extension.length + 11 }), getTestMetadata()]);
1048
1185
  const serialized = serializeSnapshot(content, options);
1049
- const { serialize: _serialize, ...serializableOptions } = options;
1186
+ const { serialize: _serialize, name: _name, ...serializableOptions } = options;
1050
1187
  if (checkIsBrowserEnv()) {
1051
1188
  const { server } = await import("vitest/browser");
1052
- return server.commands.argosSnapshot(name, serialized, serializableOptions);
1189
+ return server.commands.argosSnapshot(resolvedName, serialized, serializableOptions, test);
1053
1190
  }
1054
- const { writeSnapshotFile } = await import("./snapshot-file-jtgfnw7g.mjs");
1055
- return writeSnapshotFile(name, serialized, serializableOptions);
1191
+ const { writeSnapshotFile } = await import("./snapshot-file-DYQmhYf1.mjs");
1192
+ return writeSnapshotFile(resolvedName, serialized, serializableOptions, test);
1056
1193
  }
1057
1194
  /**
1058
1195
  * Check if we are running in a Vitest environment.
package/dist/plugin.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ArgosScreenshotOptions } from "@argos-ci/playwright";
2
2
  import { StabilizationPluginOptions, ViewportOption } from "@argos-ci/browser";
3
+ import { ScreenshotMetadata } from "@argos-ci/util";
3
4
  import { UploadParameters } from "@argos-ci/core";
4
5
  import { Plugin } from "vitest/config";
5
6
  import { BrowserCommand, Vitest } from "vitest/node";
@@ -85,12 +86,22 @@ interface VitestScreenshotOptions {
85
86
  * applied on the test side *before* the value is sent to Node.
86
87
  */
87
88
  interface VitestSnapshotOptions {
89
+ /**
90
+ * Unique name of the snapshot.
91
+ *
92
+ * When omitted, Argos generates one automatically from the current test,
93
+ * mimicking {@link https://vitest.dev/guide/snapshot Vitest's snapshot naming}.
94
+ * The generated name includes the test file path so names stay unique across
95
+ * files (Argos names are global across the build, unlike Vitest's per-file
96
+ * `.snap`).
97
+ */
98
+ name?: string;
88
99
  /**
89
100
  * Folder where the snapshot is written.
90
101
  *
91
- * In Node tests this defaults to `"./screenshots"`. In browser tests it
102
+ * In Node tests this defaults to `"./snapshots"`. In browser tests it
92
103
  * defaults to the plugin `root` and can be overridden per call.
93
- * @default "./screenshots"
104
+ * @default "./snapshots"
94
105
  */
95
106
  root?: string;
96
107
  /**
@@ -111,10 +122,11 @@ interface VitestSnapshotOptions {
111
122
  }
112
123
  /**
113
124
  * Subset of {@link VitestSnapshotOptions} that can cross the Vitest
114
- * browser/node RPC boundary (everything but `serialize`, which is applied
115
- * before the value is sent to Node).
125
+ * browser/node RPC boundary. Excludes `serialize` (applied before the value is
126
+ * sent to Node) and `name` (resolved on the test side and passed to the Node
127
+ * primitives as a separate argument).
116
128
  */
117
- type SerializableSnapshotOptions = Omit<VitestSnapshotOptions, "serialize">;
129
+ type SerializableSnapshotOptions = Omit<VitestSnapshotOptions, "serialize" | "name">;
118
130
  /**
119
131
  * Options for the Argos Vitest plugin.
120
132
  *
@@ -132,12 +144,16 @@ interface ArgosVitestPluginOptions extends ArgosReporterConfig, ArgosScreenshotO
132
144
  uploadToArgos?: boolean;
133
145
  }
134
146
  //#endregion
147
+ //#region src/metadata.d.ts
148
+ /** The `test` slice of {@link ScreenshotMetadata}. */
149
+ type TestMetadata = ScreenshotMetadata["test"];
150
+ //#endregion
135
151
  //#region src/command.d.ts
136
152
  /**
137
153
  * Arguments of the `argosScreenshot` browser command.
138
154
  * Only serializable values cross the browser/node RPC boundary.
139
155
  */
140
- type ArgosScreenshotCommandArgs = [name: string, options?: VitestScreenshotOptions];
156
+ type ArgosScreenshotCommandArgs = [name: string, options?: VitestScreenshotOptions, test?: TestMetadata];
141
157
  /**
142
158
  * Create the `argosScreenshot` browser command used to capture Argos
143
159
  * screenshots from Vitest browser tests.
@@ -154,7 +170,7 @@ declare const createArgosScreenshotCommand: (pluginOptions?: ArgosVitestPluginOp
154
170
  * Only serializable values cross the browser/node RPC boundary — the value is
155
171
  * already serialized to a string on the browser side.
156
172
  */
157
- type ArgosSnapshotCommandArgs = [name: string, content: string, options?: SerializableSnapshotOptions];
173
+ type ArgosSnapshotCommandArgs = [name: string, content: string, options?: SerializableSnapshotOptions, test?: TestMetadata];
158
174
  /**
159
175
  * Create the `argosSnapshot` browser command used to write serialized snapshots
160
176
  * from Vitest browser tests. The serialized string is produced on the browser
package/dist/plugin.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
- import { dirname, resolve } from "node:path";
2
+ import { dirname, relative, resolve } from "node:path";
3
3
  import { DO_NOT_USE_setMetadataConfig, argosScreenshot } from "@argos-ci/playwright";
4
4
  import { resolveViewport } from "@argos-ci/browser";
5
- import { createDirectory, getMetadataPath, getScreenshotName, readVersionFromPackage, writeMetadata } from "@argos-ci/util";
5
+ import { createDirectory, getGitRepositoryPath, getMetadataPath, getScreenshotName, readVersionFromPackage, writeMetadata } from "@argos-ci/util";
6
6
  import { writeFile } from "node:fs/promises";
7
- import { getSnapshotMimeType, upload } from "@argos-ci/core";
7
+ import { getSnapshotMimeType, readConfig, upload } from "@argos-ci/core";
8
8
  //#region src/iframe.ts
9
9
  /**
10
10
  * Selector of the iframe Vitest renders the test into on the orchestrator page.
@@ -163,7 +163,7 @@ async function getVitestVersion() {
163
163
  * and are merged with the serializable per-call options (per-call wins).
164
164
  */
165
165
  const createArgosScreenshotCommand = (pluginOptions = {}) => {
166
- return async (ctx, name, options) => {
166
+ return async (ctx, name, options, test) => {
167
167
  if (!name) throw new Error("The `name` argument is required.");
168
168
  const merged = {
169
169
  ...pluginOptions,
@@ -180,8 +180,9 @@ const createArgosScreenshotCommand = (pluginOptions = {}) => {
180
180
  name: "@argos-ci/vitest",
181
181
  version
182
182
  },
183
- playwrightLibraries: ["@vitest/browser-playwright"],
184
- viewport
183
+ playwrightLibraries: ["vitest"],
184
+ viewport,
185
+ test
185
186
  });
186
187
  };
187
188
  const attachments = [];
@@ -205,6 +206,24 @@ const createArgosScreenshotCommand = (pluginOptions = {}) => {
205
206
  }
206
207
  };
207
208
  };
209
+ //#endregion
210
+ //#region src/snapshot-file.ts
211
+ /**
212
+ * Resolve a `test` metadata's `location.file` relative to the git repository,
213
+ * matching the Playwright SDK. The test side reports an absolute path.
214
+ */
215
+ async function resolveTestLocation(test) {
216
+ if (!test?.location) return test;
217
+ const repositoryPath = await getGitRepositoryPath();
218
+ if (!repositoryPath) return test;
219
+ return {
220
+ ...test,
221
+ location: {
222
+ ...test.location,
223
+ file: relative(repositoryPath, test.location.file)
224
+ }
225
+ };
226
+ }
208
227
  /**
209
228
  * Default extension of a serialized snapshot file.
210
229
  */
@@ -227,12 +246,16 @@ function normalizeExtension(extension) {
227
246
  * This is the shared Node-side primitive used by both the browser command (which
228
247
  * receives the already-serialized string over RPC) and the Node code path.
229
248
  */
230
- async function writeSnapshotFile(name, content, options = {}) {
249
+ async function writeSnapshotFile(name, content, options = {}, test) {
231
250
  if (!name) throw new Error("The `name` argument is required.");
232
- const root = options.root ?? "./screenshots";
251
+ const root = options.root ?? "./snapshots";
233
252
  const extension = normalizeExtension(options.extension ?? DEFAULT_EXTENSION);
234
253
  const snapshotPath = resolve(root, `${getScreenshotName(name)}${SNAPSHOT_INFIX}${extension}`);
235
- const [vitestVersion, sdkVersion] = await Promise.all([getVitestVersion(), getArgosVitestVersion()]);
254
+ const [vitestVersion, sdkVersion, resolvedTest] = await Promise.all([
255
+ getVitestVersion(),
256
+ getArgosVitestVersion(),
257
+ resolveTestLocation(test)
258
+ ]);
236
259
  const tags = options.tag ? Array.isArray(options.tag) ? options.tag : [options.tag] : void 0;
237
260
  const metadata = {
238
261
  automationLibrary: {
@@ -243,7 +266,8 @@ async function writeSnapshotFile(name, content, options = {}) {
243
266
  name: "@argos-ci/vitest",
244
267
  version: sdkVersion
245
268
  },
246
- ...tags ? { tags } : {}
269
+ ...tags ? { tags } : {},
270
+ ...resolvedTest ? { test: resolvedTest } : {}
247
271
  };
248
272
  await createDirectory(dirname(snapshotPath));
249
273
  await Promise.all([writeFile(snapshotPath, content, "utf-8"), writeMetadata(snapshotPath, metadata)]);
@@ -265,17 +289,38 @@ async function writeSnapshotFile(name, content, options = {}) {
265
289
  * side and this command writes it (and its metadata) to disk on the Node side.
266
290
  */
267
291
  const createArgosSnapshotCommand = (pluginOptions = {}) => {
268
- return async (_ctx, name, content, options) => {
292
+ return async (_ctx, name, content, options, test) => {
269
293
  if (!name) throw new Error("The `name` argument is required.");
270
294
  return writeSnapshotFile(name, content, {
271
295
  root: pluginOptions.root,
272
296
  ...options
273
- });
297
+ }, test);
274
298
  };
275
299
  };
276
300
  //#endregion
277
301
  //#region src/reporter.ts
278
302
  /**
303
+ * Derive the Argos parallel configuration from Vitest's shard settings, matching
304
+ * the Playwright reporter.
305
+ *
306
+ * Vitest's `--shard=<index>/<count>` populates `config.shard` as
307
+ * `{ index, count }` (the `index` is 1-based, like the Argos `parallel.index`).
308
+ * When sharding is active, Argos still needs `ARGOS_PARALLEL_NONCE` to group the
309
+ * shards into a single build; the total and index default to the shard values
310
+ * but can be overridden via `ARGOS_PARALLEL_TOTAL` / `ARGOS_PARALLEL_INDEX`.
311
+ */
312
+ async function getParallelFromConfig(config) {
313
+ const { shard } = config;
314
+ if (!shard || shard.count === 1) return null;
315
+ const argosConfig = await readConfig();
316
+ if (!argosConfig.parallelNonce) throw new Error("Vitest shard mode detected. Please specify the ARGOS_PARALLEL_NONCE environment variable. Read https://argos-ci.com/docs/parallel-testing");
317
+ return {
318
+ total: argosConfig.parallelTotal ?? shard.count,
319
+ nonce: argosConfig.parallelNonce,
320
+ index: argosConfig.parallelIndex ?? shard.index
321
+ };
322
+ }
323
+ /**
279
324
  * Vitest reporter that uploads the screenshots captured during the run to Argos.
280
325
  */
281
326
  var ArgosReporter = class {
@@ -299,6 +344,7 @@ var ArgosReporter = class {
299
344
  "**/*.snapshot.*"
300
345
  ],
301
346
  ignore: ["**/*.argos.json"],
347
+ parallel: await getParallelFromConfig(this.vitest.config) ?? void 0,
302
348
  ...this.config
303
349
  });
304
350
  console.log(`✅ Argos build created: ${res.build.url}`);
@@ -330,7 +376,7 @@ const cwd = process.cwd();
330
376
  * ```
331
377
  */
332
378
  function argosVitestPlugin(options) {
333
- const { root: unresolvedRoot = "./screenshots", uploadToArgos, ...otherOptions } = options ?? {};
379
+ const { root: unresolvedRoot = "./snapshots", uploadToArgos, ...otherOptions } = options ?? {};
334
380
  const root = resolve(cwd, unresolvedRoot);
335
381
  return {
336
382
  name: "@argos-ci/vitest",
@@ -343,16 +389,19 @@ function argosVitestPlugin(options) {
343
389
  config() {
344
390
  return {
345
391
  optimizeDeps: { include: ["@argos-ci/vitest"] },
346
- test: { browser: { commands: {
347
- argosScreenshot: createArgosScreenshotCommand({
348
- ...otherOptions,
349
- root
350
- }),
351
- argosSnapshot: createArgosSnapshotCommand({
352
- ...otherOptions,
353
- root
354
- })
355
- } } }
392
+ test: {
393
+ includeTaskLocation: true,
394
+ browser: { commands: {
395
+ argosScreenshot: createArgosScreenshotCommand({
396
+ ...otherOptions,
397
+ root
398
+ }),
399
+ argosSnapshot: createArgosSnapshotCommand({
400
+ ...otherOptions,
401
+ root
402
+ })
403
+ } }
404
+ }
356
405
  };
357
406
  }
358
407
  };
@@ -1,8 +1,8 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { writeFile } from "node:fs/promises";
3
- import { dirname, resolve } from "node:path";
3
+ import { dirname, relative, resolve } from "node:path";
4
4
  import { getSnapshotMimeType } from "@argos-ci/core";
5
- import { createDirectory, getMetadataPath, getScreenshotName, readVersionFromPackage, writeMetadata } from "@argos-ci/util";
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
  /**
@@ -18,6 +18,24 @@ async function getArgosVitestVersion() {
18
18
  async function getVitestVersion() {
19
19
  return readVersionFromPackage(require.resolve("vitest/package.json"));
20
20
  }
21
+ //#endregion
22
+ //#region src/snapshot-file.ts
23
+ /**
24
+ * Resolve a `test` metadata's `location.file` relative to the git repository,
25
+ * matching the Playwright SDK. The test side reports an absolute path.
26
+ */
27
+ async function resolveTestLocation(test) {
28
+ if (!test?.location) return test;
29
+ const repositoryPath = await getGitRepositoryPath();
30
+ if (!repositoryPath) return test;
31
+ return {
32
+ ...test,
33
+ location: {
34
+ ...test.location,
35
+ file: relative(repositoryPath, test.location.file)
36
+ }
37
+ };
38
+ }
21
39
  /**
22
40
  * Default extension of a serialized snapshot file.
23
41
  */
@@ -40,12 +58,16 @@ function normalizeExtension(extension) {
40
58
  * This is the shared Node-side primitive used by both the browser command (which
41
59
  * receives the already-serialized string over RPC) and the Node code path.
42
60
  */
43
- async function writeSnapshotFile(name, content, options = {}) {
61
+ async function writeSnapshotFile(name, content, options = {}, test) {
44
62
  if (!name) throw new Error("The `name` argument is required.");
45
- const root = options.root ?? "./screenshots";
63
+ const root = options.root ?? "./snapshots";
46
64
  const extension = normalizeExtension(options.extension ?? DEFAULT_EXTENSION);
47
65
  const snapshotPath = resolve(root, `${getScreenshotName(name)}${SNAPSHOT_INFIX}${extension}`);
48
- const [vitestVersion, sdkVersion] = await Promise.all([getVitestVersion(), getArgosVitestVersion()]);
66
+ const [vitestVersion, sdkVersion, resolvedTest] = await Promise.all([
67
+ getVitestVersion(),
68
+ getArgosVitestVersion(),
69
+ resolveTestLocation(test)
70
+ ]);
49
71
  const tags = options.tag ? Array.isArray(options.tag) ? options.tag : [options.tag] : void 0;
50
72
  const metadata = {
51
73
  automationLibrary: {
@@ -56,7 +78,8 @@ async function writeSnapshotFile(name, content, options = {}) {
56
78
  name: "@argos-ci/vitest",
57
79
  version: sdkVersion
58
80
  },
59
- ...tags ? { tags } : {}
81
+ ...tags ? { tags } : {},
82
+ ...resolvedTest ? { test: resolvedTest } : {}
60
83
  };
61
84
  await createDirectory(dirname(snapshotPath));
62
85
  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.2.3",
4
+ "version": "0.3.0",
5
5
  "author": "Smooth Code",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -52,8 +52,8 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@argos-ci/browser": "6.4.4",
55
- "@argos-ci/core": "6.5.3",
56
- "@argos-ci/playwright": "7.3.4",
55
+ "@argos-ci/core": "6.6.0",
56
+ "@argos-ci/playwright": "7.3.5",
57
57
  "@argos-ci/util": "4.0.4"
58
58
  },
59
59
  "peerDependencies": {
@@ -81,5 +81,5 @@
81
81
  "check-format": "prettier --check --ignore-unknown --ignore-path=./.gitignore --ignore-path=../../.gitignore --ignore-path=../../.prettierignore .",
82
82
  "lint": "eslint ."
83
83
  },
84
- "gitHead": "1b25b7c1109a3d032d627c5f9d2f2c3937731aea"
84
+ "gitHead": "51c052e00a5361ca40c4695f16706d841be4e2da"
85
85
  }