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