@argos-ci/vitest 0.2.4 → 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 +30 -4
- package/dist/index.d.mts +44 -13
- package/dist/index.mjs +167 -30
- package/dist/plugin.d.mts +23 -7
- package/dist/plugin.mjs +72 -23
- package/dist/{snapshot-file-jtgfnw7g.mjs → snapshot-file-DYQmhYf1.mjs} +29 -6
- package/package.json +2 -2
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"
|
|
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(
|
|
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
|
-
|
|
113
|
-
|
|
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 `"./
|
|
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 "./
|
|
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
|
|
106
|
-
*
|
|
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"
|
|
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(
|
|
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
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
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
|
-
|
|
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(
|
|
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"
|
|
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(
|
|
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(
|
|
1189
|
+
return server.commands.argosSnapshot(resolvedName, serialized, serializableOptions, test);
|
|
1053
1190
|
}
|
|
1054
|
-
const { writeSnapshotFile } = await import("./snapshot-file-
|
|
1055
|
-
return writeSnapshotFile(
|
|
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 `"./
|
|
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 "./
|
|
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
|
|
115
|
-
*
|
|
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: ["
|
|
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 ?? "./
|
|
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([
|
|
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 = "./
|
|
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: {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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 ?? "./
|
|
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([
|
|
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.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"author": "Smooth Code",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -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": "
|
|
84
|
+
"gitHead": "51c052e00a5361ca40c4695f16706d841be4e2da"
|
|
85
85
|
}
|