@crvy/rprtr 0.0.4 → 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 +81 -0
- package/README.md +41 -7
- package/dist/chunk-JGS2VWQP.js +500 -0
- package/dist/{chunk-BRFA7JCQ.js → chunk-OMDYTNWY.js} +266 -65
- package/dist/cli.js +2 -2
- package/dist/index.css +55 -0
- package/dist/index.js +691 -550
- package/dist/report-state.d.ts +6 -1
- package/dist/report-state.d.ts.map +1 -1
- package/dist/report-utils.d.ts +6 -1
- package/dist/report-utils.d.ts.map +1 -1
- package/dist/reporter-utils.d.ts +20 -1
- package/dist/reporter-utils.d.ts.map +1 -1
- package/dist/reporter.cjs +474 -156
- package/dist/reporter.d.ts +13 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +224 -141
- package/dist/schemas.d.ts +217 -22
- package/dist/schemas.d.ts.map +1 -1
- package/dist/server/app.d.ts +6 -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/routes.d.ts +7 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server.cjs +520 -82
- 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 +9 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-IEYNAB6M.js +0 -263
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,12 +6,14 @@ 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 {
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
|
+
import { copyFile, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
16
|
+
import { dirname as dirname2, join } from "path";
|
|
15
17
|
import pLimit from "p-limit";
|
|
16
18
|
|
|
17
19
|
// src/report-artifact.ts
|
|
@@ -136,18 +138,88 @@ async function writeReportArtifact(options) {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
// src/reporter-utils.ts
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
var NAMED_SCREENSHOT_STEP_TITLE = /toHaveScreenshot\((.+?)\)/;
|
|
142
|
+
var UNNAMED_SCREENSHOT_STEP_TITLE = /^Expect "toHaveScreenshot"(?:\s|$)/;
|
|
143
|
+
var SYNTHETIC_SCREENSHOT_PREFIX = "__unnamed-screenshot-";
|
|
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) {
|
|
152
|
+
const unquotedName = titleMatch.trim().replace(/^['"`]|['"`]$/g, "");
|
|
153
|
+
if (unquotedName === "") {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const normalizedPath = unquotedName.replace(/\\/g, "/");
|
|
157
|
+
const declaredName = normalizedPath.replace(/\.png$/, "");
|
|
158
|
+
if (declaredName === "") {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const occurrenceIndex = (state.namedOccurrences.get(declaredName) ?? 0) + 1;
|
|
162
|
+
state.namedOccurrences.set(declaredName, occurrenceIndex);
|
|
163
|
+
return {
|
|
164
|
+
visualName: addVisualSuffix(declaredName, occurrenceIndex),
|
|
165
|
+
kind: "named",
|
|
166
|
+
declaredName,
|
|
167
|
+
snapshotBaseName: declaredName,
|
|
168
|
+
occurrenceIndex
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function visitStep(step, state) {
|
|
172
|
+
const declarationsBeforeChildren = state.declarations.length;
|
|
173
|
+
for (const nestedStep of step.steps) {
|
|
174
|
+
visitStep(nestedStep, state);
|
|
175
|
+
}
|
|
176
|
+
const namedMatch = step.title.match(NAMED_SCREENSHOT_STEP_TITLE);
|
|
177
|
+
if (namedMatch?.[1] !== void 0) {
|
|
178
|
+
const declaration = normalizeNamedScreenshot(namedMatch[1], state);
|
|
179
|
+
if (declaration !== null) {
|
|
180
|
+
state.declarations.push(declaration);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const hasNestedScreenshotDeclaration = state.declarations.length > declarationsBeforeChildren;
|
|
185
|
+
if (UNNAMED_SCREENSHOT_STEP_TITLE.test(step.title) && !hasNestedScreenshotDeclaration) {
|
|
186
|
+
const occurrenceIndex = state.nextUnnamedIndex;
|
|
187
|
+
state.declarations.push({
|
|
188
|
+
visualName: `${SYNTHETIC_SCREENSHOT_PREFIX}${occurrenceIndex}`,
|
|
189
|
+
kind: "unnamed",
|
|
190
|
+
occurrenceIndex
|
|
191
|
+
});
|
|
192
|
+
state.nextUnnamedIndex += 1;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return hasNestedScreenshotDeclaration;
|
|
196
|
+
}
|
|
197
|
+
function extractScreenshotDeclarations(steps) {
|
|
198
|
+
const state = {
|
|
199
|
+
declarations: [],
|
|
200
|
+
namedOccurrences: /* @__PURE__ */ new Map(),
|
|
201
|
+
nextUnnamedIndex: 1
|
|
202
|
+
};
|
|
141
203
|
for (const step of steps) {
|
|
142
|
-
|
|
143
|
-
if (match?.[1] !== void 0 && match[1] !== "") names.push(match[1]);
|
|
144
|
-
if (step.steps.length) names.push(...extractScreenshotNames(step.steps));
|
|
204
|
+
visitStep(step, state);
|
|
145
205
|
}
|
|
146
|
-
return
|
|
206
|
+
return state.declarations;
|
|
147
207
|
}
|
|
148
208
|
|
|
149
209
|
// src/reporter.ts
|
|
210
|
+
var CURRENT_DIRECTORY_ARTIFACT_SEGMENT = "+dot+";
|
|
211
|
+
var PARENT_DIRECTORY_ARTIFACT_SEGMENT = "+dotdot+";
|
|
150
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("/");
|
|
151
223
|
var CrvyRprtr = class {
|
|
152
224
|
ws = null;
|
|
153
225
|
serverUrl;
|
|
@@ -156,6 +228,11 @@ var CrvyRprtr = class {
|
|
|
156
228
|
workerIndex;
|
|
157
229
|
offlineReportPath;
|
|
158
230
|
reportHtmlPath;
|
|
231
|
+
configDir = process.cwd();
|
|
232
|
+
testMetadata = /* @__PURE__ */ new Map();
|
|
233
|
+
playwrightSnapshotDir;
|
|
234
|
+
playwrightSnapshotPathTemplate;
|
|
235
|
+
playwrightToHaveScreenshotPathTemplate;
|
|
159
236
|
isOfflineMode = false;
|
|
160
237
|
hadOfflineMode = false;
|
|
161
238
|
runEvents = [];
|
|
@@ -165,8 +242,12 @@ var CrvyRprtr = class {
|
|
|
165
242
|
this.workerIndex = parseInt(process.env.TEST_WORKER_INDEX ?? "0", 10) || 0;
|
|
166
243
|
this.offlineReportPath = options.offlineReportPath ?? `./crvy-rprtr-${this.workerIndex}.json`;
|
|
167
244
|
this.reportHtmlPath = options.reportHtmlPath ?? "./crvy-rprtr.html";
|
|
245
|
+
this.playwrightSnapshotDir = options.playwrightSnapshotDir;
|
|
246
|
+
this.playwrightSnapshotPathTemplate = options.playwrightSnapshotPathTemplate;
|
|
247
|
+
this.playwrightToHaveScreenshotPathTemplate = options.playwrightToHaveScreenshotPathTemplate;
|
|
168
248
|
}
|
|
169
249
|
async onBegin(config, suite) {
|
|
250
|
+
this.configDir = config.configFile === void 0 ? config.rootDir : dirname2(config.configFile);
|
|
170
251
|
console.log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
|
|
171
252
|
await mkdir2(this.screenshotDir, { recursive: true });
|
|
172
253
|
this.connect();
|
|
@@ -183,7 +264,7 @@ var CrvyRprtr = class {
|
|
|
183
264
|
this.ws.onopen = () => {
|
|
184
265
|
console.log("[CrvyRprtr] Connected to Crvy Rprtr server");
|
|
185
266
|
this.isOfflineMode = false;
|
|
186
|
-
for (const
|
|
267
|
+
for (const message of this.queue) this.ws.send(message);
|
|
187
268
|
this.queue = [];
|
|
188
269
|
};
|
|
189
270
|
this.ws.onerror = (error) => {
|
|
@@ -194,140 +275,158 @@ var CrvyRprtr = class {
|
|
|
194
275
|
console.log("[CrvyRprtr] Disconnected from Crvy Rprtr server");
|
|
195
276
|
this.enableOfflineMode();
|
|
196
277
|
};
|
|
197
|
-
} catch (
|
|
198
|
-
console.error("[CrvyRprtr] Failed to connect:",
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error("[CrvyRprtr] Failed to connect:", error);
|
|
199
280
|
this.enableOfflineMode();
|
|
200
281
|
}
|
|
201
282
|
}
|
|
202
283
|
enableOfflineMode() {
|
|
203
|
-
if (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
|
|
207
|
-
}
|
|
284
|
+
if (this.isOfflineMode) return;
|
|
285
|
+
this.isOfflineMode = this.hadOfflineMode = true;
|
|
286
|
+
console.log("[CrvyRprtr] Offline mode enabled - events will be queued to file");
|
|
208
287
|
}
|
|
209
|
-
|
|
288
|
+
describeTitlePath(test) {
|
|
210
289
|
const titlePath = [];
|
|
211
|
-
let suite = test.parent;
|
|
212
|
-
while (suite && suite.type === "describe") {
|
|
290
|
+
for (let suite = test.parent; suite?.type === "describe"; suite = suite.parent)
|
|
213
291
|
titlePath.unshift(suite.title);
|
|
214
|
-
|
|
215
|
-
|
|
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) });
|
|
216
299
|
this.send({
|
|
217
300
|
type: "test-begin",
|
|
218
301
|
data: {
|
|
219
302
|
id: test.id,
|
|
220
303
|
title: test.title,
|
|
221
|
-
titlePath,
|
|
304
|
+
titlePath: this.describeTitlePath(test),
|
|
222
305
|
browser: test.parent.project()?.name ?? "chromium",
|
|
223
|
-
location: {
|
|
224
|
-
file: test.location.file,
|
|
225
|
-
line: test.location.line
|
|
226
|
-
}
|
|
306
|
+
location: { file: test.location.file, line: test.location.line }
|
|
227
307
|
}
|
|
228
308
|
});
|
|
229
309
|
}
|
|
230
310
|
async onTestEnd(test, result) {
|
|
311
|
+
const screenshotDeclarations = extractScreenshotDeclarations(result.steps);
|
|
231
312
|
const savedAttachments = await this.saveAttachments(test.id, result);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
};
|
|
244
351
|
}
|
|
245
|
-
async
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async copySnapshotBaselines(test, status, screenshotDeclarations, savedAttachments) {
|
|
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);
|
|
252
372
|
const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
|
|
253
|
-
|
|
254
|
-
(
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
})
|
|
373
|
+
await Promise.all(
|
|
374
|
+
targets.map(
|
|
375
|
+
(target) => limit(() => this.copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments))
|
|
376
|
+
)
|
|
271
377
|
);
|
|
272
|
-
await Promise.all(copyPromises);
|
|
273
378
|
}
|
|
274
379
|
async saveAttachments(testId, result) {
|
|
275
380
|
const savedAttachments = [];
|
|
276
|
-
const
|
|
381
|
+
const safeTestId = this.sanitizeId(testId);
|
|
382
|
+
const testScreenshotDir = join(this.screenshotDir, safeTestId);
|
|
277
383
|
const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
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
|
+
)
|
|
304
410
|
);
|
|
305
|
-
await Promise.all(attachmentPromises);
|
|
306
411
|
return savedAttachments;
|
|
307
412
|
}
|
|
308
413
|
sanitizeId(id) {
|
|
309
414
|
return id.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
310
415
|
}
|
|
311
416
|
async writeOfflineReport() {
|
|
312
|
-
if (this.runEvents.length === 0)
|
|
313
|
-
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
417
|
+
if (this.runEvents.length === 0) console.log("[CrvyRprtr] No offline events to write");
|
|
418
|
+
if (this.runEvents.length === 0) return;
|
|
316
419
|
try {
|
|
317
420
|
const report = {
|
|
318
421
|
version: 1,
|
|
319
422
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
320
423
|
workers: this.workerIndex + 1,
|
|
321
|
-
events: this.runEvents.map((
|
|
322
|
-
...e,
|
|
323
|
-
timestamp: Date.now(),
|
|
324
|
-
workerIndex: this.workerIndex
|
|
325
|
-
}))
|
|
424
|
+
events: this.runEvents.map((event) => ({ ...event, timestamp: Date.now(), workerIndex: this.workerIndex }))
|
|
326
425
|
};
|
|
327
426
|
await writeFile2(this.offlineReportPath, JSON.stringify(report, null, 2));
|
|
328
427
|
console.log(`[CrvyRprtr] Wrote offline report: ${this.offlineReportPath}`);
|
|
329
|
-
} catch (
|
|
330
|
-
console.error("[CrvyRprtr] Failed to write offline report:",
|
|
428
|
+
} catch (error) {
|
|
429
|
+
console.error("[CrvyRprtr] Failed to write offline report:", error);
|
|
331
430
|
}
|
|
332
431
|
}
|
|
333
432
|
async writeStaticArtifact() {
|
|
@@ -338,51 +437,35 @@ var CrvyRprtr = class {
|
|
|
338
437
|
reportHtmlPath: this.reportHtmlPath
|
|
339
438
|
});
|
|
340
439
|
console.log(`[CrvyRprtr] Wrote report artifact: ${this.reportHtmlPath}`);
|
|
341
|
-
} catch (
|
|
342
|
-
console.error("[CrvyRprtr] Failed to write report artifact:",
|
|
440
|
+
} catch (error) {
|
|
441
|
+
console.error("[CrvyRprtr] Failed to write report artifact:", error);
|
|
343
442
|
}
|
|
344
443
|
}
|
|
345
444
|
async onEnd(result) {
|
|
346
|
-
this.send({
|
|
347
|
-
type: "run-end",
|
|
348
|
-
data: {
|
|
349
|
-
status: result.status
|
|
350
|
-
}
|
|
351
|
-
});
|
|
445
|
+
this.send({ type: "run-end", data: { status: result.status } });
|
|
352
446
|
await this.writeStaticArtifact();
|
|
353
|
-
if (this.hadOfflineMode)
|
|
354
|
-
await this.writeOfflineReport();
|
|
355
|
-
}
|
|
447
|
+
if (this.hadOfflineMode) await this.writeOfflineReport();
|
|
356
448
|
await new Promise((resolve2) => {
|
|
357
|
-
if (
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.ws
|
|
366
|
-
|
|
367
|
-
}, 1e3);
|
|
368
|
-
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();
|
|
369
459
|
});
|
|
370
460
|
}
|
|
371
|
-
send(
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
this.runEvents.push({
|
|
375
|
-
|
|
376
|
-
data: msgObj.data
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
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);
|
|
380
466
|
if (!this.isOfflineMode) {
|
|
381
|
-
if (this.ws?.readyState === WebSocket.OPEN)
|
|
382
|
-
|
|
383
|
-
} else {
|
|
384
|
-
this.queue.push(payload);
|
|
385
|
-
}
|
|
467
|
+
if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(payload);
|
|
468
|
+
else this.queue.push(payload);
|
|
386
469
|
}
|
|
387
470
|
}
|
|
388
471
|
};
|