@crvy/rprtr 0.0.3

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/LICENSE +21 -0
  3. package/README.md +130 -0
  4. package/dist/chunk-BRFA7JCQ.js +729 -0
  5. package/dist/chunk-IEYNAB6M.js +263 -0
  6. package/dist/cli.d.ts +10 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +61 -0
  9. package/dist/index.css +1335 -0
  10. package/dist/index.html +14 -0
  11. package/dist/index.js +21106 -0
  12. package/dist/is-direct-execution.d.ts +2 -0
  13. package/dist/is-direct-execution.d.ts.map +1 -0
  14. package/dist/offline-reports.d.ts +9 -0
  15. package/dist/offline-reports.d.ts.map +1 -0
  16. package/dist/report-artifact.d.ts +12 -0
  17. package/dist/report-artifact.d.ts.map +1 -0
  18. package/dist/report-state.d.ts +28 -0
  19. package/dist/report-state.d.ts.map +1 -0
  20. package/dist/report-utils.d.ts +5 -0
  21. package/dist/report-utils.d.ts.map +1 -0
  22. package/dist/reporter-utils.d.ts +3 -0
  23. package/dist/reporter-utils.d.ts.map +1 -0
  24. package/dist/reporter.d.ts +34 -0
  25. package/dist/reporter.d.ts.map +1 -0
  26. package/dist/reporter.js +393 -0
  27. package/dist/schemas.d.ts +380 -0
  28. package/dist/schemas.d.ts.map +1 -0
  29. package/dist/server/app.d.ts +16 -0
  30. package/dist/server/app.d.ts.map +1 -0
  31. package/dist/server/bun-adapter.d.ts +3 -0
  32. package/dist/server/bun-adapter.d.ts.map +1 -0
  33. package/dist/server/file-utils.d.ts +7 -0
  34. package/dist/server/file-utils.d.ts.map +1 -0
  35. package/dist/server/handlers.d.ts +21 -0
  36. package/dist/server/handlers.d.ts.map +1 -0
  37. package/dist/server/node-adapter.d.ts +3 -0
  38. package/dist/server/node-adapter.d.ts.map +1 -0
  39. package/dist/server/routes.d.ts +16 -0
  40. package/dist/server/routes.d.ts.map +1 -0
  41. package/dist/server/utils.d.ts +3 -0
  42. package/dist/server/utils.d.ts.map +1 -0
  43. package/dist/server/ws.d.ts +4 -0
  44. package/dist/server/ws.d.ts.map +1 -0
  45. package/dist/server.d.ts +4 -0
  46. package/dist/server.d.ts.map +1 -0
  47. package/dist/server.js +7 -0
  48. package/dist/types.d.ts +116 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/package.json +102 -0
@@ -0,0 +1,2 @@
1
+ export declare function isDirectExecution(moduleUrl: string): boolean;
2
+ //# sourceMappingURL=is-direct-execution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-direct-execution.d.ts","sourceRoot":"","sources":["../src/is-direct-execution.ts"],"names":[],"mappings":"AAYA,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAU5D"}
@@ -0,0 +1,9 @@
1
+ import { type OfflineReport as ParsedOfflineReport } from './schemas.ts';
2
+ import type { TestData } from './types.ts';
3
+ export declare function findOfflineReportPaths(searchDir: string): Promise<string[]>;
4
+ export declare function parseOfflineReport(value: unknown): ParsedOfflineReport | null;
5
+ export declare function mergeOfflineReportsIntoTests(existingTests: Record<string, TestData>, offlineReports: ParsedOfflineReport[], options?: {
6
+ screenshotDir?: string;
7
+ screenshotsBaseUrl?: string;
8
+ }): Record<string, TestData>;
9
+ //# sourceMappingURL=offline-reports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"offline-reports.d.ts","sourceRoot":"","sources":["../src/offline-reports.ts"],"names":[],"mappings":"AAIA,OAAO,EAKL,KAAK,aAAa,IAAI,mBAAmB,EAC1C,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAI1C,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAcjF;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,mBAAmB,GAAG,IAAI,CAO7E;AAED,wBAAgB,4BAA4B,CAC1C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EACvC,cAAc,EAAE,mBAAmB,EAAE,EACrC,OAAO,GAAE;IAAE,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAO,GACpE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAoC1B"}
@@ -0,0 +1,12 @@
1
+ import type { OfflineEvent } from './types.ts';
2
+ export declare const STATIC_ARTIFACT_APPROVAL_MESSAGE = "This artifact is read-only. Open it with the Crvy Rprtr server to approve screenshots.";
3
+ export interface WriteReportArtifactOptions {
4
+ events: Array<{
5
+ type: OfflineEvent['type'];
6
+ data: unknown;
7
+ }>;
8
+ screenshotDir: string;
9
+ reportHtmlPath?: string;
10
+ }
11
+ export declare function writeReportArtifact(options: WriteReportArtifactOptions): Promise<void>;
12
+ //# sourceMappingURL=report-artifact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-artifact.d.ts","sourceRoot":"","sources":["../src/report-artifact.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAuB,YAAY,EAAE,MAAM,YAAY,CAAA;AAInE,eAAO,MAAM,gCAAgC,2FAC6C,CAAA;AA+H1F,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;IAC5D,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAkB5F"}
@@ -0,0 +1,28 @@
1
+ import type { TestBeginData, TestEndData } from './schemas.ts';
2
+ import type { TestData } from './types.ts';
3
+ export interface MutableReportData {
4
+ isRunning: boolean;
5
+ tests: Record<string, TestData>;
6
+ browsers: string[];
7
+ isUpdateMode: boolean;
8
+ screenshotDir: string;
9
+ }
10
+ export interface MutableReportState {
11
+ reportData: MutableReportData;
12
+ currentRunIds: Set<string>;
13
+ }
14
+ export interface ApplyTestEndResult {
15
+ test: TestData;
16
+ diffCount: number;
17
+ }
18
+ export declare function createMutableReportState(screenshotDir?: string): MutableReportState;
19
+ export declare function applyTestBeginEvent(state: MutableReportState, data: TestBeginData): TestData;
20
+ export declare function applyTestEndEvent(state: MutableReportState, data: TestEndData, options?: {
21
+ screenshotsBaseUrl?: string;
22
+ }): ApplyTestEndResult | null;
23
+ export declare function finalizeRunEvent(state: MutableReportState): {
24
+ passed: number;
25
+ failed: number;
26
+ pending: number;
27
+ };
28
+ //# sourceMappingURL=report-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-state.d.ts","sourceRoot":"","sources":["../src/report-state.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC9D,OAAO,KAAK,EAAU,QAAQ,EAAE,MAAM,YAAY,CAAA;AAElD,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,iBAAiB,CAAA;IAC7B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AA0BD,wBAAgB,wBAAwB,CAAC,aAAa,SAAkB,GAAG,kBAAkB,CAW5F;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,aAAa,GAAG,QAAQ,CAa5F;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,kBAAkB,EACzB,IAAI,EAAE,WAAW,EACjB,OAAO,GAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5C,kBAAkB,GAAG,IAAI,CAkC3B;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAa/G"}
@@ -0,0 +1,5 @@
1
+ import type { Attachment } from './schemas.ts';
2
+ import type { Images, TestData } from './types.ts';
3
+ export declare function attachmentsToImages(attachments: Attachment[], screenshotsBaseUrl?: string): Partial<Record<string, Images>>;
4
+ export declare function mapStatus(status: 'passed' | 'failed' | 'skipped'): TestData['status'];
5
+ //# sourceMappingURL=report-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report-utils.d.ts","sourceRoot":"","sources":["../src/report-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAMlD,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,UAAU,EAAE,EACzB,kBAAkB,SAAkB,GACnC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAqCjC;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAWrF"}
@@ -0,0 +1,3 @@
1
+ import type { TestStep } from '@playwright/test/reporter';
2
+ export declare function extractScreenshotNames(steps: TestStep[]): string[];
3
+ //# sourceMappingURL=reporter-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter-utils.d.ts","sourceRoot":"","sources":["../src/reporter-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AAEzD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,MAAM,EAAE,CAQlE"}
@@ -0,0 +1,34 @@
1
+ import type { Reporter, FullConfig, Suite, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
2
+ export interface CrvyRprtrOptions {
3
+ serverUrl?: string;
4
+ screenshotDir?: string;
5
+ offlineReportPath?: string;
6
+ reportHtmlPath?: string;
7
+ }
8
+ export declare class CrvyRprtr implements Reporter {
9
+ private ws;
10
+ private serverUrl;
11
+ private screenshotDir;
12
+ private queue;
13
+ private workerIndex;
14
+ private offlineReportPath;
15
+ private reportHtmlPath;
16
+ private isOfflineMode;
17
+ private hadOfflineMode;
18
+ private runEvents;
19
+ constructor(options?: CrvyRprtrOptions);
20
+ onBegin(config: FullConfig, suite: Suite): Promise<void>;
21
+ private connect;
22
+ private enableOfflineMode;
23
+ onTestBegin(test: TestCase): void;
24
+ onTestEnd(test: TestCase, result: TestResult): Promise<void>;
25
+ private copySnapshotBaselines;
26
+ private saveAttachments;
27
+ private sanitizeId;
28
+ private writeOfflineReport;
29
+ private writeStaticArtifact;
30
+ onEnd(result: FullResult): Promise<void>;
31
+ private send;
32
+ }
33
+ export default CrvyRprtr;
34
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAQ9G,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAQD,qBAAa,SAAU,YAAW,QAAQ;IACxC,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,SAAS,CAAQ;IACzB,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,KAAK,CAAe;IAC5B,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,iBAAiB,CAAQ;IACjC,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,SAAS,CAA4E;gBAEjF,OAAO,GAAE,gBAAqB;IAQpC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAM9D,OAAO,CAAC,OAAO;IA8Bf,OAAO,CAAC,iBAAiB;IAQzB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAsB3B,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;YAiBpD,qBAAqB;YAsCrB,eAAe;IAyC7B,OAAO,CAAC,UAAU;YAIJ,kBAAkB;YAyBlB,mBAAmB;IAa3B,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B9C,OAAO,CAAC,IAAI;CAkBb;AAED,eAAe,SAAS,CAAA"}
@@ -0,0 +1,393 @@
1
+ import {
2
+ ClientBootstrapDataSchema,
3
+ TestBeginDataSchema,
4
+ TestEndDataSchema,
5
+ applyTestBeginEvent,
6
+ applyTestEndEvent,
7
+ createMutableReportState,
8
+ finalizeRunEvent,
9
+ safeParse
10
+ } from "./chunk-IEYNAB6M.js";
11
+
12
+ // src/reporter.ts
13
+ import { mkdir as mkdir2, copyFile, writeFile as writeFile2 } from "fs/promises";
14
+ import { join } from "path";
15
+ import pLimit from "p-limit";
16
+
17
+ // src/report-artifact.ts
18
+ import { access, mkdir, readFile, writeFile } from "fs/promises";
19
+ import { dirname, relative, resolve, sep } from "path";
20
+ import { fileURLToPath } from "url";
21
+ var DEFAULT_REPORT_HTML_PATH = "./crvy-rprtr.html";
22
+ var STATIC_ARTIFACT_APPROVAL_MESSAGE = "This artifact is read-only. Open it with the Crvy Rprtr server to approve screenshots.";
23
+ function toBrowserPath(pathValue) {
24
+ const normalized = pathValue.split(sep).join("/");
25
+ if (normalized === "") return ".";
26
+ if (normalized.startsWith(".")) return normalized;
27
+ return `./${normalized}`;
28
+ }
29
+ function toBrowserDirPath(pathValue) {
30
+ const path = toBrowserPath(pathValue);
31
+ return path === "." ? "./" : `${path}/`;
32
+ }
33
+ function serializeBootstrapData(data) {
34
+ return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
35
+ }
36
+ function escapeInlineStyle(style) {
37
+ return style.replace(/<\/style>/gi, "<\\/style>");
38
+ }
39
+ function escapeInlineScript(script) {
40
+ return script.replace(/<\/script>/gi, "<\\/script>");
41
+ }
42
+ function renderArtifactHtml(template, bootstrapData, stylesheet, script) {
43
+ const stylesheetPlaceholder = '<link rel="stylesheet" href="/dist/index.css" />';
44
+ const scriptPlaceholder = '<script type="module" src="/dist/index.js"></script>';
45
+ if (!template.includes(stylesheetPlaceholder) || !template.includes(scriptPlaceholder)) {
46
+ throw new Error("Failed to find static asset placeholders in the HTML template");
47
+ }
48
+ const withStylesheet = template.replace(
49
+ stylesheetPlaceholder,
50
+ () => `<style>${escapeInlineStyle(stylesheet)}</style>`
51
+ );
52
+ const bootstrapScript = `<script id="crvy-rprtr-bootstrap" type="application/json">${serializeBootstrapData(bootstrapData)}</script>`;
53
+ return withStylesheet.replace(
54
+ scriptPlaceholder,
55
+ () => `${bootstrapScript}
56
+ <script type="module">${escapeInlineScript(script)}</script>`
57
+ );
58
+ }
59
+ async function getPackagedAssetPath(fileName) {
60
+ const currentDir = dirname(fileURLToPath(import.meta.url));
61
+ const candidates = [currentDir, resolve(currentDir, "../dist")];
62
+ const resolvedCandidates = await Promise.all(
63
+ candidates.map(async (candidate) => {
64
+ const assetPath = resolve(candidate, fileName);
65
+ try {
66
+ await access(assetPath);
67
+ return assetPath;
68
+ } catch {
69
+ return null;
70
+ }
71
+ })
72
+ );
73
+ const resolvedAssetPath = resolvedCandidates.find((assetPath) => assetPath !== null);
74
+ if (resolvedAssetPath !== void 0) {
75
+ return resolvedAssetPath;
76
+ }
77
+ throw new Error(
78
+ `Could not find packaged asset "${fileName}". Run "bun run build" before generating report artifacts.`
79
+ );
80
+ }
81
+ function buildStaticBootstrapData(events, screenshotDir, htmlPath) {
82
+ const screenshotBaseUrl = toBrowserDirPath(relative(dirname(htmlPath), resolve(screenshotDir)));
83
+ const state = createMutableReportState(screenshotDir);
84
+ for (const event of events) {
85
+ switch (event.type) {
86
+ case "test-begin": {
87
+ const parsed = safeParse(TestBeginDataSchema, event.data);
88
+ if (parsed !== null) {
89
+ applyTestBeginEvent(state, parsed);
90
+ }
91
+ break;
92
+ }
93
+ case "test-end": {
94
+ const parsed = safeParse(TestEndDataSchema, event.data);
95
+ if (parsed !== null) {
96
+ applyTestEndEvent(state, parsed, { screenshotsBaseUrl: screenshotBaseUrl });
97
+ }
98
+ break;
99
+ }
100
+ case "run-end":
101
+ finalizeRunEvent(state);
102
+ break;
103
+ }
104
+ }
105
+ const bootstrapData = {
106
+ report: {
107
+ tests: state.reportData.tests,
108
+ isUpdateMode: state.reportData.isUpdateMode
109
+ },
110
+ liveUpdates: false,
111
+ approvalEnabled: false,
112
+ approvalMessage: STATIC_ARTIFACT_APPROVAL_MESSAGE
113
+ };
114
+ const parsedBootstrapData = safeParse(ClientBootstrapDataSchema, bootstrapData);
115
+ if (parsedBootstrapData === null) {
116
+ throw new Error("Failed to build static report bootstrap data");
117
+ }
118
+ return parsedBootstrapData;
119
+ }
120
+ async function writeReportArtifact(options) {
121
+ const reportHtmlPath = resolve(options.reportHtmlPath ?? DEFAULT_REPORT_HTML_PATH);
122
+ const [indexHtmlPath, indexJsPath, indexCssPath] = await Promise.all([
123
+ getPackagedAssetPath("index.html"),
124
+ getPackagedAssetPath("index.js"),
125
+ getPackagedAssetPath("index.css")
126
+ ]);
127
+ await mkdir(dirname(reportHtmlPath), { recursive: true });
128
+ const [template, script, stylesheet] = await Promise.all([
129
+ readFile(indexHtmlPath, "utf8"),
130
+ readFile(indexJsPath, "utf8"),
131
+ readFile(indexCssPath, "utf8")
132
+ ]);
133
+ const bootstrapData = buildStaticBootstrapData(options.events, options.screenshotDir, reportHtmlPath);
134
+ const html = renderArtifactHtml(template, bootstrapData, stylesheet, script);
135
+ await writeFile(reportHtmlPath, html);
136
+ }
137
+
138
+ // src/reporter-utils.ts
139
+ function extractScreenshotNames(steps) {
140
+ const names = [];
141
+ for (const step of steps) {
142
+ const match = step.title.match(/toHaveScreenshot\((.+?)\)/);
143
+ if (match?.[1] !== void 0 && match[1] !== "") names.push(match[1]);
144
+ if (step.steps.length) names.push(...extractScreenshotNames(step.steps));
145
+ }
146
+ return names;
147
+ }
148
+
149
+ // src/reporter.ts
150
+ var MAX_CONCURRENT_FILE_OPS = 5;
151
+ var CrvyRprtr = class {
152
+ ws = null;
153
+ serverUrl;
154
+ screenshotDir;
155
+ queue = [];
156
+ workerIndex;
157
+ offlineReportPath;
158
+ reportHtmlPath;
159
+ isOfflineMode = false;
160
+ hadOfflineMode = false;
161
+ runEvents = [];
162
+ constructor(options = {}) {
163
+ this.serverUrl = options.serverUrl ?? "ws://localhost:3000";
164
+ this.screenshotDir = options.screenshotDir ?? "./screenshots";
165
+ this.workerIndex = parseInt(process.env.TEST_WORKER_INDEX ?? "0", 10) || 0;
166
+ this.offlineReportPath = options.offlineReportPath ?? `./crvy-rprtr-${this.workerIndex}.json`;
167
+ this.reportHtmlPath = options.reportHtmlPath ?? "./crvy-rprtr.html";
168
+ }
169
+ async onBegin(config, suite) {
170
+ console.log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
171
+ await mkdir2(this.screenshotDir, { recursive: true });
172
+ this.connect();
173
+ }
174
+ connect() {
175
+ const WebSocketConstructor = globalThis.WebSocket;
176
+ if (typeof WebSocketConstructor !== "function") {
177
+ console.log("[CrvyRprtr] WebSocket unavailable in current runtime; offline mode enabled");
178
+ this.enableOfflineMode();
179
+ return;
180
+ }
181
+ try {
182
+ this.ws = new WebSocketConstructor(this.serverUrl);
183
+ this.ws.onopen = () => {
184
+ console.log("[CrvyRprtr] Connected to Crvy Rprtr server");
185
+ this.isOfflineMode = false;
186
+ for (const msg of this.queue) this.ws.send(msg);
187
+ this.queue = [];
188
+ };
189
+ this.ws.onerror = (error) => {
190
+ console.error("[CrvyRprtr] WebSocket error:", error);
191
+ this.enableOfflineMode();
192
+ };
193
+ this.ws.onclose = () => {
194
+ console.log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
195
+ this.enableOfflineMode();
196
+ };
197
+ } catch (e) {
198
+ console.error("[CrvyRprtr] Failed to connect:", e);
199
+ this.enableOfflineMode();
200
+ }
201
+ }
202
+ enableOfflineMode() {
203
+ if (!this.isOfflineMode) {
204
+ this.isOfflineMode = true;
205
+ this.hadOfflineMode = true;
206
+ console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
207
+ }
208
+ }
209
+ onTestBegin(test) {
210
+ const titlePath = [];
211
+ let suite = test.parent;
212
+ while (suite && suite.type === "describe") {
213
+ titlePath.unshift(suite.title);
214
+ suite = suite.parent;
215
+ }
216
+ this.send({
217
+ type: "test-begin",
218
+ data: {
219
+ id: test.id,
220
+ title: test.title,
221
+ titlePath,
222
+ browser: test.parent.project()?.name ?? "chromium",
223
+ location: {
224
+ file: test.location.file,
225
+ line: test.location.line
226
+ }
227
+ }
228
+ });
229
+ }
230
+ async onTestEnd(test, result) {
231
+ const savedAttachments = await this.saveAttachments(test.id, result);
232
+ await this.copySnapshotBaselines(test, result, savedAttachments);
233
+ this.send({
234
+ type: "test-end",
235
+ data: {
236
+ id: test.id,
237
+ title: test.title,
238
+ status: result.status,
239
+ attachments: savedAttachments,
240
+ error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
241
+ duration: result.duration
242
+ }
243
+ });
244
+ }
245
+ async copySnapshotBaselines(test, result, savedAttachments) {
246
+ if (result.status !== "passed") return;
247
+ const snapshotNames = extractScreenshotNames(result.steps);
248
+ if (snapshotNames.length === 0) return;
249
+ const projectName = test.parent.project()?.name ?? "chromium";
250
+ const snapshotDir = `${test.location.file}-snapshots`;
251
+ const testScreenshotDir = join(this.screenshotDir, this.sanitizeId(test.id));
252
+ const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
253
+ const copyPromises = snapshotNames.map(
254
+ (name) => limit(async () => {
255
+ const baseName = name.replace(/\.png$/, "");
256
+ const snapshotPath = join(snapshotDir, `${baseName}-${projectName}-${process.platform}.png`);
257
+ const destName = `${baseName}-expected`;
258
+ const destPath = join(testScreenshotDir, destName);
259
+ try {
260
+ await mkdir2(testScreenshotDir, { recursive: true });
261
+ await copyFile(snapshotPath, destPath);
262
+ savedAttachments.push({
263
+ name: destName,
264
+ path: `${this.sanitizeId(test.id)}/${destName}`,
265
+ contentType: "image/png"
266
+ });
267
+ console.log(`[CrvyRprtr] Attached baseline: ${snapshotPath}`);
268
+ } catch {
269
+ }
270
+ })
271
+ );
272
+ await Promise.all(copyPromises);
273
+ }
274
+ async saveAttachments(testId, result) {
275
+ const savedAttachments = [];
276
+ const testScreenshotDir = join(this.screenshotDir, this.sanitizeId(testId));
277
+ const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
278
+ const attachmentPromises = result.attachments.filter(
279
+ (attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
280
+ ).map(
281
+ (attachment) => limit(async () => {
282
+ try {
283
+ await mkdir2(testScreenshotDir, { recursive: true });
284
+ const fileName = attachment.name;
285
+ const destPath = join(testScreenshotDir, fileName);
286
+ await copyFile(attachment.path, destPath);
287
+ const attachmentData = {
288
+ name: attachment.name,
289
+ path: `${this.sanitizeId(testId)}/${fileName}`,
290
+ contentType: attachment.contentType
291
+ };
292
+ savedAttachments.push(attachmentData);
293
+ console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
294
+ } catch (e) {
295
+ console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, e);
296
+ const fallbackData = {
297
+ name: attachment.name,
298
+ path: attachment.path,
299
+ contentType: attachment.contentType
300
+ };
301
+ savedAttachments.push(fallbackData);
302
+ }
303
+ })
304
+ );
305
+ await Promise.all(attachmentPromises);
306
+ return savedAttachments;
307
+ }
308
+ sanitizeId(id) {
309
+ return id.replace(/[^a-zA-Z0-9-_]/g, "_");
310
+ }
311
+ async writeOfflineReport() {
312
+ if (this.runEvents.length === 0) {
313
+ console.log("[CrvyRprtr] No offline events to write");
314
+ return;
315
+ }
316
+ try {
317
+ const report = {
318
+ version: 1,
319
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
320
+ workers: this.workerIndex + 1,
321
+ events: this.runEvents.map((e) => ({
322
+ ...e,
323
+ timestamp: Date.now(),
324
+ workerIndex: this.workerIndex
325
+ }))
326
+ };
327
+ await writeFile2(this.offlineReportPath, JSON.stringify(report, null, 2));
328
+ console.log(`[CrvyRprtr] Wrote offline report: ${this.offlineReportPath}`);
329
+ } catch (e) {
330
+ console.error("[CrvyRprtr] Failed to write offline report:", e);
331
+ }
332
+ }
333
+ async writeStaticArtifact() {
334
+ try {
335
+ await writeReportArtifact({
336
+ events: this.runEvents,
337
+ screenshotDir: this.screenshotDir,
338
+ reportHtmlPath: this.reportHtmlPath
339
+ });
340
+ console.log(`[CrvyRprtr] Wrote report artifact: ${this.reportHtmlPath}`);
341
+ } catch (e) {
342
+ console.error("[CrvyRprtr] Failed to write report artifact:", e);
343
+ }
344
+ }
345
+ async onEnd(result) {
346
+ this.send({
347
+ type: "run-end",
348
+ data: {
349
+ status: result.status
350
+ }
351
+ });
352
+ await this.writeStaticArtifact();
353
+ if (this.hadOfflineMode) {
354
+ await this.writeOfflineReport();
355
+ }
356
+ await new Promise((resolve2) => {
357
+ if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
358
+ resolve2();
359
+ return;
360
+ }
361
+ this.ws.onclose = () => {
362
+ resolve2();
363
+ };
364
+ setTimeout(() => {
365
+ this.ws?.close();
366
+ resolve2();
367
+ }, 1e3);
368
+ this.ws.close();
369
+ });
370
+ }
371
+ send(msg) {
372
+ const msgObj = msg;
373
+ if (msgObj.type === "test-begin" || msgObj.type === "test-end" || msgObj.type === "run-end") {
374
+ this.runEvents.push({
375
+ type: msgObj.type,
376
+ data: msgObj.data
377
+ });
378
+ }
379
+ const payload = JSON.stringify(msg);
380
+ if (!this.isOfflineMode) {
381
+ if (this.ws?.readyState === WebSocket.OPEN) {
382
+ this.ws.send(payload);
383
+ } else {
384
+ this.queue.push(payload);
385
+ }
386
+ }
387
+ }
388
+ };
389
+ var reporter_default = CrvyRprtr;
390
+ export {
391
+ CrvyRprtr,
392
+ reporter_default as default
393
+ };