@crvy/rprtr 0.0.3 → 0.0.5
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/CHANGELOG.md +60 -0
- package/README.md +9 -0
- package/dist/{chunk-BRFA7JCQ.js → chunk-IH44LYNM.js} +138 -15
- package/dist/{chunk-IEYNAB6M.js → chunk-MWLM3HWS.js} +66 -16
- package/dist/cli.js +2 -2
- package/dist/index.css +52 -0
- package/dist/index.js +154 -83
- package/dist/report-state.d.ts.map +1 -1
- package/dist/report-utils.d.ts +3 -1
- package/dist/report-utils.d.ts.map +1 -1
- package/dist/reporter-utils.d.ts +11 -1
- package/dist/reporter-utils.d.ts.map +1 -1
- package/dist/reporter.cjs +773 -0
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +74 -20
- package/dist/schemas.d.ts +103 -8
- package/dist/schemas.d.ts.map +1 -1
- package/dist/server/app.d.ts +1 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/report-watch.d.ts +9 -0
- package/dist/server/report-watch.d.ts.map +1 -0
- package/dist/server.cjs +1176 -0
- package/dist/server.js +2 -2
- package/dist/types.d.ts +6 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,66 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.0.5] - 2026-05-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Classify passed visual assertions in report state
|
|
13
|
+
- Emit declared visual names for screenshot steps
|
|
14
|
+
- Refresh report state from disk changes
|
|
15
|
+
- Label passed visual fallback states in ui
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
- Describe passed screenshot fallback modes
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **ci:** Checkout hooks submodule
|
|
24
|
+
|
|
25
|
+
### Miscellaneous
|
|
26
|
+
|
|
27
|
+
- Add you-lint-not-pass hook integration
|
|
28
|
+
- Exclude hooks repo from root checks
|
|
29
|
+
## [0.0.4] - 2026-04-23
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Add CJS build for reporter and server entry points
|
|
34
|
+
- Add require export conditions for CJS resolution
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
|
|
38
|
+
- Add publint integration design spec
|
|
39
|
+
- Add publint integration plan
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Add @playwright/test to knip ignoreDependencies
|
|
44
|
+
|
|
45
|
+
### Miscellaneous
|
|
46
|
+
|
|
47
|
+
- Remove local smoke-test project
|
|
48
|
+
|
|
49
|
+
### Testing
|
|
50
|
+
|
|
51
|
+
- Approve screenshot baselines for darwin and linux
|
|
52
|
+
- Add smoke test for CJS/ESM dual resolution
|
|
53
|
+
|
|
54
|
+
### Ci
|
|
55
|
+
|
|
56
|
+
- Run Playwright tests inside Playwright Docker container
|
|
57
|
+
- Add unzip for setup-bun in Playwright container
|
|
58
|
+
- Add publint as non-blocking check in check.sh
|
|
59
|
+
|
|
60
|
+
### Deps
|
|
61
|
+
|
|
62
|
+
- Add publint for package.json validation
|
|
63
|
+
- Add publint for package.json validation
|
|
64
|
+
|
|
65
|
+
### Pub
|
|
66
|
+
|
|
67
|
+
- Chain publint after build in prepublishOnly
|
|
8
68
|
## [0.0.3] - 2026-04-10
|
|
9
69
|
|
|
10
70
|
### Fixed
|
package/README.md
CHANGED
|
@@ -93,6 +93,15 @@ When the server isn't running during tests, the reporter automatically falls bac
|
|
|
93
93
|
- On test completion, a self-contained `crvy-rprtr.html` is written for direct browser review
|
|
94
94
|
- When the server starts, it loads and merges all `crvy-rprtr-*.json` files from the offline report directory
|
|
95
95
|
|
|
96
|
+
## Passed Screenshot Modes
|
|
97
|
+
|
|
98
|
+
Crvy Rprtr keeps passed Playwright screenshot assertions visible in two fallback modes when Playwright does not emit a full passing comparison payload:
|
|
99
|
+
|
|
100
|
+
- `baseline-only`: Crvy Rprtr copied the expected snapshot into the screenshot directory, so the UI can still show the stored baseline.
|
|
101
|
+
- `declared-only`: the screenshot assertion was detected, but Playwright did not emit a passed artifact and no snapshot file could be resolved.
|
|
102
|
+
|
|
103
|
+
When the server is running, Crvy Rprtr also refreshes the UI after report JSON or screenshot artifacts change on disk.
|
|
104
|
+
|
|
96
105
|
## Programmatic API
|
|
97
106
|
|
|
98
107
|
```ts
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
createMutableReportState,
|
|
11
11
|
finalizeRunEvent,
|
|
12
12
|
safeParse
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MWLM3HWS.js";
|
|
14
14
|
|
|
15
15
|
// src/server/app.ts
|
|
16
|
-
import { dirname as dirname2, join as
|
|
16
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
17
17
|
import { fileURLToPath } from "url";
|
|
18
18
|
import pLimit from "p-limit";
|
|
19
19
|
|
|
@@ -207,14 +207,122 @@ function handleSync(ctx) {
|
|
|
207
207
|
broadcastToBrowsers(ctx.wsClients, { type: "sync", data: ctx.reportData });
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
// src/server/
|
|
210
|
+
// src/server/report-watch.ts
|
|
211
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
211
212
|
import { join as join2 } from "path";
|
|
213
|
+
var OFFLINE_REPORT_FILE_PATTERN2 = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
214
|
+
function createDebouncedRefresh(reload, delayMs = 50) {
|
|
215
|
+
let timer = null;
|
|
216
|
+
return () => {
|
|
217
|
+
if (timer !== null) {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
}
|
|
220
|
+
timer = setTimeout(() => {
|
|
221
|
+
timer = null;
|
|
222
|
+
void reload();
|
|
223
|
+
}, delayMs);
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function isFileNotFound2(error) {
|
|
227
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
228
|
+
}
|
|
229
|
+
async function describeFile(filePath, label) {
|
|
230
|
+
try {
|
|
231
|
+
const fileStats = await stat2(filePath);
|
|
232
|
+
return `${label}:${fileStats.size}:${fileStats.mtimeMs}`;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (isFileNotFound2(error)) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function createOfflineFingerprint(offlineReportDir) {
|
|
241
|
+
if (!await isDirectory(offlineReportDir)) {
|
|
242
|
+
return "offline:missing";
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const entries = await readdir2(offlineReportDir, { withFileTypes: true });
|
|
246
|
+
const relevantEntries = entries.filter(
|
|
247
|
+
(entry) => entry.isFile() && (entry.name === "report.json" || OFFLINE_REPORT_FILE_PATTERN2.test(entry.name))
|
|
248
|
+
).sort((left, right) => left.name.localeCompare(right.name));
|
|
249
|
+
const parts = await Promise.all(
|
|
250
|
+
relevantEntries.map((entry) => describeFile(join2(offlineReportDir, entry.name), entry.name))
|
|
251
|
+
);
|
|
252
|
+
return `offline:${parts.filter((part) => part !== null).join("|")}`;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
255
|
+
console.warn(`[Server] Unable to scan ${offlineReportDir}: ${errorMsg}`);
|
|
256
|
+
return "offline:error";
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async function createScreenshotFingerprint(screenshotDir, relativePath = "") {
|
|
260
|
+
const directoryPath = relativePath === "" ? screenshotDir : join2(screenshotDir, relativePath);
|
|
261
|
+
if (!await isDirectory(directoryPath)) {
|
|
262
|
+
return relativePath === "" ? ["screenshots:missing"] : [];
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const entries = await readdir2(directoryPath, { withFileTypes: true });
|
|
266
|
+
const parts = await Promise.all(
|
|
267
|
+
entries.sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
268
|
+
const entryRelativePath = relativePath === "" ? entry.name : join2(relativePath, entry.name);
|
|
269
|
+
if (entry.isDirectory()) {
|
|
270
|
+
return createScreenshotFingerprint(screenshotDir, entryRelativePath);
|
|
271
|
+
}
|
|
272
|
+
return describeFile(join2(screenshotDir, entryRelativePath), entryRelativePath);
|
|
273
|
+
})
|
|
274
|
+
);
|
|
275
|
+
return parts.flat().filter((part) => part !== null);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (isFileNotFound2(error)) {
|
|
278
|
+
return relativePath === "" ? ["screenshots:missing"] : [];
|
|
279
|
+
}
|
|
280
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
281
|
+
console.warn(`[Server] Unable to scan ${directoryPath}: ${errorMsg}`);
|
|
282
|
+
return relativePath === "" ? ["screenshots:error"] : [];
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function createArtifactsFingerprint(options) {
|
|
286
|
+
const [offlineFingerprint, screenshotFingerprint] = await Promise.all([
|
|
287
|
+
createOfflineFingerprint(options.offlineReportDir),
|
|
288
|
+
createScreenshotFingerprint(options.screenshotDir)
|
|
289
|
+
]);
|
|
290
|
+
return `${offlineFingerprint}::${screenshotFingerprint.join("|")}`;
|
|
291
|
+
}
|
|
292
|
+
async function watchReportArtifacts(options) {
|
|
293
|
+
let fingerprint = await createArtifactsFingerprint(options);
|
|
294
|
+
let isPolling = false;
|
|
295
|
+
const interval = setInterval(() => {
|
|
296
|
+
if (isPolling) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
isPolling = true;
|
|
300
|
+
void createArtifactsFingerprint(options).then((nextFingerprint) => {
|
|
301
|
+
if (nextFingerprint === fingerprint) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
fingerprint = nextFingerprint;
|
|
305
|
+
options.scheduleRefresh();
|
|
306
|
+
}).catch((error) => {
|
|
307
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
308
|
+
console.warn(`[Server] Artifact refresh poll failed: ${errorMsg}`);
|
|
309
|
+
}).finally(() => {
|
|
310
|
+
isPolling = false;
|
|
311
|
+
});
|
|
312
|
+
}, 100);
|
|
313
|
+
return () => {
|
|
314
|
+
clearInterval(interval);
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/server/routes.ts
|
|
319
|
+
import { join as join3 } from "path";
|
|
212
320
|
var LIVE_UPDATES_WEBSOCKET_PATH = "/";
|
|
213
321
|
function isWebSocketUpgradeRequest(req) {
|
|
214
322
|
return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
215
323
|
}
|
|
216
324
|
async function handleRoot(ctx) {
|
|
217
|
-
const html = await respondWithFile(
|
|
325
|
+
const html = await respondWithFile(join3(ctx.staticDir, "index.html"), "text/html");
|
|
218
326
|
return html ?? new Response("Not Found", { status: 404 });
|
|
219
327
|
}
|
|
220
328
|
async function handleAppCss() {
|
|
@@ -311,7 +419,7 @@ async function handleScreenshots(ctx, req) {
|
|
|
311
419
|
}
|
|
312
420
|
async function handleDist(ctx, req) {
|
|
313
421
|
const path = new URL(req.url).pathname.slice("/dist/".length);
|
|
314
|
-
const filePath =
|
|
422
|
+
const filePath = join3(ctx.staticDir, path);
|
|
315
423
|
const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
|
|
316
424
|
const file = await respondWithFile(filePath, contentType);
|
|
317
425
|
return file ?? new Response("Not Found", { status: 404 });
|
|
@@ -414,6 +522,10 @@ async function loadOfflineReports(offlineReportDir, reportData) {
|
|
|
414
522
|
screenshotsBaseUrl: "/screenshots/"
|
|
415
523
|
});
|
|
416
524
|
}
|
|
525
|
+
function resetReloadableReportData(reportData) {
|
|
526
|
+
reportData.tests = {};
|
|
527
|
+
reportData.isUpdateMode = false;
|
|
528
|
+
}
|
|
417
529
|
async function handleParsedWebSocketMessage(ctx, msg) {
|
|
418
530
|
switch (msg.type) {
|
|
419
531
|
case "test-begin": {
|
|
@@ -465,16 +577,16 @@ async function resolveStaticDir(staticDir) {
|
|
|
465
577
|
const currentDir = dirname2(fileURLToPath(import.meta.url));
|
|
466
578
|
const candidates = staticDir === void 0 ? [
|
|
467
579
|
currentDir,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
] : [staticDir,
|
|
580
|
+
join4(currentDir, "dist"),
|
|
581
|
+
join4(currentDir, "..", "dist"),
|
|
582
|
+
join4(currentDir, "..", "..", "dist"),
|
|
583
|
+
join4(currentDir, ".."),
|
|
584
|
+
join4(currentDir, "..", "..")
|
|
585
|
+
] : [staticDir, join4(staticDir, "dist")];
|
|
474
586
|
const resolvedCandidates = await Promise.all(
|
|
475
587
|
candidates.map(async (candidate) => ({
|
|
476
588
|
candidate,
|
|
477
|
-
exists: await fileExists(
|
|
589
|
+
exists: await fileExists(join4(candidate, "index.html"))
|
|
478
590
|
}))
|
|
479
591
|
);
|
|
480
592
|
const resolved = resolvedCandidates.find(({ exists }) => exists);
|
|
@@ -485,7 +597,7 @@ async function resolveStaticDir(staticDir) {
|
|
|
485
597
|
}
|
|
486
598
|
async function resolveReportPath(reportPath) {
|
|
487
599
|
if (await isDirectory(reportPath)) {
|
|
488
|
-
return { reportFile:
|
|
600
|
+
return { reportFile: join4(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
489
601
|
}
|
|
490
602
|
return { reportFile: reportPath, offlineReportDir: dirname2(reportPath) };
|
|
491
603
|
}
|
|
@@ -508,11 +620,22 @@ async function createServerApp(options = {}) {
|
|
|
508
620
|
const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport);
|
|
509
621
|
const handleRequest = (req) => handleHttpRequest(routesContext, req);
|
|
510
622
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
511
|
-
|
|
512
|
-
|
|
623
|
+
const reloadFromDisk = async () => {
|
|
624
|
+
resetReloadableReportData(reportData);
|
|
625
|
+
await loadReport(reportFile, reportData);
|
|
626
|
+
await loadOfflineReports(offlineReportDir, reportData);
|
|
627
|
+
broadcastToBrowsers(wsClients, { type: "sync", data: reportData });
|
|
628
|
+
};
|
|
629
|
+
await reloadFromDisk();
|
|
630
|
+
const close = await watchReportArtifacts({
|
|
631
|
+
offlineReportDir,
|
|
632
|
+
screenshotDir: reportData.screenshotDir,
|
|
633
|
+
scheduleRefresh: createDebouncedRefresh(reloadFromDisk)
|
|
634
|
+
});
|
|
513
635
|
return {
|
|
514
636
|
port,
|
|
515
637
|
wsClients,
|
|
638
|
+
close,
|
|
516
639
|
handleRequest,
|
|
517
640
|
handleWebSocketMessage
|
|
518
641
|
};
|
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
function normalizeScreenshotsBaseUrl(baseUrl) {
|
|
3
3
|
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
4
4
|
}
|
|
5
|
+
function classifyImage(image) {
|
|
6
|
+
if (image.actual !== void 0 || image.diff !== void 0) {
|
|
7
|
+
return "comparison";
|
|
8
|
+
}
|
|
9
|
+
if (image.expect !== void 0) {
|
|
10
|
+
return "baseline-only";
|
|
11
|
+
}
|
|
12
|
+
return "declared-only";
|
|
13
|
+
}
|
|
14
|
+
function withImageSource(image) {
|
|
15
|
+
return {
|
|
16
|
+
...image,
|
|
17
|
+
source: classifyImage(image)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function mergeDeclaredImages(images, visualNames) {
|
|
21
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(images), ...visualNames]);
|
|
22
|
+
return Object.fromEntries(
|
|
23
|
+
Array.from(names, (name) => {
|
|
24
|
+
const current = images[name] ?? {};
|
|
25
|
+
return [name, withImageSource(current)];
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
}
|
|
5
29
|
function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/") {
|
|
6
30
|
const images = {};
|
|
7
31
|
const baseUrl = normalizeScreenshotsBaseUrl(screenshotsBaseUrl);
|
|
@@ -12,7 +36,7 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
|
|
|
12
36
|
const baseName = match[1];
|
|
13
37
|
const role = match[2];
|
|
14
38
|
if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
|
|
15
|
-
images[baseName] ??= {
|
|
39
|
+
images[baseName] ??= {};
|
|
16
40
|
const url = `${baseUrl}${attachment.path}`;
|
|
17
41
|
const img = images[baseName];
|
|
18
42
|
if (img !== null && img !== void 0) {
|
|
@@ -23,10 +47,11 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
|
|
|
23
47
|
}
|
|
24
48
|
for (const key of Object.keys(images)) {
|
|
25
49
|
const img = images[key];
|
|
26
|
-
if (img?.actual !==
|
|
50
|
+
if (img?.actual !== void 0 && img?.expect !== void 0 && img?.diff === void 0) {
|
|
27
51
|
delete img.expect;
|
|
52
|
+
}
|
|
28
53
|
}
|
|
29
|
-
return images;
|
|
54
|
+
return mergeDeclaredImages(images, []);
|
|
30
55
|
}
|
|
31
56
|
function mapStatus(status) {
|
|
32
57
|
switch (status) {
|
|
@@ -42,17 +67,39 @@ function mapStatus(status) {
|
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
// src/report-state.ts
|
|
45
|
-
function
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
function isCurrentArtifact(image) {
|
|
71
|
+
return image?.actual !== void 0 || image?.expect !== void 0 || image?.diff !== void 0;
|
|
72
|
+
}
|
|
73
|
+
function isReusablePassingImage(image) {
|
|
74
|
+
const source = image.source ?? classifyImage(image);
|
|
75
|
+
if (source === "comparison") {
|
|
76
|
+
return image.actual !== void 0 && image.diff === void 0;
|
|
77
|
+
}
|
|
78
|
+
return source === "baseline-only";
|
|
79
|
+
}
|
|
80
|
+
function hasReusablePassingImages(images) {
|
|
81
|
+
return Object.values(images).some((img) => img !== null && img !== void 0 && isReusablePassingImage(img));
|
|
49
82
|
}
|
|
50
83
|
function preservePreviousPassingImages(test, status, images) {
|
|
51
|
-
if (status !== "passed"
|
|
84
|
+
if (status !== "passed") {
|
|
52
85
|
return images;
|
|
53
86
|
}
|
|
54
87
|
const previousImages = test.results?.[0]?.images ?? {};
|
|
55
|
-
|
|
88
|
+
if (!hasReusablePassingImages(previousImages)) {
|
|
89
|
+
return images;
|
|
90
|
+
}
|
|
91
|
+
return Object.entries(previousImages).reduce((currentImages, [name, previousImage]) => {
|
|
92
|
+
if (!Object.hasOwn(currentImages, name) || previousImage === void 0 || !isReusablePassingImage(previousImage) || isCurrentArtifact(currentImages[name])) {
|
|
93
|
+
return currentImages;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
...currentImages,
|
|
97
|
+
[name]: {
|
|
98
|
+
...previousImage,
|
|
99
|
+
source: previousImage.source ?? classifyImage(previousImage)
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}, images);
|
|
56
103
|
}
|
|
57
104
|
function countDiffImages(images) {
|
|
58
105
|
return Object.values(images).filter((img) => img?.diff !== null && img?.diff !== void 0).length;
|
|
@@ -88,10 +135,11 @@ function applyTestEndEvent(state, data, options = {}) {
|
|
|
88
135
|
return null;
|
|
89
136
|
}
|
|
90
137
|
test.status = mapStatus(data.status);
|
|
138
|
+
const resultStatus = data.status === "passed" ? "success" : data.status === "failed" ? "failed" : "pending";
|
|
91
139
|
const images = preservePreviousPassingImages(
|
|
92
140
|
test,
|
|
93
141
|
data.status,
|
|
94
|
-
attachmentsToImages(data.attachments, options.screenshotsBaseUrl)
|
|
142
|
+
mergeDeclaredImages(attachmentsToImages(data.attachments, options.screenshotsBaseUrl), data.visualNames)
|
|
95
143
|
);
|
|
96
144
|
const diffCount = countDiffImages(images);
|
|
97
145
|
const hasDiffs = diffCount > 0;
|
|
@@ -100,7 +148,7 @@ function applyTestEndEvent(state, data, options = {}) {
|
|
|
100
148
|
}
|
|
101
149
|
test.results = [
|
|
102
150
|
{
|
|
103
|
-
status:
|
|
151
|
+
status: resultStatus,
|
|
104
152
|
retries: 0,
|
|
105
153
|
images,
|
|
106
154
|
error: data.error,
|
|
@@ -132,11 +180,13 @@ var LocationSchema = z.object({
|
|
|
132
180
|
file: z.string(),
|
|
133
181
|
line: z.number()
|
|
134
182
|
});
|
|
183
|
+
var VisualSourceSchema = z.enum(["comparison", "baseline-only", "declared-only"]);
|
|
135
184
|
var ImagesSchema = z.object({
|
|
136
|
-
actual: z.string(),
|
|
185
|
+
actual: z.string().optional(),
|
|
137
186
|
expect: z.string().optional(),
|
|
138
187
|
diff: z.string().optional(),
|
|
139
|
-
error: z.string().optional()
|
|
188
|
+
error: z.string().optional(),
|
|
189
|
+
source: VisualSourceSchema.optional()
|
|
140
190
|
});
|
|
141
191
|
var AttachmentSchema = z.object({
|
|
142
192
|
name: z.string(),
|
|
@@ -144,10 +194,10 @@ var AttachmentSchema = z.object({
|
|
|
144
194
|
contentType: z.string()
|
|
145
195
|
});
|
|
146
196
|
var TestStatusSchema = z.enum(["unknown", "pending", "running", "failed", "approved", "success", "retrying"]);
|
|
197
|
+
var TestResultStatusSchema = z.enum(["failed", "success", "pending"]);
|
|
147
198
|
var TestResultSchema = z.object({
|
|
148
|
-
status:
|
|
199
|
+
status: TestResultStatusSchema,
|
|
149
200
|
retries: z.number(),
|
|
150
|
-
// eslint-disable-next-line typescript/no-unsafe-type-assertion
|
|
151
201
|
images: z.record(z.string(), ImagesSchema).optional(),
|
|
152
202
|
error: z.string().optional(),
|
|
153
203
|
duration: z.number().optional()
|
|
@@ -176,7 +226,6 @@ var CrvyRprtrSuiteSchema = z.lazy(
|
|
|
176
226
|
opened: z.boolean(),
|
|
177
227
|
checked: z.boolean(),
|
|
178
228
|
indeterminate: z.boolean(),
|
|
179
|
-
// eslint-disable-next-line typescript/no-unsafe-type-assertion
|
|
180
229
|
children: z.record(z.string(), z.union([CrvyRprtrSuiteSchema, CrvyRprtrTestSchema])).optional()
|
|
181
230
|
})
|
|
182
231
|
);
|
|
@@ -195,6 +244,7 @@ var TestEndDataSchema = z.object({
|
|
|
195
244
|
id: z.string(),
|
|
196
245
|
status: z.enum(["passed", "failed", "skipped"]),
|
|
197
246
|
attachments: z.array(AttachmentSchema),
|
|
247
|
+
visualNames: z.array(z.string()).default([]),
|
|
198
248
|
error: z.string().optional(),
|
|
199
249
|
duration: z.number().optional()
|
|
200
250
|
});
|
package/dist/cli.js
CHANGED
package/dist/index.css
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
--color-purple-800: oklch(43.8% 0.218 303.724);
|
|
24
24
|
--color-white: #fff;
|
|
25
25
|
--spacing: 0.25rem;
|
|
26
|
+
--container-xl: 36rem;
|
|
27
|
+
--container-2xl: 42rem;
|
|
26
28
|
--text-xs: 0.75rem;
|
|
27
29
|
--text-xs--line-height: calc(1 / 0.75);
|
|
28
30
|
--text-sm: 0.875rem;
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
--font-weight-medium: 500;
|
|
34
36
|
--font-weight-semibold: 600;
|
|
35
37
|
--font-weight-bold: 700;
|
|
38
|
+
--tracking-wide: 0.025em;
|
|
36
39
|
--tracking-wider: 0.05em;
|
|
37
40
|
--radius-sm: 0.25rem;
|
|
38
41
|
--radius-md: 0.375rem;
|
|
@@ -213,6 +216,9 @@
|
|
|
213
216
|
.pointer-events-none {
|
|
214
217
|
pointer-events: none;
|
|
215
218
|
}
|
|
219
|
+
.visible {
|
|
220
|
+
visibility: visible;
|
|
221
|
+
}
|
|
216
222
|
.absolute {
|
|
217
223
|
position: absolute;
|
|
218
224
|
}
|
|
@@ -285,6 +291,9 @@
|
|
|
285
291
|
.mt-2 {
|
|
286
292
|
margin-top: calc(var(--spacing) * 2);
|
|
287
293
|
}
|
|
294
|
+
.mt-3 {
|
|
295
|
+
margin-top: calc(var(--spacing) * 3);
|
|
296
|
+
}
|
|
288
297
|
.mr-3 {
|
|
289
298
|
margin-right: calc(var(--spacing) * 3);
|
|
290
299
|
}
|
|
@@ -306,6 +315,12 @@
|
|
|
306
315
|
.grid {
|
|
307
316
|
display: grid;
|
|
308
317
|
}
|
|
318
|
+
.hidden {
|
|
319
|
+
display: none;
|
|
320
|
+
}
|
|
321
|
+
.inline {
|
|
322
|
+
display: inline;
|
|
323
|
+
}
|
|
309
324
|
.inline-block {
|
|
310
325
|
display: inline-block;
|
|
311
326
|
}
|
|
@@ -373,9 +388,15 @@
|
|
|
373
388
|
.w-full {
|
|
374
389
|
width: 100%;
|
|
375
390
|
}
|
|
391
|
+
.max-w-2xl {
|
|
392
|
+
max-width: var(--container-2xl);
|
|
393
|
+
}
|
|
376
394
|
.max-w-full {
|
|
377
395
|
max-width: 100%;
|
|
378
396
|
}
|
|
397
|
+
.max-w-xl {
|
|
398
|
+
max-width: var(--container-xl);
|
|
399
|
+
}
|
|
379
400
|
.min-w-0 {
|
|
380
401
|
min-width: calc(var(--spacing) * 0);
|
|
381
402
|
}
|
|
@@ -517,6 +538,12 @@
|
|
|
517
538
|
.border-edge {
|
|
518
539
|
border-color: var(--color-edge);
|
|
519
540
|
}
|
|
541
|
+
.border-edge\/70 {
|
|
542
|
+
border-color: var(--color-edge);
|
|
543
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
544
|
+
border-color: color-mix(in oklab, var(--color-edge) 70%, transparent);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
520
547
|
.border-error {
|
|
521
548
|
border-color: var(--color-error);
|
|
522
549
|
}
|
|
@@ -529,6 +556,12 @@
|
|
|
529
556
|
border-color: color-mix(in oklab, var(--color-green-500) 60%, transparent);
|
|
530
557
|
}
|
|
531
558
|
}
|
|
559
|
+
.border-info\/40 {
|
|
560
|
+
border-color: var(--color-info);
|
|
561
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
562
|
+
border-color: color-mix(in oklab, var(--color-info) 40%, transparent);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
532
565
|
.border-purple-500\/60 {
|
|
533
566
|
border-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 60%, transparent);
|
|
534
567
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -583,6 +616,12 @@
|
|
|
583
616
|
.bg-info {
|
|
584
617
|
background-color: var(--color-info);
|
|
585
618
|
}
|
|
619
|
+
.bg-info\/10 {
|
|
620
|
+
background-color: var(--color-info);
|
|
621
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
622
|
+
background-color: color-mix(in oklab, var(--color-info) 10%, transparent);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
586
625
|
.bg-purple-500\/25 {
|
|
587
626
|
background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 25%, transparent);
|
|
588
627
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -698,6 +737,10 @@
|
|
|
698
737
|
.text-\[9px\] {
|
|
699
738
|
font-size: 9px;
|
|
700
739
|
}
|
|
740
|
+
.leading-6 {
|
|
741
|
+
--tw-leading: calc(var(--spacing) * 6);
|
|
742
|
+
line-height: calc(var(--spacing) * 6);
|
|
743
|
+
}
|
|
701
744
|
.font-bold {
|
|
702
745
|
--tw-font-weight: var(--font-weight-bold);
|
|
703
746
|
font-weight: var(--font-weight-bold);
|
|
@@ -714,6 +757,10 @@
|
|
|
714
757
|
--tw-font-weight: var(--font-weight-semibold);
|
|
715
758
|
font-weight: var(--font-weight-semibold);
|
|
716
759
|
}
|
|
760
|
+
.tracking-wide {
|
|
761
|
+
--tw-tracking: var(--tracking-wide);
|
|
762
|
+
letter-spacing: var(--tracking-wide);
|
|
763
|
+
}
|
|
717
764
|
.tracking-wider {
|
|
718
765
|
--tw-tracking: var(--tracking-wider);
|
|
719
766
|
letter-spacing: var(--tracking-wider);
|
|
@@ -1128,6 +1175,10 @@
|
|
|
1128
1175
|
inherits: false;
|
|
1129
1176
|
initial-value: solid;
|
|
1130
1177
|
}
|
|
1178
|
+
@property --tw-leading {
|
|
1179
|
+
syntax: "*";
|
|
1180
|
+
inherits: false;
|
|
1181
|
+
}
|
|
1131
1182
|
@property --tw-font-weight {
|
|
1132
1183
|
syntax: "*";
|
|
1133
1184
|
inherits: false;
|
|
@@ -1295,6 +1346,7 @@
|
|
|
1295
1346
|
--tw-translate-y: 0;
|
|
1296
1347
|
--tw-translate-z: 0;
|
|
1297
1348
|
--tw-border-style: solid;
|
|
1349
|
+
--tw-leading: initial;
|
|
1298
1350
|
--tw-font-weight: initial;
|
|
1299
1351
|
--tw-tracking: initial;
|
|
1300
1352
|
--tw-ordinal: initial;
|