@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
|
@@ -9,11 +9,12 @@ import {
|
|
|
9
9
|
applyTestEndEvent,
|
|
10
10
|
createMutableReportState,
|
|
11
11
|
finalizeRunEvent,
|
|
12
|
+
resolveBaselineTargets,
|
|
12
13
|
safeParse
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-JGS2VWQP.js";
|
|
14
15
|
|
|
15
16
|
// src/server/app.ts
|
|
16
|
-
import { dirname as
|
|
17
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
17
18
|
import { fileURLToPath } from "url";
|
|
18
19
|
import pLimit from "p-limit";
|
|
19
20
|
|
|
@@ -207,14 +208,123 @@ function handleSync(ctx) {
|
|
|
207
208
|
broadcastToBrowsers(ctx.wsClients, { type: "sync", data: ctx.reportData });
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
// src/server/
|
|
211
|
+
// src/server/report-watch.ts
|
|
212
|
+
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
211
213
|
import { join as join2 } from "path";
|
|
214
|
+
var OFFLINE_REPORT_FILE_PATTERN2 = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
215
|
+
function createDebouncedRefresh(reload, delayMs = 50) {
|
|
216
|
+
let timer = null;
|
|
217
|
+
return () => {
|
|
218
|
+
if (timer !== null) {
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
}
|
|
221
|
+
timer = setTimeout(() => {
|
|
222
|
+
timer = null;
|
|
223
|
+
void reload();
|
|
224
|
+
}, delayMs);
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function isFileNotFound2(error) {
|
|
228
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
229
|
+
}
|
|
230
|
+
async function describeFile(filePath, label) {
|
|
231
|
+
try {
|
|
232
|
+
const fileStats = await stat2(filePath);
|
|
233
|
+
return `${label}:${fileStats.size}:${fileStats.mtimeMs}`;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (isFileNotFound2(error)) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function createOfflineFingerprint(offlineReportDir) {
|
|
242
|
+
if (!await isDirectory(offlineReportDir)) {
|
|
243
|
+
return "offline:missing";
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const entries = await readdir2(offlineReportDir, { withFileTypes: true });
|
|
247
|
+
const relevantEntries = entries.filter(
|
|
248
|
+
(entry) => entry.isFile() && (entry.name === "report.json" || OFFLINE_REPORT_FILE_PATTERN2.test(entry.name))
|
|
249
|
+
).sort((left, right) => left.name.localeCompare(right.name));
|
|
250
|
+
const parts = await Promise.all(
|
|
251
|
+
relevantEntries.map((entry) => describeFile(join2(offlineReportDir, entry.name), entry.name))
|
|
252
|
+
);
|
|
253
|
+
return `offline:${parts.filter((part) => part !== null).join("|")}`;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
256
|
+
console.warn(`[Server] Unable to scan ${offlineReportDir}: ${errorMsg}`);
|
|
257
|
+
return "offline:error";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function createScreenshotFingerprint(screenshotDir, relativePath = "") {
|
|
261
|
+
const directoryPath = relativePath === "" ? screenshotDir : join2(screenshotDir, relativePath);
|
|
262
|
+
if (!await isDirectory(directoryPath)) {
|
|
263
|
+
return relativePath === "" ? ["screenshots:missing"] : [];
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const entries = await readdir2(directoryPath, { withFileTypes: true });
|
|
267
|
+
const parts = await Promise.all(
|
|
268
|
+
entries.sort((left, right) => left.name.localeCompare(right.name)).map((entry) => {
|
|
269
|
+
const entryRelativePath = relativePath === "" ? entry.name : join2(relativePath, entry.name);
|
|
270
|
+
if (entry.isDirectory()) {
|
|
271
|
+
return createScreenshotFingerprint(screenshotDir, entryRelativePath);
|
|
272
|
+
}
|
|
273
|
+
return describeFile(join2(screenshotDir, entryRelativePath), entryRelativePath);
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
return parts.flat().filter((part) => part !== null);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (isFileNotFound2(error)) {
|
|
279
|
+
return relativePath === "" ? ["screenshots:missing"] : [];
|
|
280
|
+
}
|
|
281
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
282
|
+
console.warn(`[Server] Unable to scan ${directoryPath}: ${errorMsg}`);
|
|
283
|
+
return relativePath === "" ? ["screenshots:error"] : [];
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function createArtifactsFingerprint(options) {
|
|
287
|
+
const [offlineFingerprint, screenshotFingerprint] = await Promise.all([
|
|
288
|
+
createOfflineFingerprint(options.offlineReportDir),
|
|
289
|
+
createScreenshotFingerprint(options.screenshotDir)
|
|
290
|
+
]);
|
|
291
|
+
return `${offlineFingerprint}::${screenshotFingerprint.join("|")}`;
|
|
292
|
+
}
|
|
293
|
+
async function watchReportArtifacts(options) {
|
|
294
|
+
let fingerprint = await createArtifactsFingerprint(options);
|
|
295
|
+
let isPolling = false;
|
|
296
|
+
const interval = setInterval(() => {
|
|
297
|
+
if (isPolling) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
isPolling = true;
|
|
301
|
+
void createArtifactsFingerprint(options).then((nextFingerprint) => {
|
|
302
|
+
if (nextFingerprint === fingerprint) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
fingerprint = nextFingerprint;
|
|
306
|
+
options.scheduleRefresh();
|
|
307
|
+
}).catch((error) => {
|
|
308
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
309
|
+
console.warn(`[Server] Artifact refresh poll failed: ${errorMsg}`);
|
|
310
|
+
}).finally(() => {
|
|
311
|
+
isPolling = false;
|
|
312
|
+
});
|
|
313
|
+
}, 100);
|
|
314
|
+
return () => {
|
|
315
|
+
clearInterval(interval);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/server/routes.ts
|
|
320
|
+
import { existsSync } from "fs";
|
|
321
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
212
322
|
var LIVE_UPDATES_WEBSOCKET_PATH = "/";
|
|
213
323
|
function isWebSocketUpgradeRequest(req) {
|
|
214
324
|
return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
|
|
215
325
|
}
|
|
216
326
|
async function handleRoot(ctx) {
|
|
217
|
-
const html = await respondWithFile(
|
|
327
|
+
const html = await respondWithFile(join3(ctx.staticDir, "index.html"), "text/html");
|
|
218
328
|
return html ?? new Response("Not Found", { status: 404 });
|
|
219
329
|
}
|
|
220
330
|
async function handleAppCss() {
|
|
@@ -231,6 +341,37 @@ async function handleSrcFiles(req) {
|
|
|
231
341
|
function handleApiReport(ctx) {
|
|
232
342
|
return Response.json(ctx.reportData);
|
|
233
343
|
}
|
|
344
|
+
var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
|
|
345
|
+
function actualPathFromUrl(ctx, actualUrl) {
|
|
346
|
+
return actualUrl.startsWith("/screenshots/") ? join3(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length)) : actualUrl;
|
|
347
|
+
}
|
|
348
|
+
function reporterTitlePath(test) {
|
|
349
|
+
const testFile = test.location?.file;
|
|
350
|
+
return ["", test.browser, testFile ?? "", ...test.titlePath, test.title];
|
|
351
|
+
}
|
|
352
|
+
function resolveApprovalTarget(ctx, test, retry, imageName) {
|
|
353
|
+
const testFile = test.location?.file;
|
|
354
|
+
const declaration = test.results?.[retry]?.visualDeclarations?.find((candidate) => candidate.visualName === imageName);
|
|
355
|
+
if (ctx.approvalRouting === void 0 || testFile === void 0 || declaration === void 0) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const targets = resolveBaselineTargets({
|
|
359
|
+
testFile,
|
|
360
|
+
reporterTitlePath: reporterTitlePath(test),
|
|
361
|
+
declarations: [declaration],
|
|
362
|
+
config: {
|
|
363
|
+
configDir: ctx.approvalRouting.configDir,
|
|
364
|
+
testDir: ctx.approvalRouting.playwrightTestDir ?? dirname2(testFile),
|
|
365
|
+
snapshotDir: ctx.approvalRouting.playwrightSnapshotDir ?? dirname2(testFile),
|
|
366
|
+
projectName: test.browser,
|
|
367
|
+
snapshotSuffix: process.platform,
|
|
368
|
+
snapshotPathTemplate: ctx.approvalRouting.playwrightSnapshotPathTemplate,
|
|
369
|
+
toHaveScreenshotPathTemplate: ctx.approvalRouting.playwrightToHaveScreenshotPathTemplate
|
|
370
|
+
},
|
|
371
|
+
snapshotPathExists: existsSync
|
|
372
|
+
});
|
|
373
|
+
return targets.length === 1 ? targets[0]?.snapshotPath ?? null : null;
|
|
374
|
+
}
|
|
234
375
|
async function handleApiApprove(ctx, req) {
|
|
235
376
|
try {
|
|
236
377
|
const rawBody = await req.json();
|
|
@@ -241,61 +382,96 @@ async function handleApiApprove(ctx, req) {
|
|
|
241
382
|
}
|
|
242
383
|
const { id, retry, image } = parsed;
|
|
243
384
|
const test = ctx.reportData.tests[id];
|
|
244
|
-
if (test
|
|
245
|
-
|
|
246
|
-
|
|
385
|
+
if (test === void 0) {
|
|
386
|
+
return Response.json({ success: false, error: "Test not found" }, { status: 404 });
|
|
387
|
+
}
|
|
388
|
+
const actualUrl = test.results?.[retry]?.images?.[image]?.actual;
|
|
389
|
+
if (actualUrl === void 0) {
|
|
390
|
+
return Response.json({ success: false, error: "Actual image not found" }, { status: 409 });
|
|
391
|
+
}
|
|
392
|
+
const snapshotPath = resolveApprovalTarget(ctx, test, retry, image);
|
|
393
|
+
if (snapshotPath === null) {
|
|
394
|
+
return Response.json({ success: false, error: APPROVAL_TARGET_ERROR }, { status: 409 });
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
await copyFilePortable(actualPathFromUrl(ctx, actualUrl), snapshotPath);
|
|
398
|
+
test.approved = { ...test.approved ?? {}, [image]: retry };
|
|
247
399
|
await ctx.saveReport();
|
|
248
|
-
|
|
249
|
-
if (actualUrl !== null && actualUrl !== void 0 && test.location?.file !== null && test.location?.file !== void 0) {
|
|
250
|
-
const actualPath = actualUrl.replace("/screenshots/", `${ctx.reportData.screenshotDir}/`);
|
|
251
|
-
const snapshotPath = `${test.location.file}-snapshots/${image}-${test.browser}-${process.platform}.png`;
|
|
252
|
-
try {
|
|
253
|
-
await copyFilePortable(actualPath, snapshotPath);
|
|
254
|
-
console.log(` \u2714 Updated baseline: ${snapshotPath}`);
|
|
255
|
-
} catch (err) {
|
|
256
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
257
|
-
console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
400
|
+
console.log(` \u2714 Updated baseline: ${snapshotPath}`);
|
|
260
401
|
console.log(` \u2714 Approved [${test.browser}] ${test.title} \u2014 ${image}`);
|
|
402
|
+
return Response.json({ success: true });
|
|
403
|
+
} catch (err) {
|
|
404
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
405
|
+
console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
|
|
406
|
+
return Response.json({ success: false, error: "Failed to update baseline" }, { status: 500 });
|
|
261
407
|
}
|
|
262
|
-
return Response.json({ success: true });
|
|
263
408
|
} catch {
|
|
264
409
|
return Response.json({ success: false, error: "Invalid request" }, { status: 400 });
|
|
265
410
|
}
|
|
266
411
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const approved = {};
|
|
412
|
+
function createBulkApprovalUpdates(ctx) {
|
|
413
|
+
return Object.values(ctx.reportData.tests).flatMap((test) => {
|
|
414
|
+
if (!test.results || test.results.length === 0) {
|
|
415
|
+
return [];
|
|
416
|
+
}
|
|
273
417
|
const lastRetry = test.results.length - 1;
|
|
274
418
|
const lastResult = test.results[lastRetry];
|
|
275
|
-
if (!lastResult?.images)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
419
|
+
if (!lastResult?.images) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
return Object.keys(lastResult.images).flatMap((imageName) => {
|
|
279
423
|
const actualUrl = lastResult.images?.[imageName]?.actual;
|
|
280
|
-
if (actualUrl
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}).catch((err) => {
|
|
287
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
288
|
-
console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
|
|
289
|
-
})
|
|
290
|
-
);
|
|
424
|
+
if (actualUrl === void 0) {
|
|
425
|
+
return [Promise.resolve({ kind: "unresolved" })];
|
|
426
|
+
}
|
|
427
|
+
const snapshotPath = resolveApprovalTarget(ctx, test, lastRetry, imageName);
|
|
428
|
+
if (snapshotPath === null) {
|
|
429
|
+
return [Promise.resolve({ kind: "unresolved" })];
|
|
291
430
|
}
|
|
431
|
+
return [
|
|
432
|
+
copyFilePortable(actualPathFromUrl(ctx, actualUrl), snapshotPath).then(
|
|
433
|
+
() => ({
|
|
434
|
+
kind: "approved",
|
|
435
|
+
imageName,
|
|
436
|
+
retry: lastRetry,
|
|
437
|
+
snapshotPath,
|
|
438
|
+
test
|
|
439
|
+
})
|
|
440
|
+
).catch((err) => {
|
|
441
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
442
|
+
console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
|
|
443
|
+
return { kind: "failed" };
|
|
444
|
+
})
|
|
445
|
+
];
|
|
292
446
|
});
|
|
293
|
-
test.approved = approved;
|
|
294
447
|
});
|
|
448
|
+
}
|
|
449
|
+
function summarizeBulkApprovalOutcomes(outcomes) {
|
|
450
|
+
return outcomes.reduce(
|
|
451
|
+
(summary, outcome) => {
|
|
452
|
+
switch (outcome.kind) {
|
|
453
|
+
case "approved": {
|
|
454
|
+
outcome.test.approved = { ...outcome.test.approved ?? {}, [outcome.imageName]: outcome.retry };
|
|
455
|
+
console.log(` \u2714 Updated baseline: ${outcome.snapshotPath}`);
|
|
456
|
+
return { ...summary, approved: summary.approved + 1 };
|
|
457
|
+
}
|
|
458
|
+
case "unresolved":
|
|
459
|
+
return { ...summary, unresolved: summary.unresolved + 1 };
|
|
460
|
+
case "failed":
|
|
461
|
+
return { ...summary, failed: summary.failed + 1 };
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
{ approved: 0, unresolved: 0, failed: 0 }
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
async function handleApiApproveAll(ctx) {
|
|
468
|
+
const outcomes = await Promise.all(createBulkApprovalUpdates(ctx));
|
|
469
|
+
const counts = summarizeBulkApprovalOutcomes(outcomes);
|
|
295
470
|
await ctx.saveReport();
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
471
|
+
console.log(
|
|
472
|
+
` \u2714 Approved all \u2014 approved: ${counts.approved}, unresolved: ${counts.unresolved}, failed: ${counts.failed}`
|
|
473
|
+
);
|
|
474
|
+
return Response.json({ success: counts.failed === 0, ...counts });
|
|
299
475
|
}
|
|
300
476
|
async function handleApiImages(req) {
|
|
301
477
|
const path = new URL(req.url).pathname.slice("/api/images/".length);
|
|
@@ -311,7 +487,7 @@ async function handleScreenshots(ctx, req) {
|
|
|
311
487
|
}
|
|
312
488
|
async function handleDist(ctx, req) {
|
|
313
489
|
const path = new URL(req.url).pathname.slice("/dist/".length);
|
|
314
|
-
const filePath =
|
|
490
|
+
const filePath = join3(ctx.staticDir, path);
|
|
315
491
|
const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
|
|
316
492
|
const file = await respondWithFile(filePath, contentType);
|
|
317
493
|
return file ?? new Response("Not Found", { status: 404 });
|
|
@@ -330,10 +506,10 @@ function handleHttpRequest(ctx, req) {
|
|
|
330
506
|
if (pathname === "/api/report") {
|
|
331
507
|
return Promise.resolve(handleApiReport(ctx));
|
|
332
508
|
}
|
|
333
|
-
if (pathname === "/api/approve") {
|
|
509
|
+
if (pathname === "/api/approve" && req.method === "POST") {
|
|
334
510
|
return handleApiApprove(ctx, req);
|
|
335
511
|
}
|
|
336
|
-
if (pathname === "/api/approve-all") {
|
|
512
|
+
if (pathname === "/api/approve-all" && req.method === "POST") {
|
|
337
513
|
return handleApiApproveAll(ctx);
|
|
338
514
|
}
|
|
339
515
|
if (pathname.startsWith("/api/images/")) {
|
|
@@ -414,6 +590,10 @@ async function loadOfflineReports(offlineReportDir, reportData) {
|
|
|
414
590
|
screenshotsBaseUrl: "/screenshots/"
|
|
415
591
|
});
|
|
416
592
|
}
|
|
593
|
+
function resetReloadableReportData(reportData) {
|
|
594
|
+
reportData.tests = {};
|
|
595
|
+
reportData.isUpdateMode = false;
|
|
596
|
+
}
|
|
417
597
|
async function handleParsedWebSocketMessage(ctx, msg) {
|
|
418
598
|
switch (msg.type) {
|
|
419
599
|
case "test-begin": {
|
|
@@ -462,19 +642,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
462
642
|
};
|
|
463
643
|
}
|
|
464
644
|
async function resolveStaticDir(staticDir) {
|
|
465
|
-
const currentDir =
|
|
645
|
+
const currentDir = dirname3(fileURLToPath(import.meta.url));
|
|
466
646
|
const candidates = staticDir === void 0 ? [
|
|
467
647
|
currentDir,
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
] : [staticDir,
|
|
648
|
+
join4(currentDir, "dist"),
|
|
649
|
+
join4(currentDir, "..", "dist"),
|
|
650
|
+
join4(currentDir, "..", "..", "dist"),
|
|
651
|
+
join4(currentDir, ".."),
|
|
652
|
+
join4(currentDir, "..", "..")
|
|
653
|
+
] : [staticDir, join4(staticDir, "dist")];
|
|
474
654
|
const resolvedCandidates = await Promise.all(
|
|
475
655
|
candidates.map(async (candidate) => ({
|
|
476
656
|
candidate,
|
|
477
|
-
exists: await fileExists(
|
|
657
|
+
exists: await fileExists(join4(candidate, "index.html"))
|
|
478
658
|
}))
|
|
479
659
|
);
|
|
480
660
|
const resolved = resolvedCandidates.find(({ exists }) => exists);
|
|
@@ -485,9 +665,23 @@ async function resolveStaticDir(staticDir) {
|
|
|
485
665
|
}
|
|
486
666
|
async function resolveReportPath(reportPath) {
|
|
487
667
|
if (await isDirectory(reportPath)) {
|
|
488
|
-
return { reportFile:
|
|
668
|
+
return { reportFile: join4(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
489
669
|
}
|
|
490
|
-
return { reportFile: reportPath, offlineReportDir:
|
|
670
|
+
return { reportFile: reportPath, offlineReportDir: dirname3(reportPath) };
|
|
671
|
+
}
|
|
672
|
+
function createRoutesContext(reportData, staticDir, saveReport, options) {
|
|
673
|
+
return {
|
|
674
|
+
reportData,
|
|
675
|
+
staticDir,
|
|
676
|
+
saveReport,
|
|
677
|
+
approvalRouting: {
|
|
678
|
+
configDir: options.configDir ?? process.cwd(),
|
|
679
|
+
playwrightTestDir: options.playwrightTestDir,
|
|
680
|
+
playwrightSnapshotDir: options.playwrightSnapshotDir,
|
|
681
|
+
playwrightSnapshotPathTemplate: options.playwrightSnapshotPathTemplate,
|
|
682
|
+
playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate
|
|
683
|
+
}
|
|
684
|
+
};
|
|
491
685
|
}
|
|
492
686
|
async function createServerApp(options = {}) {
|
|
493
687
|
const port = options.port ?? 3e3;
|
|
@@ -500,19 +694,26 @@ async function createServerApp(options = {}) {
|
|
|
500
694
|
async function saveReport() {
|
|
501
695
|
await writeJsonFile(reportFile, reportData);
|
|
502
696
|
}
|
|
503
|
-
const routesContext =
|
|
504
|
-
reportData,
|
|
505
|
-
staticDir,
|
|
506
|
-
saveReport
|
|
507
|
-
};
|
|
697
|
+
const routesContext = createRoutesContext(reportData, staticDir, saveReport, options);
|
|
508
698
|
const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport);
|
|
509
699
|
const handleRequest = (req) => handleHttpRequest(routesContext, req);
|
|
510
700
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
511
|
-
|
|
512
|
-
|
|
701
|
+
const reloadFromDisk = async () => {
|
|
702
|
+
resetReloadableReportData(reportData);
|
|
703
|
+
await loadReport(reportFile, reportData);
|
|
704
|
+
await loadOfflineReports(offlineReportDir, reportData);
|
|
705
|
+
broadcastToBrowsers(wsClients, { type: "sync", data: reportData });
|
|
706
|
+
};
|
|
707
|
+
await reloadFromDisk();
|
|
708
|
+
const close = await watchReportArtifacts({
|
|
709
|
+
offlineReportDir,
|
|
710
|
+
screenshotDir: reportData.screenshotDir,
|
|
711
|
+
scheduleRefresh: createDebouncedRefresh(reloadFromDisk)
|
|
712
|
+
});
|
|
513
713
|
return {
|
|
514
714
|
port,
|
|
515
715
|
wsClients,
|
|
716
|
+
close,
|
|
516
717
|
handleRequest,
|
|
517
718
|
handleWebSocketMessage
|
|
518
719
|
};
|
package/dist/cli.js
CHANGED
package/dist/index.css
CHANGED
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
--color-purple-800: oklch(43.8% 0.218 303.724);
|
|
24
24
|
--color-white: #fff;
|
|
25
25
|
--spacing: 0.25rem;
|
|
26
|
+
--container-xl: 36rem;
|
|
27
|
+
--container-2xl: 42rem;
|
|
26
28
|
--text-xs: 0.75rem;
|
|
27
29
|
--text-xs--line-height: calc(1 / 0.75);
|
|
28
30
|
--text-sm: 0.875rem;
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
--font-weight-medium: 500;
|
|
34
36
|
--font-weight-semibold: 600;
|
|
35
37
|
--font-weight-bold: 700;
|
|
38
|
+
--tracking-wide: 0.025em;
|
|
36
39
|
--tracking-wider: 0.05em;
|
|
37
40
|
--radius-sm: 0.25rem;
|
|
38
41
|
--radius-md: 0.375rem;
|
|
@@ -213,6 +216,9 @@
|
|
|
213
216
|
.pointer-events-none {
|
|
214
217
|
pointer-events: none;
|
|
215
218
|
}
|
|
219
|
+
.visible {
|
|
220
|
+
visibility: visible;
|
|
221
|
+
}
|
|
216
222
|
.absolute {
|
|
217
223
|
position: absolute;
|
|
218
224
|
}
|
|
@@ -285,6 +291,9 @@
|
|
|
285
291
|
.mt-2 {
|
|
286
292
|
margin-top: calc(var(--spacing) * 2);
|
|
287
293
|
}
|
|
294
|
+
.mt-3 {
|
|
295
|
+
margin-top: calc(var(--spacing) * 3);
|
|
296
|
+
}
|
|
288
297
|
.mr-3 {
|
|
289
298
|
margin-right: calc(var(--spacing) * 3);
|
|
290
299
|
}
|
|
@@ -306,9 +315,18 @@
|
|
|
306
315
|
.grid {
|
|
307
316
|
display: grid;
|
|
308
317
|
}
|
|
318
|
+
.hidden {
|
|
319
|
+
display: none;
|
|
320
|
+
}
|
|
321
|
+
.inline {
|
|
322
|
+
display: inline;
|
|
323
|
+
}
|
|
309
324
|
.inline-block {
|
|
310
325
|
display: inline-block;
|
|
311
326
|
}
|
|
327
|
+
.table {
|
|
328
|
+
display: table;
|
|
329
|
+
}
|
|
312
330
|
.size-2 {
|
|
313
331
|
width: calc(var(--spacing) * 2);
|
|
314
332
|
height: calc(var(--spacing) * 2);
|
|
@@ -373,9 +391,15 @@
|
|
|
373
391
|
.w-full {
|
|
374
392
|
width: 100%;
|
|
375
393
|
}
|
|
394
|
+
.max-w-2xl {
|
|
395
|
+
max-width: var(--container-2xl);
|
|
396
|
+
}
|
|
376
397
|
.max-w-full {
|
|
377
398
|
max-width: 100%;
|
|
378
399
|
}
|
|
400
|
+
.max-w-xl {
|
|
401
|
+
max-width: var(--container-xl);
|
|
402
|
+
}
|
|
379
403
|
.min-w-0 {
|
|
380
404
|
min-width: calc(var(--spacing) * 0);
|
|
381
405
|
}
|
|
@@ -517,6 +541,12 @@
|
|
|
517
541
|
.border-edge {
|
|
518
542
|
border-color: var(--color-edge);
|
|
519
543
|
}
|
|
544
|
+
.border-edge\/70 {
|
|
545
|
+
border-color: var(--color-edge);
|
|
546
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
547
|
+
border-color: color-mix(in oklab, var(--color-edge) 70%, transparent);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
520
550
|
.border-error {
|
|
521
551
|
border-color: var(--color-error);
|
|
522
552
|
}
|
|
@@ -529,6 +559,12 @@
|
|
|
529
559
|
border-color: color-mix(in oklab, var(--color-green-500) 60%, transparent);
|
|
530
560
|
}
|
|
531
561
|
}
|
|
562
|
+
.border-info\/40 {
|
|
563
|
+
border-color: var(--color-info);
|
|
564
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
565
|
+
border-color: color-mix(in oklab, var(--color-info) 40%, transparent);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
532
568
|
.border-purple-500\/60 {
|
|
533
569
|
border-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 60%, transparent);
|
|
534
570
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -583,6 +619,12 @@
|
|
|
583
619
|
.bg-info {
|
|
584
620
|
background-color: var(--color-info);
|
|
585
621
|
}
|
|
622
|
+
.bg-info\/10 {
|
|
623
|
+
background-color: var(--color-info);
|
|
624
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
625
|
+
background-color: color-mix(in oklab, var(--color-info) 10%, transparent);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
586
628
|
.bg-purple-500\/25 {
|
|
587
629
|
background-color: color-mix(in srgb, oklch(62.7% 0.265 303.9) 25%, transparent);
|
|
588
630
|
@supports (color: color-mix(in lab, red, red)) {
|
|
@@ -698,6 +740,10 @@
|
|
|
698
740
|
.text-\[9px\] {
|
|
699
741
|
font-size: 9px;
|
|
700
742
|
}
|
|
743
|
+
.leading-6 {
|
|
744
|
+
--tw-leading: calc(var(--spacing) * 6);
|
|
745
|
+
line-height: calc(var(--spacing) * 6);
|
|
746
|
+
}
|
|
701
747
|
.font-bold {
|
|
702
748
|
--tw-font-weight: var(--font-weight-bold);
|
|
703
749
|
font-weight: var(--font-weight-bold);
|
|
@@ -714,6 +760,10 @@
|
|
|
714
760
|
--tw-font-weight: var(--font-weight-semibold);
|
|
715
761
|
font-weight: var(--font-weight-semibold);
|
|
716
762
|
}
|
|
763
|
+
.tracking-wide {
|
|
764
|
+
--tw-tracking: var(--tracking-wide);
|
|
765
|
+
letter-spacing: var(--tracking-wide);
|
|
766
|
+
}
|
|
717
767
|
.tracking-wider {
|
|
718
768
|
--tw-tracking: var(--tracking-wider);
|
|
719
769
|
letter-spacing: var(--tracking-wider);
|
|
@@ -1128,6 +1178,10 @@
|
|
|
1128
1178
|
inherits: false;
|
|
1129
1179
|
initial-value: solid;
|
|
1130
1180
|
}
|
|
1181
|
+
@property --tw-leading {
|
|
1182
|
+
syntax: "*";
|
|
1183
|
+
inherits: false;
|
|
1184
|
+
}
|
|
1131
1185
|
@property --tw-font-weight {
|
|
1132
1186
|
syntax: "*";
|
|
1133
1187
|
inherits: false;
|
|
@@ -1295,6 +1349,7 @@
|
|
|
1295
1349
|
--tw-translate-y: 0;
|
|
1296
1350
|
--tw-translate-z: 0;
|
|
1297
1351
|
--tw-border-style: solid;
|
|
1352
|
+
--tw-leading: initial;
|
|
1298
1353
|
--tw-font-weight: initial;
|
|
1299
1354
|
--tw-tracking: initial;
|
|
1300
1355
|
--tw-ordinal: initial;
|