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