@reportforge/playwright-pdf 0.26.1 → 0.28.0
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/README.md +48 -2
- package/dist/cli/index.js +19 -3
- package/dist/index.d.mts +37 -42
- package/dist/index.d.ts +37 -42
- package/dist/index.js +1425 -1350
- package/dist/index.mjs +1528 -1453
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -11009,8 +11009,22 @@ function acceptDeliveredModel(d) {
|
|
|
11009
11009
|
}
|
|
11010
11010
|
}
|
|
11011
11011
|
|
|
11012
|
-
// src/license/
|
|
11012
|
+
// src/license/tls-teardown-settle.ts
|
|
11013
|
+
init_esm_shims();
|
|
11014
|
+
var WINDOWS_TLS_TEARDOWN_SETTLE_MS = 500;
|
|
11015
|
+
async function settleWindowsTlsTeardown() {
|
|
11016
|
+
if (process.platform !== "win32") return;
|
|
11017
|
+
logger.debug(`settling ${WINDOWS_TLS_TEARDOWN_SETTLE_MS}ms before fast exit (win32 TLS teardown, #175)`);
|
|
11018
|
+
await new Promise((resolve) => {
|
|
11019
|
+
setTimeout(resolve, WINDOWS_TLS_TEARDOWN_SETTLE_MS);
|
|
11020
|
+
});
|
|
11021
|
+
}
|
|
11022
|
+
|
|
11023
|
+
// src/license/constants.ts
|
|
11024
|
+
init_esm_shims();
|
|
11013
11025
|
var DEFAULT_SERVER_URL = "https://reportforge.org";
|
|
11026
|
+
|
|
11027
|
+
// src/license/LicenseClient.ts
|
|
11014
11028
|
var REFRESH_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
|
|
11015
11029
|
var NETWORK_TIMEOUT_MS = 8e3;
|
|
11016
11030
|
var TERMINAL = /* @__PURE__ */ Symbol("terminal");
|
|
@@ -11048,9 +11062,16 @@ var LicenseClient = class {
|
|
|
11048
11062
|
return toLicenseInfo(cacheValid, cached, normalized, "jwt-cache");
|
|
11049
11063
|
}
|
|
11050
11064
|
const network = await this.tryNetwork(cacheValid ? cached : null, normalized, verifier);
|
|
11051
|
-
if (network === TERMINAL)
|
|
11065
|
+
if (network === TERMINAL) {
|
|
11066
|
+
await settleWindowsTlsTeardown();
|
|
11067
|
+
return null;
|
|
11068
|
+
}
|
|
11052
11069
|
if (network) return network;
|
|
11053
|
-
|
|
11070
|
+
const fallback = this._offlineFallback(cacheValid, normalized, nowMs);
|
|
11071
|
+
if (!fallback) {
|
|
11072
|
+
await settleWindowsTlsTeardown();
|
|
11073
|
+
}
|
|
11074
|
+
return fallback;
|
|
11054
11075
|
}
|
|
11055
11076
|
_validateKey(key) {
|
|
11056
11077
|
const parsed = this.parser.parse(key);
|
|
@@ -11238,111 +11259,24 @@ function normalizeUrl(url) {
|
|
|
11238
11259
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
11239
11260
|
}
|
|
11240
11261
|
|
|
11241
|
-
// src/
|
|
11242
|
-
init_esm_shims();
|
|
11243
|
-
|
|
11244
|
-
// src/license/version.ts
|
|
11245
|
-
init_esm_shims();
|
|
11246
|
-
function parseTriple(v) {
|
|
11247
|
-
if (!v) return null;
|
|
11248
|
-
const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(v.trim());
|
|
11249
|
-
if (!m) return null;
|
|
11250
|
-
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
11251
|
-
}
|
|
11252
|
-
function isNewer(latest, current) {
|
|
11253
|
-
const l = parseTriple(latest);
|
|
11254
|
-
const c = parseTriple(current);
|
|
11255
|
-
if (!l || !c) return false;
|
|
11256
|
-
if (l[0] !== c[0]) return l[0] > c[0];
|
|
11257
|
-
if (l[1] !== c[1]) return l[1] > c[1];
|
|
11258
|
-
return l[2] > c[2];
|
|
11259
|
-
}
|
|
11260
|
-
|
|
11261
|
-
// src/license/update-notice.ts
|
|
11262
|
-
function maybePrintUpdateNotice(licenseInfo, currentVersion) {
|
|
11263
|
-
try {
|
|
11264
|
-
const latest = licenseInfo?.latestVersion;
|
|
11265
|
-
if (!latest || !isNewer(latest, currentVersion)) return;
|
|
11266
|
-
const shown = latest.replace(/[^\w.+-]/g, "");
|
|
11267
|
-
logger.info(
|
|
11268
|
-
`Update available: ${currentVersion} -> ${shown}. Run: npm i -D @reportforge/playwright-pdf@latest`
|
|
11269
|
-
);
|
|
11270
|
-
} catch (err) {
|
|
11271
|
-
logger.debug("update notice failed (ignored)", { error: err.message });
|
|
11272
|
-
}
|
|
11273
|
-
}
|
|
11274
|
-
|
|
11275
|
-
// src/analysis/index.ts
|
|
11276
|
-
init_esm_shims();
|
|
11277
|
-
|
|
11278
|
-
// src/analysis/LocalModelStore.ts
|
|
11279
|
-
init_esm_shims();
|
|
11280
|
-
import fs4 from "fs";
|
|
11281
|
-
import os5 from "os";
|
|
11282
|
-
import path5 from "path";
|
|
11283
|
-
var testDeps2 = null;
|
|
11284
|
-
function defaultLocalModelPath() {
|
|
11285
|
-
return testDeps2?.file ?? path5.join(os5.homedir(), ".reportforge", "model-local.json");
|
|
11286
|
-
}
|
|
11287
|
-
function loadLocalModel(overridePath) {
|
|
11288
|
-
const file = overridePath ?? defaultLocalModelPath();
|
|
11289
|
-
const explicit = overridePath !== void 0;
|
|
11290
|
-
const reject = (why) => {
|
|
11291
|
-
const msg = `local model at ${file} not used: ${why}`;
|
|
11292
|
-
if (explicit) logger.warn(msg);
|
|
11293
|
-
else logger.debug(msg);
|
|
11294
|
-
return null;
|
|
11295
|
-
};
|
|
11296
|
-
let parsed;
|
|
11297
|
-
try {
|
|
11298
|
-
parsed = JSON.parse(fs4.readFileSync(file, "utf8"));
|
|
11299
|
-
} catch (e) {
|
|
11300
|
-
return explicit ? reject(`unreadable (${e.message})`) : null;
|
|
11301
|
-
}
|
|
11302
|
-
if (parsed.formatVersion !== 1) return reject(`unsupported formatVersion ${String(parsed.formatVersion)}`);
|
|
11303
|
-
if (!isValidNaiveBayesModel(parsed.model)) return reject("embedded model failed schema validation");
|
|
11304
|
-
const meta = parsed.meta;
|
|
11305
|
-
if (!meta || typeof meta.gatePassed !== "boolean") return reject("missing meta");
|
|
11306
|
-
if (!meta.gatePassed) return reject("its evaluation gate did not pass - run train-model after labeling more feedback");
|
|
11307
|
-
if (typeof meta.acceptThreshold !== "number") return reject("gate passed but no calibrated threshold (corrupt meta)");
|
|
11308
|
-
return {
|
|
11309
|
-
model: parsed.model,
|
|
11310
|
-
acceptThreshold: meta.acceptThreshold,
|
|
11311
|
-
strongThreshold: typeof meta.strongThreshold === "number" ? meta.strongThreshold : null,
|
|
11312
|
-
meta
|
|
11313
|
-
};
|
|
11314
|
-
}
|
|
11315
|
-
|
|
11316
|
-
// src/analysis/FailureClusterer.ts
|
|
11262
|
+
// src/collector/DataCollector.ts
|
|
11317
11263
|
init_esm_shims();
|
|
11318
11264
|
|
|
11319
|
-
// src/
|
|
11265
|
+
// src/utils/strip-ansi.ts
|
|
11320
11266
|
init_esm_shims();
|
|
11321
|
-
var
|
|
11322
|
-
|
|
11323
|
-
|
|
11324
|
-
|
|
11325
|
-
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
return
|
|
11330
|
-
}
|
|
11331
|
-
function localInferenceTokens(message) {
|
|
11332
|
-
return redactForStorage(message).split(" ").filter(Boolean);
|
|
11267
|
+
var ANSI_PATTERN = new RegExp(
|
|
11268
|
+
[
|
|
11269
|
+
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
|
|
11270
|
+
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"
|
|
11271
|
+
].join("|"),
|
|
11272
|
+
"g"
|
|
11273
|
+
);
|
|
11274
|
+
function stripAnsi(input) {
|
|
11275
|
+
return input.replace(ANSI_PATTERN, "");
|
|
11333
11276
|
}
|
|
11334
11277
|
|
|
11335
|
-
// src/
|
|
11278
|
+
// src/collector/execution-capture.ts
|
|
11336
11279
|
init_esm_shims();
|
|
11337
|
-
var STRENGTH = {
|
|
11338
|
-
unknownMaxMargin: 0.05,
|
|
11339
|
-
collectMaxMargin: 0.1,
|
|
11340
|
-
harvestMinMargin: 0.25,
|
|
11341
|
-
moderateMinMargin: 0.1,
|
|
11342
|
-
strongMinMargin: 0.25
|
|
11343
|
-
};
|
|
11344
|
-
var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
|
|
11345
|
-
var OVERRIDE_MARGIN = 999;
|
|
11346
11280
|
|
|
11347
11281
|
// src/utils/truncate.ts
|
|
11348
11282
|
init_esm_shims();
|
|
@@ -11350,949 +11284,569 @@ function clampWithEllipsis(text, max) {
|
|
|
11350
11284
|
return text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
11351
11285
|
}
|
|
11352
11286
|
|
|
11353
|
-
// src/
|
|
11354
|
-
var
|
|
11355
|
-
var
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
return
|
|
11362
|
-
}
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
|
|
11370
|
-
|
|
11371
|
-
|
|
11372
|
-
|
|
11373
|
-
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
11379
|
-
|
|
11380
|
-
|
|
11381
|
-
return "weak";
|
|
11382
|
-
}
|
|
11383
|
-
function classifyInput(f) {
|
|
11384
|
-
const head = (f.error.stack ?? "").split("\n").slice(0, 3).join(" ");
|
|
11385
|
-
return `${f.error.message} ${head}`;
|
|
11287
|
+
// src/collector/execution-capture.ts
|
|
11288
|
+
var MAX_TITLE_CHARS = 200;
|
|
11289
|
+
var DEFAULT_MAX_STEPS = 50;
|
|
11290
|
+
var DEFAULT_MAX_CONSOLE_LINES = 50;
|
|
11291
|
+
var isTeardown = (category) => category === "hook" || category === "fixture";
|
|
11292
|
+
var inHookSubtree = (flat, f) => isTeardown(f.category) || f.ancestors.some((a) => isTeardown(flat[a].category));
|
|
11293
|
+
var isTopLevel = (flat, f) => !f.ancestors.some((a) => {
|
|
11294
|
+
const c = flat[a].category;
|
|
11295
|
+
return c === "pw:api" || c === "expect";
|
|
11296
|
+
});
|
|
11297
|
+
var testStepDepth = (flat, f) => f.ancestors.reduce((n, a) => n + (flat[a].category === "test.step" ? 1 : 0), 0);
|
|
11298
|
+
function flatten(steps) {
|
|
11299
|
+
const out = [];
|
|
11300
|
+
const walk = (nodes, depth, ancestors) => {
|
|
11301
|
+
for (const s of nodes ?? []) {
|
|
11302
|
+
const index = out.length;
|
|
11303
|
+
out.push({
|
|
11304
|
+
raw: s,
|
|
11305
|
+
category: s.category ?? "",
|
|
11306
|
+
depth,
|
|
11307
|
+
hasError: s.error != null,
|
|
11308
|
+
ancestors
|
|
11309
|
+
});
|
|
11310
|
+
if (s.steps?.length) walk(s.steps, depth + 1, [...ancestors, index]);
|
|
11311
|
+
}
|
|
11312
|
+
};
|
|
11313
|
+
walk(steps ?? [], 0, []);
|
|
11314
|
+
return out;
|
|
11386
11315
|
}
|
|
11387
|
-
function
|
|
11388
|
-
|
|
11389
|
-
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11316
|
+
function materialize(n, isFailing, depth) {
|
|
11317
|
+
const loc = n.raw.location;
|
|
11318
|
+
const firstLine = isFailing && n.raw.error?.message ? stripAnsi(n.raw.error.message).split("\n").map((s) => s.trim()).find((s) => s.length) ?? "" : "";
|
|
11319
|
+
return {
|
|
11320
|
+
title: clampWithEllipsis(n.raw.title ?? "", MAX_TITLE_CHARS),
|
|
11321
|
+
category: n.category,
|
|
11322
|
+
durationMs: Math.max(0, Math.round(n.raw.duration ?? 0)),
|
|
11323
|
+
depth,
|
|
11324
|
+
failed: n.hasError,
|
|
11325
|
+
// Marks the single failing leaf so the template renders the error there once —
|
|
11326
|
+
// independent of whether a step-level message survives (falls back to the record error).
|
|
11327
|
+
failingLeaf: isFailing || void 0,
|
|
11328
|
+
errorMessage: firstLine ? clampWithEllipsis(firstLine, MAX_TITLE_CHARS) : void 0,
|
|
11329
|
+
location: loc?.file != null ? `${loc.file}:${loc.line ?? 0}` : void 0
|
|
11330
|
+
};
|
|
11394
11331
|
}
|
|
11395
|
-
function
|
|
11396
|
-
|
|
11332
|
+
function pickFailingIdx(flat) {
|
|
11333
|
+
const pick = (allowTeardown) => {
|
|
11334
|
+
let idx = -1;
|
|
11335
|
+
for (let i = 0; i < flat.length; i++) {
|
|
11336
|
+
const f = flat[i];
|
|
11337
|
+
if (!f.hasError || !allowTeardown && inHookSubtree(flat, f)) continue;
|
|
11338
|
+
if (idx === -1 || f.depth >= flat[idx].depth) idx = i;
|
|
11339
|
+
}
|
|
11340
|
+
return idx;
|
|
11341
|
+
};
|
|
11342
|
+
const body = pick(false);
|
|
11343
|
+
return body >= 0 ? body : pick(true);
|
|
11397
11344
|
}
|
|
11398
|
-
function
|
|
11399
|
-
const
|
|
11400
|
-
if (
|
|
11401
|
-
|
|
11345
|
+
function compactSteps(rawSteps, opts = {}) {
|
|
11346
|
+
const flat = flatten(rawSteps);
|
|
11347
|
+
if (flat.length === 0) return { steps: [] };
|
|
11348
|
+
const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
11349
|
+
const failingIdx = pickFailingIdx(flat);
|
|
11350
|
+
const keep = /* @__PURE__ */ new Set();
|
|
11351
|
+
flat.forEach((f, i) => {
|
|
11352
|
+
if (inHookSubtree(flat, f)) return;
|
|
11353
|
+
if (f.category === "test.step") keep.add(i);
|
|
11354
|
+
else if (f.category === "expect" && isTopLevel(flat, f)) keep.add(i);
|
|
11355
|
+
else if (opts.apiSteps && f.category === "pw:api" && isTopLevel(flat, f)) keep.add(i);
|
|
11356
|
+
});
|
|
11357
|
+
if (failingIdx >= 0) {
|
|
11358
|
+
keep.add(failingIdx);
|
|
11359
|
+
for (const a of flat[failingIdx].ancestors) keep.add(a);
|
|
11402
11360
|
}
|
|
11403
|
-
|
|
11404
|
-
|
|
11405
|
-
|
|
11361
|
+
let keepArr = [...keep].sort((a, b) => a - b);
|
|
11362
|
+
let omitted = 0;
|
|
11363
|
+
if (keepArr.length > maxSteps) {
|
|
11364
|
+
omitted = keepArr.length - maxSteps;
|
|
11365
|
+
keepArr = keepArr.slice(-maxSteps);
|
|
11366
|
+
if (failingIdx >= 0 && !keepArr.includes(failingIdx)) {
|
|
11367
|
+
keepArr = [failingIdx, ...keepArr.slice(1)];
|
|
11406
11368
|
}
|
|
11407
11369
|
}
|
|
11408
|
-
|
|
11409
|
-
|
|
11410
|
-
|
|
11411
|
-
if (
|
|
11412
|
-
|
|
11413
|
-
|
|
11370
|
+
const cache = /* @__PURE__ */ new Map();
|
|
11371
|
+
const mat = (i) => {
|
|
11372
|
+
let es = cache.get(i);
|
|
11373
|
+
if (!es) {
|
|
11374
|
+
es = materialize(flat[i], i === failingIdx, testStepDepth(flat, flat[i]));
|
|
11375
|
+
cache.set(i, es);
|
|
11414
11376
|
}
|
|
11377
|
+
return es;
|
|
11378
|
+
};
|
|
11379
|
+
const steps = keepArr.map(mat);
|
|
11380
|
+
if (omitted > 0) {
|
|
11381
|
+
steps.unshift({
|
|
11382
|
+
title: `\u2026 ${omitted} earlier step${omitted === 1 ? "" : "s"} omitted`,
|
|
11383
|
+
category: "",
|
|
11384
|
+
durationMs: 0,
|
|
11385
|
+
depth: 0,
|
|
11386
|
+
failed: false
|
|
11387
|
+
});
|
|
11415
11388
|
}
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
return failures.map((f) => classifyOne(f, chain));
|
|
11389
|
+
return {
|
|
11390
|
+
steps,
|
|
11391
|
+
failingStep: failingIdx >= 0 ? mat(failingIdx) : void 0
|
|
11392
|
+
};
|
|
11421
11393
|
}
|
|
11422
|
-
function
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
}
|
|
11394
|
+
function tailConsole(stdout, stderr, maxLines = DEFAULT_MAX_CONSOLE_LINES) {
|
|
11395
|
+
const tail = (chunks) => {
|
|
11396
|
+
if (chunks.length === 0) return { lines: [], truncated: false };
|
|
11397
|
+
const all = chunks.join("").split(/\r?\n/);
|
|
11398
|
+
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
11399
|
+
const lines = all.slice(-maxLines).map(stripAnsi);
|
|
11400
|
+
return { lines, truncated: all.length > lines.length };
|
|
11401
|
+
};
|
|
11402
|
+
const o = tail(stdout);
|
|
11403
|
+
const e = tail(stderr);
|
|
11404
|
+
if (o.lines.length === 0 && e.lines.length === 0) return void 0;
|
|
11405
|
+
return {
|
|
11406
|
+
stdout: o.lines,
|
|
11407
|
+
stderr: e.lines,
|
|
11408
|
+
truncated: o.truncated || e.truncated,
|
|
11409
|
+
stdoutTruncated: o.truncated,
|
|
11410
|
+
stderrTruncated: e.truncated
|
|
11411
|
+
};
|
|
11430
11412
|
}
|
|
11431
|
-
function
|
|
11432
|
-
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
|
|
11413
|
+
function extractEvidence(attachments) {
|
|
11414
|
+
let tracePath;
|
|
11415
|
+
let videoPath;
|
|
11416
|
+
for (const a of attachments) {
|
|
11417
|
+
if (!a.path) continue;
|
|
11418
|
+
const looksLikeTrace = a.name === "trace" || a.contentType === "application/zip" && (a.name ?? "").includes("trace");
|
|
11419
|
+
if (!tracePath && looksLikeTrace) tracePath = a.path;
|
|
11420
|
+
if (!videoPath && a.contentType?.startsWith("video/")) videoPath = a.path;
|
|
11438
11421
|
}
|
|
11439
|
-
|
|
11440
|
-
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11446
|
-
|
|
11447
|
-
|
|
11448
|
-
margin: Math.max(...members.map((m) => m.margin)),
|
|
11449
|
-
count: members.length,
|
|
11450
|
-
tests: members.slice(0, MAX_TESTS_PER_CLUSTER).map((m) => m.failure.testTitle),
|
|
11451
|
-
testsTotal: members.length,
|
|
11452
|
-
representativeMessage: representativeMessage(members[0].failure.error.message),
|
|
11453
|
-
stackFrame: members[0].frame
|
|
11422
|
+
if (!tracePath && !videoPath) return void 0;
|
|
11423
|
+
return { tracePath, videoPath };
|
|
11424
|
+
}
|
|
11425
|
+
function buildExecution(input, opts, source) {
|
|
11426
|
+
const capture = {};
|
|
11427
|
+
if (opts.steps) {
|
|
11428
|
+
const { steps, failingStep } = compactSteps(input.steps, {
|
|
11429
|
+
maxSteps: opts.maxSteps,
|
|
11430
|
+
apiSteps: opts.apiSteps
|
|
11454
11431
|
});
|
|
11432
|
+
if (steps.length > 0) capture.steps = steps;
|
|
11433
|
+
if (failingStep) capture.failingStep = failingStep;
|
|
11455
11434
|
}
|
|
11456
|
-
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
// src/analysis/SummaryWriter.ts
|
|
11460
|
-
init_esm_shims();
|
|
11461
|
-
var CATEGORY_LABEL = {
|
|
11462
|
-
assertion: "Assertion failure",
|
|
11463
|
-
timeout: "Timeout",
|
|
11464
|
-
"locator-not-found": "Element not found",
|
|
11465
|
-
network: "Network error",
|
|
11466
|
-
navigation: "Navigation error",
|
|
11467
|
-
"env-config": "Environment / config",
|
|
11468
|
-
"flaky-by-retry": "Flaky (passed on retry)",
|
|
11469
|
-
unknown: "Unclassified"
|
|
11470
|
-
};
|
|
11471
|
-
var SHORT_LABEL = {
|
|
11472
|
-
assertion: "assertion",
|
|
11473
|
-
timeout: "timeout",
|
|
11474
|
-
"locator-not-found": "element not found",
|
|
11475
|
-
network: "network",
|
|
11476
|
-
navigation: "navigation",
|
|
11477
|
-
"env-config": "env/config",
|
|
11478
|
-
"flaky-by-retry": "flaky",
|
|
11479
|
-
unknown: "unclassified"
|
|
11480
|
-
};
|
|
11481
|
-
function tallyEntries(clusters) {
|
|
11482
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
11483
|
-
for (const c of clusters) {
|
|
11484
|
-
byCategory.set(c.category, (byCategory.get(c.category) ?? 0) + c.count);
|
|
11435
|
+
if (opts.console) {
|
|
11436
|
+
const console2 = tailConsole(input.stdout, input.stderr, opts.maxConsoleLines);
|
|
11437
|
+
if (console2) capture.console = console2;
|
|
11485
11438
|
}
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
return
|
|
11493
|
-
}
|
|
11494
|
-
function buildFailureTally(analysis) {
|
|
11495
|
-
if (!analysis) return "";
|
|
11496
|
-
const failureClusters = analysis.clusters.filter((c) => c.category !== "flaky-by-retry");
|
|
11497
|
-
return tallyEntries(failureClusters).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`).join(", ");
|
|
11498
|
-
}
|
|
11499
|
-
function dominantCategory(clusters) {
|
|
11500
|
-
return clusters[0]?.category ?? "unknown";
|
|
11439
|
+
if (opts.evidence) {
|
|
11440
|
+
const evidence = extractEvidence(input.attachments);
|
|
11441
|
+
if (evidence) capture.evidence = evidence;
|
|
11442
|
+
}
|
|
11443
|
+
if (Object.keys(capture).length === 0) return void 0;
|
|
11444
|
+
capture.source = source;
|
|
11445
|
+
return capture;
|
|
11501
11446
|
}
|
|
11502
|
-
function
|
|
11503
|
-
|
|
11504
|
-
const verb = delta > 0 ? "climbed" : "dropped";
|
|
11505
|
-
const abs = Math.abs(delta);
|
|
11506
|
-
const points = abs === 1 ? "point" : "points";
|
|
11507
|
-
return `Pass rate ${verb} ${abs} ${points} to ${passRate}% since the last run.`;
|
|
11447
|
+
function chunkToString(chunk) {
|
|
11448
|
+
return typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
11508
11449
|
}
|
|
11509
|
-
function
|
|
11510
|
-
|
|
11511
|
-
const verb = analysis.clusters.length === 1 ? "remains" : "remain";
|
|
11512
|
-
return `${head} ${verb}: ${tallyParts.join(", ")}.`;
|
|
11450
|
+
function decodeShardChunk(c) {
|
|
11451
|
+
return c.text ?? (c.buffer ? Buffer.from(c.buffer, "base64").toString("utf8") : "");
|
|
11513
11452
|
}
|
|
11514
|
-
function
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
|
|
11518
|
-
|
|
11519
|
-
|
|
11453
|
+
function executionFromResult(result, opts) {
|
|
11454
|
+
return buildExecution(
|
|
11455
|
+
{
|
|
11456
|
+
steps: result.steps,
|
|
11457
|
+
stdout: result.stdout.map(chunkToString),
|
|
11458
|
+
stderr: result.stderr.map(chunkToString),
|
|
11459
|
+
attachments: result.attachments
|
|
11460
|
+
},
|
|
11461
|
+
opts,
|
|
11462
|
+
"reporter"
|
|
11463
|
+
);
|
|
11520
11464
|
}
|
|
11521
11465
|
|
|
11522
|
-
// src/
|
|
11466
|
+
// src/collector/SuiteWalker.ts
|
|
11523
11467
|
init_esm_shims();
|
|
11524
|
-
import fs6 from "fs";
|
|
11525
|
-
import os7 from "os";
|
|
11526
|
-
import path7 from "path";
|
|
11527
11468
|
|
|
11528
|
-
// src/
|
|
11469
|
+
// src/collector/stats-utils.ts
|
|
11529
11470
|
init_esm_shims();
|
|
11530
|
-
var
|
|
11531
|
-
|
|
11532
|
-
|
|
11533
|
-
const
|
|
11534
|
-
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
if (
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
|
|
11541
|
-
const parts = line.split(",");
|
|
11542
|
-
if (parts.length < minParts) continue;
|
|
11543
|
-
const row = isV2 ? { category: parts[0], margin: parts[1], label: parts[2], text: parts.slice(3).join(",") } : { category: parts[0], margin: parts[1], label: "", text: parts.slice(2).join(",") };
|
|
11544
|
-
if (row.text.trim().length === 0) continue;
|
|
11545
|
-
rows.push(row);
|
|
11546
|
-
}
|
|
11547
|
-
return rows;
|
|
11471
|
+
var SUITE_CHART_MAX_ROWS = 150;
|
|
11472
|
+
function resolveTestStatus(test, result) {
|
|
11473
|
+
if (!result) return "skipped";
|
|
11474
|
+
const expected = test.expectedStatus ?? "passed";
|
|
11475
|
+
const actual = result.status;
|
|
11476
|
+
if (actual === "skipped") return "skipped";
|
|
11477
|
+
if (actual === "timedOut") return "timedOut";
|
|
11478
|
+
if (actual === "passed" && result.retry > 0) return "flaky";
|
|
11479
|
+
if (actual === "passed" && expected === "passed") return "passed";
|
|
11480
|
+
if (actual === "failed" && expected === "failed") return "passed";
|
|
11481
|
+
return "failed";
|
|
11548
11482
|
}
|
|
11549
|
-
function
|
|
11550
|
-
|
|
11551
|
-
|
|
11483
|
+
function statsFromTests(tests) {
|
|
11484
|
+
return {
|
|
11485
|
+
total: tests.length,
|
|
11486
|
+
passed: tests.filter((t) => t.status === "passed").length,
|
|
11487
|
+
failed: tests.filter((t) => t.status === "failed").length,
|
|
11488
|
+
timedOut: tests.filter((t) => t.status === "timedOut").length,
|
|
11489
|
+
skipped: tests.filter((t) => t.status === "skipped").length,
|
|
11490
|
+
flaky: tests.filter((t) => t.status === "flaky").length
|
|
11491
|
+
};
|
|
11552
11492
|
}
|
|
11553
|
-
function
|
|
11554
|
-
return
|
|
11493
|
+
function aggregateStats(parts) {
|
|
11494
|
+
return parts.reduce(
|
|
11495
|
+
(acc, s) => ({
|
|
11496
|
+
total: acc.total + s.total,
|
|
11497
|
+
passed: acc.passed + s.passed,
|
|
11498
|
+
failed: acc.failed + s.failed,
|
|
11499
|
+
timedOut: acc.timedOut + s.timedOut,
|
|
11500
|
+
skipped: acc.skipped + s.skipped,
|
|
11501
|
+
flaky: acc.flaky + s.flaky
|
|
11502
|
+
}),
|
|
11503
|
+
{ total: 0, passed: 0, failed: 0, timedOut: 0, skipped: 0, flaky: 0 }
|
|
11504
|
+
);
|
|
11555
11505
|
}
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
|
|
11564
|
-
try {
|
|
11565
|
-
atomicWrite(path6.join(projectDir, MARKER_FILENAME), JSON.stringify(marker));
|
|
11566
|
-
} catch {
|
|
11567
|
-
}
|
|
11506
|
+
function toRow(s) {
|
|
11507
|
+
return {
|
|
11508
|
+
label: s.title,
|
|
11509
|
+
passed: s.stats.passed,
|
|
11510
|
+
failed: s.stats.failed,
|
|
11511
|
+
timedOut: s.stats.timedOut,
|
|
11512
|
+
skipped: s.stats.skipped
|
|
11513
|
+
};
|
|
11568
11514
|
}
|
|
11569
|
-
function
|
|
11570
|
-
|
|
11571
|
-
|
|
11572
|
-
|
|
11515
|
+
function capRows(rows, limit) {
|
|
11516
|
+
if (rows.length <= limit) return { rows };
|
|
11517
|
+
const size = (r) => r.passed + r.failed + r.timedOut + r.skipped;
|
|
11518
|
+
const scored = [...rows].sort(
|
|
11519
|
+
(a, b) => b.failed + b.timedOut - (a.failed + a.timedOut) || size(b) - size(a)
|
|
11520
|
+
);
|
|
11521
|
+
const rest = scored.slice(limit);
|
|
11522
|
+
const sums = rest.reduce(
|
|
11523
|
+
(acc, r) => ({
|
|
11524
|
+
passed: acc.passed + r.passed,
|
|
11525
|
+
failed: acc.failed + r.failed,
|
|
11526
|
+
timedOut: acc.timedOut + r.timedOut,
|
|
11527
|
+
skipped: acc.skipped + r.skipped
|
|
11528
|
+
}),
|
|
11529
|
+
{ passed: 0, failed: 0, timedOut: 0, skipped: 0 }
|
|
11530
|
+
);
|
|
11531
|
+
const parts = [
|
|
11532
|
+
sums.failed > 0 ? `${sums.failed} failed` : "",
|
|
11533
|
+
sums.timedOut > 0 ? `${sums.timedOut} timed out` : "",
|
|
11534
|
+
sums.passed > 0 ? `${sums.passed} passed` : "",
|
|
11535
|
+
sums.skipped > 0 ? `${sums.skipped} skipped` : ""
|
|
11536
|
+
].filter(Boolean);
|
|
11537
|
+
const overflowNote = `+ ${rest.length} more suites` + (parts.length > 0 ? `: ${parts.join(" \xB7 ")}` : "");
|
|
11538
|
+
return { rows: scored.slice(0, limit), overflowNote };
|
|
11573
11539
|
}
|
|
11574
|
-
function
|
|
11575
|
-
|
|
11576
|
-
|
|
11577
|
-
|
|
11578
|
-
|
|
11540
|
+
function buildSuiteResults(projects, limit) {
|
|
11541
|
+
const top = projects.flatMap((p) => p.suites);
|
|
11542
|
+
if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
|
|
11543
|
+
const file = top[0];
|
|
11544
|
+
const fs16 = statsFromTests(file.tests);
|
|
11545
|
+
const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs16.passed, failed: fs16.failed, timedOut: fs16.timedOut, skipped: fs16.skipped }] : [];
|
|
11546
|
+
return capRows([...rootRow, ...file.suites.map(toRow)], limit);
|
|
11547
|
+
}
|
|
11548
|
+
return capRows(top.map(toRow), limit);
|
|
11579
11549
|
}
|
|
11580
11550
|
|
|
11581
|
-
// src/
|
|
11582
|
-
var
|
|
11583
|
-
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
if (c.category === "unknown" || c.margin <= STRENGTH.collectMaxMargin) return true;
|
|
11587
|
-
return scope === "all";
|
|
11588
|
-
}
|
|
11589
|
-
function tildeify(p) {
|
|
11590
|
-
const home = os7.homedir();
|
|
11591
|
-
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
11592
|
-
}
|
|
11593
|
-
function collectUnclassified(classified, projectDir, opts = {}) {
|
|
11594
|
-
const scope = opts.scope ?? "blind-spots";
|
|
11595
|
-
const rows = classified.filter((c) => shouldCollect(c, scope)).map((c) => ({ text: redactForStorage(c.failure.error.message), category: c.category, margin: c.margin })).filter((r) => r.text.length > 0);
|
|
11596
|
-
if (rows.length === 0) return;
|
|
11597
|
-
const file = path7.join(projectDir, FILENAME);
|
|
11598
|
-
const firstTime = !fs6.existsSync(file);
|
|
11599
|
-
const byIdentity = /* @__PURE__ */ new Map();
|
|
11600
|
-
if (!firstTime) {
|
|
11601
|
-
for (const r of parseFeedbackRows(fs6.readFileSync(file, "utf8"))) {
|
|
11602
|
-
byIdentity.set(feedbackIdentity(r.category, r.text), r);
|
|
11603
|
-
}
|
|
11604
|
-
}
|
|
11605
|
-
for (const r of rows) {
|
|
11606
|
-
const id = feedbackIdentity(r.category, r.text);
|
|
11607
|
-
const label = byIdentity.get(id)?.label ?? "";
|
|
11608
|
-
byIdentity.delete(id);
|
|
11609
|
-
byIdentity.set(id, { category: r.category, margin: String(r.margin), label, text: r.text });
|
|
11610
|
-
}
|
|
11611
|
-
const all = evictOldestUnlabeledFirst([...byIdentity.values()], MAX_ROWS);
|
|
11612
|
-
fs6.mkdirSync(projectDir, { recursive: true });
|
|
11613
|
-
rewriteFeedbackFile(file, all);
|
|
11614
|
-
if (opts.project) {
|
|
11615
|
-
writeProjectMarker(projectDir, { cwd: opts.project.cwd, outputFile: opts.project.outputFile, updatedAt: Date.now() });
|
|
11616
|
-
}
|
|
11617
|
-
if (firstTime) {
|
|
11618
|
-
logger.info(
|
|
11619
|
-
`Unrecognized failures collected locally at ${tildeify(file)} to improve offline classification. Local only - never sent anywhere. Disable: failureAnalysis.collectUnclassified=false`
|
|
11551
|
+
// src/collector/SuiteWalker.ts
|
|
11552
|
+
var SuiteWalker = class {
|
|
11553
|
+
build(rootSuite, resultMap) {
|
|
11554
|
+
return rootSuite.suites.map(
|
|
11555
|
+
(projectSuite) => this.buildProject(projectSuite, resultMap)
|
|
11620
11556
|
);
|
|
11621
11557
|
}
|
|
11622
|
-
|
|
11623
|
-
|
|
11624
|
-
|
|
11625
|
-
if (excess <= 0) return rows;
|
|
11626
|
-
let unlabeledToDrop = Math.min(excess, rows.filter((r) => r.label === "").length);
|
|
11627
|
-
let labeledToDrop = excess - unlabeledToDrop;
|
|
11628
|
-
const out = [];
|
|
11629
|
-
for (const r of rows) {
|
|
11630
|
-
if (r.label === "" && unlabeledToDrop > 0) {
|
|
11631
|
-
unlabeledToDrop--;
|
|
11632
|
-
continue;
|
|
11633
|
-
}
|
|
11634
|
-
if (r.label !== "" && labeledToDrop > 0) {
|
|
11635
|
-
labeledToDrop--;
|
|
11636
|
-
continue;
|
|
11637
|
-
}
|
|
11638
|
-
out.push(r);
|
|
11639
|
-
}
|
|
11640
|
-
return out;
|
|
11641
|
-
}
|
|
11642
|
-
|
|
11643
|
-
// src/utils/paths.ts
|
|
11644
|
-
init_esm_shims();
|
|
11645
|
-
import os8 from "os";
|
|
11646
|
-
import path8 from "path";
|
|
11647
|
-
import crypto from "crypto";
|
|
11648
|
-
function resolveProjectDir(options, cwd) {
|
|
11649
|
-
const projectKey = crypto.createHash("sha256").update(cwd + (options.outputFile ?? "")).digest("hex").slice(0, 8);
|
|
11650
|
-
return path8.join(os8.homedir(), ".reportforge", projectKey);
|
|
11651
|
-
}
|
|
11652
|
-
|
|
11653
|
-
// src/analysis/index.ts
|
|
11654
|
-
var DEFAULTS = { maxClusters: 10, minStrength: "weak", maxFailuresToAnalyse: 500 };
|
|
11655
|
-
var STRENGTH_RANK = Object.fromEntries(
|
|
11656
|
-
STRENGTH_LEVELS.map((level, i) => [level, i])
|
|
11657
|
-
);
|
|
11658
|
-
function analyseClassified(classified, totalFailures, opts = {}) {
|
|
11659
|
-
const maxClusters = opts.maxClusters ?? DEFAULTS.maxClusters;
|
|
11660
|
-
const minRank = STRENGTH_RANK[opts.minStrength ?? DEFAULTS.minStrength];
|
|
11661
|
-
const cap = opts.maxFailuresToAnalyse ?? DEFAULTS.maxFailuresToAnalyse;
|
|
11662
|
-
const clusters = clusterClassified(classified).filter((c) => STRENGTH_RANK[c.strength] >= minRank).sort((a, b) => b.count - a.count).slice(0, maxClusters);
|
|
11663
|
-
return {
|
|
11664
|
-
clusters,
|
|
11665
|
-
totalFailures,
|
|
11666
|
-
analysedCount: classified.length,
|
|
11667
|
-
truncated: totalFailures > cap,
|
|
11668
|
-
// dominantCategory is part of the AnalysisResult contract; reserved for
|
|
11669
|
-
// consumers (e.g. a future executive badge / notify summary). Not rendered
|
|
11670
|
-
// by the v1 partials.
|
|
11671
|
-
dominantCategory: dominantCategory(clusters),
|
|
11672
|
-
oneLiner: buildOneLiner(clusters)
|
|
11673
|
-
};
|
|
11674
|
-
}
|
|
11675
|
-
function templatesConsumeAnalysis(options) {
|
|
11676
|
-
if (options.templatePath && options.templatePath.length > 0) return true;
|
|
11677
|
-
const templates = Array.isArray(options.template) ? options.template : [options.template];
|
|
11678
|
-
return templates.some((t) => t === "detailed" || t === "executive");
|
|
11679
|
-
}
|
|
11680
|
-
function runFailureAnalysis(reportData, options, cwd) {
|
|
11681
|
-
const fa = options.failureAnalysis;
|
|
11682
|
-
if (reportData.stats.failed === 0 || !fa.enabled) return;
|
|
11683
|
-
const cap = fa.maxFailuresToAnalyse;
|
|
11684
|
-
const analysed = reportData.failures.slice(0, cap);
|
|
11685
|
-
const base = loadModel(fa.autoUpdateModel);
|
|
11686
|
-
const local = fa.localModel ? loadLocalModel(fa.localModelPath) ?? void 0 : void 0;
|
|
11687
|
-
if (local) {
|
|
11688
|
-
logger.debug(
|
|
11689
|
-
`local model active (trained ${local.meta.trainedAt}, ${local.meta.labeledCount} labeled rows, accept margin >= ${local.acceptThreshold})`
|
|
11558
|
+
buildProject(projectSuite, resultMap) {
|
|
11559
|
+
const suites = projectSuite.suites.map(
|
|
11560
|
+
(s) => this.buildSuite(s, resultMap)
|
|
11690
11561
|
);
|
|
11691
|
-
|
|
11692
|
-
|
|
11693
|
-
`local model was gate-checked against base v${local.meta.baseVersionAtEval} but base v${base.version} is active - re-run train-model to re-validate against the current base`
|
|
11694
|
-
);
|
|
11695
|
-
}
|
|
11562
|
+
const stats = aggregateStats(suites.map((s) => s.stats));
|
|
11563
|
+
return { name: projectSuite.title || "default", suites, stats };
|
|
11696
11564
|
}
|
|
11697
|
-
|
|
11698
|
-
|
|
11699
|
-
|
|
11700
|
-
|
|
11701
|
-
|
|
11565
|
+
buildSuite(suite, resultMap) {
|
|
11566
|
+
const childSuites = suite.suites.map((s) => this.buildSuite(s, resultMap));
|
|
11567
|
+
const tests = suite.tests.map((t) => this.buildTest(t, resultMap));
|
|
11568
|
+
const stats = aggregateStats([
|
|
11569
|
+
...childSuites.map((s) => s.stats),
|
|
11570
|
+
statsFromTests(tests)
|
|
11571
|
+
]);
|
|
11572
|
+
return {
|
|
11573
|
+
title: suite.title,
|
|
11574
|
+
type: suite.type === "describe" ? "describe" : "file",
|
|
11575
|
+
location: suite.location ? `${suite.location.file}:${suite.location.line}` : void 0,
|
|
11576
|
+
suites: childSuites,
|
|
11577
|
+
tests,
|
|
11578
|
+
stats
|
|
11579
|
+
};
|
|
11702
11580
|
}
|
|
11703
|
-
|
|
11704
|
-
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11581
|
+
buildTest(test, resultMap) {
|
|
11582
|
+
const result = resultMap.get(test.id);
|
|
11583
|
+
const status = resolveTestStatus(test, result);
|
|
11584
|
+
return {
|
|
11585
|
+
id: test.id,
|
|
11586
|
+
title: test.title,
|
|
11587
|
+
fullTitle: test.titlePath().join(" > "),
|
|
11588
|
+
status,
|
|
11589
|
+
duration: result?.duration ?? 0,
|
|
11590
|
+
retryCount: result?.retry ?? 0,
|
|
11591
|
+
tags: test.tags ?? [],
|
|
11592
|
+
annotations: test.annotations ?? [],
|
|
11593
|
+
location: test.location ? `${test.location.file}:${test.location.line}` : void 0
|
|
11594
|
+
};
|
|
11708
11595
|
}
|
|
11709
|
-
}
|
|
11710
|
-
function sourceTally(classified) {
|
|
11711
|
-
const counts = /* @__PURE__ */ new Map();
|
|
11712
|
-
for (const c of classified) counts.set(c.source, (counts.get(c.source) ?? 0) + 1);
|
|
11713
|
-
return ["rule", "retry", "local", "base"].filter((s) => counts.has(s)).map((s) => `${counts.get(s)} ${s}`).join(" / ");
|
|
11714
|
-
}
|
|
11715
|
-
|
|
11716
|
-
// src/collector/DataCollector.ts
|
|
11717
|
-
init_esm_shims();
|
|
11596
|
+
};
|
|
11718
11597
|
|
|
11719
|
-
// src/
|
|
11598
|
+
// src/collector/severity.ts
|
|
11720
11599
|
init_esm_shims();
|
|
11721
|
-
var
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11725
|
-
|
|
11726
|
-
|
|
11727
|
-
)
|
|
11728
|
-
|
|
11729
|
-
|
|
11600
|
+
var SEVERITY_RANK = {
|
|
11601
|
+
critical: 0,
|
|
11602
|
+
high: 1,
|
|
11603
|
+
medium: 2,
|
|
11604
|
+
low: 3
|
|
11605
|
+
};
|
|
11606
|
+
function sortBySeverity(failures) {
|
|
11607
|
+
return [...failures].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
11608
|
+
}
|
|
11609
|
+
function inferSeverity(annotations, tags) {
|
|
11610
|
+
const allLabels = [
|
|
11611
|
+
...annotations.map((a) => a.type.toLowerCase()),
|
|
11612
|
+
...tags.map((t) => t.toLowerCase())
|
|
11613
|
+
];
|
|
11614
|
+
if (allLabels.some((l) => l.includes("critical") || l.includes("blocker"))) return "critical";
|
|
11615
|
+
if (allLabels.some((l) => l.includes("high") || l.includes("major"))) return "high";
|
|
11616
|
+
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
11617
|
+
return "medium";
|
|
11730
11618
|
}
|
|
11731
11619
|
|
|
11732
|
-
// src/
|
|
11620
|
+
// src/utils/env.ts
|
|
11733
11621
|
init_esm_shims();
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
var
|
|
11737
|
-
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
|
|
11746
|
-
|
|
11747
|
-
|
|
11748
|
-
|
|
11749
|
-
|
|
11750
|
-
raw: s,
|
|
11751
|
-
category: s.category ?? "",
|
|
11752
|
-
depth,
|
|
11753
|
-
hasError: s.error != null,
|
|
11754
|
-
ancestors
|
|
11755
|
-
});
|
|
11756
|
-
if (s.steps?.length) walk(s.steps, depth + 1, [...ancestors, index]);
|
|
11757
|
-
}
|
|
11758
|
-
};
|
|
11759
|
-
walk(steps ?? [], 0, []);
|
|
11760
|
-
return out;
|
|
11761
|
-
}
|
|
11762
|
-
function materialize(n, isFailing, depth) {
|
|
11763
|
-
const loc = n.raw.location;
|
|
11764
|
-
const firstLine = isFailing && n.raw.error?.message ? stripAnsi(n.raw.error.message).split("\n").map((s) => s.trim()).find((s) => s.length) ?? "" : "";
|
|
11765
|
-
return {
|
|
11766
|
-
title: clampWithEllipsis(n.raw.title ?? "", MAX_TITLE_CHARS),
|
|
11767
|
-
category: n.category,
|
|
11768
|
-
durationMs: Math.max(0, Math.round(n.raw.duration ?? 0)),
|
|
11769
|
-
depth,
|
|
11770
|
-
failed: n.hasError,
|
|
11771
|
-
// Marks the single failing leaf so the template renders the error there once —
|
|
11772
|
-
// independent of whether a step-level message survives (falls back to the record error).
|
|
11773
|
-
failingLeaf: isFailing || void 0,
|
|
11774
|
-
errorMessage: firstLine ? clampWithEllipsis(firstLine, MAX_TITLE_CHARS) : void 0,
|
|
11775
|
-
location: loc?.file != null ? `${loc.file}:${loc.line ?? 0}` : void 0
|
|
11776
|
-
};
|
|
11777
|
-
}
|
|
11778
|
-
function pickFailingIdx(flat) {
|
|
11779
|
-
const pick = (allowTeardown) => {
|
|
11780
|
-
let idx = -1;
|
|
11781
|
-
for (let i = 0; i < flat.length; i++) {
|
|
11782
|
-
const f = flat[i];
|
|
11783
|
-
if (!f.hasError || !allowTeardown && inHookSubtree(flat, f)) continue;
|
|
11784
|
-
if (idx === -1 || f.depth >= flat[idx].depth) idx = i;
|
|
11785
|
-
}
|
|
11786
|
-
return idx;
|
|
11787
|
-
};
|
|
11788
|
-
const body = pick(false);
|
|
11789
|
-
return body >= 0 ? body : pick(true);
|
|
11790
|
-
}
|
|
11791
|
-
function compactSteps(rawSteps, opts = {}) {
|
|
11792
|
-
const flat = flatten(rawSteps);
|
|
11793
|
-
if (flat.length === 0) return { steps: [] };
|
|
11794
|
-
const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
11795
|
-
const failingIdx = pickFailingIdx(flat);
|
|
11796
|
-
const keep = /* @__PURE__ */ new Set();
|
|
11797
|
-
flat.forEach((f, i) => {
|
|
11798
|
-
if (inHookSubtree(flat, f)) return;
|
|
11799
|
-
if (f.category === "test.step") keep.add(i);
|
|
11800
|
-
else if (f.category === "expect" && isTopLevel(flat, f)) keep.add(i);
|
|
11801
|
-
else if (opts.apiSteps && f.category === "pw:api" && isTopLevel(flat, f)) keep.add(i);
|
|
11802
|
-
});
|
|
11803
|
-
if (failingIdx >= 0) {
|
|
11804
|
-
keep.add(failingIdx);
|
|
11805
|
-
for (const a of flat[failingIdx].ancestors) keep.add(a);
|
|
11622
|
+
import { execSync } from "child_process";
|
|
11623
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
11624
|
+
var COMMIT_HASH_LENGTH = 8;
|
|
11625
|
+
function detectCiEnv() {
|
|
11626
|
+
const env = process.env;
|
|
11627
|
+
if (env.GITHUB_ACTIONS === "true") {
|
|
11628
|
+
const ghServer = env.GITHUB_SERVER_URL;
|
|
11629
|
+
const ghRepo = env.GITHUB_REPOSITORY;
|
|
11630
|
+
const ghRunId = env.GITHUB_RUN_ID;
|
|
11631
|
+
const buildUrl = ghServer && ghRepo && ghRunId ? `${ghServer}/${ghRepo}/actions/runs/${ghRunId}` : "";
|
|
11632
|
+
return {
|
|
11633
|
+
branch: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME || "unknown",
|
|
11634
|
+
commit: env.GITHUB_SHA?.slice(0, COMMIT_HASH_LENGTH) || "unknown",
|
|
11635
|
+
buildUrl,
|
|
11636
|
+
ciProvider: "GitHub Actions"
|
|
11637
|
+
};
|
|
11806
11638
|
}
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
|
|
11813
|
-
|
|
11814
|
-
}
|
|
11639
|
+
if (env.GITLAB_CI === "true") {
|
|
11640
|
+
return {
|
|
11641
|
+
branch: env.CI_COMMIT_REF_NAME || "unknown",
|
|
11642
|
+
commit: env.CI_COMMIT_SHORT_SHA || "unknown",
|
|
11643
|
+
buildUrl: env.CI_JOB_URL || "",
|
|
11644
|
+
ciProvider: "GitLab CI"
|
|
11645
|
+
};
|
|
11815
11646
|
}
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
}
|
|
11823
|
-
|
|
11824
|
-
|
|
11825
|
-
|
|
11826
|
-
|
|
11827
|
-
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11833
|
-
|
|
11647
|
+
if (env.JENKINS_URL) {
|
|
11648
|
+
return {
|
|
11649
|
+
branch: env.GIT_BRANCH?.replace("origin/", "") || env.BRANCH_NAME || "unknown",
|
|
11650
|
+
commit: env.GIT_COMMIT?.slice(0, COMMIT_HASH_LENGTH) || "unknown",
|
|
11651
|
+
buildUrl: env.BUILD_URL || "",
|
|
11652
|
+
ciProvider: "Jenkins"
|
|
11653
|
+
};
|
|
11654
|
+
}
|
|
11655
|
+
if (env.TF_BUILD === "True") {
|
|
11656
|
+
const adoUri = env.SYSTEM_TEAMFOUNDATIONSERVERURI;
|
|
11657
|
+
const adoProject = env.SYSTEM_TEAMPROJECT;
|
|
11658
|
+
const adoBuildId = env.BUILD_BUILDID;
|
|
11659
|
+
const buildUrl = adoUri && adoProject && adoBuildId ? `${adoUri}${adoProject}/_build/results?buildId=${adoBuildId}` : "";
|
|
11660
|
+
return {
|
|
11661
|
+
branch: env.BUILD_SOURCEBRANCH?.replace("refs/heads/", "") || "unknown",
|
|
11662
|
+
commit: env.BUILD_SOURCEVERSION?.slice(0, COMMIT_HASH_LENGTH) || "unknown",
|
|
11663
|
+
buildUrl,
|
|
11664
|
+
ciProvider: "Azure DevOps"
|
|
11665
|
+
};
|
|
11666
|
+
}
|
|
11667
|
+
if (env.CIRCLECI === "true") {
|
|
11668
|
+
return {
|
|
11669
|
+
branch: env.CIRCLE_BRANCH || "unknown",
|
|
11670
|
+
commit: env.CIRCLE_SHA1?.slice(0, COMMIT_HASH_LENGTH) || "unknown",
|
|
11671
|
+
buildUrl: env.CIRCLE_BUILD_URL || "",
|
|
11672
|
+
ciProvider: "CircleCI"
|
|
11673
|
+
};
|
|
11674
|
+
}
|
|
11675
|
+
if (env.BITBUCKET_BUILD_NUMBER) {
|
|
11676
|
+
const workspace = env.BITBUCKET_WORKSPACE;
|
|
11677
|
+
const repoSlug = env.BITBUCKET_REPO_SLUG;
|
|
11678
|
+
const buildNumber = env.BITBUCKET_BUILD_NUMBER;
|
|
11679
|
+
const buildUrl = workspace && repoSlug && buildNumber ? `https://bitbucket.org/${workspace}/${repoSlug}/pipelines/results/${buildNumber}` : "";
|
|
11680
|
+
return {
|
|
11681
|
+
branch: env.BITBUCKET_BRANCH || "unknown",
|
|
11682
|
+
commit: env.BITBUCKET_COMMIT?.slice(0, COMMIT_HASH_LENGTH) || "unknown",
|
|
11683
|
+
buildUrl,
|
|
11684
|
+
ciProvider: "Bitbucket Pipelines"
|
|
11685
|
+
};
|
|
11834
11686
|
}
|
|
11835
11687
|
return {
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
function tailConsole(stdout, stderr, maxLines = DEFAULT_MAX_CONSOLE_LINES) {
|
|
11841
|
-
const tail = (chunks) => {
|
|
11842
|
-
if (chunks.length === 0) return { lines: [], truncated: false };
|
|
11843
|
-
const all = chunks.join("").split(/\r?\n/);
|
|
11844
|
-
if (all.length > 0 && all[all.length - 1] === "") all.pop();
|
|
11845
|
-
const lines = all.slice(-maxLines).map(stripAnsi);
|
|
11846
|
-
return { lines, truncated: all.length > lines.length };
|
|
11847
|
-
};
|
|
11848
|
-
const o = tail(stdout);
|
|
11849
|
-
const e = tail(stderr);
|
|
11850
|
-
if (o.lines.length === 0 && e.lines.length === 0) return void 0;
|
|
11851
|
-
return {
|
|
11852
|
-
stdout: o.lines,
|
|
11853
|
-
stderr: e.lines,
|
|
11854
|
-
truncated: o.truncated || e.truncated,
|
|
11855
|
-
stdoutTruncated: o.truncated,
|
|
11856
|
-
stderrTruncated: e.truncated
|
|
11688
|
+
branch: gitBranch(),
|
|
11689
|
+
commit: gitCommit(),
|
|
11690
|
+
buildUrl: "",
|
|
11691
|
+
ciProvider: "local"
|
|
11857
11692
|
};
|
|
11858
11693
|
}
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11694
|
+
var CI_RUN_SOURCES = [
|
|
11695
|
+
{ provider: "github.com", repoKeys: ["GITHUB_REPOSITORY"], runKeys: ["GITHUB_RUN_ID"], attemptKeys: ["GITHUB_RUN_ATTEMPT"] },
|
|
11696
|
+
{ provider: "gitlab.com", repoKeys: ["CI_PROJECT_PATH"], runKeys: ["CI_PIPELINE_ID"] },
|
|
11697
|
+
{ provider: "circleci.com", repoKeys: ["CIRCLE_PROJECT_REPONAME"], runKeys: ["CIRCLE_WORKFLOW_ID"] },
|
|
11698
|
+
{ provider: "buildkite.com", repoKeys: ["BUILDKITE_REPO"], runKeys: ["BUILDKITE_BUILD_ID"] },
|
|
11699
|
+
{ provider: "dev.azure.com", repoKeys: ["BUILD_REPOSITORY_NAME"], runKeys: ["BUILD_BUILDID"] },
|
|
11700
|
+
{ provider: "bitbucket.org", repoKeys: ["BITBUCKET_REPO_FULL_NAME"], runKeys: ["BITBUCKET_PIPELINE_UUID"] },
|
|
11701
|
+
{ provider: "jenkins", repoKeys: ["JOB_NAME"], runKeys: ["BUILD_NUMBER"] }
|
|
11702
|
+
];
|
|
11703
|
+
var RUN_ID_FALLBACK_SECRET = "rf-live-runid";
|
|
11704
|
+
function detectCiRunId() {
|
|
11705
|
+
for (const src of CI_RUN_SOURCES) {
|
|
11706
|
+
const runId = firstEnv(src.runKeys);
|
|
11707
|
+
if (!runId) continue;
|
|
11708
|
+
const repo = firstEnv(src.repoKeys) ?? "unknown";
|
|
11709
|
+
const attempt = src.attemptKeys ? firstEnv(src.attemptKeys) ?? "1" : "1";
|
|
11710
|
+
const secret = process.env.RF_LICENSE_KEY?.trim() || RUN_ID_FALLBACK_SECRET;
|
|
11711
|
+
return createHmac2("sha256", secret).update(`${src.provider}:${repo}:${runId}:${attempt}`).digest("hex").slice(0, 32);
|
|
11867
11712
|
}
|
|
11868
|
-
|
|
11869
|
-
return { tracePath, videoPath };
|
|
11713
|
+
return null;
|
|
11870
11714
|
}
|
|
11871
|
-
function
|
|
11872
|
-
const
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
maxSteps: opts.maxSteps,
|
|
11876
|
-
apiSteps: opts.apiSteps
|
|
11877
|
-
});
|
|
11878
|
-
if (steps.length > 0) capture.steps = steps;
|
|
11879
|
-
if (failingStep) capture.failingStep = failingStep;
|
|
11880
|
-
}
|
|
11881
|
-
if (opts.console) {
|
|
11882
|
-
const console2 = tailConsole(input.stdout, input.stderr, opts.maxConsoleLines);
|
|
11883
|
-
if (console2) capture.console = console2;
|
|
11884
|
-
}
|
|
11885
|
-
if (opts.evidence) {
|
|
11886
|
-
const evidence = extractEvidence(input.attachments);
|
|
11887
|
-
if (evidence) capture.evidence = evidence;
|
|
11715
|
+
function firstEnv(keys) {
|
|
11716
|
+
for (const k of keys) {
|
|
11717
|
+
const v = process.env[k]?.trim();
|
|
11718
|
+
if (v) return v;
|
|
11888
11719
|
}
|
|
11889
|
-
|
|
11890
|
-
capture.source = source;
|
|
11891
|
-
return capture;
|
|
11892
|
-
}
|
|
11893
|
-
function chunkToString(chunk) {
|
|
11894
|
-
return typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
11720
|
+
return void 0;
|
|
11895
11721
|
}
|
|
11896
|
-
function
|
|
11897
|
-
|
|
11722
|
+
function gitBranch() {
|
|
11723
|
+
try {
|
|
11724
|
+
return execSync("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" }).toString().trim();
|
|
11725
|
+
} catch {
|
|
11726
|
+
return "unknown";
|
|
11727
|
+
}
|
|
11898
11728
|
}
|
|
11899
|
-
function
|
|
11900
|
-
|
|
11901
|
-
{
|
|
11902
|
-
|
|
11903
|
-
|
|
11904
|
-
|
|
11905
|
-
attachments: result.attachments
|
|
11906
|
-
},
|
|
11907
|
-
opts,
|
|
11908
|
-
"reporter"
|
|
11909
|
-
);
|
|
11729
|
+
function gitCommit() {
|
|
11730
|
+
try {
|
|
11731
|
+
return execSync("git rev-parse --short HEAD", { stdio: "pipe" }).toString().trim();
|
|
11732
|
+
} catch {
|
|
11733
|
+
return "unknown";
|
|
11734
|
+
}
|
|
11910
11735
|
}
|
|
11911
11736
|
|
|
11912
|
-
// src/collector/
|
|
11913
|
-
|
|
11914
|
-
|
|
11915
|
-
|
|
11916
|
-
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
|
|
11920
|
-
|
|
11921
|
-
|
|
11922
|
-
|
|
11923
|
-
|
|
11924
|
-
if (actual === "passed" && result.retry > 0) return "flaky";
|
|
11925
|
-
if (actual === "passed" && expected === "passed") return "passed";
|
|
11926
|
-
if (actual === "failed" && expected === "failed") return "passed";
|
|
11927
|
-
return "failed";
|
|
11928
|
-
}
|
|
11929
|
-
function statsFromTests(tests) {
|
|
11930
|
-
return {
|
|
11931
|
-
total: tests.length,
|
|
11932
|
-
passed: tests.filter((t) => t.status === "passed").length,
|
|
11933
|
-
failed: tests.filter((t) => t.status === "failed").length,
|
|
11934
|
-
timedOut: tests.filter((t) => t.status === "timedOut").length,
|
|
11935
|
-
skipped: tests.filter((t) => t.status === "skipped").length,
|
|
11936
|
-
flaky: tests.filter((t) => t.status === "flaky").length
|
|
11937
|
-
};
|
|
11938
|
-
}
|
|
11939
|
-
function aggregateStats(parts) {
|
|
11940
|
-
return parts.reduce(
|
|
11941
|
-
(acc, s) => ({
|
|
11942
|
-
total: acc.total + s.total,
|
|
11943
|
-
passed: acc.passed + s.passed,
|
|
11944
|
-
failed: acc.failed + s.failed,
|
|
11945
|
-
timedOut: acc.timedOut + s.timedOut,
|
|
11946
|
-
skipped: acc.skipped + s.skipped,
|
|
11947
|
-
flaky: acc.flaky + s.flaky
|
|
11948
|
-
}),
|
|
11949
|
-
{ total: 0, passed: 0, failed: 0, timedOut: 0, skipped: 0, flaky: 0 }
|
|
11950
|
-
);
|
|
11951
|
-
}
|
|
11952
|
-
function toRow(s) {
|
|
11953
|
-
return {
|
|
11954
|
-
label: s.title,
|
|
11955
|
-
passed: s.stats.passed,
|
|
11956
|
-
failed: s.stats.failed,
|
|
11957
|
-
timedOut: s.stats.timedOut,
|
|
11958
|
-
skipped: s.stats.skipped
|
|
11959
|
-
};
|
|
11960
|
-
}
|
|
11961
|
-
function capRows(rows, limit) {
|
|
11962
|
-
if (rows.length <= limit) return { rows };
|
|
11963
|
-
const size = (r) => r.passed + r.failed + r.timedOut + r.skipped;
|
|
11964
|
-
const scored = [...rows].sort(
|
|
11965
|
-
(a, b) => b.failed + b.timedOut - (a.failed + a.timedOut) || size(b) - size(a)
|
|
11966
|
-
);
|
|
11967
|
-
const rest = scored.slice(limit);
|
|
11968
|
-
const sums = rest.reduce(
|
|
11969
|
-
(acc, r) => ({
|
|
11970
|
-
passed: acc.passed + r.passed,
|
|
11971
|
-
failed: acc.failed + r.failed,
|
|
11972
|
-
timedOut: acc.timedOut + r.timedOut,
|
|
11973
|
-
skipped: acc.skipped + r.skipped
|
|
11974
|
-
}),
|
|
11975
|
-
{ passed: 0, failed: 0, timedOut: 0, skipped: 0 }
|
|
11976
|
-
);
|
|
11977
|
-
const parts = [
|
|
11978
|
-
sums.failed > 0 ? `${sums.failed} failed` : "",
|
|
11979
|
-
sums.timedOut > 0 ? `${sums.timedOut} timed out` : "",
|
|
11980
|
-
sums.passed > 0 ? `${sums.passed} passed` : "",
|
|
11981
|
-
sums.skipped > 0 ? `${sums.skipped} skipped` : ""
|
|
11982
|
-
].filter(Boolean);
|
|
11983
|
-
const overflowNote = `+ ${rest.length} more suites` + (parts.length > 0 ? `: ${parts.join(" \xB7 ")}` : "");
|
|
11984
|
-
return { rows: scored.slice(0, limit), overflowNote };
|
|
11985
|
-
}
|
|
11986
|
-
function buildSuiteResults(projects, limit) {
|
|
11987
|
-
const top = projects.flatMap((p) => p.suites);
|
|
11988
|
-
if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
|
|
11989
|
-
const file = top[0];
|
|
11990
|
-
const fs16 = statsFromTests(file.tests);
|
|
11991
|
-
const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs16.passed, failed: fs16.failed, timedOut: fs16.timedOut, skipped: fs16.skipped }] : [];
|
|
11992
|
-
return capRows([...rootRow, ...file.suites.map(toRow)], limit);
|
|
11993
|
-
}
|
|
11994
|
-
return capRows(top.map(toRow), limit);
|
|
11995
|
-
}
|
|
11996
|
-
|
|
11997
|
-
// src/collector/SuiteWalker.ts
|
|
11998
|
-
var SuiteWalker = class {
|
|
11999
|
-
build(rootSuite, resultMap) {
|
|
12000
|
-
return rootSuite.suites.map(
|
|
12001
|
-
(projectSuite) => this.buildProject(projectSuite, resultMap)
|
|
12002
|
-
);
|
|
11737
|
+
// src/collector/DataCollector.ts
|
|
11738
|
+
var DataCollector = class {
|
|
11739
|
+
/** @param captureOpts Per-run rich-capture knobs (steps/console/evidence); all off by default. */
|
|
11740
|
+
constructor(captureOpts = {}) {
|
|
11741
|
+
this.captureOpts = captureOpts;
|
|
11742
|
+
this.resultMap = /* @__PURE__ */ new Map();
|
|
11743
|
+
// Parallel to resultMap — needed so computeStats() (which only sees
|
|
11744
|
+
// TestResult, not TestCase) can reconcile against expectedStatus the same
|
|
11745
|
+
// way resolveTestStatus does for the suite tree and the failure list.
|
|
11746
|
+
this.expectedStatusMap = /* @__PURE__ */ new Map();
|
|
11747
|
+
this.failureRecords = [];
|
|
11748
|
+
this.startTime = Date.now();
|
|
12003
11749
|
}
|
|
12004
|
-
|
|
12005
|
-
|
|
12006
|
-
|
|
12007
|
-
);
|
|
12008
|
-
const stats = aggregateStats(suites.map((s) => s.stats));
|
|
12009
|
-
return { name: projectSuite.title || "default", suites, stats };
|
|
11750
|
+
onBegin(config, rootSuite) {
|
|
11751
|
+
this.config = config;
|
|
11752
|
+
this.rootSuite = rootSuite;
|
|
11753
|
+
this.startTime = Date.now();
|
|
12010
11754
|
}
|
|
12011
|
-
|
|
12012
|
-
|
|
12013
|
-
|
|
12014
|
-
const
|
|
12015
|
-
|
|
12016
|
-
|
|
12017
|
-
|
|
12018
|
-
|
|
12019
|
-
|
|
12020
|
-
|
|
12021
|
-
|
|
12022
|
-
|
|
12023
|
-
|
|
12024
|
-
|
|
12025
|
-
|
|
11755
|
+
onTestEnd(test, result) {
|
|
11756
|
+
this.resultMap.set(test.id, result);
|
|
11757
|
+
this.expectedStatusMap.set(test.id, test.expectedStatus);
|
|
11758
|
+
const resolved = resolveTestStatus(test, result);
|
|
11759
|
+
const isRealFailure = resolved === "failed" || resolved === "timedOut";
|
|
11760
|
+
if (isRealFailure) {
|
|
11761
|
+
const existing = this.failureRecords.findIndex((f) => f.testId === test.id);
|
|
11762
|
+
const record = this.buildFailureRecord(test, result);
|
|
11763
|
+
if (existing >= 0) {
|
|
11764
|
+
this.failureRecords[existing] = record;
|
|
11765
|
+
} else {
|
|
11766
|
+
this.failureRecords.push(record);
|
|
11767
|
+
}
|
|
11768
|
+
} else {
|
|
11769
|
+
const existing = this.failureRecords.findIndex((f) => f.testId === test.id);
|
|
11770
|
+
if (existing >= 0) this.failureRecords.splice(existing, 1);
|
|
11771
|
+
}
|
|
12026
11772
|
}
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
11773
|
+
/**
|
|
11774
|
+
* Builds the final `ReportData` payload from all accumulated hook data.
|
|
11775
|
+
*
|
|
11776
|
+
* @param fullResult - Playwright's `FullResult` (contains `duration` + `status`).
|
|
11777
|
+
* @returns Structured data for the PDF generator, including projects tree,
|
|
11778
|
+
* aggregated stats, failures, environment info, and chart inputs.
|
|
11779
|
+
*/
|
|
11780
|
+
finalize(fullResult) {
|
|
11781
|
+
const walker = new SuiteWalker();
|
|
11782
|
+
const projects = walker.build(this.rootSuite, this.resultMap);
|
|
11783
|
+
const stats = this.computeStats(fullResult);
|
|
11784
|
+
const environment = this.buildEnvironment();
|
|
11785
|
+
const charts = this.buildChartData(stats, projects);
|
|
12030
11786
|
return {
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
retryCount: result?.retry ?? 0,
|
|
12037
|
-
tags: test.tags ?? [],
|
|
12038
|
-
annotations: test.annotations ?? [],
|
|
12039
|
-
location: test.location ? `${test.location.file}:${test.location.line}` : void 0
|
|
11787
|
+
projects,
|
|
11788
|
+
stats,
|
|
11789
|
+
failures: sortBySeverity(this.failureRecords),
|
|
11790
|
+
environment,
|
|
11791
|
+
charts
|
|
12040
11792
|
};
|
|
12041
11793
|
}
|
|
12042
|
-
|
|
12043
|
-
|
|
12044
|
-
|
|
12045
|
-
|
|
12046
|
-
|
|
12047
|
-
|
|
12048
|
-
|
|
12049
|
-
medium: 2,
|
|
12050
|
-
low: 3
|
|
12051
|
-
};
|
|
12052
|
-
function sortBySeverity(failures) {
|
|
12053
|
-
return [...failures].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
12054
|
-
}
|
|
12055
|
-
function inferSeverity(annotations, tags) {
|
|
12056
|
-
const allLabels = [
|
|
12057
|
-
...annotations.map((a) => a.type.toLowerCase()),
|
|
12058
|
-
...tags.map((t) => t.toLowerCase())
|
|
12059
|
-
];
|
|
12060
|
-
if (allLabels.some((l) => l.includes("critical") || l.includes("blocker"))) return "critical";
|
|
12061
|
-
if (allLabels.some((l) => l.includes("high") || l.includes("major"))) return "high";
|
|
12062
|
-
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
12063
|
-
return "medium";
|
|
12064
|
-
}
|
|
12065
|
-
|
|
12066
|
-
// src/utils/env.ts
|
|
12067
|
-
init_esm_shims();
|
|
12068
|
-
import { execSync } from "child_process";
|
|
12069
|
-
import { createHmac as createHmac2 } from "crypto";
|
|
12070
|
-
var COMMIT_HASH_LENGTH = 8;
|
|
12071
|
-
function detectCiEnv() {
|
|
12072
|
-
const env = process.env;
|
|
12073
|
-
if (env.GITHUB_ACTIONS === "true") {
|
|
12074
|
-
const ghServer = env.GITHUB_SERVER_URL;
|
|
12075
|
-
const ghRepo = env.GITHUB_REPOSITORY;
|
|
12076
|
-
const ghRunId = env.GITHUB_RUN_ID;
|
|
12077
|
-
const buildUrl = ghServer && ghRepo && ghRunId ? `${ghServer}/${ghRepo}/actions/runs/${ghRunId}` : "";
|
|
11794
|
+
buildFailureRecord(test, result) {
|
|
11795
|
+
const error = result.errors[0] ?? { message: "Unknown error" };
|
|
11796
|
+
const screenshotAttachment = result.attachments.find(
|
|
11797
|
+
(a) => a.name.toLowerCase().includes("screenshot") && a.contentType.startsWith("image/")
|
|
11798
|
+
);
|
|
11799
|
+
const suitePath = test.titlePath().slice(0, -1);
|
|
11800
|
+
const severity = inferSeverity(test.annotations ?? [], test.tags ?? []);
|
|
12078
11801
|
return {
|
|
12079
|
-
|
|
12080
|
-
|
|
12081
|
-
|
|
12082
|
-
|
|
11802
|
+
testId: test.id,
|
|
11803
|
+
testTitle: test.title,
|
|
11804
|
+
suitePath,
|
|
11805
|
+
error: {
|
|
11806
|
+
message: stripAnsi(error.message ?? String(error)),
|
|
11807
|
+
stack: error.stack ? stripAnsi(error.stack) : void 0
|
|
11808
|
+
},
|
|
11809
|
+
screenshotPath: screenshotAttachment?.path,
|
|
11810
|
+
screenshotBuffer: screenshotAttachment?.body,
|
|
11811
|
+
retryHistory: [
|
|
11812
|
+
{
|
|
11813
|
+
attempt: result.retry,
|
|
11814
|
+
status: result.status,
|
|
11815
|
+
duration: result.duration
|
|
11816
|
+
}
|
|
11817
|
+
],
|
|
11818
|
+
durationMs: result.duration,
|
|
11819
|
+
severity,
|
|
11820
|
+
execution: executionFromResult(result, this.captureOpts)
|
|
12083
11821
|
};
|
|
12084
11822
|
}
|
|
12085
|
-
|
|
12086
|
-
|
|
12087
|
-
|
|
12088
|
-
|
|
12089
|
-
|
|
12090
|
-
|
|
12091
|
-
|
|
12092
|
-
|
|
12093
|
-
|
|
12094
|
-
|
|
12095
|
-
|
|
12096
|
-
|
|
12097
|
-
|
|
12098
|
-
|
|
12099
|
-
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
|
|
12103
|
-
|
|
12104
|
-
|
|
12105
|
-
|
|
12106
|
-
|
|
12107
|
-
|
|
12108
|
-
|
|
12109
|
-
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
}
|
|
12113
|
-
if (env.CIRCLECI === "true") {
|
|
12114
|
-
return {
|
|
12115
|
-
branch: env.CIRCLE_BRANCH || "unknown",
|
|
12116
|
-
commit: env.CIRCLE_SHA1?.slice(0, COMMIT_HASH_LENGTH) || "unknown",
|
|
12117
|
-
buildUrl: env.CIRCLE_BUILD_URL || "",
|
|
12118
|
-
ciProvider: "CircleCI"
|
|
12119
|
-
};
|
|
12120
|
-
}
|
|
12121
|
-
if (env.BITBUCKET_BUILD_NUMBER) {
|
|
12122
|
-
const workspace = env.BITBUCKET_WORKSPACE;
|
|
12123
|
-
const repoSlug = env.BITBUCKET_REPO_SLUG;
|
|
12124
|
-
const buildNumber = env.BITBUCKET_BUILD_NUMBER;
|
|
12125
|
-
const buildUrl = workspace && repoSlug && buildNumber ? `https://bitbucket.org/${workspace}/${repoSlug}/pipelines/results/${buildNumber}` : "";
|
|
12126
|
-
return {
|
|
12127
|
-
branch: env.BITBUCKET_BRANCH || "unknown",
|
|
12128
|
-
commit: env.BITBUCKET_COMMIT?.slice(0, COMMIT_HASH_LENGTH) || "unknown",
|
|
12129
|
-
buildUrl,
|
|
12130
|
-
ciProvider: "Bitbucket Pipelines"
|
|
12131
|
-
};
|
|
12132
|
-
}
|
|
12133
|
-
return {
|
|
12134
|
-
branch: gitBranch(),
|
|
12135
|
-
commit: gitCommit(),
|
|
12136
|
-
buildUrl: "",
|
|
12137
|
-
ciProvider: "local"
|
|
12138
|
-
};
|
|
12139
|
-
}
|
|
12140
|
-
var CI_RUN_SOURCES = [
|
|
12141
|
-
{ provider: "github.com", repoKeys: ["GITHUB_REPOSITORY"], runKeys: ["GITHUB_RUN_ID"], attemptKeys: ["GITHUB_RUN_ATTEMPT"] },
|
|
12142
|
-
{ provider: "gitlab.com", repoKeys: ["CI_PROJECT_PATH"], runKeys: ["CI_PIPELINE_ID"] },
|
|
12143
|
-
{ provider: "circleci.com", repoKeys: ["CIRCLE_PROJECT_REPONAME"], runKeys: ["CIRCLE_WORKFLOW_ID"] },
|
|
12144
|
-
{ provider: "buildkite.com", repoKeys: ["BUILDKITE_REPO"], runKeys: ["BUILDKITE_BUILD_ID"] },
|
|
12145
|
-
{ provider: "dev.azure.com", repoKeys: ["BUILD_REPOSITORY_NAME"], runKeys: ["BUILD_BUILDID"] },
|
|
12146
|
-
{ provider: "bitbucket.org", repoKeys: ["BITBUCKET_REPO_FULL_NAME"], runKeys: ["BITBUCKET_PIPELINE_UUID"] },
|
|
12147
|
-
{ provider: "jenkins", repoKeys: ["JOB_NAME"], runKeys: ["BUILD_NUMBER"] }
|
|
12148
|
-
];
|
|
12149
|
-
var RUN_ID_FALLBACK_SECRET = "rf-live-runid";
|
|
12150
|
-
function detectCiRunId() {
|
|
12151
|
-
for (const src of CI_RUN_SOURCES) {
|
|
12152
|
-
const runId = firstEnv(src.runKeys);
|
|
12153
|
-
if (!runId) continue;
|
|
12154
|
-
const repo = firstEnv(src.repoKeys) ?? "unknown";
|
|
12155
|
-
const attempt = src.attemptKeys ? firstEnv(src.attemptKeys) ?? "1" : "1";
|
|
12156
|
-
const secret = process.env.RF_LICENSE_KEY?.trim() || RUN_ID_FALLBACK_SECRET;
|
|
12157
|
-
return createHmac2("sha256", secret).update(`${src.provider}:${repo}:${runId}:${attempt}`).digest("hex").slice(0, 32);
|
|
12158
|
-
}
|
|
12159
|
-
return null;
|
|
12160
|
-
}
|
|
12161
|
-
function firstEnv(keys) {
|
|
12162
|
-
for (const k of keys) {
|
|
12163
|
-
const v = process.env[k]?.trim();
|
|
12164
|
-
if (v) return v;
|
|
12165
|
-
}
|
|
12166
|
-
return void 0;
|
|
12167
|
-
}
|
|
12168
|
-
function gitBranch() {
|
|
12169
|
-
try {
|
|
12170
|
-
return execSync("git rev-parse --abbrev-ref HEAD", { stdio: "pipe" }).toString().trim();
|
|
12171
|
-
} catch {
|
|
12172
|
-
return "unknown";
|
|
12173
|
-
}
|
|
12174
|
-
}
|
|
12175
|
-
function gitCommit() {
|
|
12176
|
-
try {
|
|
12177
|
-
return execSync("git rev-parse --short HEAD", { stdio: "pipe" }).toString().trim();
|
|
12178
|
-
} catch {
|
|
12179
|
-
return "unknown";
|
|
12180
|
-
}
|
|
12181
|
-
}
|
|
12182
|
-
|
|
12183
|
-
// src/collector/DataCollector.ts
|
|
12184
|
-
var DataCollector = class {
|
|
12185
|
-
/** @param captureOpts Per-run rich-capture knobs (steps/console/evidence); all off by default. */
|
|
12186
|
-
constructor(captureOpts = {}) {
|
|
12187
|
-
this.captureOpts = captureOpts;
|
|
12188
|
-
this.resultMap = /* @__PURE__ */ new Map();
|
|
12189
|
-
// Parallel to resultMap — needed so computeStats() (which only sees
|
|
12190
|
-
// TestResult, not TestCase) can reconcile against expectedStatus the same
|
|
12191
|
-
// way resolveTestStatus does for the suite tree and the failure list.
|
|
12192
|
-
this.expectedStatusMap = /* @__PURE__ */ new Map();
|
|
12193
|
-
this.failureRecords = [];
|
|
12194
|
-
this.startTime = Date.now();
|
|
12195
|
-
}
|
|
12196
|
-
onBegin(config, rootSuite) {
|
|
12197
|
-
this.config = config;
|
|
12198
|
-
this.rootSuite = rootSuite;
|
|
12199
|
-
this.startTime = Date.now();
|
|
12200
|
-
}
|
|
12201
|
-
onTestEnd(test, result) {
|
|
12202
|
-
this.resultMap.set(test.id, result);
|
|
12203
|
-
this.expectedStatusMap.set(test.id, test.expectedStatus);
|
|
12204
|
-
const resolved = resolveTestStatus(test, result);
|
|
12205
|
-
const isRealFailure = resolved === "failed" || resolved === "timedOut";
|
|
12206
|
-
if (isRealFailure) {
|
|
12207
|
-
const existing = this.failureRecords.findIndex((f) => f.testId === test.id);
|
|
12208
|
-
const record = this.buildFailureRecord(test, result);
|
|
12209
|
-
if (existing >= 0) {
|
|
12210
|
-
this.failureRecords[existing] = record;
|
|
12211
|
-
} else {
|
|
12212
|
-
this.failureRecords.push(record);
|
|
12213
|
-
}
|
|
12214
|
-
} else {
|
|
12215
|
-
const existing = this.failureRecords.findIndex((f) => f.testId === test.id);
|
|
12216
|
-
if (existing >= 0) this.failureRecords.splice(existing, 1);
|
|
12217
|
-
}
|
|
12218
|
-
}
|
|
12219
|
-
/**
|
|
12220
|
-
* Builds the final `ReportData` payload from all accumulated hook data.
|
|
12221
|
-
*
|
|
12222
|
-
* @param fullResult - Playwright's `FullResult` (contains `duration` + `status`).
|
|
12223
|
-
* @returns Structured data for the PDF generator, including projects tree,
|
|
12224
|
-
* aggregated stats, failures, environment info, and chart inputs.
|
|
12225
|
-
*/
|
|
12226
|
-
finalize(fullResult) {
|
|
12227
|
-
const walker = new SuiteWalker();
|
|
12228
|
-
const projects = walker.build(this.rootSuite, this.resultMap);
|
|
12229
|
-
const stats = this.computeStats(fullResult);
|
|
12230
|
-
const environment = this.buildEnvironment();
|
|
12231
|
-
const charts = this.buildChartData(stats, projects);
|
|
12232
|
-
return {
|
|
12233
|
-
projects,
|
|
12234
|
-
stats,
|
|
12235
|
-
failures: sortBySeverity(this.failureRecords),
|
|
12236
|
-
environment,
|
|
12237
|
-
charts
|
|
12238
|
-
};
|
|
12239
|
-
}
|
|
12240
|
-
buildFailureRecord(test, result) {
|
|
12241
|
-
const error = result.errors[0] ?? { message: "Unknown error" };
|
|
12242
|
-
const screenshotAttachment = result.attachments.find(
|
|
12243
|
-
(a) => a.name.toLowerCase().includes("screenshot") && a.contentType.startsWith("image/")
|
|
12244
|
-
);
|
|
12245
|
-
const suitePath = test.titlePath().slice(0, -1);
|
|
12246
|
-
const severity = inferSeverity(test.annotations ?? [], test.tags ?? []);
|
|
12247
|
-
return {
|
|
12248
|
-
testId: test.id,
|
|
12249
|
-
testTitle: test.title,
|
|
12250
|
-
suitePath,
|
|
12251
|
-
error: {
|
|
12252
|
-
message: stripAnsi(error.message ?? String(error)),
|
|
12253
|
-
stack: error.stack ? stripAnsi(error.stack) : void 0
|
|
12254
|
-
},
|
|
12255
|
-
screenshotPath: screenshotAttachment?.path,
|
|
12256
|
-
screenshotBuffer: screenshotAttachment?.body,
|
|
12257
|
-
retryHistory: [
|
|
12258
|
-
{
|
|
12259
|
-
attempt: result.retry,
|
|
12260
|
-
status: result.status,
|
|
12261
|
-
duration: result.duration
|
|
12262
|
-
}
|
|
12263
|
-
],
|
|
12264
|
-
durationMs: result.duration,
|
|
12265
|
-
severity,
|
|
12266
|
-
execution: executionFromResult(result, this.captureOpts)
|
|
12267
|
-
};
|
|
12268
|
-
}
|
|
12269
|
-
computeStats(fullResult) {
|
|
12270
|
-
let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
|
|
12271
|
-
for (const [testId, result] of this.resultMap) {
|
|
12272
|
-
total++;
|
|
12273
|
-
const expectedStatus = this.expectedStatusMap.get(testId);
|
|
12274
|
-
const resolved = resolveTestStatus({ expectedStatus }, result);
|
|
12275
|
-
switch (resolved) {
|
|
12276
|
-
case "passed":
|
|
12277
|
-
passed++;
|
|
12278
|
-
break;
|
|
12279
|
-
case "flaky":
|
|
12280
|
-
flaky++;
|
|
12281
|
-
break;
|
|
12282
|
-
case "failed":
|
|
12283
|
-
failed++;
|
|
12284
|
-
break;
|
|
12285
|
-
case "timedOut":
|
|
12286
|
-
timedOut++;
|
|
12287
|
-
break;
|
|
12288
|
-
// counted separately, not in failed
|
|
12289
|
-
case "skipped":
|
|
12290
|
-
skipped++;
|
|
12291
|
-
break;
|
|
12292
|
-
}
|
|
12293
|
-
}
|
|
12294
|
-
const duration = fullResult.duration ?? Date.now() - this.startTime;
|
|
12295
|
-
const passRate = total > 0 ? Math.round((passed + flaky) / total * 100) : 0;
|
|
11823
|
+
computeStats(fullResult) {
|
|
11824
|
+
let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
|
|
11825
|
+
for (const [testId, result] of this.resultMap) {
|
|
11826
|
+
total++;
|
|
11827
|
+
const expectedStatus = this.expectedStatusMap.get(testId);
|
|
11828
|
+
const resolved = resolveTestStatus({ expectedStatus }, result);
|
|
11829
|
+
switch (resolved) {
|
|
11830
|
+
case "passed":
|
|
11831
|
+
passed++;
|
|
11832
|
+
break;
|
|
11833
|
+
case "flaky":
|
|
11834
|
+
flaky++;
|
|
11835
|
+
break;
|
|
11836
|
+
case "failed":
|
|
11837
|
+
failed++;
|
|
11838
|
+
break;
|
|
11839
|
+
case "timedOut":
|
|
11840
|
+
timedOut++;
|
|
11841
|
+
break;
|
|
11842
|
+
// counted separately, not in failed
|
|
11843
|
+
case "skipped":
|
|
11844
|
+
skipped++;
|
|
11845
|
+
break;
|
|
11846
|
+
}
|
|
11847
|
+
}
|
|
11848
|
+
const duration = fullResult.duration ?? Date.now() - this.startTime;
|
|
11849
|
+
const passRate = total > 0 ? Math.round((passed + flaky) / total * 100) : 0;
|
|
12296
11850
|
return {
|
|
12297
11851
|
total,
|
|
12298
11852
|
passed,
|
|
@@ -12579,7 +12133,9 @@ var ShardMerger = class {
|
|
|
12579
12133
|
{
|
|
12580
12134
|
// Guard each field — a partial / non-conformant / older shard JSON can
|
|
12581
12135
|
// omit steps or the stdout/stderr arrays; an unguarded .map/iterate would
|
|
12582
|
-
// throw and
|
|
12136
|
+
// throw, and merge errors follow the caller's policy (the reporter
|
|
12137
|
+
// catches and silently skips the PDF; the standalone engine treats
|
|
12138
|
+
// them as a hard error).
|
|
12583
12139
|
steps: lastResult.steps ?? [],
|
|
12584
12140
|
stdout: (lastResult.stdout ?? []).map(decodeShardChunk),
|
|
12585
12141
|
stderr: (lastResult.stderr ?? []).map(decodeShardChunk),
|
|
@@ -12595,13 +12151,16 @@ var ShardMerger = class {
|
|
|
12595
12151
|
const browsers = [
|
|
12596
12152
|
...new Set(report.config.projects.map((p) => p.use?.browserName ?? p.name).filter(Boolean))
|
|
12597
12153
|
];
|
|
12598
|
-
let playwrightVersion =
|
|
12599
|
-
|
|
12600
|
-
|
|
12601
|
-
|
|
12602
|
-
|
|
12603
|
-
|
|
12154
|
+
let playwrightVersion = report.config.metadata?.playwrightVersion;
|
|
12155
|
+
if (!playwrightVersion) {
|
|
12156
|
+
try {
|
|
12157
|
+
const req = createRequire(import.meta.url);
|
|
12158
|
+
const pwPkg = req("@playwright/test/package.json");
|
|
12159
|
+
playwrightVersion = pwPkg.version;
|
|
12160
|
+
} catch {
|
|
12161
|
+
}
|
|
12604
12162
|
}
|
|
12163
|
+
if (!playwrightVersion) playwrightVersion = "unknown";
|
|
12605
12164
|
return {
|
|
12606
12165
|
branch: ci.branch,
|
|
12607
12166
|
commit: ci.commit,
|
|
@@ -12612,7 +12171,7 @@ var ShardMerger = class {
|
|
|
12612
12171
|
nodeVersion: process.version,
|
|
12613
12172
|
playwrightVersion,
|
|
12614
12173
|
projectCount: report.config.projects.length,
|
|
12615
|
-
workerCount: 1
|
|
12174
|
+
workerCount: report.config.workers ?? 1
|
|
12616
12175
|
};
|
|
12617
12176
|
}
|
|
12618
12177
|
buildCharts(stats, projects) {
|
|
@@ -12621,8 +12180,9 @@ var ShardMerger = class {
|
|
|
12621
12180
|
passRate: {
|
|
12622
12181
|
passed: stats.passed,
|
|
12623
12182
|
failed: stats.failed,
|
|
12183
|
+
// Recovered from per-result status by mergeStats (aggregate shard
|
|
12184
|
+
// stats fold timeouts into `unexpected`; the tree pulls them back out).
|
|
12624
12185
|
timedOut: stats.timedOut,
|
|
12625
|
-
// always 0 in shard JSON (no separate timedOut count)
|
|
12626
12186
|
skipped: stats.skipped,
|
|
12627
12187
|
flaky: stats.flaky
|
|
12628
12188
|
},
|
|
@@ -12634,14 +12194,14 @@ var ShardMerger = class {
|
|
|
12634
12194
|
|
|
12635
12195
|
// src/pdf/PdfGenerator.ts
|
|
12636
12196
|
init_esm_shims();
|
|
12637
|
-
import
|
|
12638
|
-
import
|
|
12197
|
+
import fs11 from "fs";
|
|
12198
|
+
import path9 from "path";
|
|
12639
12199
|
|
|
12640
12200
|
// src/templates/engine.ts
|
|
12641
12201
|
init_esm_shims();
|
|
12642
12202
|
var import_handlebars = __toESM(require_lib());
|
|
12643
|
-
import
|
|
12644
|
-
import
|
|
12203
|
+
import fs4 from "fs";
|
|
12204
|
+
import path5 from "path";
|
|
12645
12205
|
|
|
12646
12206
|
// src/utils/duration.ts
|
|
12647
12207
|
init_esm_shims();
|
|
@@ -12655,11 +12215,74 @@ function formatDuration(ms) {
|
|
|
12655
12215
|
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
12656
12216
|
}
|
|
12657
12217
|
|
|
12658
|
-
// src/
|
|
12659
|
-
|
|
12660
|
-
|
|
12218
|
+
// src/analysis/SummaryWriter.ts
|
|
12219
|
+
init_esm_shims();
|
|
12220
|
+
var CATEGORY_LABEL = {
|
|
12221
|
+
assertion: "Assertion failure",
|
|
12222
|
+
timeout: "Timeout",
|
|
12223
|
+
"locator-not-found": "Element not found",
|
|
12224
|
+
network: "Network error",
|
|
12225
|
+
navigation: "Navigation error",
|
|
12226
|
+
"env-config": "Environment / config",
|
|
12227
|
+
"flaky-by-retry": "Flaky (passed on retry)",
|
|
12228
|
+
unknown: "Unclassified"
|
|
12229
|
+
};
|
|
12230
|
+
var SHORT_LABEL = {
|
|
12231
|
+
assertion: "assertion",
|
|
12232
|
+
timeout: "timeout",
|
|
12233
|
+
"locator-not-found": "element not found",
|
|
12234
|
+
network: "network",
|
|
12235
|
+
navigation: "navigation",
|
|
12236
|
+
"env-config": "env/config",
|
|
12237
|
+
"flaky-by-retry": "flaky",
|
|
12238
|
+
unknown: "unclassified"
|
|
12239
|
+
};
|
|
12240
|
+
function tallyEntries(clusters) {
|
|
12241
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
12242
|
+
for (const c of clusters) {
|
|
12243
|
+
byCategory.set(c.category, (byCategory.get(c.category) ?? 0) + c.count);
|
|
12244
|
+
}
|
|
12245
|
+
return [...byCategory.entries()].sort((a, b) => b[1] - a[1]);
|
|
12661
12246
|
}
|
|
12662
|
-
|
|
12247
|
+
function buildOneLiner(clusters) {
|
|
12248
|
+
if (clusters.length === 0) return "";
|
|
12249
|
+
const head = `${clusters.length} root ${clusters.length === 1 ? "cause" : "causes"}`;
|
|
12250
|
+
const tally = tallyEntries(clusters).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`);
|
|
12251
|
+
return [head, ...tally].join(" \xB7 ");
|
|
12252
|
+
}
|
|
12253
|
+
function buildFailureTally(analysis) {
|
|
12254
|
+
if (!analysis) return "";
|
|
12255
|
+
const failureClusters = analysis.clusters.filter((c) => c.category !== "flaky-by-retry");
|
|
12256
|
+
return tallyEntries(failureClusters).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`).join(", ");
|
|
12257
|
+
}
|
|
12258
|
+
function dominantCategory(clusters) {
|
|
12259
|
+
return clusters[0]?.category ?? "unknown";
|
|
12260
|
+
}
|
|
12261
|
+
function trendClause(passRate, delta) {
|
|
12262
|
+
if (delta === 0) return `Pass rate held steady at ${passRate}%.`;
|
|
12263
|
+
const verb = delta > 0 ? "climbed" : "dropped";
|
|
12264
|
+
const abs = Math.abs(delta);
|
|
12265
|
+
const points = abs === 1 ? "point" : "points";
|
|
12266
|
+
return `Pass rate ${verb} ${abs} ${points} to ${passRate}% since the last run.`;
|
|
12267
|
+
}
|
|
12268
|
+
function causesClause(analysis) {
|
|
12269
|
+
const [head, ...tallyParts] = buildOneLiner(analysis.clusters).split(" \xB7 ");
|
|
12270
|
+
const verb = analysis.clusters.length === 1 ? "remains" : "remain";
|
|
12271
|
+
return `${head} ${verb}: ${tallyParts.join(", ")}.`;
|
|
12272
|
+
}
|
|
12273
|
+
function buildBriefSentence(stats, analysis, trend) {
|
|
12274
|
+
if (trend && trend.delta !== null) {
|
|
12275
|
+
const trendSentence = trendClause(stats.passRate, trend.delta);
|
|
12276
|
+
return analysis && analysis.clusters.length > 0 ? `${trendSentence} ${causesClause(analysis)}` : trendSentence;
|
|
12277
|
+
}
|
|
12278
|
+
return analysis && analysis.clusters.length > 0 ? causesClause(analysis) : void 0;
|
|
12279
|
+
}
|
|
12280
|
+
|
|
12281
|
+
// src/templates/engine.ts
|
|
12282
|
+
function jsonForInlineScript(value) {
|
|
12283
|
+
return JSON.stringify(value).replace(/<\//g, "<\\/");
|
|
12284
|
+
}
|
|
12285
|
+
var A4_HEIGHT_MM = 297;
|
|
12663
12286
|
var PAGE_MARGIN_Y_MM = 16;
|
|
12664
12287
|
var PX_PER_MM = 96 / 25.4;
|
|
12665
12288
|
var SUITE_CHART_SMALL_MAX = 10;
|
|
@@ -12702,9 +12325,9 @@ var TemplateEngine = class {
|
|
|
12702
12325
|
this.customTemplateCache = /* @__PURE__ */ new Map();
|
|
12703
12326
|
this.styleCache = /* @__PURE__ */ new Map();
|
|
12704
12327
|
this.handlebars = import_handlebars.default.create();
|
|
12705
|
-
this.templatesDir =
|
|
12706
|
-
if (!
|
|
12707
|
-
this.templatesDir =
|
|
12328
|
+
this.templatesDir = path5.resolve(__dirname, "templates");
|
|
12329
|
+
if (!fs4.existsSync(this.templatesDir)) {
|
|
12330
|
+
this.templatesDir = path5.resolve(__dirname, "../templates");
|
|
12708
12331
|
}
|
|
12709
12332
|
this.registerHelpers();
|
|
12710
12333
|
this.registerPartials();
|
|
@@ -12721,11 +12344,11 @@ var TemplateEngine = class {
|
|
|
12721
12344
|
this.chartjsInline = inline;
|
|
12722
12345
|
}
|
|
12723
12346
|
render(data, template, opts = {}) {
|
|
12724
|
-
const layoutPath =
|
|
12725
|
-
if (!
|
|
12347
|
+
const layoutPath = path5.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
12348
|
+
if (!fs4.existsSync(layoutPath)) {
|
|
12726
12349
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
12727
12350
|
}
|
|
12728
|
-
const layoutSource =
|
|
12351
|
+
const layoutSource = fs4.readFileSync(layoutPath, "utf8");
|
|
12729
12352
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
12730
12353
|
const ctx = this.assembleContext(
|
|
12731
12354
|
data,
|
|
@@ -12737,10 +12360,10 @@ var TemplateEngine = class {
|
|
|
12737
12360
|
}
|
|
12738
12361
|
renderFromPath(data, filePath, opts = {}) {
|
|
12739
12362
|
if (!this.customTemplateCache.has(filePath)) {
|
|
12740
|
-
if (!
|
|
12363
|
+
if (!fs4.existsSync(filePath)) {
|
|
12741
12364
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
12742
12365
|
}
|
|
12743
|
-
const source =
|
|
12366
|
+
const source = fs4.readFileSync(filePath, "utf8");
|
|
12744
12367
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
12745
12368
|
}
|
|
12746
12369
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
@@ -12862,18 +12485,18 @@ var TemplateEngine = class {
|
|
|
12862
12485
|
readStyle(filename) {
|
|
12863
12486
|
const cached = this.styleCache.get(filename);
|
|
12864
12487
|
if (cached !== void 0) return cached;
|
|
12865
|
-
const stylePath =
|
|
12866
|
-
const css =
|
|
12488
|
+
const stylePath = path5.join(this.templatesDir, "styles", filename);
|
|
12489
|
+
const css = fs4.existsSync(stylePath) ? fs4.readFileSync(stylePath, "utf8") : "";
|
|
12867
12490
|
this.styleCache.set(filename, css);
|
|
12868
12491
|
return css;
|
|
12869
12492
|
}
|
|
12870
12493
|
registerPartials() {
|
|
12871
|
-
const partialsDir =
|
|
12872
|
-
if (!
|
|
12873
|
-
for (const file of
|
|
12494
|
+
const partialsDir = path5.join(this.templatesDir, "partials");
|
|
12495
|
+
if (!fs4.existsSync(partialsDir)) return;
|
|
12496
|
+
for (const file of fs4.readdirSync(partialsDir)) {
|
|
12874
12497
|
if (!file.endsWith(".hbs")) continue;
|
|
12875
|
-
const name =
|
|
12876
|
-
const src =
|
|
12498
|
+
const name = path5.basename(file, ".hbs");
|
|
12499
|
+
const src = fs4.readFileSync(path5.join(partialsDir, file), "utf8");
|
|
12877
12500
|
this.handlebars.registerPartial(name, src);
|
|
12878
12501
|
}
|
|
12879
12502
|
}
|
|
@@ -12978,8 +12601,21 @@ var TemplateEngine = class {
|
|
|
12978
12601
|
|
|
12979
12602
|
// src/screenshots/ScreenshotEmbedder.ts
|
|
12980
12603
|
init_esm_shims();
|
|
12981
|
-
import
|
|
12982
|
-
|
|
12604
|
+
import fs5 from "fs";
|
|
12605
|
+
var sharpModule;
|
|
12606
|
+
function loadSharp() {
|
|
12607
|
+
if (sharpModule === void 0) {
|
|
12608
|
+
try {
|
|
12609
|
+
sharpModule = __require("sharp");
|
|
12610
|
+
} catch {
|
|
12611
|
+
sharpModule = null;
|
|
12612
|
+
logger.warn(
|
|
12613
|
+
"sharp is not available \u2014 screenshots are embedded at original size. Reports with many screenshots may exceed maxFileSizeMb more often."
|
|
12614
|
+
);
|
|
12615
|
+
}
|
|
12616
|
+
}
|
|
12617
|
+
return sharpModule;
|
|
12618
|
+
}
|
|
12983
12619
|
var ScreenshotEmbedder = class {
|
|
12984
12620
|
async embedAll(failures, opts) {
|
|
12985
12621
|
await Promise.all(failures.map((f) => this.embed(f, opts)));
|
|
@@ -12987,7 +12623,7 @@ var ScreenshotEmbedder = class {
|
|
|
12987
12623
|
async embed(failure, opts) {
|
|
12988
12624
|
if (!failure.screenshotPath && !failure.screenshotBuffer) return;
|
|
12989
12625
|
try {
|
|
12990
|
-
const raw = failure.screenshotBuffer ?? await
|
|
12626
|
+
const raw = failure.screenshotBuffer ?? await fs5.promises.readFile(failure.screenshotPath);
|
|
12991
12627
|
const { buffer, mime } = opts.reencode ? await this.recompress(raw, opts) : { buffer: raw, mime: this.detectMimeType(raw) };
|
|
12992
12628
|
failure.screenshotBase64 = `data:${mime};base64,${buffer.toString("base64")}`;
|
|
12993
12629
|
} catch (e) {
|
|
@@ -12999,7 +12635,7 @@ var ScreenshotEmbedder = class {
|
|
|
12999
12635
|
}
|
|
13000
12636
|
async embedLogoFile(logoPath) {
|
|
13001
12637
|
try {
|
|
13002
|
-
const buffer = await
|
|
12638
|
+
const buffer = await fs5.promises.readFile(logoPath);
|
|
13003
12639
|
const ext = logoPath.split(".").pop()?.toLowerCase();
|
|
13004
12640
|
let mime = "image/png";
|
|
13005
12641
|
if (ext === "jpg" || ext === "jpeg") mime = "image/jpeg";
|
|
@@ -13015,6 +12651,10 @@ var ScreenshotEmbedder = class {
|
|
|
13015
12651
|
if (this.looksLikeSvg(buf)) {
|
|
13016
12652
|
return { buffer: buf, mime: "image/svg+xml" };
|
|
13017
12653
|
}
|
|
12654
|
+
const sharp = loadSharp();
|
|
12655
|
+
if (!sharp) {
|
|
12656
|
+
return { buffer: buf, mime: this.detectMimeType(buf) };
|
|
12657
|
+
}
|
|
13018
12658
|
const img = sharp(buf, { failOn: "none" });
|
|
13019
12659
|
const meta = await img.metadata();
|
|
13020
12660
|
const needsResize = (meta.width ?? 0) > opts.maxWidth;
|
|
@@ -13044,7 +12684,7 @@ var ScreenshotEmbedder = class {
|
|
|
13044
12684
|
// src/pdf/BrowserManager.ts
|
|
13045
12685
|
init_esm_shims();
|
|
13046
12686
|
import { execSync as execSync2 } from "child_process";
|
|
13047
|
-
import
|
|
12687
|
+
import fs6 from "fs";
|
|
13048
12688
|
var LAUNCH_ARGS = [
|
|
13049
12689
|
"--no-sandbox",
|
|
13050
12690
|
"--disable-setuid-sandbox",
|
|
@@ -13076,13 +12716,13 @@ var BrowserManager = class {
|
|
|
13076
12716
|
}
|
|
13077
12717
|
}
|
|
13078
12718
|
resolveChromePath(explicit, puppeteer) {
|
|
13079
|
-
if (explicit &&
|
|
12719
|
+
if (explicit && fs6.existsSync(explicit)) return explicit;
|
|
13080
12720
|
const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
13081
|
-
if (envPath &&
|
|
12721
|
+
if (envPath && fs6.existsSync(envPath)) return envPath;
|
|
13082
12722
|
try {
|
|
13083
12723
|
const chromeFinder = require_lib2();
|
|
13084
12724
|
const found = chromeFinder();
|
|
13085
|
-
if (found &&
|
|
12725
|
+
if (found && fs6.existsSync(found)) return found;
|
|
13086
12726
|
} catch (err) {
|
|
13087
12727
|
logger.debug("chrome discovery: chrome-finder failed", { error: err.message });
|
|
13088
12728
|
}
|
|
@@ -13094,20 +12734,20 @@ var BrowserManager = class {
|
|
|
13094
12734
|
"/snap/bin/chromium"
|
|
13095
12735
|
];
|
|
13096
12736
|
for (const p of linuxPaths) {
|
|
13097
|
-
if (
|
|
12737
|
+
if (fs6.existsSync(p)) return p;
|
|
13098
12738
|
}
|
|
13099
12739
|
try {
|
|
13100
12740
|
const found = execSync2("which google-chrome chromium chromium-browser 2>/dev/null", {
|
|
13101
12741
|
stdio: "pipe"
|
|
13102
12742
|
}).toString().trim().split("\n")[0];
|
|
13103
|
-
if (found &&
|
|
12743
|
+
if (found && fs6.existsSync(found)) return found;
|
|
13104
12744
|
} catch {
|
|
13105
12745
|
logger.debug("chrome discovery: which lookup found nothing");
|
|
13106
12746
|
}
|
|
13107
12747
|
if (puppeteer.executablePath) {
|
|
13108
12748
|
try {
|
|
13109
12749
|
const bundled = puppeteer.executablePath();
|
|
13110
|
-
if (bundled &&
|
|
12750
|
+
if (bundled && fs6.existsSync(bundled)) return bundled;
|
|
13111
12751
|
} catch (err) {
|
|
13112
12752
|
logger.debug("chrome discovery: no puppeteer bundled executable", { error: err.message });
|
|
13113
12753
|
}
|
|
@@ -13121,22 +12761,22 @@ var BrowserManager = class {
|
|
|
13121
12761
|
|
|
13122
12762
|
// src/pdf/HtmlWriter.ts
|
|
13123
12763
|
init_esm_shims();
|
|
13124
|
-
import
|
|
13125
|
-
import
|
|
13126
|
-
import
|
|
13127
|
-
import
|
|
12764
|
+
import fs7 from "fs";
|
|
12765
|
+
import os5 from "os";
|
|
12766
|
+
import path6 from "path";
|
|
12767
|
+
import crypto from "crypto";
|
|
13128
12768
|
var HtmlWriter = class {
|
|
13129
12769
|
async write(html) {
|
|
13130
|
-
const tmpDir =
|
|
13131
|
-
const filename = `rf-report-${
|
|
13132
|
-
this.tempPath =
|
|
13133
|
-
await
|
|
12770
|
+
const tmpDir = os5.tmpdir();
|
|
12771
|
+
const filename = `rf-report-${crypto.randomBytes(8).toString("hex")}.html`;
|
|
12772
|
+
this.tempPath = path6.join(tmpDir, filename);
|
|
12773
|
+
await fs7.promises.writeFile(this.tempPath, html, "utf8");
|
|
13134
12774
|
return this.tempPath;
|
|
13135
12775
|
}
|
|
13136
12776
|
async cleanup() {
|
|
13137
12777
|
if (this.tempPath) {
|
|
13138
12778
|
const tempPath = this.tempPath;
|
|
13139
|
-
await
|
|
12779
|
+
await fs7.promises.unlink(tempPath).catch(
|
|
13140
12780
|
(err) => logger.debug("temp HTML cleanup failed (ignored)", { path: tempPath, error: err.message })
|
|
13141
12781
|
);
|
|
13142
12782
|
this.tempPath = void 0;
|
|
@@ -13170,14 +12810,14 @@ var ChartRenderer = class {
|
|
|
13170
12810
|
|
|
13171
12811
|
// src/pdf/PdfEncryptor.ts
|
|
13172
12812
|
init_esm_shims();
|
|
13173
|
-
import
|
|
12813
|
+
import fs8 from "fs";
|
|
13174
12814
|
import { PDFDocument, PDFHeader } from "@cantoo/pdf-lib";
|
|
13175
12815
|
var AES256_HEADER_MINOR = "7ext3";
|
|
13176
12816
|
var PdfEncryptor = class {
|
|
13177
12817
|
async encrypt(pdfPath, password) {
|
|
13178
12818
|
if (!password) return;
|
|
13179
12819
|
try {
|
|
13180
|
-
const doc = await PDFDocument.load(await
|
|
12820
|
+
const doc = await PDFDocument.load(await fs8.promises.readFile(pdfPath));
|
|
13181
12821
|
doc.context.header = PDFHeader.forVersion(1, AES256_HEADER_MINOR);
|
|
13182
12822
|
doc.encrypt({
|
|
13183
12823
|
userPassword: password,
|
|
@@ -13187,10 +12827,10 @@ var PdfEncryptor = class {
|
|
|
13187
12827
|
const encryptedBytes = await doc.save();
|
|
13188
12828
|
const tmpPath = `${pdfPath}.rf-enc-tmp`;
|
|
13189
12829
|
try {
|
|
13190
|
-
await
|
|
13191
|
-
await
|
|
12830
|
+
await fs8.promises.writeFile(tmpPath, encryptedBytes);
|
|
12831
|
+
await fs8.promises.rename(tmpPath, pdfPath);
|
|
13192
12832
|
} finally {
|
|
13193
|
-
|
|
12833
|
+
fs8.rmSync(tmpPath, { force: true });
|
|
13194
12834
|
}
|
|
13195
12835
|
logger.info("PDF encrypted with password (AES-256).");
|
|
13196
12836
|
} catch (e) {
|
|
@@ -13201,8 +12841,8 @@ var PdfEncryptor = class {
|
|
|
13201
12841
|
|
|
13202
12842
|
// src/filename/FilenameResolver.ts
|
|
13203
12843
|
init_esm_shims();
|
|
13204
|
-
import
|
|
13205
|
-
import
|
|
12844
|
+
import path7 from "path";
|
|
12845
|
+
import fs9 from "fs";
|
|
13206
12846
|
var MAX_BRANCH_LENGTH = 50;
|
|
13207
12847
|
var FilenameResolver = class {
|
|
13208
12848
|
/**
|
|
@@ -13219,8 +12859,8 @@ var FilenameResolver = class {
|
|
|
13219
12859
|
const branch = this.sanitiseBranch(ctx.branch);
|
|
13220
12860
|
const status = ctx.status;
|
|
13221
12861
|
const resolved = template.replace(/\{date\}/g, date).replace(/\{branch\}/g, branch).replace(/\{status\}/g, status);
|
|
13222
|
-
const absolute =
|
|
13223
|
-
|
|
12862
|
+
const absolute = path7.isAbsolute(resolved) ? resolved : path7.resolve(process.cwd(), resolved);
|
|
12863
|
+
fs9.mkdirSync(path7.dirname(absolute), { recursive: true });
|
|
13224
12864
|
return absolute;
|
|
13225
12865
|
}
|
|
13226
12866
|
/**
|
|
@@ -13232,7 +12872,7 @@ var FilenameResolver = class {
|
|
|
13232
12872
|
* // → '/out/2026-report-executive.pdf'
|
|
13233
12873
|
*/
|
|
13234
12874
|
injectTemplateSuffix(outputPath, suffix) {
|
|
13235
|
-
const ext =
|
|
12875
|
+
const ext = path7.extname(outputPath);
|
|
13236
12876
|
const base = outputPath.slice(0, outputPath.length - ext.length);
|
|
13237
12877
|
return `${base}-${suffix}${ext}`;
|
|
13238
12878
|
}
|
|
@@ -13290,12 +12930,12 @@ var PRESETS = {
|
|
|
13290
12930
|
|
|
13291
12931
|
// src/pdf/FailurePaginator.ts
|
|
13292
12932
|
init_esm_shims();
|
|
13293
|
-
import
|
|
13294
|
-
import
|
|
12933
|
+
import fs10 from "fs";
|
|
12934
|
+
import path8 from "path";
|
|
13295
12935
|
function sidecarPathFor(pdfPath) {
|
|
13296
|
-
const dir =
|
|
13297
|
-
const base =
|
|
13298
|
-
return
|
|
12936
|
+
const dir = path8.dirname(pdfPath);
|
|
12937
|
+
const base = path8.basename(pdfPath, path8.extname(pdfPath));
|
|
12938
|
+
return path8.join(dir, `${base}-failures.json`);
|
|
13299
12939
|
}
|
|
13300
12940
|
var FailurePaginator = class {
|
|
13301
12941
|
async paginate(failures, maxInline, pdfOutputPath) {
|
|
@@ -13306,13 +12946,13 @@ var FailurePaginator = class {
|
|
|
13306
12946
|
const overflow = failures.slice(maxInline);
|
|
13307
12947
|
const sidecarPath = sidecarPathFor(pdfOutputPath);
|
|
13308
12948
|
try {
|
|
13309
|
-
await
|
|
12949
|
+
await fs10.promises.writeFile(
|
|
13310
12950
|
sidecarPath,
|
|
13311
12951
|
JSON.stringify(this.serialize(overflow), null, 2),
|
|
13312
12952
|
"utf8"
|
|
13313
12953
|
);
|
|
13314
12954
|
logger.info(
|
|
13315
|
-
`Emitted ${overflow.length} overflow failures \u2192 ${
|
|
12955
|
+
`Emitted ${overflow.length} overflow failures \u2192 ${path8.basename(sidecarPath)}`
|
|
13316
12956
|
);
|
|
13317
12957
|
} catch (e) {
|
|
13318
12958
|
logger.warn(`Could not write failure sidecar: ${e.message}`);
|
|
@@ -13336,15 +12976,15 @@ var FailurePaginator = class {
|
|
|
13336
12976
|
|
|
13337
12977
|
// src/pdf/sidecar-crypto.ts
|
|
13338
12978
|
init_esm_shims();
|
|
13339
|
-
import
|
|
12979
|
+
import crypto2 from "crypto";
|
|
13340
12980
|
var MAGIC = Buffer.from("RFENC1");
|
|
13341
12981
|
var SALT_LEN = 16;
|
|
13342
12982
|
var IV_LEN = 12;
|
|
13343
12983
|
function encryptSidecar(plain, password) {
|
|
13344
|
-
const salt =
|
|
13345
|
-
const key =
|
|
13346
|
-
const iv =
|
|
13347
|
-
const cipher =
|
|
12984
|
+
const salt = crypto2.randomBytes(SALT_LEN);
|
|
12985
|
+
const key = crypto2.scryptSync(password, salt, 32, { N: 16384, r: 8, p: 1 });
|
|
12986
|
+
const iv = crypto2.randomBytes(IV_LEN);
|
|
12987
|
+
const cipher = crypto2.createCipheriv("aes-256-gcm", key, iv);
|
|
13348
12988
|
const enc = Buffer.concat([cipher.update(plain), cipher.final()]);
|
|
13349
12989
|
return Buffer.concat([MAGIC, salt, iv, cipher.getAuthTag(), enc]);
|
|
13350
12990
|
}
|
|
@@ -13412,7 +13052,7 @@ var PdfGenerator = class {
|
|
|
13412
13052
|
const sources = this.resolveTemplateSources(options);
|
|
13413
13053
|
const isMulti = sources.length > 1;
|
|
13414
13054
|
for (const source of sources) {
|
|
13415
|
-
if (source.kind === "custom" && !
|
|
13055
|
+
if (source.kind === "custom" && !fs11.existsSync(source.absolutePath)) {
|
|
13416
13056
|
throw new Error(
|
|
13417
13057
|
`[reportforge] Custom template not found: ${source.absolutePath}`
|
|
13418
13058
|
);
|
|
@@ -13449,7 +13089,7 @@ var PdfGenerator = class {
|
|
|
13449
13089
|
let { sidecarPath } = await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
|
|
13450
13090
|
const capBytes = (options.maxFileSizeMb ?? 0) * 1024 * 1024;
|
|
13451
13091
|
if (capBytes > 0) {
|
|
13452
|
-
const sizeBytes = (await
|
|
13092
|
+
const sizeBytes = (await fs11.promises.stat(outputPath)).size;
|
|
13453
13093
|
if (sizeBytes > capBytes) {
|
|
13454
13094
|
const inlinedCount = Math.min(templateData.failures.length, compression.maxInlineFailures);
|
|
13455
13095
|
const tighter = this.tightenForCap(compression, sizeBytes, capBytes, inlinedCount);
|
|
@@ -13458,7 +13098,7 @@ var PdfGenerator = class {
|
|
|
13458
13098
|
);
|
|
13459
13099
|
const retuned = await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
|
|
13460
13100
|
sidecarPath = retuned.sidecarPath ?? sidecarPath;
|
|
13461
|
-
const finalSize = (await
|
|
13101
|
+
const finalSize = (await fs11.promises.stat(outputPath)).size;
|
|
13462
13102
|
if (finalSize > capBytes) {
|
|
13463
13103
|
logger.warn(
|
|
13464
13104
|
`PDF still ${fmtMb(finalSize)} after retune. Consider lowering maxInlineFailures or splitting the report.`
|
|
@@ -13473,16 +13113,16 @@ var PdfGenerator = class {
|
|
|
13473
13113
|
try {
|
|
13474
13114
|
await this.pdfEncryptor.encrypt(outputPath, options.pdfPassword);
|
|
13475
13115
|
pdfEncrypted = true;
|
|
13476
|
-
if (sidecarPath &&
|
|
13477
|
-
const blob = encryptSidecar(await
|
|
13478
|
-
await
|
|
13479
|
-
await
|
|
13480
|
-
logger.info(`Overflow sidecar encrypted \u2192 ${
|
|
13116
|
+
if (sidecarPath && fs11.existsSync(sidecarPath)) {
|
|
13117
|
+
const blob = encryptSidecar(await fs11.promises.readFile(sidecarPath), options.pdfPassword);
|
|
13118
|
+
await fs11.promises.writeFile(`${sidecarPath}.enc`, blob);
|
|
13119
|
+
await fs11.promises.unlink(sidecarPath);
|
|
13120
|
+
logger.info(`Overflow sidecar encrypted \u2192 ${path9.basename(sidecarPath)}.enc`);
|
|
13481
13121
|
} else {
|
|
13482
13122
|
const staleSidecarPath = sidecarPathFor(outputPath);
|
|
13483
|
-
if (
|
|
13484
|
-
|
|
13485
|
-
logger.info(`Removed stale plaintext sidecar: ${
|
|
13123
|
+
if (fs11.existsSync(staleSidecarPath)) {
|
|
13124
|
+
fs11.rmSync(staleSidecarPath, { force: true });
|
|
13125
|
+
logger.info(`Removed stale plaintext sidecar: ${path9.basename(staleSidecarPath)}`);
|
|
13486
13126
|
}
|
|
13487
13127
|
}
|
|
13488
13128
|
} catch (err) {
|
|
@@ -13490,7 +13130,7 @@ var PdfGenerator = class {
|
|
|
13490
13130
|
const cleanupTargets = pdfEncrypted ? sidecarTargets : [outputPath, ...sidecarTargets];
|
|
13491
13131
|
for (const target of cleanupTargets) {
|
|
13492
13132
|
try {
|
|
13493
|
-
|
|
13133
|
+
fs11.rmSync(target, { force: true });
|
|
13494
13134
|
} catch (cleanupErr) {
|
|
13495
13135
|
logger.warn(
|
|
13496
13136
|
`Could not remove leftover file after encryption failure: ${target} (${cleanupErr.message})`
|
|
@@ -13500,7 +13140,7 @@ var PdfGenerator = class {
|
|
|
13500
13140
|
throw err;
|
|
13501
13141
|
}
|
|
13502
13142
|
}
|
|
13503
|
-
const finalSizeMb = fmtMb((await
|
|
13143
|
+
const finalSizeMb = fmtMb((await fs11.promises.stat(outputPath)).size);
|
|
13504
13144
|
logger.info(`PDF report generated: ${outputPath} (${finalSizeMb})`);
|
|
13505
13145
|
outputPaths.push(outputPath);
|
|
13506
13146
|
}
|
|
@@ -13513,17 +13153,17 @@ var PdfGenerator = class {
|
|
|
13513
13153
|
if (options.templatePath && options.templatePath.length > 0) {
|
|
13514
13154
|
const byAbsolutePath = /* @__PURE__ */ new Map();
|
|
13515
13155
|
for (const p of options.templatePath) {
|
|
13516
|
-
const absolutePath =
|
|
13156
|
+
const absolutePath = path9.isAbsolute(p) ? p : path9.resolve(process.cwd(), p);
|
|
13517
13157
|
if (!byAbsolutePath.has(absolutePath)) byAbsolutePath.set(absolutePath, p);
|
|
13518
13158
|
}
|
|
13519
13159
|
const labelCounts = /* @__PURE__ */ new Map();
|
|
13520
13160
|
for (const rawPath of byAbsolutePath.values()) {
|
|
13521
|
-
const label =
|
|
13161
|
+
const label = path9.basename(rawPath, ".hbs");
|
|
13522
13162
|
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
|
13523
13163
|
}
|
|
13524
13164
|
const seenPerLabel = /* @__PURE__ */ new Map();
|
|
13525
13165
|
return Array.from(byAbsolutePath.entries()).map(([absolutePath, rawPath]) => {
|
|
13526
|
-
const baseLabel =
|
|
13166
|
+
const baseLabel = path9.basename(rawPath, ".hbs");
|
|
13527
13167
|
let label = baseLabel;
|
|
13528
13168
|
if ((labelCounts.get(baseLabel) ?? 0) > 1) {
|
|
13529
13169
|
const n = (seenPerLabel.get(baseLabel) ?? 0) + 1;
|
|
@@ -13536,154 +13176,565 @@ var PdfGenerator = class {
|
|
|
13536
13176
|
return { kind: "custom", absolutePath, label };
|
|
13537
13177
|
});
|
|
13538
13178
|
}
|
|
13539
|
-
const rawIds = Array.isArray(options.template) ? options.template : [options.template];
|
|
13540
|
-
return [...new Set(rawIds)].map((id) => ({ kind: "builtin", id }));
|
|
13179
|
+
const rawIds = Array.isArray(options.template) ? options.template : [options.template];
|
|
13180
|
+
return [...new Set(rawIds)].map((id) => ({ kind: "builtin", id }));
|
|
13181
|
+
}
|
|
13182
|
+
resolveSettings(options, data) {
|
|
13183
|
+
return resolveCompression({
|
|
13184
|
+
level: options.compressionLevel ?? "auto",
|
|
13185
|
+
testCount: data.stats.total,
|
|
13186
|
+
failureCount: data.failures.length,
|
|
13187
|
+
inlineFailuresOverride: options.maxInlineFailures
|
|
13188
|
+
});
|
|
13189
|
+
}
|
|
13190
|
+
/**
|
|
13191
|
+
* Builds retune settings sized against the cap using *measured* bytes from
|
|
13192
|
+
* the previous pass. The per-failure cost in a PDF includes more than just
|
|
13193
|
+
* the JPEG payload — page layout, font glyph rendering, and per-page chrome
|
|
13194
|
+
* all scale with failure count. Compute observed bytes per inline failure,
|
|
13195
|
+
* then pick an inline count that fits the cap with a 15% safety margin.
|
|
13196
|
+
* Also tightens JPEG quality and width to claw back bytes from each
|
|
13197
|
+
* remaining image.
|
|
13198
|
+
*/
|
|
13199
|
+
tightenForCap(prev, actualBytes, capBytes, actualInlinedCount) {
|
|
13200
|
+
const observedPerFailure = actualBytes / Math.max(1, actualInlinedCount);
|
|
13201
|
+
const targetInline = Math.max(20, Math.floor(capBytes * 0.85 / observedPerFailure));
|
|
13202
|
+
return {
|
|
13203
|
+
effective: "max",
|
|
13204
|
+
reencode: true,
|
|
13205
|
+
quality: 55,
|
|
13206
|
+
maxWidth: 800,
|
|
13207
|
+
maxInlineFailures: Math.min(prev.maxInlineFailures, targetInline)
|
|
13208
|
+
};
|
|
13209
|
+
}
|
|
13210
|
+
/**
|
|
13211
|
+
* One full render pass: paginate failures, re-embed screenshots at the
|
|
13212
|
+
* target compression, render HTML, PDF the page, write to outputPath.
|
|
13213
|
+
* Separated from generateAll() so the size-cap retune can reuse it with
|
|
13214
|
+
* stricter settings without reshaping the rest of the flow.
|
|
13215
|
+
*/
|
|
13216
|
+
async renderPdf(data, options, source, compression, outputPath, page) {
|
|
13217
|
+
const paginated = await this.failurePaginator.paginate(
|
|
13218
|
+
data.failures,
|
|
13219
|
+
compression.maxInlineFailures,
|
|
13220
|
+
outputPath
|
|
13221
|
+
);
|
|
13222
|
+
const renderData = {
|
|
13223
|
+
...data,
|
|
13224
|
+
failures: paginated.inline,
|
|
13225
|
+
overflowFailureCount: paginated.overflowCount,
|
|
13226
|
+
overflowSidecarName: paginated.sidecarPath ? path9.basename(paginated.sidecarPath) + (options.pdfPassword ? ".enc" : "") : void 0
|
|
13227
|
+
};
|
|
13228
|
+
for (const f of renderData.failures) f.screenshotBase64 = void 0;
|
|
13229
|
+
if (options.includeScreenshots !== false) {
|
|
13230
|
+
await this.screenshotEmbedder.embedAll(renderData.failures, {
|
|
13231
|
+
reencode: compression.reencode,
|
|
13232
|
+
quality: compression.quality,
|
|
13233
|
+
maxWidth: compression.maxWidth
|
|
13234
|
+
});
|
|
13235
|
+
}
|
|
13236
|
+
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
13237
|
+
logger.info(`Rendering template: ${templateLabel}`);
|
|
13238
|
+
const renderOpts = {
|
|
13239
|
+
sections: options.sections,
|
|
13240
|
+
slowTestThreshold: options.slowTestThreshold,
|
|
13241
|
+
requirementTagPattern: options.requirementTagPattern
|
|
13242
|
+
};
|
|
13243
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, renderOpts) : this.templateEngine.renderFromPath(renderData, source.absolutePath, renderOpts);
|
|
13244
|
+
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
13245
|
+
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
13246
|
+
try {
|
|
13247
|
+
logger.info(`Navigating to temp HTML (${Math.round(html.length / 1024)} KB)...`);
|
|
13248
|
+
await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
13249
|
+
const showCharts = source.kind === "builtin" ? resolveSections(source.id, options.sections).charts : true;
|
|
13250
|
+
await this.chartRenderer.waitForCharts(page, showCharts);
|
|
13251
|
+
logger.info(`Generating PDF \u2192 ${path9.relative(process.cwd(), outputPath)}`);
|
|
13252
|
+
await page.pdf({
|
|
13253
|
+
path: outputPath,
|
|
13254
|
+
format: "A4",
|
|
13255
|
+
printBackground: true,
|
|
13256
|
+
preferCSSPageSize: true
|
|
13257
|
+
});
|
|
13258
|
+
} finally {
|
|
13259
|
+
await this.htmlWriter.cleanup();
|
|
13260
|
+
}
|
|
13261
|
+
return { sidecarPath: paginated.overflowCount > 0 ? paginated.sidecarPath : void 0 };
|
|
13262
|
+
}
|
|
13263
|
+
};
|
|
13264
|
+
function fmtMb(bytes) {
|
|
13265
|
+
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
13266
|
+
}
|
|
13267
|
+
|
|
13268
|
+
// src/redact/Redactor.ts
|
|
13269
|
+
init_esm_shims();
|
|
13270
|
+
var Redactor = class {
|
|
13271
|
+
constructor(opts) {
|
|
13272
|
+
/** Indexes of user patterns disabled after throwing (warned once each). */
|
|
13273
|
+
this.disabled = /* @__PURE__ */ new Set();
|
|
13274
|
+
this.maskRaw = opts.mask;
|
|
13275
|
+
this.maskReplacement = opts.mask.replace(/\$/g, "$$$$");
|
|
13276
|
+
this.useBuiltins = opts.builtins;
|
|
13277
|
+
this.userPatterns = opts.patterns.map((p) => new RegExp(p, "g"));
|
|
13278
|
+
}
|
|
13279
|
+
redactText(s) {
|
|
13280
|
+
if (!s) return s;
|
|
13281
|
+
let out = s;
|
|
13282
|
+
if (this.useBuiltins) out = this.applyBuiltins(out);
|
|
13283
|
+
for (let i = 0; i < this.userPatterns.length; i++) {
|
|
13284
|
+
if (this.disabled.has(i)) continue;
|
|
13285
|
+
try {
|
|
13286
|
+
out = out.replace(this.userPatterns[i], this.maskReplacement);
|
|
13287
|
+
} catch (err) {
|
|
13288
|
+
this.disabled.add(i);
|
|
13289
|
+
logger.warn(
|
|
13290
|
+
`redact: custom pattern #${i + 1} threw and is disabled for this run: ${err.message}`
|
|
13291
|
+
);
|
|
13292
|
+
}
|
|
13293
|
+
}
|
|
13294
|
+
return out;
|
|
13295
|
+
}
|
|
13296
|
+
applyBuiltins(s) {
|
|
13297
|
+
const M = this.maskRaw;
|
|
13298
|
+
const MR = this.maskReplacement;
|
|
13299
|
+
return s.replace(/(\b[a-z][\w+.-]{0,63}:\/\/[^/\s:@]{1,512}:)([^@\s/]{1,512})(?=@)/gi, `$1${MR}`).replace(
|
|
13300
|
+
/\b(?:(authorization\s*[:=]\s*basic\s+)([A-Za-z0-9._~+/=-]{8,})|((?:authorization\s*[:=]\s*)?bearer\s+)([A-Za-z0-9._~+/=-]{8,}))/gi,
|
|
13301
|
+
(fullMatch, basicPrefix, _basicToken, bearerPrefix, bearerToken) => {
|
|
13302
|
+
if (basicPrefix) return `${basicPrefix}${M}`;
|
|
13303
|
+
if (bearerToken && /[^a-z]/.test(bearerToken)) return `${bearerPrefix}${M}`;
|
|
13304
|
+
return fullMatch;
|
|
13305
|
+
}
|
|
13306
|
+
).replace(/\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/g, MR).replace(
|
|
13307
|
+
/\b(?:AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{16,}|gho_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|sk-[A-Za-z0-9_-]{16,}|rzp_(?:live|test)_[A-Za-z0-9]{8,}|AIza[A-Za-z0-9_-]{30,})\b/g,
|
|
13308
|
+
MR
|
|
13309
|
+
).replace(
|
|
13310
|
+
/\b((?:[A-Za-z0-9]{1,32}[_-]){0,8}(?:password|passwd|passcode|pwd|secret|token|api[_-]?key|apikey|auth|authorization|credential|private[_-]?key|access[_-]?key|client[_-]?secret))\b(["']?\s*[=:]\s*)(?!(?:bearer|basic)\b)(?:(["'])((?:(?!\3).){1,512})\3|([^\s"',;&]{1,512}))/gi,
|
|
13311
|
+
(_m, key, sep, q, _quotedVal, _plainVal) => q ? `${key}${sep}${q}${M}${q}` : `${key}${sep}${M}`
|
|
13312
|
+
).replace(
|
|
13313
|
+
/\b(fill|type|press)\b([^\n]{0,80}?\b(?:password|secret|token|pass|passcode|pin|otp)\b[^\n]{0,80}?\s)(["'])((?:(?!\3).)+)\3/gi,
|
|
13314
|
+
(_m, action, mid, q) => `${action}${mid}${q}${M}${q}`
|
|
13315
|
+
).replace(/\b([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]{0,63}@([A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,})\b/g, "$1***@$2").replace(/[A-Za-z0-9_+/=-]{20,}/g, (m, offset, str) => {
|
|
13316
|
+
const before = offset > 0 ? str[offset - 1] : " ";
|
|
13317
|
+
const after = offset + m.length < str.length ? str[offset + m.length] : " ";
|
|
13318
|
+
const beforeIsWord = /[A-Za-z0-9_]/.test(before);
|
|
13319
|
+
const afterIsWord = /[A-Za-z0-9_]/.test(after);
|
|
13320
|
+
if (!beforeIsWord && !afterIsWord && /\d/.test(m) && /[A-Za-z]/.test(m)) {
|
|
13321
|
+
return M;
|
|
13322
|
+
}
|
|
13323
|
+
return m;
|
|
13324
|
+
});
|
|
13325
|
+
}
|
|
13326
|
+
};
|
|
13327
|
+
|
|
13328
|
+
// src/pipeline/orchestrator.ts
|
|
13329
|
+
init_esm_shims();
|
|
13330
|
+
import path15 from "path";
|
|
13331
|
+
|
|
13332
|
+
// src/license/update-notice.ts
|
|
13333
|
+
init_esm_shims();
|
|
13334
|
+
|
|
13335
|
+
// src/license/version.ts
|
|
13336
|
+
init_esm_shims();
|
|
13337
|
+
function parseTriple(v) {
|
|
13338
|
+
if (!v) return null;
|
|
13339
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(v.trim());
|
|
13340
|
+
if (!m) return null;
|
|
13341
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
13342
|
+
}
|
|
13343
|
+
function isNewer(latest, current) {
|
|
13344
|
+
const l = parseTriple(latest);
|
|
13345
|
+
const c = parseTriple(current);
|
|
13346
|
+
if (!l || !c) return false;
|
|
13347
|
+
if (l[0] !== c[0]) return l[0] > c[0];
|
|
13348
|
+
if (l[1] !== c[1]) return l[1] > c[1];
|
|
13349
|
+
return l[2] > c[2];
|
|
13350
|
+
}
|
|
13351
|
+
|
|
13352
|
+
// src/license/update-notice.ts
|
|
13353
|
+
var NPM_UPDATE_HINT = "npm i -D @reportforge/playwright-pdf@latest";
|
|
13354
|
+
function maybePrintUpdateNotice(licenseInfo, currentVersion, updateHint = NPM_UPDATE_HINT) {
|
|
13355
|
+
try {
|
|
13356
|
+
const latest = licenseInfo?.latestVersion;
|
|
13357
|
+
if (!latest || !isNewer(latest, currentVersion)) return;
|
|
13358
|
+
const shown = latest.replace(/[^\w.+-]/g, "");
|
|
13359
|
+
logger.info(
|
|
13360
|
+
`Update available: ${currentVersion} -> ${shown}. Run: ${updateHint}`
|
|
13361
|
+
);
|
|
13362
|
+
} catch (err) {
|
|
13363
|
+
logger.debug("update notice failed (ignored)", { error: err.message });
|
|
13364
|
+
}
|
|
13365
|
+
}
|
|
13366
|
+
|
|
13367
|
+
// src/analysis/index.ts
|
|
13368
|
+
init_esm_shims();
|
|
13369
|
+
|
|
13370
|
+
// src/analysis/LocalModelStore.ts
|
|
13371
|
+
init_esm_shims();
|
|
13372
|
+
import fs12 from "fs";
|
|
13373
|
+
import os6 from "os";
|
|
13374
|
+
import path10 from "path";
|
|
13375
|
+
var testDeps2 = null;
|
|
13376
|
+
function defaultLocalModelPath() {
|
|
13377
|
+
return testDeps2?.file ?? path10.join(os6.homedir(), ".reportforge", "model-local.json");
|
|
13378
|
+
}
|
|
13379
|
+
function loadLocalModel(overridePath) {
|
|
13380
|
+
const file = overridePath ?? defaultLocalModelPath();
|
|
13381
|
+
const explicit = overridePath !== void 0;
|
|
13382
|
+
const reject = (why) => {
|
|
13383
|
+
const msg = `local model at ${file} not used: ${why}`;
|
|
13384
|
+
if (explicit) logger.warn(msg);
|
|
13385
|
+
else logger.debug(msg);
|
|
13386
|
+
return null;
|
|
13387
|
+
};
|
|
13388
|
+
let parsed;
|
|
13389
|
+
try {
|
|
13390
|
+
parsed = JSON.parse(fs12.readFileSync(file, "utf8"));
|
|
13391
|
+
} catch (e) {
|
|
13392
|
+
return explicit ? reject(`unreadable (${e.message})`) : null;
|
|
13393
|
+
}
|
|
13394
|
+
if (parsed.formatVersion !== 1) return reject(`unsupported formatVersion ${String(parsed.formatVersion)}`);
|
|
13395
|
+
if (!isValidNaiveBayesModel(parsed.model)) return reject("embedded model failed schema validation");
|
|
13396
|
+
const meta = parsed.meta;
|
|
13397
|
+
if (!meta || typeof meta.gatePassed !== "boolean") return reject("missing meta");
|
|
13398
|
+
if (!meta.gatePassed) return reject("its evaluation gate did not pass - run train-model after labeling more feedback");
|
|
13399
|
+
if (typeof meta.acceptThreshold !== "number") return reject("gate passed but no calibrated threshold (corrupt meta)");
|
|
13400
|
+
return {
|
|
13401
|
+
model: parsed.model,
|
|
13402
|
+
acceptThreshold: meta.acceptThreshold,
|
|
13403
|
+
strongThreshold: typeof meta.strongThreshold === "number" ? meta.strongThreshold : null,
|
|
13404
|
+
meta
|
|
13405
|
+
};
|
|
13406
|
+
}
|
|
13407
|
+
|
|
13408
|
+
// src/analysis/FailureClusterer.ts
|
|
13409
|
+
init_esm_shims();
|
|
13410
|
+
|
|
13411
|
+
// src/analysis/redact.ts
|
|
13412
|
+
init_esm_shims();
|
|
13413
|
+
var URI_RE = /[a-z][\w+.-]*:\/\/\S+/gi;
|
|
13414
|
+
var EMAIL_RE = /\b[\w.+-]+@[\w.-]+\.\w+\b/g;
|
|
13415
|
+
var SECRET_RE = /\b(?=[A-Za-z0-9_+/=-]*[0-9])(?=[A-Za-z0-9_+/=-]*[A-Za-z])[A-Za-z0-9_+/=-]{16,}\b/g;
|
|
13416
|
+
var IP_RE = /\b\d{1,3}(?:\.\d{1,3}){3}\b/g;
|
|
13417
|
+
var WIN_PATH_RE = /[A-Za-z]:\\[^\s'"]+/g;
|
|
13418
|
+
var UNIX_PATH_RE = /(?:\/[\w.-]+){2,}/g;
|
|
13419
|
+
function redactForStorage(raw) {
|
|
13420
|
+
const redacted = raw.replace(URI_RE, " URL ").replace(EMAIL_RE, " EMAIL ").replace(SECRET_RE, " SECRET ").replace(IP_RE, " IP ").replace(WIN_PATH_RE, " PATH ").replace(UNIX_PATH_RE, " PATH ");
|
|
13421
|
+
return tokenize(redacted).join(" ");
|
|
13422
|
+
}
|
|
13423
|
+
function localInferenceTokens(message) {
|
|
13424
|
+
return redactForStorage(message).split(" ").filter(Boolean);
|
|
13425
|
+
}
|
|
13426
|
+
|
|
13427
|
+
// src/analysis/constants.ts
|
|
13428
|
+
init_esm_shims();
|
|
13429
|
+
var STRENGTH = {
|
|
13430
|
+
unknownMaxMargin: 0.05,
|
|
13431
|
+
collectMaxMargin: 0.1,
|
|
13432
|
+
harvestMinMargin: 0.25,
|
|
13433
|
+
moderateMinMargin: 0.1,
|
|
13434
|
+
strongMinMargin: 0.25
|
|
13435
|
+
};
|
|
13436
|
+
var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
|
|
13437
|
+
var OVERRIDE_MARGIN = 999;
|
|
13438
|
+
|
|
13439
|
+
// src/analysis/FailureClusterer.ts
|
|
13440
|
+
var MAX_TESTS_PER_CLUSTER = 20;
|
|
13441
|
+
var MAX_MESSAGE_CHARS = 200;
|
|
13442
|
+
function errorHeader(message) {
|
|
13443
|
+
const beforeCallLog = message.split(/\n\s*Call log:/i)[0];
|
|
13444
|
+
return beforeCallLog.split("\n").filter((line) => !/^\s*[-+]?\s*(?:Expected|Received)\b/i.test(line)).join("\n");
|
|
13445
|
+
}
|
|
13446
|
+
function isAssertionTimeout(message) {
|
|
13447
|
+
return /(?:Expect "[^"]+"|expect\.\w+) with timeout/.test(message) && /resolved to [1-9]\d* element/.test(message);
|
|
13448
|
+
}
|
|
13449
|
+
function isNetwork(message) {
|
|
13450
|
+
return /net::ERR_(CONNECTION|NAME_NOT_RESOLVED|INTERNET_DISCONNECTED|SSL|TIMED_OUT|ADDRESS|NETWORK_CHANGED|EMPTY_RESPONSE)|\bECONN(?:REFUSED|RESET)\b|\bENOTFOUND\b|\bEAI_AGAIN\b|getaddrinfo|socket hang up|fetch failed/.test(errorHeader(message));
|
|
13451
|
+
}
|
|
13452
|
+
function isNavigation(message) {
|
|
13453
|
+
return /net::ERR_(?:ABORTED|TOO_MANY_REDIRECTS)|navigation interrupted|interrupted by (?:a|another) navigation|because of a navigation|no history entry|page crashed|Execution context was destroyed|frame was detached/.test(errorHeader(message));
|
|
13454
|
+
}
|
|
13455
|
+
function isLocatorNotFound(message) {
|
|
13456
|
+
return /strict mode violation|resolved to 0 element|element not found|not attached to the DOM|No node found for selector|no element matches|Unable to find an element/i.test(errorHeader(message));
|
|
13457
|
+
}
|
|
13458
|
+
var MESSAGE_RULES = [
|
|
13459
|
+
[isAssertionTimeout, "assertion"],
|
|
13460
|
+
[isNavigation, "navigation"],
|
|
13461
|
+
[isNetwork, "network"],
|
|
13462
|
+
[isLocatorNotFound, "locator-not-found"]
|
|
13463
|
+
];
|
|
13464
|
+
function strengthOf(margin) {
|
|
13465
|
+
if (margin >= STRENGTH.strongMinMargin) return "strong";
|
|
13466
|
+
if (margin >= STRENGTH.moderateMinMargin) return "moderate";
|
|
13467
|
+
return "weak";
|
|
13468
|
+
}
|
|
13469
|
+
function classifyInput(f) {
|
|
13470
|
+
const head = (f.error.stack ?? "").split("\n").slice(0, 3).join(" ");
|
|
13471
|
+
return `${f.error.message} ${head}`;
|
|
13472
|
+
}
|
|
13473
|
+
function topFrame(stack) {
|
|
13474
|
+
if (!stack) return void 0;
|
|
13475
|
+
for (const line of stack.split("\n")) {
|
|
13476
|
+
const t = line.trim();
|
|
13477
|
+
if (t.startsWith("at ") && !t.replace(/\\/g, "/").includes("node_modules/playwright")) return t;
|
|
13478
|
+
}
|
|
13479
|
+
return void 0;
|
|
13480
|
+
}
|
|
13481
|
+
function representativeMessage(message) {
|
|
13482
|
+
return clampWithEllipsis(message.split("\n")[0], MAX_MESSAGE_CHARS);
|
|
13483
|
+
}
|
|
13484
|
+
function classifyOne(f, chain) {
|
|
13485
|
+
const frame = topFrame(f.error.stack);
|
|
13486
|
+
if (f.retryHistory.some((r) => r.status === "passed")) {
|
|
13487
|
+
return { category: "flaky-by-retry", margin: OVERRIDE_MARGIN, strength: "strong", source: "retry", failure: f, frame };
|
|
13488
|
+
}
|
|
13489
|
+
for (const [match, category] of MESSAGE_RULES) {
|
|
13490
|
+
if (match(f.error.message)) {
|
|
13491
|
+
return { category, margin: OVERRIDE_MARGIN, strength: "strong", source: "rule", failure: f, frame };
|
|
13492
|
+
}
|
|
13493
|
+
}
|
|
13494
|
+
if (chain.local) {
|
|
13495
|
+
const { model, acceptThreshold, strongThreshold } = chain.local;
|
|
13496
|
+
const r = classifyTokens(model, localInferenceTokens(f.error.message));
|
|
13497
|
+
if (r.label !== "unknown" && r.margin >= acceptThreshold) {
|
|
13498
|
+
const strength = strongThreshold !== null && r.margin >= strongThreshold ? "strong" : "moderate";
|
|
13499
|
+
return { category: r.label, margin: r.margin, strength, source: "local", failure: f, frame };
|
|
13500
|
+
}
|
|
13501
|
+
}
|
|
13502
|
+
const { label, margin } = classify(chain.base, classifyInput(f));
|
|
13503
|
+
return { category: label, margin, strength: strengthOf(margin), source: "base", failure: f, frame };
|
|
13504
|
+
}
|
|
13505
|
+
function classifyAll(failures, chain) {
|
|
13506
|
+
return failures.map((f) => classifyOne(f, chain));
|
|
13507
|
+
}
|
|
13508
|
+
function perTestClassifications(classified) {
|
|
13509
|
+
return classified.map((c) => ({
|
|
13510
|
+
testId: c.failure.testId,
|
|
13511
|
+
category: c.category,
|
|
13512
|
+
message: representativeMessage(c.failure.error.message),
|
|
13513
|
+
strength: c.strength
|
|
13514
|
+
// resolved at classification time, per producing source
|
|
13515
|
+
}));
|
|
13516
|
+
}
|
|
13517
|
+
function clusterClassified(classified) {
|
|
13518
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13519
|
+
for (const c of classified) {
|
|
13520
|
+
const key = `${c.category}::${c.frame ?? "__no-stack__"}`;
|
|
13521
|
+
const bucket = groups.get(key);
|
|
13522
|
+
if (bucket) bucket.push(c);
|
|
13523
|
+
else groups.set(key, [c]);
|
|
13524
|
+
}
|
|
13525
|
+
const rank = Object.fromEntries(
|
|
13526
|
+
STRENGTH_LEVELS.map((level, i) => [level, i])
|
|
13527
|
+
);
|
|
13528
|
+
const clusters = [];
|
|
13529
|
+
for (const members of groups.values()) {
|
|
13530
|
+
const strongest = members.reduce((a, b) => rank[b.strength] > rank[a.strength] ? b : a);
|
|
13531
|
+
clusters.push({
|
|
13532
|
+
category: members[0].category,
|
|
13533
|
+
strength: strongest.strength,
|
|
13534
|
+
margin: Math.max(...members.map((m) => m.margin)),
|
|
13535
|
+
count: members.length,
|
|
13536
|
+
tests: members.slice(0, MAX_TESTS_PER_CLUSTER).map((m) => m.failure.testTitle),
|
|
13537
|
+
testsTotal: members.length,
|
|
13538
|
+
representativeMessage: representativeMessage(members[0].failure.error.message),
|
|
13539
|
+
stackFrame: members[0].frame
|
|
13540
|
+
});
|
|
13541
|
+
}
|
|
13542
|
+
return clusters;
|
|
13543
|
+
}
|
|
13544
|
+
|
|
13545
|
+
// src/analysis/UnclassifiedCollector.ts
|
|
13546
|
+
init_esm_shims();
|
|
13547
|
+
import fs14 from "fs";
|
|
13548
|
+
import os8 from "os";
|
|
13549
|
+
import path12 from "path";
|
|
13550
|
+
|
|
13551
|
+
// src/analysis/feedback-csv.ts
|
|
13552
|
+
init_esm_shims();
|
|
13553
|
+
var FEEDBACK_HEADER = "category,margin,label,text";
|
|
13554
|
+
var LEGACY_FEEDBACK_HEADER = "category,margin,text";
|
|
13555
|
+
function parseFeedbackRows(content) {
|
|
13556
|
+
const lines = content.trim().split("\n").filter(Boolean);
|
|
13557
|
+
if (lines.length === 0) return [];
|
|
13558
|
+
const header = lines[0].trim();
|
|
13559
|
+
const isV2 = header === FEEDBACK_HEADER;
|
|
13560
|
+
if (!isV2 && header !== LEGACY_FEEDBACK_HEADER) return [];
|
|
13561
|
+
const minParts = isV2 ? 4 : 3;
|
|
13562
|
+
const rows = [];
|
|
13563
|
+
for (const line of lines.slice(1)) {
|
|
13564
|
+
const parts = line.split(",");
|
|
13565
|
+
if (parts.length < minParts) continue;
|
|
13566
|
+
const row = isV2 ? { category: parts[0], margin: parts[1], label: parts[2], text: parts.slice(3).join(",") } : { category: parts[0], margin: parts[1], label: "", text: parts.slice(2).join(",") };
|
|
13567
|
+
if (row.text.trim().length === 0) continue;
|
|
13568
|
+
rows.push(row);
|
|
13569
|
+
}
|
|
13570
|
+
return rows;
|
|
13571
|
+
}
|
|
13572
|
+
function serializeFeedbackRow(r) {
|
|
13573
|
+
const label = r.label.replace(/[,\r\n]/g, "").trim();
|
|
13574
|
+
return `${r.category},${r.margin},${label},${r.text}`;
|
|
13575
|
+
}
|
|
13576
|
+
function feedbackIdentity(category, text) {
|
|
13577
|
+
return `${category} ${text}`;
|
|
13578
|
+
}
|
|
13579
|
+
|
|
13580
|
+
// src/analysis/FeedbackStore.ts
|
|
13581
|
+
init_esm_shims();
|
|
13582
|
+
import fs13 from "fs";
|
|
13583
|
+
import os7 from "os";
|
|
13584
|
+
import path11 from "path";
|
|
13585
|
+
var MARKER_FILENAME = "project.json";
|
|
13586
|
+
function writeProjectMarker(projectDir, marker) {
|
|
13587
|
+
try {
|
|
13588
|
+
atomicWrite(path11.join(projectDir, MARKER_FILENAME), JSON.stringify(marker));
|
|
13589
|
+
} catch {
|
|
13590
|
+
}
|
|
13591
|
+
}
|
|
13592
|
+
function rewriteFeedbackFile(file, rows) {
|
|
13593
|
+
const body = rows.map(serializeFeedbackRow).join("\n");
|
|
13594
|
+
atomicWrite(file, `${FEEDBACK_HEADER}
|
|
13595
|
+
${body}${body ? "\n" : ""}`);
|
|
13596
|
+
}
|
|
13597
|
+
function atomicWrite(file, content) {
|
|
13598
|
+
fs13.mkdirSync(path11.dirname(file), { recursive: true });
|
|
13599
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
13600
|
+
fs13.writeFileSync(tmp, content, "utf8");
|
|
13601
|
+
fs13.renameSync(tmp, file);
|
|
13602
|
+
}
|
|
13603
|
+
|
|
13604
|
+
// src/analysis/UnclassifiedCollector.ts
|
|
13605
|
+
var MAX_ROWS = 500;
|
|
13606
|
+
var FILENAME = "unclassified.csv";
|
|
13607
|
+
function shouldCollect(c, scope) {
|
|
13608
|
+
if (c.source !== "base") return false;
|
|
13609
|
+
if (c.category === "unknown" || c.margin <= STRENGTH.collectMaxMargin) return true;
|
|
13610
|
+
return scope === "all";
|
|
13611
|
+
}
|
|
13612
|
+
function tildeify(p) {
|
|
13613
|
+
const home = os8.homedir();
|
|
13614
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
13615
|
+
}
|
|
13616
|
+
function collectUnclassified(classified, projectDir, opts = {}) {
|
|
13617
|
+
const scope = opts.scope ?? "blind-spots";
|
|
13618
|
+
const rows = classified.filter((c) => shouldCollect(c, scope)).map((c) => ({ text: redactForStorage(c.failure.error.message), category: c.category, margin: c.margin })).filter((r) => r.text.length > 0);
|
|
13619
|
+
if (rows.length === 0) return;
|
|
13620
|
+
const file = path12.join(projectDir, FILENAME);
|
|
13621
|
+
const firstTime = !fs14.existsSync(file);
|
|
13622
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
13623
|
+
if (!firstTime) {
|
|
13624
|
+
for (const r of parseFeedbackRows(fs14.readFileSync(file, "utf8"))) {
|
|
13625
|
+
byIdentity.set(feedbackIdentity(r.category, r.text), r);
|
|
13626
|
+
}
|
|
13541
13627
|
}
|
|
13542
|
-
|
|
13543
|
-
|
|
13544
|
-
|
|
13545
|
-
|
|
13546
|
-
|
|
13547
|
-
inlineFailuresOverride: options.maxInlineFailures
|
|
13548
|
-
});
|
|
13628
|
+
for (const r of rows) {
|
|
13629
|
+
const id = feedbackIdentity(r.category, r.text);
|
|
13630
|
+
const label = byIdentity.get(id)?.label ?? "";
|
|
13631
|
+
byIdentity.delete(id);
|
|
13632
|
+
byIdentity.set(id, { category: r.category, margin: String(r.margin), label, text: r.text });
|
|
13549
13633
|
}
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
* then pick an inline count that fits the cap with a 15% safety margin.
|
|
13556
|
-
* Also tightens JPEG quality and width to claw back bytes from each
|
|
13557
|
-
* remaining image.
|
|
13558
|
-
*/
|
|
13559
|
-
tightenForCap(prev, actualBytes, capBytes, actualInlinedCount) {
|
|
13560
|
-
const observedPerFailure = actualBytes / Math.max(1, actualInlinedCount);
|
|
13561
|
-
const targetInline = Math.max(20, Math.floor(capBytes * 0.85 / observedPerFailure));
|
|
13562
|
-
return {
|
|
13563
|
-
effective: "max",
|
|
13564
|
-
reencode: true,
|
|
13565
|
-
quality: 55,
|
|
13566
|
-
maxWidth: 800,
|
|
13567
|
-
maxInlineFailures: Math.min(prev.maxInlineFailures, targetInline)
|
|
13568
|
-
};
|
|
13634
|
+
const all = evictOldestUnlabeledFirst([...byIdentity.values()], MAX_ROWS);
|
|
13635
|
+
fs14.mkdirSync(projectDir, { recursive: true });
|
|
13636
|
+
rewriteFeedbackFile(file, all);
|
|
13637
|
+
if (opts.project) {
|
|
13638
|
+
writeProjectMarker(projectDir, { cwd: opts.project.cwd, outputFile: opts.project.outputFile, updatedAt: Date.now() });
|
|
13569
13639
|
}
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
* Separated from generateAll() so the size-cap retune can reuse it with
|
|
13574
|
-
* stricter settings without reshaping the rest of the flow.
|
|
13575
|
-
*/
|
|
13576
|
-
async renderPdf(data, options, source, compression, outputPath, page) {
|
|
13577
|
-
const paginated = await this.failurePaginator.paginate(
|
|
13578
|
-
data.failures,
|
|
13579
|
-
compression.maxInlineFailures,
|
|
13580
|
-
outputPath
|
|
13640
|
+
if (firstTime) {
|
|
13641
|
+
logger.info(
|
|
13642
|
+
`Unrecognized failures collected locally at ${tildeify(file)} to improve offline classification. Local only - never sent anywhere. Disable: failureAnalysis.collectUnclassified=false`
|
|
13581
13643
|
);
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
13585
|
-
|
|
13586
|
-
|
|
13587
|
-
|
|
13588
|
-
|
|
13589
|
-
|
|
13590
|
-
|
|
13591
|
-
|
|
13592
|
-
|
|
13593
|
-
|
|
13594
|
-
});
|
|
13644
|
+
}
|
|
13645
|
+
}
|
|
13646
|
+
function evictOldestUnlabeledFirst(rows, max) {
|
|
13647
|
+
const excess = rows.length - max;
|
|
13648
|
+
if (excess <= 0) return rows;
|
|
13649
|
+
let unlabeledToDrop = Math.min(excess, rows.filter((r) => r.label === "").length);
|
|
13650
|
+
let labeledToDrop = excess - unlabeledToDrop;
|
|
13651
|
+
const out = [];
|
|
13652
|
+
for (const r of rows) {
|
|
13653
|
+
if (r.label === "" && unlabeledToDrop > 0) {
|
|
13654
|
+
unlabeledToDrop--;
|
|
13655
|
+
continue;
|
|
13595
13656
|
}
|
|
13596
|
-
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
sections: options.sections,
|
|
13600
|
-
slowTestThreshold: options.slowTestThreshold,
|
|
13601
|
-
requirementTagPattern: options.requirementTagPattern
|
|
13602
|
-
};
|
|
13603
|
-
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, renderOpts) : this.templateEngine.renderFromPath(renderData, source.absolutePath, renderOpts);
|
|
13604
|
-
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
13605
|
-
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
13606
|
-
try {
|
|
13607
|
-
logger.info(`Navigating to temp HTML (${Math.round(html.length / 1024)} KB)...`);
|
|
13608
|
-
await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
|
|
13609
|
-
const showCharts = source.kind === "builtin" ? resolveSections(source.id, options.sections).charts : true;
|
|
13610
|
-
await this.chartRenderer.waitForCharts(page, showCharts);
|
|
13611
|
-
logger.info(`Generating PDF \u2192 ${path13.relative(process.cwd(), outputPath)}`);
|
|
13612
|
-
await page.pdf({
|
|
13613
|
-
path: outputPath,
|
|
13614
|
-
format: "A4",
|
|
13615
|
-
printBackground: true,
|
|
13616
|
-
preferCSSPageSize: true
|
|
13617
|
-
});
|
|
13618
|
-
} finally {
|
|
13619
|
-
await this.htmlWriter.cleanup();
|
|
13657
|
+
if (r.label !== "" && labeledToDrop > 0) {
|
|
13658
|
+
labeledToDrop--;
|
|
13659
|
+
continue;
|
|
13620
13660
|
}
|
|
13621
|
-
|
|
13661
|
+
out.push(r);
|
|
13622
13662
|
}
|
|
13623
|
-
|
|
13624
|
-
function fmtMb(bytes) {
|
|
13625
|
-
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
|
13663
|
+
return out;
|
|
13626
13664
|
}
|
|
13627
13665
|
|
|
13628
|
-
// src/
|
|
13666
|
+
// src/utils/paths.ts
|
|
13629
13667
|
init_esm_shims();
|
|
13630
|
-
|
|
13631
|
-
|
|
13632
|
-
|
|
13633
|
-
|
|
13634
|
-
|
|
13635
|
-
|
|
13636
|
-
|
|
13637
|
-
|
|
13638
|
-
|
|
13639
|
-
|
|
13640
|
-
|
|
13641
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
13644
|
-
|
|
13645
|
-
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
|
|
13650
|
-
|
|
13651
|
-
|
|
13652
|
-
|
|
13668
|
+
import os9 from "os";
|
|
13669
|
+
import path13 from "path";
|
|
13670
|
+
import crypto3 from "crypto";
|
|
13671
|
+
function resolveProjectDir(options, cwd) {
|
|
13672
|
+
const projectKey = crypto3.createHash("sha256").update(cwd + (options.outputFile ?? "")).digest("hex").slice(0, 8);
|
|
13673
|
+
return path13.join(os9.homedir(), ".reportforge", projectKey);
|
|
13674
|
+
}
|
|
13675
|
+
|
|
13676
|
+
// src/analysis/index.ts
|
|
13677
|
+
var DEFAULTS = { maxClusters: 10, minStrength: "weak", maxFailuresToAnalyse: 500 };
|
|
13678
|
+
var STRENGTH_RANK = Object.fromEntries(
|
|
13679
|
+
STRENGTH_LEVELS.map((level, i) => [level, i])
|
|
13680
|
+
);
|
|
13681
|
+
function analyseClassified(classified, totalFailures, opts = {}) {
|
|
13682
|
+
const maxClusters = opts.maxClusters ?? DEFAULTS.maxClusters;
|
|
13683
|
+
const minRank = STRENGTH_RANK[opts.minStrength ?? DEFAULTS.minStrength];
|
|
13684
|
+
const cap = opts.maxFailuresToAnalyse ?? DEFAULTS.maxFailuresToAnalyse;
|
|
13685
|
+
const clusters = clusterClassified(classified).filter((c) => STRENGTH_RANK[c.strength] >= minRank).sort((a, b) => b.count - a.count).slice(0, maxClusters);
|
|
13686
|
+
return {
|
|
13687
|
+
clusters,
|
|
13688
|
+
totalFailures,
|
|
13689
|
+
analysedCount: classified.length,
|
|
13690
|
+
truncated: totalFailures > cap,
|
|
13691
|
+
// dominantCategory is part of the AnalysisResult contract; reserved for
|
|
13692
|
+
// consumers (e.g. a future executive badge / notify summary). Not rendered
|
|
13693
|
+
// by the v1 partials.
|
|
13694
|
+
dominantCategory: dominantCategory(clusters),
|
|
13695
|
+
oneLiner: buildOneLiner(clusters)
|
|
13696
|
+
};
|
|
13697
|
+
}
|
|
13698
|
+
function templatesConsumeAnalysis(options) {
|
|
13699
|
+
if (options.templatePath && options.templatePath.length > 0) return true;
|
|
13700
|
+
const templates = Array.isArray(options.template) ? options.template : [options.template];
|
|
13701
|
+
return templates.some((t) => t === "detailed" || t === "executive");
|
|
13702
|
+
}
|
|
13703
|
+
function runFailureAnalysis(reportData, options, cwd) {
|
|
13704
|
+
const fa = options.failureAnalysis;
|
|
13705
|
+
if (reportData.stats.failed === 0 || !fa.enabled) return;
|
|
13706
|
+
const cap = fa.maxFailuresToAnalyse;
|
|
13707
|
+
const analysed = reportData.failures.slice(0, cap);
|
|
13708
|
+
const base = loadModel(fa.autoUpdateModel);
|
|
13709
|
+
const local = fa.localModel ? loadLocalModel(fa.localModelPath) ?? void 0 : void 0;
|
|
13710
|
+
if (local) {
|
|
13711
|
+
logger.debug(
|
|
13712
|
+
`local model active (trained ${local.meta.trainedAt}, ${local.meta.labeledCount} labeled rows, accept margin >= ${local.acceptThreshold})`
|
|
13713
|
+
);
|
|
13714
|
+
if (local.meta.baseVersionAtEval !== base.version) {
|
|
13715
|
+
logger.debug(
|
|
13716
|
+
`local model was gate-checked against base v${local.meta.baseVersionAtEval} but base v${base.version} is active - re-run train-model to re-validate against the current base`
|
|
13717
|
+
);
|
|
13653
13718
|
}
|
|
13654
|
-
return out;
|
|
13655
13719
|
}
|
|
13656
|
-
|
|
13657
|
-
|
|
13658
|
-
|
|
13659
|
-
|
|
13660
|
-
|
|
13661
|
-
|
|
13662
|
-
|
|
13663
|
-
|
|
13664
|
-
|
|
13665
|
-
}
|
|
13666
|
-
).replace(/\beyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/g, MR).replace(
|
|
13667
|
-
/\b(?:AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{16,}|gho_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|sk-[A-Za-z0-9_-]{16,}|rzp_(?:live|test)_[A-Za-z0-9]{8,}|AIza[A-Za-z0-9_-]{30,})\b/g,
|
|
13668
|
-
MR
|
|
13669
|
-
).replace(
|
|
13670
|
-
/\b((?:[A-Za-z0-9]{1,32}[_-]){0,8}(?:password|passwd|passcode|pwd|secret|token|api[_-]?key|apikey|auth|authorization|credential|private[_-]?key|access[_-]?key|client[_-]?secret))\b(["']?\s*[=:]\s*)(?!(?:bearer|basic)\b)(?:(["'])((?:(?!\3).){1,512})\3|([^\s"',;&]{1,512}))/gi,
|
|
13671
|
-
(_m, key, sep, q, _quotedVal, _plainVal) => q ? `${key}${sep}${q}${M}${q}` : `${key}${sep}${M}`
|
|
13672
|
-
).replace(
|
|
13673
|
-
/\b(fill|type|press)\b([^\n]{0,80}?\b(?:password|secret|token|pass|passcode|pin|otp)\b[^\n]{0,80}?\s)(["'])((?:(?!\3).)+)\3/gi,
|
|
13674
|
-
(_m, action, mid, q) => `${action}${mid}${q}${M}${q}`
|
|
13675
|
-
).replace(/\b([A-Za-z0-9._%+-])[A-Za-z0-9._%+-]{0,63}@([A-Za-z0-9.-]{1,255}\.[A-Za-z]{2,})\b/g, "$1***@$2").replace(/[A-Za-z0-9_+/=-]{20,}/g, (m, offset, str) => {
|
|
13676
|
-
const before = offset > 0 ? str[offset - 1] : " ";
|
|
13677
|
-
const after = offset + m.length < str.length ? str[offset + m.length] : " ";
|
|
13678
|
-
const beforeIsWord = /[A-Za-z0-9_]/.test(before);
|
|
13679
|
-
const afterIsWord = /[A-Za-z0-9_]/.test(after);
|
|
13680
|
-
if (!beforeIsWord && !afterIsWord && /\d/.test(m) && /[A-Za-z]/.test(m)) {
|
|
13681
|
-
return M;
|
|
13682
|
-
}
|
|
13683
|
-
return m;
|
|
13720
|
+
const classified = classifyAll(analysed, { base, local });
|
|
13721
|
+
logger.debug(`failure analysis: ${classified.length} classified - ${sourceTally(classified)}`);
|
|
13722
|
+
reportData.classifications = perTestClassifications(classified);
|
|
13723
|
+
if (templatesConsumeAnalysis(options)) {
|
|
13724
|
+
reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
|
|
13725
|
+
}
|
|
13726
|
+
if (fa.collectUnclassified) {
|
|
13727
|
+
collectUnclassified(classified, resolveProjectDir(options, cwd), {
|
|
13728
|
+
scope: fa.collectScope,
|
|
13729
|
+
project: { cwd, outputFile: options.outputFile }
|
|
13684
13730
|
});
|
|
13685
13731
|
}
|
|
13686
|
-
}
|
|
13732
|
+
}
|
|
13733
|
+
function sourceTally(classified) {
|
|
13734
|
+
const counts = /* @__PURE__ */ new Map();
|
|
13735
|
+
for (const c of classified) counts.set(c.source, (counts.get(c.source) ?? 0) + 1);
|
|
13736
|
+
return ["rule", "retry", "local", "base"].filter((s) => counts.has(s)).map((s) => `${counts.get(s)} ${s}`).join(" / ");
|
|
13737
|
+
}
|
|
13687
13738
|
|
|
13688
13739
|
// src/redact/redact-report.ts
|
|
13689
13740
|
init_esm_shims();
|
|
@@ -13734,9 +13785,6 @@ function redactSuite(s, r) {
|
|
|
13734
13785
|
s.suites.forEach((child) => redactSuite(child, r));
|
|
13735
13786
|
}
|
|
13736
13787
|
|
|
13737
|
-
// src/reporter.ts
|
|
13738
|
-
import path15 from "path";
|
|
13739
|
-
|
|
13740
13788
|
// src/history/HistoryManager.ts
|
|
13741
13789
|
init_esm_shims();
|
|
13742
13790
|
import fs15 from "fs";
|
|
@@ -14321,6 +14369,261 @@ var NotificationSender = class {
|
|
|
14321
14369
|
}
|
|
14322
14370
|
};
|
|
14323
14371
|
|
|
14372
|
+
// src/pipeline/orchestrator.ts
|
|
14373
|
+
async function runReportPipeline(hooks) {
|
|
14374
|
+
const { options } = hooks;
|
|
14375
|
+
let licenseInfo;
|
|
14376
|
+
try {
|
|
14377
|
+
licenseInfo = await hooks.license();
|
|
14378
|
+
} catch (err) {
|
|
14379
|
+
logger.warn(`License resolution errored: ${err.message}. PDF generation skipped.`);
|
|
14380
|
+
licenseInfo = null;
|
|
14381
|
+
}
|
|
14382
|
+
if (!licenseInfo) return { status: "skipped-license" };
|
|
14383
|
+
const collected = hooks.collect();
|
|
14384
|
+
if (!collected) {
|
|
14385
|
+
maybePrintUpdateNotice(licenseInfo, hooks.version, hooks.updateHint);
|
|
14386
|
+
return { status: "skipped-collect" };
|
|
14387
|
+
}
|
|
14388
|
+
if (collected.stats.total === 0) {
|
|
14389
|
+
logger.warn("No tests were executed \u2014 check your --grep / project filters. The report has nothing to assess.");
|
|
14390
|
+
}
|
|
14391
|
+
if (options.showTrend && collected.stats.total > 0) {
|
|
14392
|
+
try {
|
|
14393
|
+
await appendTrend(collected, licenseInfo, hooks);
|
|
14394
|
+
} catch (err) {
|
|
14395
|
+
logger.warn(`History write failed \u2014 trend data skipped: ${err.message}`);
|
|
14396
|
+
}
|
|
14397
|
+
}
|
|
14398
|
+
const reportData = buildReportData(collected, licenseInfo, hooks);
|
|
14399
|
+
runFailureAnalysis(reportData, options, hooks.cwd);
|
|
14400
|
+
reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
|
|
14401
|
+
if (hooks.redactor) {
|
|
14402
|
+
redactReportData(reportData, hooks.redactor);
|
|
14403
|
+
}
|
|
14404
|
+
let pdfPaths = [];
|
|
14405
|
+
let pdfFailed = false;
|
|
14406
|
+
try {
|
|
14407
|
+
pdfPaths = await hooks.pdfGenerator.generateAll(reportData, options, licenseInfo);
|
|
14408
|
+
if (options.open) {
|
|
14409
|
+
for (const pdfPath of pdfPaths) await openPdf(pdfPath);
|
|
14410
|
+
}
|
|
14411
|
+
} catch (err) {
|
|
14412
|
+
pdfFailed = true;
|
|
14413
|
+
logger.error(`Failed to generate PDF report: ${err.message}`);
|
|
14414
|
+
if (process.env.RF_DEBUG) console.error(err);
|
|
14415
|
+
}
|
|
14416
|
+
if (options.notify) {
|
|
14417
|
+
const sender = new NotificationSender();
|
|
14418
|
+
await sender.sendAll(reportData, options.notify, pdfPaths);
|
|
14419
|
+
}
|
|
14420
|
+
maybePrintUpdateNotice(licenseInfo, hooks.version, hooks.updateHint);
|
|
14421
|
+
return pdfFailed ? { status: "pdf-failed" } : { status: "ok", pdfPaths };
|
|
14422
|
+
}
|
|
14423
|
+
async function appendTrend(collected, licenseInfo, hooks) {
|
|
14424
|
+
const { options, cwd, redactor } = hooks;
|
|
14425
|
+
const historyPath = resolveHistoryPath(options, cwd);
|
|
14426
|
+
const hm = new HistoryManager(historyPath);
|
|
14427
|
+
const suffix = Math.random().toString(36).padEnd(8, "0").slice(2, 6);
|
|
14428
|
+
const flakyTests = collectFlakyTests(collected.projects).map(
|
|
14429
|
+
(t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
|
|
14430
|
+
);
|
|
14431
|
+
const { redactedFailing, ...failedTestsFields } = buildFailedTestsFields(collected.projects, redactor);
|
|
14432
|
+
const entry = {
|
|
14433
|
+
runId: `${(/* @__PURE__ */ new Date()).toISOString()}-${suffix}`,
|
|
14434
|
+
timestamp: Date.now(),
|
|
14435
|
+
passRate: collected.stats.passRate,
|
|
14436
|
+
total: collected.stats.total,
|
|
14437
|
+
passed: collected.stats.passed,
|
|
14438
|
+
failed: collected.stats.failed,
|
|
14439
|
+
flaky: collected.stats.flaky,
|
|
14440
|
+
duration: collected.stats.duration,
|
|
14441
|
+
verdict: deriveVerdict(collected.stats),
|
|
14442
|
+
flakyTests,
|
|
14443
|
+
...failedTestsFields,
|
|
14444
|
+
branch: collected.environment.branch
|
|
14445
|
+
};
|
|
14446
|
+
const localEntries = hm.append(entry, options.historySize);
|
|
14447
|
+
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch);
|
|
14448
|
+
let trendEntries = localEntries;
|
|
14449
|
+
if (options.remoteHistory && licenseInfo.jwt) {
|
|
14450
|
+
const serverUrl = options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
14451
|
+
const remote = await appendRemoteHistory({
|
|
14452
|
+
serverUrl,
|
|
14453
|
+
jwt: licenseInfo.jwt,
|
|
14454
|
+
projectId: buildProjectId(options.projectName, options.outputFile),
|
|
14455
|
+
branchHash: buildBranchHash(collected.environment.branch),
|
|
14456
|
+
entry
|
|
14457
|
+
});
|
|
14458
|
+
if (remote) {
|
|
14459
|
+
trendEntries = remote;
|
|
14460
|
+
logger.debug(`remote history: ${remote.length} entr${remote.length === 1 ? "y" : "ies"} for trend`);
|
|
14461
|
+
}
|
|
14462
|
+
}
|
|
14463
|
+
populateHistoryCharts(trendEntries, localEntries, collected, options);
|
|
14464
|
+
}
|
|
14465
|
+
function populateHistoryCharts(trendEntries, localEntries, collected, options) {
|
|
14466
|
+
if (trendEntries.length >= 2) {
|
|
14467
|
+
collected.charts.trend = {
|
|
14468
|
+
entries: trendEntries.map((e) => ({
|
|
14469
|
+
runId: e.runId,
|
|
14470
|
+
timestamp: e.timestamp,
|
|
14471
|
+
passRate: e.passRate,
|
|
14472
|
+
total: e.total,
|
|
14473
|
+
duration: e.duration,
|
|
14474
|
+
verdict: e.verdict
|
|
14475
|
+
})),
|
|
14476
|
+
delta: trendEntries[0].passRate - trendEntries[1].passRate
|
|
14477
|
+
};
|
|
14478
|
+
}
|
|
14479
|
+
if (options.flakinessTopN > 0) {
|
|
14480
|
+
const table = computeTopN(localEntries, options.flakinessTopN);
|
|
14481
|
+
if (table.length > 0) {
|
|
14482
|
+
collected.charts.flakinessTable = table;
|
|
14483
|
+
}
|
|
14484
|
+
}
|
|
14485
|
+
}
|
|
14486
|
+
function buildReportData(collected, licenseInfo, hooks) {
|
|
14487
|
+
const { options } = hooks;
|
|
14488
|
+
const { projects, stats, failures, environment, charts, runDiff } = collected;
|
|
14489
|
+
const projectName = options.projectName ?? hooks.resolveProjectName?.() ?? resolveProjectNameFromPkg(hooks.cwd);
|
|
14490
|
+
const firstTemplate = options.templatePath?.length ? "detailed" : Array.isArray(options.template) ? options.template[0] : options.template;
|
|
14491
|
+
return {
|
|
14492
|
+
meta: {
|
|
14493
|
+
title: options.reportTitle,
|
|
14494
|
+
generatedAt: /* @__PURE__ */ new Date(),
|
|
14495
|
+
template: firstTemplate,
|
|
14496
|
+
branding: {
|
|
14497
|
+
primaryColor: options.primaryColor,
|
|
14498
|
+
accentColor: options.accentColor,
|
|
14499
|
+
watermark: options.watermark
|
|
14500
|
+
},
|
|
14501
|
+
licenseInfo,
|
|
14502
|
+
projectName
|
|
14503
|
+
},
|
|
14504
|
+
stats,
|
|
14505
|
+
projects,
|
|
14506
|
+
failures,
|
|
14507
|
+
environment,
|
|
14508
|
+
charts,
|
|
14509
|
+
runDiff
|
|
14510
|
+
};
|
|
14511
|
+
}
|
|
14512
|
+
function resolveProjectNameFromPkg(cwd) {
|
|
14513
|
+
try {
|
|
14514
|
+
const pkgPath = path15.resolve(cwd, "package.json");
|
|
14515
|
+
const pkg = __require(pkgPath);
|
|
14516
|
+
return pkg.name ?? "Playwright Tests";
|
|
14517
|
+
} catch {
|
|
14518
|
+
return "Playwright Tests";
|
|
14519
|
+
}
|
|
14520
|
+
}
|
|
14521
|
+
async function openPdf(pdfPath) {
|
|
14522
|
+
try {
|
|
14523
|
+
const { existsSync } = await import("fs");
|
|
14524
|
+
if (!existsSync(pdfPath)) {
|
|
14525
|
+
logger.warn(`open: true \u2014 PDF not found at ${pdfPath}; file may not have been written yet.`);
|
|
14526
|
+
return;
|
|
14527
|
+
}
|
|
14528
|
+
const { spawn } = await import("child_process");
|
|
14529
|
+
const platform = process.platform;
|
|
14530
|
+
const [cmd, args] = platform === "darwin" ? ["open", [pdfPath]] : platform === "win32" ? ["cmd", ["/c", "start", "", pdfPath]] : ["xdg-open", [pdfPath]];
|
|
14531
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore", shell: false });
|
|
14532
|
+
child.on("error", (err) => {
|
|
14533
|
+
logger.warn(`open: true \u2014 could not open ${pdfPath}: ${err.message}`);
|
|
14534
|
+
});
|
|
14535
|
+
child.unref();
|
|
14536
|
+
if (platform === "win32") {
|
|
14537
|
+
child.on("close", (code) => {
|
|
14538
|
+
if (code !== 0 && code !== null) {
|
|
14539
|
+
logger.warn(
|
|
14540
|
+
`open: true \u2014 shell-open exited with code ${code}. No PDF viewer may be registered for .pdf files on this machine. File is at: ${pdfPath}`
|
|
14541
|
+
);
|
|
14542
|
+
}
|
|
14543
|
+
});
|
|
14544
|
+
}
|
|
14545
|
+
} catch (err) {
|
|
14546
|
+
logger.warn(`open: true \u2014 unexpected error: ${err.message}`);
|
|
14547
|
+
}
|
|
14548
|
+
}
|
|
14549
|
+
function deriveVerdict(stats) {
|
|
14550
|
+
if (stats.total === 0) return "unknown";
|
|
14551
|
+
if (stats.verdict === "interrupted" || stats.verdict === "timedout") return "failed";
|
|
14552
|
+
if (stats.failed === 0 && stats.timedOut === 0) {
|
|
14553
|
+
if (stats.passed === 0 && stats.flaky === 0) return "unknown";
|
|
14554
|
+
return "passed";
|
|
14555
|
+
}
|
|
14556
|
+
if (stats.failed / stats.total > 0.5) return "failed";
|
|
14557
|
+
if (stats.timedOut > 0) return "failed";
|
|
14558
|
+
if (stats.failed > 0) return "partial";
|
|
14559
|
+
return "unknown";
|
|
14560
|
+
}
|
|
14561
|
+
function resolveHistoryPath(options, cwd) {
|
|
14562
|
+
if (options.historyFile) {
|
|
14563
|
+
return path15.isAbsolute(options.historyFile) ? options.historyFile : path15.resolve(cwd, options.historyFile);
|
|
14564
|
+
}
|
|
14565
|
+
return path15.join(resolveProjectDir(options, cwd), "history.json");
|
|
14566
|
+
}
|
|
14567
|
+
function flattenSuiteNodes(suites) {
|
|
14568
|
+
return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);
|
|
14569
|
+
}
|
|
14570
|
+
function collectFlakyTests(projects) {
|
|
14571
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14572
|
+
const result = [];
|
|
14573
|
+
for (const project of projects) {
|
|
14574
|
+
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
14575
|
+
for (const test of suite.tests) {
|
|
14576
|
+
if (test.status === "flaky" && !seen.has(test.id)) {
|
|
14577
|
+
seen.add(test.id);
|
|
14578
|
+
result.push({ id: test.id, title: test.fullTitle });
|
|
14579
|
+
}
|
|
14580
|
+
}
|
|
14581
|
+
}
|
|
14582
|
+
}
|
|
14583
|
+
return result;
|
|
14584
|
+
}
|
|
14585
|
+
function collectFailedTests(projects) {
|
|
14586
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14587
|
+
const result = [];
|
|
14588
|
+
for (const project of projects) {
|
|
14589
|
+
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
14590
|
+
for (const test of suite.tests) {
|
|
14591
|
+
if ((test.status === "failed" || test.status === "timedOut") && !seen.has(test.id)) {
|
|
14592
|
+
seen.add(test.id);
|
|
14593
|
+
result.push({ id: test.id, title: test.fullTitle });
|
|
14594
|
+
}
|
|
14595
|
+
}
|
|
14596
|
+
}
|
|
14597
|
+
}
|
|
14598
|
+
return result;
|
|
14599
|
+
}
|
|
14600
|
+
function collectCurrentStatuses(projects) {
|
|
14601
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
14602
|
+
for (const project of projects) {
|
|
14603
|
+
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
14604
|
+
for (const test of suite.tests) {
|
|
14605
|
+
statuses.set(test.id, test.status);
|
|
14606
|
+
}
|
|
14607
|
+
}
|
|
14608
|
+
}
|
|
14609
|
+
return statuses;
|
|
14610
|
+
}
|
|
14611
|
+
function buildFailedTestsFields(projects, redactor) {
|
|
14612
|
+
const redactedFailing = collectFailedTests(projects).map(
|
|
14613
|
+
(t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
|
|
14614
|
+
);
|
|
14615
|
+
const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
|
|
14616
|
+
return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
|
|
14617
|
+
}
|
|
14618
|
+
function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
|
|
14619
|
+
return computeRunDiff({
|
|
14620
|
+
currentFailing,
|
|
14621
|
+
currentStatuses: collectCurrentStatuses(projects),
|
|
14622
|
+
priorEntries,
|
|
14623
|
+
branch
|
|
14624
|
+
}) ?? void 0;
|
|
14625
|
+
}
|
|
14626
|
+
|
|
14324
14627
|
// src/live/LiveStreamer.ts
|
|
14325
14628
|
init_esm_shims();
|
|
14326
14629
|
var NETWORK_TIMEOUT_MS3 = 8e3;
|
|
@@ -14941,7 +15244,6 @@ function chunkToString2(chunk) {
|
|
|
14941
15244
|
}
|
|
14942
15245
|
|
|
14943
15246
|
// src/reporter.ts
|
|
14944
|
-
var DEFAULT_SERVER_URL2 = "https://reportforge.org";
|
|
14945
15247
|
var LICENSE_STALE_MARGIN_MS = 5 * 60 * 1e3;
|
|
14946
15248
|
var PdfReporter = class {
|
|
14947
15249
|
constructor(rawOptions = {}) {
|
|
@@ -14956,7 +15258,7 @@ var PdfReporter = class {
|
|
|
14956
15258
|
if (this.options.redact.enabled) {
|
|
14957
15259
|
this.redactor = new Redactor(this.options.redact);
|
|
14958
15260
|
}
|
|
14959
|
-
this.version = true ? "0.
|
|
15261
|
+
this.version = true ? "0.28.0" : "0.x";
|
|
14960
15262
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
14961
15263
|
this.licenseClient = new LicenseClient({
|
|
14962
15264
|
licenseKey: this.options.licenseKey,
|
|
@@ -15027,44 +15329,19 @@ var PdfReporter = class {
|
|
|
15027
15329
|
}
|
|
15028
15330
|
}
|
|
15029
15331
|
async onEnd(result) {
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
} catch (err) {
|
|
15044
|
-
logger.warn(`History write failed \u2014 trend data skipped: ${err.message}`);
|
|
15045
|
-
}
|
|
15046
|
-
}
|
|
15047
|
-
const reportData = this._buildReportData(collected, licenseInfo);
|
|
15048
|
-
runFailureAnalysis(reportData, this.options, this.cwd);
|
|
15049
|
-
reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
|
|
15050
|
-
if (this.redactor) {
|
|
15051
|
-
redactReportData(reportData, this.redactor);
|
|
15052
|
-
}
|
|
15053
|
-
let pdfPaths = [];
|
|
15054
|
-
try {
|
|
15055
|
-
pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
|
|
15056
|
-
if (this.options.open) {
|
|
15057
|
-
for (const pdfPath of pdfPaths) await this.openPdf(pdfPath);
|
|
15058
|
-
}
|
|
15059
|
-
} catch (err) {
|
|
15060
|
-
logger.error(`Failed to generate PDF report: ${err.message}`);
|
|
15061
|
-
if (process.env.RF_DEBUG) console.error(err);
|
|
15062
|
-
}
|
|
15063
|
-
if (this.options.notify) {
|
|
15064
|
-
const sender = new NotificationSender();
|
|
15065
|
-
await sender.sendAll(reportData, this.options.notify, pdfPaths);
|
|
15066
|
-
}
|
|
15067
|
-
maybePrintUpdateNotice(licenseInfo, this.version);
|
|
15332
|
+
await runReportPipeline({
|
|
15333
|
+
options: this.options,
|
|
15334
|
+
cwd: this.cwd,
|
|
15335
|
+
version: this.version,
|
|
15336
|
+
license: () => this.freshLicense(),
|
|
15337
|
+
collect: () => this._collectData(result),
|
|
15338
|
+
// Call-time cwd on purpose: pre-extraction behavior read process.cwd()
|
|
15339
|
+
// at onEnd for the package.json-name fallback (a mid-run chdir is then
|
|
15340
|
+
// honored), while history paths keep the construction-time this.cwd.
|
|
15341
|
+
resolveProjectName: () => resolveProjectNameFromPkg(process.cwd()),
|
|
15342
|
+
pdfGenerator: this.pdfGenerator,
|
|
15343
|
+
redactor: this.redactor
|
|
15344
|
+
});
|
|
15068
15345
|
}
|
|
15069
15346
|
_collectData(result) {
|
|
15070
15347
|
if (this.options.shardResults && this.options.shardResults.length > 0) {
|
|
@@ -15077,94 +15354,6 @@ var PdfReporter = class {
|
|
|
15077
15354
|
}
|
|
15078
15355
|
return this.dataCollector.finalize(result);
|
|
15079
15356
|
}
|
|
15080
|
-
async _appendTrend(collected, licenseInfo) {
|
|
15081
|
-
const historyPath = resolveHistoryPath(this.options, this.cwd);
|
|
15082
|
-
const hm = new HistoryManager(historyPath);
|
|
15083
|
-
const suffix = Math.random().toString(36).padEnd(8, "0").slice(2, 6);
|
|
15084
|
-
const redactor = this.redactor;
|
|
15085
|
-
const flakyTests = collectFlakyTests(collected.projects).map(
|
|
15086
|
-
(t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
|
|
15087
|
-
);
|
|
15088
|
-
const { redactedFailing, ...failedTestsFields } = buildFailedTestsFields(collected.projects, redactor);
|
|
15089
|
-
const entry = {
|
|
15090
|
-
runId: `${(/* @__PURE__ */ new Date()).toISOString()}-${suffix}`,
|
|
15091
|
-
timestamp: Date.now(),
|
|
15092
|
-
passRate: collected.stats.passRate,
|
|
15093
|
-
total: collected.stats.total,
|
|
15094
|
-
passed: collected.stats.passed,
|
|
15095
|
-
failed: collected.stats.failed,
|
|
15096
|
-
flaky: collected.stats.flaky,
|
|
15097
|
-
duration: collected.stats.duration,
|
|
15098
|
-
verdict: deriveVerdict(collected.stats),
|
|
15099
|
-
flakyTests,
|
|
15100
|
-
...failedTestsFields,
|
|
15101
|
-
branch: collected.environment.branch
|
|
15102
|
-
};
|
|
15103
|
-
const localEntries = hm.append(entry, this.options.historySize);
|
|
15104
|
-
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch);
|
|
15105
|
-
let trendEntries = localEntries;
|
|
15106
|
-
if (this.options.remoteHistory && licenseInfo.jwt) {
|
|
15107
|
-
const serverUrl = this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL2;
|
|
15108
|
-
const remote = await appendRemoteHistory({
|
|
15109
|
-
serverUrl,
|
|
15110
|
-
jwt: licenseInfo.jwt,
|
|
15111
|
-
projectId: buildProjectId(this.options.projectName, this.options.outputFile),
|
|
15112
|
-
branchHash: buildBranchHash(collected.environment.branch),
|
|
15113
|
-
entry
|
|
15114
|
-
});
|
|
15115
|
-
if (remote) {
|
|
15116
|
-
trendEntries = remote;
|
|
15117
|
-
logger.debug(`remote history: ${remote.length} entr${remote.length === 1 ? "y" : "ies"} for trend`);
|
|
15118
|
-
}
|
|
15119
|
-
}
|
|
15120
|
-
this._populateHistoryCharts(trendEntries, localEntries, collected);
|
|
15121
|
-
}
|
|
15122
|
-
_populateHistoryCharts(trendEntries, localEntries, collected) {
|
|
15123
|
-
if (trendEntries.length >= 2) {
|
|
15124
|
-
collected.charts.trend = {
|
|
15125
|
-
entries: trendEntries.map((e) => ({
|
|
15126
|
-
runId: e.runId,
|
|
15127
|
-
timestamp: e.timestamp,
|
|
15128
|
-
passRate: e.passRate,
|
|
15129
|
-
total: e.total,
|
|
15130
|
-
duration: e.duration,
|
|
15131
|
-
verdict: e.verdict
|
|
15132
|
-
})),
|
|
15133
|
-
delta: trendEntries[0].passRate - trendEntries[1].passRate
|
|
15134
|
-
};
|
|
15135
|
-
}
|
|
15136
|
-
if (this.options.flakinessTopN > 0) {
|
|
15137
|
-
const table = computeTopN(localEntries, this.options.flakinessTopN);
|
|
15138
|
-
if (table.length > 0) {
|
|
15139
|
-
collected.charts.flakinessTable = table;
|
|
15140
|
-
}
|
|
15141
|
-
}
|
|
15142
|
-
}
|
|
15143
|
-
_buildReportData(collected, licenseInfo) {
|
|
15144
|
-
const { projects, stats, failures, environment, charts, runDiff } = collected;
|
|
15145
|
-
const projectName = this.options.projectName ?? this.resolveProjectName();
|
|
15146
|
-
const firstTemplate = this.options.templatePath?.length ? "detailed" : Array.isArray(this.options.template) ? this.options.template[0] : this.options.template;
|
|
15147
|
-
return {
|
|
15148
|
-
meta: {
|
|
15149
|
-
title: this.options.reportTitle,
|
|
15150
|
-
generatedAt: /* @__PURE__ */ new Date(),
|
|
15151
|
-
template: firstTemplate,
|
|
15152
|
-
branding: {
|
|
15153
|
-
primaryColor: this.options.primaryColor,
|
|
15154
|
-
accentColor: this.options.accentColor,
|
|
15155
|
-
watermark: this.options.watermark
|
|
15156
|
-
},
|
|
15157
|
-
licenseInfo,
|
|
15158
|
-
projectName
|
|
15159
|
-
},
|
|
15160
|
-
stats,
|
|
15161
|
-
projects,
|
|
15162
|
-
failures,
|
|
15163
|
-
environment,
|
|
15164
|
-
charts,
|
|
15165
|
-
runDiff
|
|
15166
|
-
};
|
|
15167
|
-
}
|
|
15168
15357
|
printsToStdio() {
|
|
15169
15358
|
return false;
|
|
15170
15359
|
}
|
|
@@ -15220,7 +15409,7 @@ var PdfReporter = class {
|
|
|
15220
15409
|
setupLive(config) {
|
|
15221
15410
|
const live = this.options.live;
|
|
15222
15411
|
const runId = deriveRunId({ runId: live.runId });
|
|
15223
|
-
const serverUrl = live.serverUrl ?? this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ??
|
|
15412
|
+
const serverUrl = live.serverUrl ?? this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL;
|
|
15224
15413
|
const shard = config.shard ?? null;
|
|
15225
15414
|
this.liveShardKey = shardKeyOf(shard);
|
|
15226
15415
|
this.liveSteps = live.steps;
|
|
@@ -15292,121 +15481,7 @@ var PdfReporter = class {
|
|
|
15292
15481
|
await new NotificationSender().sendLiveStarted(notify, watchUrl, env?.branch);
|
|
15293
15482
|
}
|
|
15294
15483
|
}
|
|
15295
|
-
resolveProjectName() {
|
|
15296
|
-
try {
|
|
15297
|
-
const pkgPath = path15.resolve(process.cwd(), "package.json");
|
|
15298
|
-
const pkg = __require(pkgPath);
|
|
15299
|
-
return pkg.name ?? "Playwright Tests";
|
|
15300
|
-
} catch {
|
|
15301
|
-
return "Playwright Tests";
|
|
15302
|
-
}
|
|
15303
|
-
}
|
|
15304
|
-
async openPdf(pdfPath) {
|
|
15305
|
-
try {
|
|
15306
|
-
const { existsSync } = await import("fs");
|
|
15307
|
-
if (!existsSync(pdfPath)) {
|
|
15308
|
-
logger.warn(`open: true \u2014 PDF not found at ${pdfPath}; file may not have been written yet.`);
|
|
15309
|
-
return;
|
|
15310
|
-
}
|
|
15311
|
-
const { spawn } = await import("child_process");
|
|
15312
|
-
const platform = process.platform;
|
|
15313
|
-
const [cmd, args] = platform === "darwin" ? ["open", [pdfPath]] : platform === "win32" ? ["cmd", ["/c", "start", "", pdfPath]] : ["xdg-open", [pdfPath]];
|
|
15314
|
-
const child = spawn(cmd, args, { detached: true, stdio: "ignore", shell: false });
|
|
15315
|
-
child.on("error", (err) => {
|
|
15316
|
-
logger.warn(`open: true \u2014 could not open ${pdfPath}: ${err.message}`);
|
|
15317
|
-
});
|
|
15318
|
-
child.unref();
|
|
15319
|
-
if (platform === "win32") {
|
|
15320
|
-
child.on("close", (code) => {
|
|
15321
|
-
if (code !== 0 && code !== null) {
|
|
15322
|
-
logger.warn(
|
|
15323
|
-
`open: true \u2014 shell-open exited with code ${code}. No PDF viewer may be registered for .pdf files on this machine. File is at: ${pdfPath}`
|
|
15324
|
-
);
|
|
15325
|
-
}
|
|
15326
|
-
});
|
|
15327
|
-
}
|
|
15328
|
-
} catch (err) {
|
|
15329
|
-
logger.warn(`open: true \u2014 unexpected error: ${err.message}`);
|
|
15330
|
-
}
|
|
15331
|
-
}
|
|
15332
15484
|
};
|
|
15333
|
-
function deriveVerdict(stats) {
|
|
15334
|
-
if (stats.total === 0) return "unknown";
|
|
15335
|
-
if (stats.verdict === "interrupted" || stats.verdict === "timedout") return "failed";
|
|
15336
|
-
if (stats.failed === 0 && stats.timedOut === 0) {
|
|
15337
|
-
if (stats.passed === 0 && stats.flaky === 0) return "unknown";
|
|
15338
|
-
return "passed";
|
|
15339
|
-
}
|
|
15340
|
-
if (stats.failed / stats.total > 0.5) return "failed";
|
|
15341
|
-
if (stats.timedOut > 0) return "failed";
|
|
15342
|
-
if (stats.failed > 0) return "partial";
|
|
15343
|
-
return "unknown";
|
|
15344
|
-
}
|
|
15345
|
-
function resolveHistoryPath(options, cwd) {
|
|
15346
|
-
if (options.historyFile) {
|
|
15347
|
-
return path15.isAbsolute(options.historyFile) ? options.historyFile : path15.resolve(cwd, options.historyFile);
|
|
15348
|
-
}
|
|
15349
|
-
return path15.join(resolveProjectDir(options, cwd), "history.json");
|
|
15350
|
-
}
|
|
15351
|
-
function flattenSuiteNodes(suites) {
|
|
15352
|
-
return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);
|
|
15353
|
-
}
|
|
15354
|
-
function collectFlakyTests(projects) {
|
|
15355
|
-
const seen = /* @__PURE__ */ new Set();
|
|
15356
|
-
const result = [];
|
|
15357
|
-
for (const project of projects) {
|
|
15358
|
-
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
15359
|
-
for (const test of suite.tests) {
|
|
15360
|
-
if (test.status === "flaky" && !seen.has(test.id)) {
|
|
15361
|
-
seen.add(test.id);
|
|
15362
|
-
result.push({ id: test.id, title: test.fullTitle });
|
|
15363
|
-
}
|
|
15364
|
-
}
|
|
15365
|
-
}
|
|
15366
|
-
}
|
|
15367
|
-
return result;
|
|
15368
|
-
}
|
|
15369
|
-
function collectFailedTests(projects) {
|
|
15370
|
-
const seen = /* @__PURE__ */ new Set();
|
|
15371
|
-
const result = [];
|
|
15372
|
-
for (const project of projects) {
|
|
15373
|
-
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
15374
|
-
for (const test of suite.tests) {
|
|
15375
|
-
if ((test.status === "failed" || test.status === "timedOut") && !seen.has(test.id)) {
|
|
15376
|
-
seen.add(test.id);
|
|
15377
|
-
result.push({ id: test.id, title: test.fullTitle });
|
|
15378
|
-
}
|
|
15379
|
-
}
|
|
15380
|
-
}
|
|
15381
|
-
}
|
|
15382
|
-
return result;
|
|
15383
|
-
}
|
|
15384
|
-
function collectCurrentStatuses(projects) {
|
|
15385
|
-
const statuses = /* @__PURE__ */ new Map();
|
|
15386
|
-
for (const project of projects) {
|
|
15387
|
-
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
15388
|
-
for (const test of suite.tests) {
|
|
15389
|
-
statuses.set(test.id, test.status);
|
|
15390
|
-
}
|
|
15391
|
-
}
|
|
15392
|
-
}
|
|
15393
|
-
return statuses;
|
|
15394
|
-
}
|
|
15395
|
-
function buildFailedTestsFields(projects, redactor) {
|
|
15396
|
-
const redactedFailing = collectFailedTests(projects).map(
|
|
15397
|
-
(t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
|
|
15398
|
-
);
|
|
15399
|
-
const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
|
|
15400
|
-
return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
|
|
15401
|
-
}
|
|
15402
|
-
function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
|
|
15403
|
-
return computeRunDiff({
|
|
15404
|
-
currentFailing,
|
|
15405
|
-
currentStatuses: collectCurrentStatuses(projects),
|
|
15406
|
-
priorEntries,
|
|
15407
|
-
branch
|
|
15408
|
-
}) ?? void 0;
|
|
15409
|
-
}
|
|
15410
15485
|
|
|
15411
15486
|
// src/index.ts
|
|
15412
15487
|
function defineReporterConfig(options) {
|