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