@kinora/vitest-reporter 0.1.0 → 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 +7 -0
- package/dist/index.d.mts +11 -3
- package/dist/index.mjs +65 -6
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -37,6 +37,13 @@ Create an API token in the kinora dashboard (Settings → Workspace). Self-hosti
|
|
|
37
37
|
|
|
38
38
|
Only browser tests are uploaded by default: Node unit tests are usually thousands of fast tests with no trace, and would eat your plan's monthly result quota. Upload them too with `include: 'all'`.
|
|
39
39
|
|
|
40
|
+
## Accessibility
|
|
41
|
+
|
|
42
|
+
With Storybook's a11y addon, each story's check is uploaded too: the rules broken, their impact
|
|
43
|
+
and the elements they hit, ranked by component on the Components page. Use the addon's `todo`
|
|
44
|
+
mode to keep the impact: in `error` mode, Storybook drops the check when the story fails and
|
|
45
|
+
kinora reads the violations from the failure instead.
|
|
46
|
+
|
|
40
47
|
## Documentation
|
|
41
48
|
|
|
42
49
|
**https://docs.kinora.dev/guides/vitest/**
|
package/dist/index.d.mts
CHANGED
|
@@ -4,7 +4,7 @@ import { Reporter, TestModule, Vitest } from "vitest/node";
|
|
|
4
4
|
type IncludeTests = 'browser' | 'all';
|
|
5
5
|
//#endregion
|
|
6
6
|
//#region src/index.d.ts
|
|
7
|
-
interface KinoraVitestReporterOptions {
|
|
7
|
+
export interface KinoraVitestReporterOptions {
|
|
8
8
|
/** kinora server base URL. Defaults to env KINORA_URL, then the hosted cloud. Set for self-host. */
|
|
9
9
|
url?: string;
|
|
10
10
|
/** Project API token. Defaults to env KINORA_TOKEN (keep it out of the config file). */
|
|
@@ -24,14 +24,22 @@ interface KinoraVitestReporterOptions {
|
|
|
24
24
|
* plan's monthly result quota with large unit suites.
|
|
25
25
|
*/
|
|
26
26
|
include?: IncludeTests;
|
|
27
|
+
/**
|
|
28
|
+
* Sharded runs (`--shard`): the uploads of every shard of a CI run are assembled into a single
|
|
29
|
+
* kinora run. Shards are grouped by this key, defaulting to env KINORA_GROUP_KEY, then the CI run
|
|
30
|
+
* (GITHUB_RUN_ID / CI_PIPELINE_ID). Set it when a matrix runs the sharded suite once per leg, to
|
|
31
|
+
* tell the legs apart. `false` uploads each shard as its own run.
|
|
32
|
+
*/
|
|
33
|
+
groupKey?: string | false;
|
|
27
34
|
}
|
|
28
|
-
|
|
35
|
+
export default class KinoraVitestReporter implements Reporter {
|
|
29
36
|
private readonly options;
|
|
30
37
|
private startedAt;
|
|
38
|
+
private shard;
|
|
31
39
|
constructor(options: KinoraVitestReporterOptions);
|
|
32
40
|
onInit(vitest: Vitest): void;
|
|
33
41
|
onTestRunStart(): void;
|
|
34
42
|
onTestRunEnd(modules: ReadonlyArray<TestModule>): Promise<void>;
|
|
35
43
|
}
|
|
36
44
|
//#endregion
|
|
37
|
-
export {
|
|
45
|
+
export {};
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import process from "node:process";
|
|
3
|
-
import { DEFAULT_KINORA_URL, DEFAULT_UPLOAD_ATTACHMENTS, IngestError, STORY_ANNOTATION, createIngestClient, detectCiEnv, isUploadableAttachment, makeTestKey, stripAnsi } from "@kinora/core";
|
|
3
|
+
import { DEFAULT_KINORA_URL, DEFAULT_UPLOAD_ATTACHMENTS, IngestError, MAX_A11Y_NODES, MAX_A11Y_VIOLATIONS, STORY_ANNOTATION, axeViolations, createIngestClient, detectCiEnv, isUploadableAttachment, makeTestKey, resolveShardGroup, stripAnsi } from "@kinora/core";
|
|
4
4
|
//#region src/convert.ts
|
|
5
5
|
function identity(test) {
|
|
6
6
|
const file = test.module.relativeModuleId;
|
|
@@ -28,6 +28,49 @@ function storyOf(test, titlePath) {
|
|
|
28
28
|
if (!meta.componentName) return meta.storyId.replace("--", "/");
|
|
29
29
|
return `${meta.componentName}/${titlePath[1]}`;
|
|
30
30
|
}
|
|
31
|
+
const MAX_TEXT = 300;
|
|
32
|
+
const truncate = (s) => s.length > MAX_TEXT ? `${s.slice(0, MAX_TEXT)}…` : s;
|
|
33
|
+
const AXE_FAILURE = /Expected the HTML found at \$\('(.+?)'\) to have no violations:[\s\S]*?Received:\s*"(.+?) \(([\w-]+)\)"[\s\S]*?more information on this issue here:\s*(https:\/\/\S+)/g;
|
|
34
|
+
function a11yFromError(test) {
|
|
35
|
+
const message = test.result().errors?.find((e) => e.message.includes("toHaveNoViolations"))?.message;
|
|
36
|
+
if (!message) return void 0;
|
|
37
|
+
const byRule = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const [, target, help, id, helpUrl] of stripAnsi(message).matchAll(AXE_FAILURE)) {
|
|
39
|
+
const v = byRule.get(id) ?? {
|
|
40
|
+
id,
|
|
41
|
+
help: truncate(help),
|
|
42
|
+
helpUrl,
|
|
43
|
+
nodes: [],
|
|
44
|
+
nodeCount: 0
|
|
45
|
+
};
|
|
46
|
+
if (v.nodes.length < MAX_A11Y_NODES) v.nodes.push(truncate(target));
|
|
47
|
+
v.nodeCount++;
|
|
48
|
+
byRule.set(id, v);
|
|
49
|
+
}
|
|
50
|
+
return byRule.size ? {
|
|
51
|
+
status: "failed",
|
|
52
|
+
violations: [...byRule.values()].slice(0, MAX_A11Y_VIOLATIONS)
|
|
53
|
+
} : void 0;
|
|
54
|
+
}
|
|
55
|
+
function a11yOf(test) {
|
|
56
|
+
const report = test.meta().reports?.find((r) => r.type === "a11y");
|
|
57
|
+
const status = report?.status;
|
|
58
|
+
if (status !== "passed" && status !== "warning" && status !== "failed") return a11yFromError(test);
|
|
59
|
+
const result = report?.result ?? {};
|
|
60
|
+
if (result.error) {
|
|
61
|
+
const { message } = result.error;
|
|
62
|
+
const text = typeof message === "string" ? message : typeof result.error === "string" ? result.error : "The accessibility check failed.";
|
|
63
|
+
return {
|
|
64
|
+
status: "failed",
|
|
65
|
+
violations: [],
|
|
66
|
+
error: truncate(text)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
status,
|
|
71
|
+
violations: axeViolations(result.violations)
|
|
72
|
+
};
|
|
73
|
+
}
|
|
31
74
|
function slug(name) {
|
|
32
75
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "screenshot";
|
|
33
76
|
}
|
|
@@ -75,6 +118,7 @@ function toNormTest(test) {
|
|
|
75
118
|
const { file, titlePath, projectName } = identity(test);
|
|
76
119
|
const diagnostic = test.diagnostic();
|
|
77
120
|
const story = storyOf(test, titlePath);
|
|
121
|
+
const a11y = a11yOf(test);
|
|
78
122
|
return {
|
|
79
123
|
testKey: makeTestKey(file, titlePath, projectName),
|
|
80
124
|
title: test.name,
|
|
@@ -99,7 +143,8 @@ function toNormTest(test) {
|
|
|
99
143
|
message: cleanMessage(e.message),
|
|
100
144
|
stack: e.stack
|
|
101
145
|
})),
|
|
102
|
-
attachments: attachmentsOf(test)
|
|
146
|
+
attachments: attachmentsOf(test),
|
|
147
|
+
...a11y && { a11y }
|
|
103
148
|
};
|
|
104
149
|
}
|
|
105
150
|
function toNormTests(modules, include = "browser") {
|
|
@@ -134,11 +179,17 @@ function countsOf(tests) {
|
|
|
134
179
|
var KinoraVitestReporter = class {
|
|
135
180
|
options;
|
|
136
181
|
startedAt = /* @__PURE__ */ new Date();
|
|
182
|
+
shard;
|
|
137
183
|
constructor(options) {
|
|
138
184
|
this.options = options;
|
|
139
185
|
}
|
|
140
186
|
onInit(vitest) {
|
|
141
187
|
this.startedAt = /* @__PURE__ */ new Date();
|
|
188
|
+
const shard = vitest.config.shard;
|
|
189
|
+
this.shard = shard ? {
|
|
190
|
+
current: shard.index,
|
|
191
|
+
total: shard.count
|
|
192
|
+
} : void 0;
|
|
142
193
|
vitest.config.includeTaskLocation = true;
|
|
143
194
|
for (const project of vitest.projects) project.config.includeTaskLocation = true;
|
|
144
195
|
}
|
|
@@ -153,9 +204,13 @@ var KinoraVitestReporter = class {
|
|
|
153
204
|
return;
|
|
154
205
|
}
|
|
155
206
|
const { tests, skippedNodeTests } = toNormTests(modules, this.options.include);
|
|
207
|
+
const group = this.shard && this.options.groupKey !== false ? resolveShardGroup(process.env, { groupKey: this.options.groupKey }) : void 0;
|
|
156
208
|
if (!tests.length) {
|
|
157
|
-
|
|
158
|
-
|
|
209
|
+
if (!group) {
|
|
210
|
+
console.warn(skippedNodeTests ? `[kinora] nothing uploaded: no browser tests in this run (${skippedNodeTests} Node tests left out). Set include: 'all' to upload Node tests too.` : "[kinora] nothing uploaded: no tests ran");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
console.warn("[kinora] no tests to upload in this shard: filling its slot so the run can complete");
|
|
159
214
|
}
|
|
160
215
|
const detected = detectCiEnv(process.env);
|
|
161
216
|
const payload = {
|
|
@@ -168,7 +223,11 @@ var KinoraVitestReporter = class {
|
|
|
168
223
|
duration: Date.now() - this.startedAt.getTime(),
|
|
169
224
|
counts: countsOf(tests),
|
|
170
225
|
git: this.options.git ?? detected.git,
|
|
171
|
-
ci: this.options.ci ?? detected.ci
|
|
226
|
+
ci: this.options.ci ?? detected.ci,
|
|
227
|
+
...this.shard && group && {
|
|
228
|
+
...group,
|
|
229
|
+
shard: this.shard
|
|
230
|
+
}
|
|
172
231
|
},
|
|
173
232
|
tests
|
|
174
233
|
};
|
|
@@ -195,7 +254,7 @@ var KinoraVitestReporter = class {
|
|
|
195
254
|
console.warn(`[kinora] ${a.name} upload failed for ${t.testKey}:`, err instanceof Error ? err.message : err);
|
|
196
255
|
}
|
|
197
256
|
}
|
|
198
|
-
console.log(`[kinora] uploaded ${res.tests} tests + ${artifacts} artifacts (run ${res.runId})`);
|
|
257
|
+
console.log(`[kinora] uploaded ${res.tests} tests + ${artifacts} artifacts (run ${res.runId}${res.shardProgress ? `, ${res.shardProgress.landed} of ${res.shardProgress.total} shards` : ""})`);
|
|
199
258
|
} catch (err) {
|
|
200
259
|
if (err instanceof IngestError && err.status === 402) console.warn(`[kinora] ${err.message}`);
|
|
201
260
|
else console.error("[kinora] upload failed:", err instanceof Error ? err.message : err);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kinora/vitest-reporter",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "Vitest reporter that uploads browser-mode and Storybook test results to a kinora server",
|
|
6
6
|
"author": "Joris Gallot",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,23 +28,23 @@
|
|
|
28
28
|
"dist"
|
|
29
29
|
],
|
|
30
30
|
"engines": {
|
|
31
|
-
"node": ">=20"
|
|
31
|
+
"node": ">=20.20.2"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"vitest": ">=4.1.
|
|
37
|
+
"vitest": ">=4.1.12"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@kinora/core": "0.
|
|
40
|
+
"@kinora/core": "0.13.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@types/node": "^26.5
|
|
44
|
-
"@vitest/coverage-v8": "^5.0.
|
|
45
|
-
"tsdown": "^0.
|
|
43
|
+
"@types/node": "^26.6.5",
|
|
44
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
45
|
+
"tsdown": "^0.23.1",
|
|
46
46
|
"typescript": "~6.0.3",
|
|
47
|
-
"vitest": "^5.0.
|
|
47
|
+
"vitest": "^5.0.5"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsdown",
|