@crvy/rprtr 0.0.5 → 0.0.8
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 +65 -0
- package/README.md +34 -9
- package/dist/{chunk-MWLM3HWS.js → chunk-EJRLZZ22.js} +312 -125
- package/dist/{chunk-IH44LYNM.js → chunk-XU5VQ3FZ.js} +133 -53
- package/dist/cli.js +2 -2
- package/dist/debug-log.d.ts +3 -0
- package/dist/debug-log.d.ts.map +1 -0
- package/dist/index.css +3 -22
- package/dist/index.js +626 -600
- package/dist/report-state.d.ts +6 -1
- package/dist/report-state.d.ts.map +1 -1
- package/dist/report-utils.d.ts +3 -0
- package/dist/report-utils.d.ts.map +1 -1
- package/dist/reporter-artifact-ops.d.ts +22 -0
- package/dist/reporter-artifact-ops.d.ts.map +1 -0
- package/dist/reporter-utils.d.ts +11 -2
- package/dist/reporter-utils.d.ts.map +1 -1
- package/dist/reporter.cjs +417 -185
- package/dist/reporter.d.ts +12 -5
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +228 -181
- package/dist/schemas.d.ts +115 -15
- package/dist/schemas.d.ts.map +1 -1
- package/dist/server/app.d.ts +5 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server.cjs +331 -64
- package/dist/server.js +2 -2
- package/dist/snapshot-path-resolver.d.ts +30 -0
- package/dist/snapshot-path-resolver.d.ts.map +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/reporter.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
|
|
2
2
|
export interface CrvyRprtrOptions {
|
|
3
3
|
serverUrl?: string;
|
|
4
4
|
screenshotDir?: string;
|
|
5
5
|
offlineReportPath?: string;
|
|
6
6
|
reportHtmlPath?: string;
|
|
7
|
+
playwrightSnapshotDir?: string;
|
|
8
|
+
playwrightSnapshotPathTemplate?: string;
|
|
9
|
+
playwrightToHaveScreenshotPathTemplate?: string;
|
|
7
10
|
}
|
|
8
11
|
export declare class CrvyRprtr implements Reporter {
|
|
9
12
|
private ws;
|
|
@@ -13,6 +16,11 @@ export declare class CrvyRprtr implements Reporter {
|
|
|
13
16
|
private workerIndex;
|
|
14
17
|
private offlineReportPath;
|
|
15
18
|
private reportHtmlPath;
|
|
19
|
+
private configDir;
|
|
20
|
+
private testMetadata;
|
|
21
|
+
private playwrightSnapshotDir?;
|
|
22
|
+
private playwrightSnapshotPathTemplate?;
|
|
23
|
+
private playwrightToHaveScreenshotPathTemplate?;
|
|
16
24
|
private isOfflineMode;
|
|
17
25
|
private hadOfflineMode;
|
|
18
26
|
private runEvents;
|
|
@@ -20,13 +28,12 @@ export declare class CrvyRprtr implements Reporter {
|
|
|
20
28
|
onBegin(config: FullConfig, suite: Suite): Promise<void>;
|
|
21
29
|
private connect;
|
|
22
30
|
private enableOfflineMode;
|
|
31
|
+
private describeTitlePath;
|
|
32
|
+
private reporterTitlePath;
|
|
23
33
|
onTestBegin(test: TestCase): void;
|
|
24
34
|
onTestEnd(test: TestCase, result: TestResult): Promise<void>;
|
|
35
|
+
private baselineInput;
|
|
25
36
|
private copySnapshotBaselines;
|
|
26
|
-
private saveAttachments;
|
|
27
|
-
private sanitizeId;
|
|
28
|
-
private writeOfflineReport;
|
|
29
|
-
private writeStaticArtifact;
|
|
30
37
|
onEnd(result: FullResult): Promise<void>;
|
|
31
38
|
private send;
|
|
32
39
|
}
|
package/dist/reporter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AAgB9G,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;IACvB,qBAAqB,CAAC,EAAE,MAAM,CAAA;IAC9B,8BAA8B,CAAC,EAAE,MAAM,CAAA;IACvC,sCAAsC,CAAC,EAAE,MAAM,CAAA;CAChD;AAED,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,SAAS,CAAgB;IACjC,OAAO,CAAC,YAAY,CAAqD;IACzE,OAAO,CAAC,qBAAqB,CAAC,CAAQ;IACtC,OAAO,CAAC,8BAA8B,CAAC,CAAQ;IAC/C,OAAO,CAAC,sCAAsC,CAAC,CAAQ;IACvD,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,SAAS,CAAiB;gBAEtB,OAAO,GAAE,gBAAqB;IAWpC,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9D,OAAO,CAAC,OAAO;IA6Bf,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,iBAAiB;IAMzB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI;IAc3B,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBlE,OAAO,CAAC,aAAa;YAqBP,qBAAqB;IAqB7B,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAoB9C,OAAO,CAAC,IAAI;CAUb;AAED,eAAe,SAAS,CAAA"}
|
package/dist/reporter.js
CHANGED
|
@@ -6,11 +6,27 @@ import {
|
|
|
6
6
|
applyTestEndEvent,
|
|
7
7
|
createMutableReportState,
|
|
8
8
|
finalizeRunEvent,
|
|
9
|
+
resolveBaselineTargets,
|
|
9
10
|
safeParse
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-EJRLZZ22.js";
|
|
11
12
|
|
|
12
13
|
// src/reporter.ts
|
|
13
|
-
import {
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { mkdir as mkdir3 } from "fs/promises";
|
|
16
|
+
import { dirname as dirname3, join as join2 } from "path";
|
|
17
|
+
import pLimit2 from "p-limit";
|
|
18
|
+
|
|
19
|
+
// src/debug-log.ts
|
|
20
|
+
var isDebug = () => process.env.DEBUG !== void 0 && process.env.DEBUG !== "";
|
|
21
|
+
var log = (...args) => {
|
|
22
|
+
if (isDebug()) console.log(...args);
|
|
23
|
+
};
|
|
24
|
+
var logError = (...args) => {
|
|
25
|
+
if (isDebug()) console.error(...args);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// src/reporter-artifact-ops.ts
|
|
29
|
+
import { copyFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
14
30
|
import { dirname as dirname2, join } from "path";
|
|
15
31
|
import pLimit from "p-limit";
|
|
16
32
|
|
|
@@ -135,32 +151,129 @@ async function writeReportArtifact(options) {
|
|
|
135
151
|
await writeFile(reportHtmlPath, html);
|
|
136
152
|
}
|
|
137
153
|
|
|
154
|
+
// src/reporter-artifact-ops.ts
|
|
155
|
+
var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
|
|
156
|
+
var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
|
|
157
|
+
var MAX_CONCURRENT_FILE_OPS = 5;
|
|
158
|
+
var SAFE_ARTIFACT_CHARACTER = /^[A-Za-z0-9._-]$/;
|
|
159
|
+
function encodeArtifactPathSegment(segment) {
|
|
160
|
+
if (segment === ".") return CURRENT_DIRECTORY_ARTIFACT_SEGMENT;
|
|
161
|
+
if (segment === "..") return PARENT_DIRECTORY_ARTIFACT_SEGMENT;
|
|
162
|
+
return Array.from(
|
|
163
|
+
segment,
|
|
164
|
+
(character) => SAFE_ARTIFACT_CHARACTER.test(character) ? character : encodeURIComponent(character)
|
|
165
|
+
).join("");
|
|
166
|
+
}
|
|
167
|
+
function safeArtifactPath(name) {
|
|
168
|
+
return name.split("/").map(encodeArtifactPathSegment).join("/");
|
|
169
|
+
}
|
|
170
|
+
function sanitizeId(id) {
|
|
171
|
+
return id.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
172
|
+
}
|
|
173
|
+
async function copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments) {
|
|
174
|
+
const attachmentName = `${target.attachmentBaseName}-expected.png`;
|
|
175
|
+
const artifactPath = safeArtifactPath(attachmentName);
|
|
176
|
+
const destPath = join(testScreenshotDir, artifactPath);
|
|
177
|
+
try {
|
|
178
|
+
await mkdir2(dirname2(destPath), { recursive: true });
|
|
179
|
+
await copyFile(target.snapshotPath, destPath);
|
|
180
|
+
savedAttachments.push({ name: attachmentName, path: `${safeTestId}/${artifactPath}`, contentType: "image/png" });
|
|
181
|
+
log(`[CrvyRprtr] Attached baseline: ${target.snapshotPath}`);
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async function saveAttachments(screenshotDir, testId, result) {
|
|
186
|
+
const savedAttachments = [];
|
|
187
|
+
const safeTestId = sanitizeId(testId);
|
|
188
|
+
const testScreenshotDir = join(screenshotDir, safeTestId);
|
|
189
|
+
const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
|
|
190
|
+
await Promise.all(
|
|
191
|
+
result.attachments.filter(
|
|
192
|
+
(attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
|
|
193
|
+
).map(
|
|
194
|
+
(attachment) => limit(async () => {
|
|
195
|
+
try {
|
|
196
|
+
const artifactPath = safeArtifactPath(attachment.name);
|
|
197
|
+
const destPath = join(testScreenshotDir, artifactPath);
|
|
198
|
+
await mkdir2(dirname2(destPath), { recursive: true });
|
|
199
|
+
await copyFile(attachment.path, destPath);
|
|
200
|
+
savedAttachments.push({
|
|
201
|
+
name: attachment.name,
|
|
202
|
+
path: `${safeTestId}/${artifactPath}`,
|
|
203
|
+
contentType: attachment.contentType ?? "image/png"
|
|
204
|
+
});
|
|
205
|
+
log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
logError(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, error);
|
|
208
|
+
savedAttachments.push({
|
|
209
|
+
name: attachment.name,
|
|
210
|
+
path: attachment.path,
|
|
211
|
+
contentType: attachment.contentType ?? "image/png"
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
return savedAttachments;
|
|
218
|
+
}
|
|
219
|
+
async function writeOfflineReport(runEvents, offlineReportPath, workerIndex) {
|
|
220
|
+
if (runEvents.length === 0) {
|
|
221
|
+
log("[CrvyRprtr] No offline events to write");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
const report = {
|
|
226
|
+
version: 1,
|
|
227
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
228
|
+
workers: workerIndex + 1,
|
|
229
|
+
events: runEvents.map((event) => ({ ...event, timestamp: Date.now(), workerIndex }))
|
|
230
|
+
};
|
|
231
|
+
await writeFile2(offlineReportPath, JSON.stringify(report, null, 2));
|
|
232
|
+
log(`[CrvyRprtr] Wrote offline report: ${offlineReportPath}`);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
logError("[CrvyRprtr] Failed to write offline report:", error);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function writeStaticArtifact(runEvents, screenshotDir, reportHtmlPath) {
|
|
238
|
+
try {
|
|
239
|
+
await writeReportArtifact({ events: runEvents, screenshotDir, reportHtmlPath });
|
|
240
|
+
log(`[CrvyRprtr] Wrote report artifact: ${reportHtmlPath}`);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
logError("[CrvyRprtr] Failed to write report artifact:", error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
138
246
|
// src/reporter-utils.ts
|
|
139
247
|
var NAMED_SCREENSHOT_STEP_TITLE = /toHaveScreenshot\((.+?)\)/;
|
|
140
248
|
var UNNAMED_SCREENSHOT_STEP_TITLE = /^Expect "toHaveScreenshot"(?:\s|$)/;
|
|
141
249
|
var SYNTHETIC_SCREENSHOT_PREFIX = "__unnamed-screenshot-";
|
|
142
|
-
function
|
|
250
|
+
function addVisualSuffix(visualName, occurrenceIndex) {
|
|
251
|
+
if (occurrenceIndex === 1) {
|
|
252
|
+
return visualName;
|
|
253
|
+
}
|
|
254
|
+
const slashIndex = visualName.lastIndexOf("/");
|
|
255
|
+
return slashIndex === -1 ? `${visualName}-${occurrenceIndex - 1}` : `${visualName.slice(0, slashIndex + 1)}${visualName.slice(slashIndex + 1)}-${occurrenceIndex - 1}`;
|
|
256
|
+
}
|
|
257
|
+
function normalizeNamedScreenshot(titleMatch, state) {
|
|
143
258
|
const unquotedName = titleMatch.trim().replace(/^['"`]|['"`]$/g, "");
|
|
144
259
|
if (unquotedName === "") {
|
|
145
260
|
return null;
|
|
146
261
|
}
|
|
147
262
|
const normalizedPath = unquotedName.replace(/\\/g, "/");
|
|
148
|
-
const
|
|
149
|
-
if (
|
|
263
|
+
const declaredName = normalizedPath.replace(/\.png$/, "");
|
|
264
|
+
if (declaredName === "") {
|
|
150
265
|
return null;
|
|
151
266
|
}
|
|
267
|
+
const occurrenceIndex = (state.namedOccurrences.get(declaredName) ?? 0) + 1;
|
|
268
|
+
state.namedOccurrences.set(declaredName, occurrenceIndex);
|
|
152
269
|
return {
|
|
153
|
-
visualName:
|
|
154
|
-
|
|
270
|
+
visualName: addVisualSuffix(declaredName, occurrenceIndex),
|
|
271
|
+
kind: "named",
|
|
272
|
+
declaredName,
|
|
273
|
+
snapshotBaseName: declaredName,
|
|
274
|
+
occurrenceIndex
|
|
155
275
|
};
|
|
156
276
|
}
|
|
157
|
-
function appendDeclaration(state, declaration) {
|
|
158
|
-
if (state.seenVisualNames.has(declaration.visualName)) {
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
state.seenVisualNames.add(declaration.visualName);
|
|
162
|
-
state.declarations.push(declaration);
|
|
163
|
-
}
|
|
164
277
|
function visitStep(step, state) {
|
|
165
278
|
const declarationsBeforeChildren = state.declarations.length;
|
|
166
279
|
for (const nestedStep of step.steps) {
|
|
@@ -168,16 +281,19 @@ function visitStep(step, state) {
|
|
|
168
281
|
}
|
|
169
282
|
const namedMatch = step.title.match(NAMED_SCREENSHOT_STEP_TITLE);
|
|
170
283
|
if (namedMatch?.[1] !== void 0) {
|
|
171
|
-
const declaration = normalizeNamedScreenshot(namedMatch[1]);
|
|
284
|
+
const declaration = normalizeNamedScreenshot(namedMatch[1], state);
|
|
172
285
|
if (declaration !== null) {
|
|
173
|
-
|
|
286
|
+
state.declarations.push(declaration);
|
|
174
287
|
return true;
|
|
175
288
|
}
|
|
176
289
|
}
|
|
177
290
|
const hasNestedScreenshotDeclaration = state.declarations.length > declarationsBeforeChildren;
|
|
178
291
|
if (UNNAMED_SCREENSHOT_STEP_TITLE.test(step.title) && !hasNestedScreenshotDeclaration) {
|
|
179
|
-
|
|
180
|
-
|
|
292
|
+
const occurrenceIndex = state.nextUnnamedIndex;
|
|
293
|
+
state.declarations.push({
|
|
294
|
+
visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${occurrenceIndex}`,
|
|
295
|
+
kind: "unnamed",
|
|
296
|
+
occurrenceIndex
|
|
181
297
|
});
|
|
182
298
|
state.nextUnnamedIndex += 1;
|
|
183
299
|
return true;
|
|
@@ -187,7 +303,7 @@ function visitStep(step, state) {
|
|
|
187
303
|
function extractScreenshotDeclarations(steps) {
|
|
188
304
|
const state = {
|
|
189
305
|
declarations: [],
|
|
190
|
-
|
|
306
|
+
namedOccurrences: /* @__PURE__ */ new Map(),
|
|
191
307
|
nextUnnamedIndex: 1
|
|
192
308
|
};
|
|
193
309
|
for (const step of steps) {
|
|
@@ -197,7 +313,6 @@ function extractScreenshotDeclarations(steps) {
|
|
|
197
313
|
}
|
|
198
314
|
|
|
199
315
|
// src/reporter.ts
|
|
200
|
-
var MAX_CONCURRENT_FILE_OPS = 5;
|
|
201
316
|
var CrvyRprtr = class {
|
|
202
317
|
ws = null;
|
|
203
318
|
serverUrl;
|
|
@@ -206,6 +321,11 @@ var CrvyRprtr = class {
|
|
|
206
321
|
workerIndex;
|
|
207
322
|
offlineReportPath;
|
|
208
323
|
reportHtmlPath;
|
|
324
|
+
configDir = process.cwd();
|
|
325
|
+
testMetadata = /* @__PURE__ */ new Map();
|
|
326
|
+
playwrightSnapshotDir;
|
|
327
|
+
playwrightSnapshotPathTemplate;
|
|
328
|
+
playwrightToHaveScreenshotPathTemplate;
|
|
209
329
|
isOfflineMode = false;
|
|
210
330
|
hadOfflineMode = false;
|
|
211
331
|
runEvents = [];
|
|
@@ -215,198 +335,132 @@ var CrvyRprtr = class {
|
|
|
215
335
|
this.workerIndex = parseInt(process.env.TEST_WORKER_INDEX ?? "0", 10) || 0;
|
|
216
336
|
this.offlineReportPath = options.offlineReportPath ?? `./crvy-rprtr-${this.workerIndex}.json`;
|
|
217
337
|
this.reportHtmlPath = options.reportHtmlPath ?? "./crvy-rprtr.html";
|
|
338
|
+
this.playwrightSnapshotDir = options.playwrightSnapshotDir;
|
|
339
|
+
this.playwrightSnapshotPathTemplate = options.playwrightSnapshotPathTemplate;
|
|
340
|
+
this.playwrightToHaveScreenshotPathTemplate = options.playwrightToHaveScreenshotPathTemplate;
|
|
218
341
|
}
|
|
219
342
|
async onBegin(config, suite) {
|
|
220
|
-
|
|
221
|
-
|
|
343
|
+
this.configDir = config.configFile === void 0 ? config.rootDir : dirname3(config.configFile);
|
|
344
|
+
log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
|
|
345
|
+
await mkdir3(this.screenshotDir, { recursive: true });
|
|
222
346
|
this.connect();
|
|
223
347
|
}
|
|
224
348
|
connect() {
|
|
225
349
|
const WebSocketConstructor = globalThis.WebSocket;
|
|
226
350
|
if (typeof WebSocketConstructor !== "function") {
|
|
227
|
-
|
|
351
|
+
log("[CrvyRprtr] WebSocket unavailable in current runtime; offline mode enabled");
|
|
228
352
|
this.enableOfflineMode();
|
|
229
353
|
return;
|
|
230
354
|
}
|
|
231
355
|
try {
|
|
232
356
|
this.ws = new WebSocketConstructor(this.serverUrl);
|
|
233
357
|
this.ws.onopen = () => {
|
|
234
|
-
|
|
358
|
+
log("[CrvyRprtr] Connected to Crvy Rprtr server");
|
|
235
359
|
this.isOfflineMode = false;
|
|
236
|
-
for (const
|
|
360
|
+
for (const message of this.queue) this.ws.send(message);
|
|
237
361
|
this.queue = [];
|
|
238
362
|
};
|
|
239
363
|
this.ws.onerror = (error) => {
|
|
240
|
-
|
|
364
|
+
logError("[CrvyRprtr] WebSocket error:", error);
|
|
241
365
|
this.enableOfflineMode();
|
|
242
366
|
};
|
|
243
367
|
this.ws.onclose = () => {
|
|
244
|
-
|
|
368
|
+
log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
|
|
245
369
|
this.enableOfflineMode();
|
|
246
370
|
};
|
|
247
|
-
} catch (
|
|
248
|
-
|
|
371
|
+
} catch (error) {
|
|
372
|
+
logError("[CrvyRprtr] Failed to connect:", error);
|
|
249
373
|
this.enableOfflineMode();
|
|
250
374
|
}
|
|
251
375
|
}
|
|
252
376
|
enableOfflineMode() {
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
|
|
257
|
-
}
|
|
377
|
+
if (this.isOfflineMode) return;
|
|
378
|
+
this.isOfflineMode = this.hadOfflineMode = true;
|
|
379
|
+
log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
|
|
258
380
|
}
|
|
259
|
-
|
|
381
|
+
describeTitlePath(test) {
|
|
260
382
|
const titlePath = [];
|
|
261
|
-
let suite = test.parent;
|
|
262
|
-
while (suite && suite.type === "describe") {
|
|
383
|
+
for (let suite = test.parent; suite?.type === "describe"; suite = suite.parent)
|
|
263
384
|
titlePath.unshift(suite.title);
|
|
264
|
-
|
|
265
|
-
|
|
385
|
+
return titlePath;
|
|
386
|
+
}
|
|
387
|
+
reporterTitlePath(test) {
|
|
388
|
+
return typeof test.titlePath === "function" ? test.titlePath() : ["", test.parent.project()?.name ?? "chromium", test.location.file, ...this.describeTitlePath(test), test.title];
|
|
389
|
+
}
|
|
390
|
+
onTestBegin(test) {
|
|
391
|
+
this.testMetadata.set(test.id, { reporterTitlePath: this.reporterTitlePath(test) });
|
|
266
392
|
this.send({
|
|
267
393
|
type: "test-begin",
|
|
268
394
|
data: {
|
|
269
395
|
id: test.id,
|
|
270
396
|
title: test.title,
|
|
271
|
-
titlePath,
|
|
397
|
+
titlePath: this.describeTitlePath(test),
|
|
272
398
|
browser: test.parent.project()?.name ?? "chromium",
|
|
273
|
-
location: {
|
|
274
|
-
file: test.location.file,
|
|
275
|
-
line: test.location.line
|
|
276
|
-
}
|
|
399
|
+
location: { file: test.location.file, line: test.location.line }
|
|
277
400
|
}
|
|
278
401
|
});
|
|
279
402
|
}
|
|
280
403
|
async onTestEnd(test, result) {
|
|
281
404
|
const screenshotDeclarations = extractScreenshotDeclarations(result.steps);
|
|
282
|
-
const
|
|
283
|
-
const savedAttachments = await this.saveAttachments(test.id, result);
|
|
284
|
-
await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
|
|
285
|
-
this.send({
|
|
286
|
-
type: "test-end",
|
|
287
|
-
data: {
|
|
288
|
-
id: test.id,
|
|
289
|
-
title: test.title,
|
|
290
|
-
status: result.status,
|
|
291
|
-
attachments: savedAttachments,
|
|
292
|
-
visualNames,
|
|
293
|
-
error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
|
|
294
|
-
duration: result.duration
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
|
|
299
|
-
if (status !== "passed") return;
|
|
300
|
-
const namedDeclarations = screenshotDeclarations.filter(
|
|
301
|
-
(declaration) => declaration.snapshotBaseName !== void 0
|
|
302
|
-
);
|
|
303
|
-
if (namedDeclarations.length === 0) return;
|
|
304
|
-
const projectName = test.parent.project()?.name;
|
|
305
|
-
const snapshotDir = `${test.location.file}-snapshots`;
|
|
306
|
-
const testScreenshotDir = join(this.screenshotDir, this.sanitizeId(test.id));
|
|
307
|
-
const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
|
|
308
|
-
const copyPromises = namedDeclarations.map(
|
|
309
|
-
({ visualName, snapshotBaseName }) => limit(async () => {
|
|
310
|
-
const destName = `${visualName}-expected.png`;
|
|
311
|
-
const destPath = join(testScreenshotDir, destName);
|
|
312
|
-
const snapshotPath = projectName === "" ? join(snapshotDir, `${snapshotBaseName}-${process.platform}.png`) : join(snapshotDir, `${snapshotBaseName}-${projectName ?? "chromium"}-${process.platform}.png`);
|
|
313
|
-
try {
|
|
314
|
-
await mkdir2(dirname2(destPath), { recursive: true });
|
|
315
|
-
await copyFile(snapshotPath, destPath);
|
|
316
|
-
savedAttachments.push({
|
|
317
|
-
name: destName,
|
|
318
|
-
path: `${this.sanitizeId(test.id)}/${destName}`,
|
|
319
|
-
contentType: "image/png"
|
|
320
|
-
});
|
|
321
|
-
console.log(`[CrvyRprtr] Attached baseline: ${snapshotPath}`);
|
|
322
|
-
} catch {
|
|
323
|
-
}
|
|
324
|
-
})
|
|
325
|
-
);
|
|
326
|
-
await Promise.all(copyPromises);
|
|
327
|
-
}
|
|
328
|
-
async saveAttachments(testId, result) {
|
|
329
|
-
const savedAttachments = [];
|
|
330
|
-
const testScreenshotDir = join(this.screenshotDir, this.sanitizeId(testId));
|
|
331
|
-
const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
|
|
332
|
-
const attachmentPromises = result.attachments.filter(
|
|
333
|
-
(attachment) => attachment.contentType === "image/png" && attachment.path !== void 0
|
|
334
|
-
).map(
|
|
335
|
-
(attachment) => limit(async () => {
|
|
336
|
-
try {
|
|
337
|
-
await mkdir2(testScreenshotDir, { recursive: true });
|
|
338
|
-
const fileName = attachment.name;
|
|
339
|
-
const destPath = join(testScreenshotDir, fileName);
|
|
340
|
-
await copyFile(attachment.path, destPath);
|
|
341
|
-
const attachmentData = {
|
|
342
|
-
name: attachment.name,
|
|
343
|
-
path: `${this.sanitizeId(testId)}/${fileName}`,
|
|
344
|
-
contentType: attachment.contentType
|
|
345
|
-
};
|
|
346
|
-
savedAttachments.push(attachmentData);
|
|
347
|
-
console.log(`[CrvyRprtr] Saved screenshot: ${destPath}`);
|
|
348
|
-
} catch (e) {
|
|
349
|
-
console.error(`[CrvyRprtr] Failed to save screenshot: ${attachment.path}`, e);
|
|
350
|
-
const fallbackData = {
|
|
351
|
-
name: attachment.name,
|
|
352
|
-
path: attachment.path,
|
|
353
|
-
contentType: attachment.contentType
|
|
354
|
-
};
|
|
355
|
-
savedAttachments.push(fallbackData);
|
|
356
|
-
}
|
|
357
|
-
})
|
|
358
|
-
);
|
|
359
|
-
await Promise.all(attachmentPromises);
|
|
360
|
-
return savedAttachments;
|
|
361
|
-
}
|
|
362
|
-
sanitizeId(id) {
|
|
363
|
-
return id.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
364
|
-
}
|
|
365
|
-
async writeOfflineReport() {
|
|
366
|
-
if (this.runEvents.length === 0) {
|
|
367
|
-
console.log("[CrvyRprtr] No offline events to write");
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
try {
|
|
371
|
-
const report = {
|
|
372
|
-
version: 1,
|
|
373
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
374
|
-
workers: this.workerIndex + 1,
|
|
375
|
-
events: this.runEvents.map((e) => ({
|
|
376
|
-
...e,
|
|
377
|
-
timestamp: Date.now(),
|
|
378
|
-
workerIndex: this.workerIndex
|
|
379
|
-
}))
|
|
380
|
-
};
|
|
381
|
-
await writeFile2(this.offlineReportPath, JSON.stringify(report, null, 2));
|
|
382
|
-
console.log(`[CrvyRprtr] Wrote offline report: ${this.offlineReportPath}`);
|
|
383
|
-
} catch (e) {
|
|
384
|
-
console.error("[CrvyRprtr] Failed to write offline report:", e);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
async writeStaticArtifact() {
|
|
405
|
+
const savedAttachments = await saveAttachments(this.screenshotDir, test.id, result);
|
|
388
406
|
try {
|
|
389
|
-
await
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
407
|
+
await this.copySnapshotBaselines(test, result.status, screenshotDeclarations, savedAttachments);
|
|
408
|
+
this.send({
|
|
409
|
+
type: "test-end",
|
|
410
|
+
data: {
|
|
411
|
+
id: test.id,
|
|
412
|
+
title: test.title,
|
|
413
|
+
status: result.status,
|
|
414
|
+
attachments: savedAttachments,
|
|
415
|
+
visualNames: screenshotDeclarations.map(({ visualName }) => visualName),
|
|
416
|
+
visualDeclarations: screenshotDeclarations,
|
|
417
|
+
error: result.errors.length > 0 ? result.errors[0]?.message : void 0,
|
|
418
|
+
duration: result.duration
|
|
419
|
+
}
|
|
393
420
|
});
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
console.error("[CrvyRprtr] Failed to write report artifact:", e);
|
|
421
|
+
} finally {
|
|
422
|
+
this.testMetadata.delete(test.id);
|
|
397
423
|
}
|
|
398
424
|
}
|
|
425
|
+
baselineInput(test, shots) {
|
|
426
|
+
const project = test.parent.project();
|
|
427
|
+
const snapshotDir = this.playwrightSnapshotDir ?? project?.snapshotDir;
|
|
428
|
+
if (project === void 0 || typeof project.testDir !== "string" || typeof snapshotDir !== "string") return null;
|
|
429
|
+
return {
|
|
430
|
+
testFile: test.location.file,
|
|
431
|
+
reporterTitlePath: this.testMetadata.get(test.id)?.reporterTitlePath ?? this.reporterTitlePath(test),
|
|
432
|
+
declarations: shots,
|
|
433
|
+
config: {
|
|
434
|
+
configDir: this.configDir,
|
|
435
|
+
testDir: project.testDir,
|
|
436
|
+
snapshotDir,
|
|
437
|
+
projectName: project.name,
|
|
438
|
+
snapshotSuffix: process.platform,
|
|
439
|
+
snapshotPathTemplate: this.playwrightSnapshotPathTemplate,
|
|
440
|
+
toHaveScreenshotPathTemplate: this.playwrightToHaveScreenshotPathTemplate
|
|
441
|
+
},
|
|
442
|
+
snapshotPathExists: existsSync
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
|
|
446
|
+
if (status !== "passed" || screenshotDeclarations.length === 0) return;
|
|
447
|
+
const input = this.baselineInput(test, screenshotDeclarations);
|
|
448
|
+
if (input === null) return;
|
|
449
|
+
const targets = resolveBaselineTargets(input);
|
|
450
|
+
if (targets.length === 0) return;
|
|
451
|
+
const safeTestId = sanitizeId(test.id);
|
|
452
|
+
const testScreenshotDir = join2(this.screenshotDir, safeTestId);
|
|
453
|
+
const limit = pLimit2(5);
|
|
454
|
+
await Promise.all(
|
|
455
|
+
targets.map(
|
|
456
|
+
(target) => limit(() => copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments))
|
|
457
|
+
)
|
|
458
|
+
);
|
|
459
|
+
}
|
|
399
460
|
async onEnd(result) {
|
|
400
|
-
this.send({
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
status: result.status
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
await this.writeStaticArtifact();
|
|
407
|
-
if (this.hadOfflineMode) {
|
|
408
|
-
await this.writeOfflineReport();
|
|
409
|
-
}
|
|
461
|
+
this.send({ type: "run-end", data: { status: result.status } });
|
|
462
|
+
await writeStaticArtifact(this.runEvents, this.screenshotDir, this.reportHtmlPath);
|
|
463
|
+
if (this.hadOfflineMode) await writeOfflineReport(this.runEvents, this.offlineReportPath, this.workerIndex);
|
|
410
464
|
await new Promise((resolve2) => {
|
|
411
465
|
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
|
412
466
|
resolve2();
|
|
@@ -422,21 +476,14 @@ var CrvyRprtr = class {
|
|
|
422
476
|
this.ws.close();
|
|
423
477
|
});
|
|
424
478
|
}
|
|
425
|
-
send(
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
428
|
-
this.runEvents.push({
|
|
429
|
-
|
|
430
|
-
data: msgObj.data
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
const payload = JSON.stringify(msg);
|
|
479
|
+
send(message) {
|
|
480
|
+
const event = message;
|
|
481
|
+
if (event.type === "test-begin" || event.type === "test-end" || event.type === "run-end")
|
|
482
|
+
this.runEvents.push({ type: event.type, data: event.data });
|
|
483
|
+
const payload = JSON.stringify(message);
|
|
434
484
|
if (!this.isOfflineMode) {
|
|
435
|
-
if (this.ws?.readyState === WebSocket.OPEN)
|
|
436
|
-
|
|
437
|
-
} else {
|
|
438
|
-
this.queue.push(payload);
|
|
439
|
-
}
|
|
485
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(payload);
|
|
486
|
+
else this.queue.push(payload);
|
|
440
487
|
}
|
|
441
488
|
}
|
|
442
489
|
};
|