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