@reportforge/playwright-pdf 0.28.0 → 0.30.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/dist/index.mjs CHANGED
@@ -1229,13 +1229,13 @@ var require_ast = __commonJS({
1229
1229
  helperExpression: function helperExpression(node) {
1230
1230
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
1231
1231
  },
1232
- scopedId: function scopedId(path16) {
1233
- return /^\.|this\b/.test(path16.original);
1232
+ scopedId: function scopedId(path17) {
1233
+ return /^\.|this\b/.test(path17.original);
1234
1234
  },
1235
1235
  // an ID is simple if it only has one part, and that part is not
1236
1236
  // `..` or `this`.
1237
- simpleId: function simpleId(path16) {
1238
- return path16.parts.length === 1 && !AST.helpers.scopedId(path16) && !path16.depth;
1237
+ simpleId: function simpleId(path17) {
1238
+ return path17.parts.length === 1 && !AST.helpers.scopedId(path17) && !path17.depth;
1239
1239
  }
1240
1240
  }
1241
1241
  };
@@ -2309,12 +2309,12 @@ var require_helpers2 = __commonJS({
2309
2309
  loc
2310
2310
  };
2311
2311
  }
2312
- function prepareMustache(path16, params, hash, open, strip, locInfo) {
2312
+ function prepareMustache(path17, params, hash, open, strip, locInfo) {
2313
2313
  var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
2314
2314
  var decorator = /\*/.test(open);
2315
2315
  return {
2316
2316
  type: decorator ? "Decorator" : "MustacheStatement",
2317
- path: path16,
2317
+ path: path17,
2318
2318
  params,
2319
2319
  hash,
2320
2320
  escaped,
@@ -2634,9 +2634,9 @@ var require_compiler = __commonJS({
2634
2634
  },
2635
2635
  DecoratorBlock: function DecoratorBlock(decorator) {
2636
2636
  var program = decorator.program && this.compileProgram(decorator.program);
2637
- var params = this.setupFullMustacheParams(decorator, program, void 0), path16 = decorator.path;
2637
+ var params = this.setupFullMustacheParams(decorator, program, void 0), path17 = decorator.path;
2638
2638
  this.useDecorators = true;
2639
- this.opcode("registerDecorator", params.length, path16.original);
2639
+ this.opcode("registerDecorator", params.length, path17.original);
2640
2640
  },
2641
2641
  PartialStatement: function PartialStatement(partial) {
2642
2642
  this.usePartial = true;
@@ -2700,46 +2700,46 @@ var require_compiler = __commonJS({
2700
2700
  }
2701
2701
  },
2702
2702
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
2703
- var path16 = sexpr.path, name = path16.parts[0], isBlock = program != null || inverse != null;
2704
- this.opcode("getContext", path16.depth);
2703
+ var path17 = sexpr.path, name = path17.parts[0], isBlock = program != null || inverse != null;
2704
+ this.opcode("getContext", path17.depth);
2705
2705
  this.opcode("pushProgram", program);
2706
2706
  this.opcode("pushProgram", inverse);
2707
- path16.strict = true;
2708
- this.accept(path16);
2707
+ path17.strict = true;
2708
+ this.accept(path17);
2709
2709
  this.opcode("invokeAmbiguous", name, isBlock);
2710
2710
  },
2711
2711
  simpleSexpr: function simpleSexpr(sexpr) {
2712
- var path16 = sexpr.path;
2713
- path16.strict = true;
2714
- this.accept(path16);
2712
+ var path17 = sexpr.path;
2713
+ path17.strict = true;
2714
+ this.accept(path17);
2715
2715
  this.opcode("resolvePossibleLambda");
2716
2716
  },
2717
2717
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
2718
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path16 = sexpr.path, name = path16.parts[0];
2718
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path17 = sexpr.path, name = path17.parts[0];
2719
2719
  if (this.options.knownHelpers[name]) {
2720
2720
  this.opcode("invokeKnownHelper", params.length, name);
2721
2721
  } else if (this.options.knownHelpersOnly) {
2722
2722
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
2723
2723
  } else {
2724
- path16.strict = true;
2725
- path16.falsy = true;
2726
- this.accept(path16);
2727
- this.opcode("invokeHelper", params.length, path16.original, _ast2["default"].helpers.simpleId(path16));
2724
+ path17.strict = true;
2725
+ path17.falsy = true;
2726
+ this.accept(path17);
2727
+ this.opcode("invokeHelper", params.length, path17.original, _ast2["default"].helpers.simpleId(path17));
2728
2728
  }
2729
2729
  },
2730
- PathExpression: function PathExpression(path16) {
2731
- this.addDepth(path16.depth);
2732
- this.opcode("getContext", path16.depth);
2733
- var name = path16.parts[0], scoped = _ast2["default"].helpers.scopedId(path16), blockParamId = !path16.depth && !scoped && this.blockParamIndex(name);
2730
+ PathExpression: function PathExpression(path17) {
2731
+ this.addDepth(path17.depth);
2732
+ this.opcode("getContext", path17.depth);
2733
+ var name = path17.parts[0], scoped = _ast2["default"].helpers.scopedId(path17), blockParamId = !path17.depth && !scoped && this.blockParamIndex(name);
2734
2734
  if (blockParamId) {
2735
- this.opcode("lookupBlockParam", blockParamId, path16.parts);
2735
+ this.opcode("lookupBlockParam", blockParamId, path17.parts);
2736
2736
  } else if (!name) {
2737
2737
  this.opcode("pushContext");
2738
- } else if (path16.data) {
2738
+ } else if (path17.data) {
2739
2739
  this.options.data = true;
2740
- this.opcode("lookupData", path16.depth, path16.parts, path16.strict);
2740
+ this.opcode("lookupData", path17.depth, path17.parts, path17.strict);
2741
2741
  } else {
2742
- this.opcode("lookupOnContext", path16.parts, path16.falsy, path16.strict, scoped);
2742
+ this.opcode("lookupOnContext", path17.parts, path17.falsy, path17.strict, scoped);
2743
2743
  }
2744
2744
  },
2745
2745
  StringLiteral: function StringLiteral(string) {
@@ -3095,16 +3095,16 @@ var require_util = __commonJS({
3095
3095
  }
3096
3096
  exports.urlGenerate = urlGenerate;
3097
3097
  function normalize(aPath) {
3098
- var path16 = aPath;
3098
+ var path17 = aPath;
3099
3099
  var url = urlParse(aPath);
3100
3100
  if (url) {
3101
3101
  if (!url.path) {
3102
3102
  return aPath;
3103
3103
  }
3104
- path16 = url.path;
3104
+ path17 = url.path;
3105
3105
  }
3106
- var isAbsolute = exports.isAbsolute(path16);
3107
- var parts = path16.split(/\/+/);
3106
+ var isAbsolute = exports.isAbsolute(path17);
3107
+ var parts = path17.split(/\/+/);
3108
3108
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
3109
3109
  part = parts[i];
3110
3110
  if (part === ".") {
@@ -3121,15 +3121,15 @@ var require_util = __commonJS({
3121
3121
  }
3122
3122
  }
3123
3123
  }
3124
- path16 = parts.join("/");
3125
- if (path16 === "") {
3126
- path16 = isAbsolute ? "/" : ".";
3124
+ path17 = parts.join("/");
3125
+ if (path17 === "") {
3126
+ path17 = isAbsolute ? "/" : ".";
3127
3127
  }
3128
3128
  if (url) {
3129
- url.path = path16;
3129
+ url.path = path17;
3130
3130
  return urlGenerate(url);
3131
3131
  }
3132
- return path16;
3132
+ return path17;
3133
3133
  }
3134
3134
  exports.normalize = normalize;
3135
3135
  function join(aRoot, aPath) {
@@ -5932,8 +5932,8 @@ var require_printer = __commonJS({
5932
5932
  return this.accept(sexpr.path) + " " + params + hash;
5933
5933
  };
5934
5934
  PrintVisitor.prototype.PathExpression = function(id) {
5935
- var path16 = id.parts.join("/");
5936
- return (id.data ? "@" : "") + "PATH:" + path16;
5935
+ var path17 = id.parts.join("/");
5936
+ return (id.data ? "@" : "") + "PATH:" + path17;
5937
5937
  };
5938
5938
  PrintVisitor.prototype.StringLiteral = function(string) {
5939
5939
  return '"' + string.value + '"';
@@ -6053,7 +6053,7 @@ var require_win32 = __commonJS({
6053
6053
  "node_modules/chrome-finder/lib/win32.js"(exports, module) {
6054
6054
  "use strict";
6055
6055
  init_esm_shims();
6056
- var path16 = __require("path");
6056
+ var path17 = __require("path");
6057
6057
  var { canAccess } = require_util2();
6058
6058
  function win32() {
6059
6059
  const installations = [];
@@ -6067,7 +6067,7 @@ var require_win32 = __commonJS({
6067
6067
  const prefixes = [process.env.LOCALAPPDATA, process.env.PROGRAMFILES, process.env["PROGRAMFILES(X86)"]];
6068
6068
  prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
6069
6069
  if (prefix) {
6070
- const chromePath = path16.join(prefix, suffix);
6070
+ const chromePath = path17.join(prefix, suffix);
6071
6071
  if (canAccess(chromePath)) {
6072
6072
  installations.push(chromePath);
6073
6073
  }
@@ -6085,7 +6085,7 @@ var require_darwin = __commonJS({
6085
6085
  "use strict";
6086
6086
  init_esm_shims();
6087
6087
  var { execSync: execSync3 } = __require("child_process");
6088
- var path16 = __require("path");
6088
+ var path17 = __require("path");
6089
6089
  var { canAccess, newLineRegex, sort } = require_util2();
6090
6090
  function darwin() {
6091
6091
  const suffixes = ["/Contents/MacOS/Google Chrome Canary", "/Contents/MacOS/Google Chrome", "/Contents/MacOS/Chromium"];
@@ -6095,7 +6095,7 @@ var require_darwin = __commonJS({
6095
6095
  ${LSREGISTER} -dump | grep -E -i -o '/.+(google chrome( canary)?|chromium)\\.app(\\s|$)' | grep -E -v 'Caches|TimeMachine|Temporary|/Volumes|\\.Trash'
6096
6096
  `).toString().split(newLineRegex).forEach((inst) => {
6097
6097
  suffixes.forEach((suffix) => {
6098
- const execPath = path16.join(inst.trim(), suffix);
6098
+ const execPath = path17.join(inst.trim(), suffix);
6099
6099
  if (canAccess(execPath)) {
6100
6100
  installations.push(execPath);
6101
6101
  }
@@ -6124,7 +6124,7 @@ var require_linux = __commonJS({
6124
6124
  "use strict";
6125
6125
  init_esm_shims();
6126
6126
  var { execSync: execSync3, execFileSync } = __require("child_process");
6127
- var path16 = __require("path");
6127
+ var path17 = __require("path");
6128
6128
  var fs16 = __require("fs");
6129
6129
  var { canAccess, sort, isExecutable, newLineRegex } = require_util2();
6130
6130
  function findChromeExecutablesForLinuxDesktop(folder) {
@@ -6142,7 +6142,7 @@ var require_linux = __commonJS({
6142
6142
  function linux() {
6143
6143
  let installations = [];
6144
6144
  const desktopInstallationFolders = [
6145
- path16.join(__require("os").homedir(), ".local/share/applications/"),
6145
+ path17.join(__require("os").homedir(), ".local/share/applications/"),
6146
6146
  "/usr/share/applications/"
6147
6147
  ];
6148
6148
  desktopInstallationFolders.forEach((folder) => {
@@ -6750,8 +6750,8 @@ function getErrorMap() {
6750
6750
  // node_modules/zod/v3/helpers/parseUtil.js
6751
6751
  init_esm_shims();
6752
6752
  var makeIssue = (params) => {
6753
- const { data, path: path16, errorMaps, issueData } = params;
6754
- const fullPath = [...path16, ...issueData.path || []];
6753
+ const { data, path: path17, errorMaps, issueData } = params;
6754
+ const fullPath = [...path17, ...issueData.path || []];
6755
6755
  const fullIssue = {
6756
6756
  ...issueData,
6757
6757
  path: fullPath
@@ -6871,11 +6871,11 @@ var errorUtil;
6871
6871
 
6872
6872
  // node_modules/zod/v3/types.js
6873
6873
  var ParseInputLazyPath = class {
6874
- constructor(parent, value, path16, key) {
6874
+ constructor(parent, value, path17, key) {
6875
6875
  this._cachedPath = [];
6876
6876
  this.parent = parent;
6877
6877
  this.data = value;
6878
- this._path = path16;
6878
+ this._path = path17;
6879
6879
  this._key = key;
6880
6880
  }
6881
6881
  get path() {
@@ -10331,6 +10331,7 @@ var DEFAULT_OPTIONS = {
10331
10331
  historySize: 10,
10332
10332
  showTrend: true,
10333
10333
  flakinessTopN: 5,
10334
+ quarantineThreshold: 40,
10334
10335
  slowTestThreshold: 10,
10335
10336
  // Ticket-ID shape: @ODP-5328, REQ-001, JIRA-123. Tags matching this land in
10336
10337
  // the Requirements Traceability matrix; the rest fall to the tag summary.
@@ -10356,6 +10357,7 @@ var SECTION_KEYS = [
10356
10357
  "defectLog",
10357
10358
  "briefBand",
10358
10359
  "runDiff",
10360
+ "browserMatrix",
10359
10361
  // display modifiers
10360
10362
  "passRate",
10361
10363
  "fullEnvironment",
@@ -10380,6 +10382,7 @@ var SECTION_DEFAULTS = {
10380
10382
  defectLog: false,
10381
10383
  briefBand: false,
10382
10384
  runDiff: true,
10385
+ browserMatrix: false,
10383
10386
  passRate: true,
10384
10387
  fullEnvironment: false,
10385
10388
  retries: true,
@@ -10402,6 +10405,7 @@ var SECTION_DEFAULTS = {
10402
10405
  defectLog: true,
10403
10406
  briefBand: false,
10404
10407
  runDiff: true,
10408
+ browserMatrix: true,
10405
10409
  passRate: true,
10406
10410
  fullEnvironment: true,
10407
10411
  retries: true,
@@ -10429,6 +10433,7 @@ var SECTION_DEFAULTS = {
10429
10433
  defectLog: false,
10430
10434
  briefBand: true,
10431
10435
  runDiff: true,
10436
+ browserMatrix: false,
10432
10437
  passRate: true,
10433
10438
  fullEnvironment: false,
10434
10439
  retries: false,
@@ -10581,6 +10586,10 @@ var ReporterOptionsSchema = external_exports.object({
10581
10586
  // local history file is wiped every run. Aggregate numbers only.
10582
10587
  remoteHistory: external_exports.boolean().default(false),
10583
10588
  flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
10589
+ quarantineThreshold: external_exports.number().int().min(0, "quarantineThreshold must be between 0 and 100").max(100, "quarantineThreshold must be between 0 and 100").optional().default(DEFAULT_OPTIONS.quarantineThreshold),
10590
+ evidenceUrlTemplate: external_exports.string().refine((s) => s.includes("{path}"), {
10591
+ message: "evidenceUrlTemplate must contain the {path} token"
10592
+ }).optional(),
10584
10593
  slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
10585
10594
  requirementTagPattern: external_exports.string().refine((v) => {
10586
10595
  try {
@@ -11297,7 +11306,7 @@ var isTopLevel = (flat, f) => !f.ancestors.some((a) => {
11297
11306
  var testStepDepth = (flat, f) => f.ancestors.reduce((n, a) => n + (flat[a].category === "test.step" ? 1 : 0), 0);
11298
11307
  function flatten(steps) {
11299
11308
  const out = [];
11300
- const walk = (nodes, depth, ancestors) => {
11309
+ const walk2 = (nodes, depth, ancestors) => {
11301
11310
  for (const s of nodes ?? []) {
11302
11311
  const index = out.length;
11303
11312
  out.push({
@@ -11307,10 +11316,10 @@ function flatten(steps) {
11307
11316
  hasError: s.error != null,
11308
11317
  ancestors
11309
11318
  });
11310
- if (s.steps?.length) walk(s.steps, depth + 1, [...ancestors, index]);
11319
+ if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
11311
11320
  }
11312
11321
  };
11313
- walk(steps ?? [], 0, []);
11322
+ walk2(steps ?? [], 0, []);
11314
11323
  return out;
11315
11324
  }
11316
11325
  function materialize(n, isFailing, depth) {
@@ -11918,7 +11927,7 @@ var ShardMerger = class {
11918
11927
  merge(filePaths) {
11919
11928
  if (filePaths.length === 0) throw new Error("shardResults: no valid JSON shard files found");
11920
11929
  const resolved = this.resolveShardPaths(filePaths);
11921
- const reports = this.loadReports(resolved);
11930
+ const reports = this.loadReports(resolved).map((r) => this.normalizeReport(r));
11922
11931
  const projects = this.mergeProjects(reports);
11923
11932
  const stats = this.mergeStats(reports, projects);
11924
11933
  const failures = sortBySeverity(this.extractFailures(reports));
@@ -11956,6 +11965,49 @@ var ShardMerger = class {
11956
11965
  return report;
11957
11966
  });
11958
11967
  }
11968
+ /**
11969
+ * Two top-level shapes reach the merger. Our language adapters (Python,
11970
+ * .NET) and the engine fixtures emit PROJECT wrapper suites (no `file`,
11971
+ * children are the file suites). Playwright's own --reporter=json emits
11972
+ * FILE suites at top level and hangs the project on each
11973
+ * spec.tests[].projectName instead. Everything downstream (mergeProjects,
11974
+ * extractFailures) assumes the wrapper shape, so real Playwright reports
11975
+ * are pivoted into it here: one wrapper per project, file trees filtered
11976
+ * to that project's tests. An unnamed project ('') renders as 'default',
11977
+ * matching the live reporter path.
11978
+ */
11979
+ normalizeReport(report) {
11980
+ if (report.suites.every((s) => s.file === void 0)) return report;
11981
+ const wrappers = this.collectProjectNames(report).map((name) => ({
11982
+ title: name === "" ? "default" : name,
11983
+ specs: [],
11984
+ suites: report.suites.map((fileSuite) => this.filterSuiteForProject(fileSuite, name)).filter((s) => s !== null)
11985
+ })).filter((w) => w.suites.length > 0);
11986
+ return { ...report, suites: wrappers };
11987
+ }
11988
+ /** Config order first, then any project discovered only on the tests. */
11989
+ collectProjectNames(report) {
11990
+ const names = [];
11991
+ const push = (n) => {
11992
+ if (!names.includes(n)) names.push(n);
11993
+ };
11994
+ for (const p of report.config?.projects ?? []) push(p.name ?? "");
11995
+ const walk2 = (suite) => {
11996
+ for (const spec of suite.specs ?? []) for (const t of spec.tests ?? []) push(t.projectName ?? "");
11997
+ for (const child of suite.suites ?? []) walk2(child);
11998
+ };
11999
+ for (const s of report.suites) walk2(s);
12000
+ return names.length > 0 ? names : [""];
12001
+ }
12002
+ filterSuiteForProject(suite, project) {
12003
+ const specs = (suite.specs ?? []).map((spec) => ({
12004
+ ...spec,
12005
+ tests: (spec.tests ?? []).filter((t) => (t.projectName ?? "") === project)
12006
+ })).filter((spec) => spec.tests.length > 0);
12007
+ const children = (suite.suites ?? []).map((child) => this.filterSuiteForProject(child, project)).filter((c) => c !== null);
12008
+ if (specs.length === 0 && children.length === 0) return null;
12009
+ return { ...suite, specs, suites: children };
12010
+ }
11959
12011
  mergeProjects(reports) {
11960
12012
  const projectMap = /* @__PURE__ */ new Map();
11961
12013
  for (const report of reports) {
@@ -13327,7 +13379,7 @@ var Redactor = class {
13327
13379
 
13328
13380
  // src/pipeline/orchestrator.ts
13329
13381
  init_esm_shims();
13330
- import path15 from "path";
13382
+ import path16 from "path";
13331
13383
 
13332
13384
  // src/license/update-notice.ts
13333
13385
  init_esm_shims();
@@ -13736,6 +13788,118 @@ function sourceTally(classified) {
13736
13788
  return ["rule", "retry", "local", "base"].filter((s) => counts.has(s)).map((s) => `${counts.get(s)} ${s}`).join(" / ");
13737
13789
  }
13738
13790
 
13791
+ // src/collector/BrowserMatrix.ts
13792
+ init_esm_shims();
13793
+ var DEFAULT_ROW_CAP = 40;
13794
+ function buildBrowserMatrix(projects, cap = DEFAULT_ROW_CAP) {
13795
+ if (projects.length < 2) return void 0;
13796
+ let projectNames = projects.map((p, i) => p.name || `project ${i + 1}`);
13797
+ const byTest = /* @__PURE__ */ new Map();
13798
+ projects.forEach((project, column) => {
13799
+ walk(project.suites, [], (path17, test) => {
13800
+ const key = [...path17, test.title].join(" > ");
13801
+ let statuses = byTest.get(key);
13802
+ if (!statuses) {
13803
+ statuses = projectNames.map(() => null);
13804
+ byTest.set(key, statuses);
13805
+ }
13806
+ statuses[column] = test.status;
13807
+ });
13808
+ });
13809
+ const divergent = [];
13810
+ let identicalCount = 0;
13811
+ const contributing = /* @__PURE__ */ new Set();
13812
+ for (const [title, statuses] of byTest) {
13813
+ const present = statuses.filter((s) => s !== null);
13814
+ if (present.length < 2) continue;
13815
+ statuses.forEach((s, i) => {
13816
+ if (s !== null) contributing.add(i);
13817
+ });
13818
+ if (present.every((s) => s === present[0])) {
13819
+ identicalCount++;
13820
+ } else {
13821
+ divergent.push({ title, statuses });
13822
+ }
13823
+ }
13824
+ if (divergent.length === 0 && identicalCount === 0) return void 0;
13825
+ if (contributing.size < projectNames.length) {
13826
+ const keep = [...contributing].sort((a, b) => a - b);
13827
+ projectNames = keep.map((i) => projectNames[i]);
13828
+ for (const row of divergent) row.statuses = keep.map((i) => row.statuses[i]);
13829
+ }
13830
+ divergent.sort((a, b) => severityRank(b) - severityRank(a) || a.title.localeCompare(b.title));
13831
+ return {
13832
+ projects: projectNames,
13833
+ rows: divergent.slice(0, cap),
13834
+ divergentCount: divergent.length,
13835
+ identicalCount,
13836
+ truncated: divergent.length > cap,
13837
+ hasFailures: divergent.some((r) => severityRank(r) > 0)
13838
+ };
13839
+ }
13840
+ function severityRank(row) {
13841
+ return row.statuses.some((s) => s === "failed" || s === "timedOut") ? 1 : 0;
13842
+ }
13843
+ function walk(suites, path17, visit) {
13844
+ for (const suite of suites) {
13845
+ const next = [...path17, suite.title];
13846
+ for (const test of suite.tests) visit(next, test);
13847
+ walk(suite.suites, next, visit);
13848
+ }
13849
+ }
13850
+
13851
+ // src/utils/evidence-links.ts
13852
+ init_esm_shims();
13853
+ import path14 from "path";
13854
+ import qrcodeFactory from "qrcode-generator";
13855
+ function applyEvidenceUrls(data, template, cwd) {
13856
+ let warned = false;
13857
+ const warnOnce = (err) => {
13858
+ if (warned) return;
13859
+ warned = true;
13860
+ const message = err instanceof Error ? err.message : String(err);
13861
+ logger.warn(`evidenceUrlTemplate: link generation failed for at least one artifact \u2014 ${message}`);
13862
+ };
13863
+ for (const failure of data.failures) {
13864
+ const evidence = failure.execution?.evidence;
13865
+ if (!evidence) continue;
13866
+ if (evidence.tracePath) {
13867
+ try {
13868
+ evidence.traceUrl = expandTemplate(template, relativizeArtifact(evidence.tracePath, cwd));
13869
+ evidence.traceQr = qrDataUri(evidence.traceUrl);
13870
+ } catch (err) {
13871
+ warnOnce(err);
13872
+ }
13873
+ }
13874
+ if (evidence.videoPath) {
13875
+ try {
13876
+ evidence.videoUrl = expandTemplate(template, relativizeArtifact(evidence.videoPath, cwd));
13877
+ evidence.videoQr = qrDataUri(evidence.videoUrl);
13878
+ } catch (err) {
13879
+ warnOnce(err);
13880
+ }
13881
+ }
13882
+ }
13883
+ }
13884
+ function relativizeArtifact(artifactPath, cwd) {
13885
+ if (!path14.isAbsolute(artifactPath)) return artifactPath;
13886
+ const rel = path14.relative(cwd, artifactPath);
13887
+ if (rel === ".." || rel.startsWith(`..${path14.sep}`) || path14.isAbsolute(rel)) return artifactPath;
13888
+ return rel;
13889
+ }
13890
+ function expandTemplate(template, artifactPath) {
13891
+ const normalized = artifactPath.replace(/\\/g, "/");
13892
+ const encoded = normalized.split("/").map((seg) => encodeURIComponent(seg)).join("/");
13893
+ return template.split("{path}").join(encoded);
13894
+ }
13895
+ function qrDataUri(url) {
13896
+ const qr = qrcodeFactory(0, "M");
13897
+ qr.addData(url);
13898
+ qr.make();
13899
+ const svg = qr.createSvgTag({ cellSize: 2, margin: 2, scalable: true });
13900
+ return `data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`;
13901
+ }
13902
+
13739
13903
  // src/redact/redact-report.ts
13740
13904
  init_esm_shims();
13741
13905
  function redactReportData(data, r) {
@@ -13766,6 +13930,10 @@ function redactReportData(data, r) {
13766
13930
  data.briefSentence = rd(data.briefSentence);
13767
13931
  data.charts.suiteResults.forEach((s) => s.label = r.redactText(s.label));
13768
13932
  data.charts.flakinessTable?.forEach((row) => row.title = r.redactText(row.title));
13933
+ if (data.browserMatrix) {
13934
+ data.browserMatrix.projects = data.browserMatrix.projects.map((p) => r.redactText(p));
13935
+ data.browserMatrix.rows.forEach((row) => row.title = r.redactText(row.title));
13936
+ }
13769
13937
  data.environment.buildUrl = r.redactText(data.environment.buildUrl);
13770
13938
  }
13771
13939
  function redactStep(s, r) {
@@ -13788,7 +13956,7 @@ function redactSuite(s, r) {
13788
13956
  // src/history/HistoryManager.ts
13789
13957
  init_esm_shims();
13790
13958
  import fs15 from "fs";
13791
- import path14 from "path";
13959
+ import path15 from "path";
13792
13960
  var HistoryManager = class {
13793
13961
  constructor(filePath) {
13794
13962
  this.filePath = filePath;
@@ -13809,7 +13977,7 @@ var HistoryManager = class {
13809
13977
  }
13810
13978
  append(entry, maxSize) {
13811
13979
  try {
13812
- fs15.mkdirSync(path14.dirname(this.filePath), { recursive: true });
13980
+ fs15.mkdirSync(path15.dirname(this.filePath), { recursive: true });
13813
13981
  const next = {
13814
13982
  version: 1,
13815
13983
  entries: [entry, ...this.read()].slice(0, maxSize)
@@ -13897,7 +14065,7 @@ function splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses
13897
14065
  return { fixed, noLongerRun };
13898
14066
  }
13899
14067
  function computeRunDiff(args) {
13900
- const { currentFailing, currentStatuses, priorEntries, branch } = args;
14068
+ const { currentFailing, currentFlaky = [], currentStatuses, priorEntries, branch } = args;
13901
14069
  const baseline = priorEntries.find((e) => e.branch === branch && Array.isArray(e.failedTests));
13902
14070
  if (!baseline) return null;
13903
14071
  const baselineFailed = /* @__PURE__ */ new Map();
@@ -13906,7 +14074,15 @@ function computeRunDiff(args) {
13906
14074
  }
13907
14075
  const { newFailures, stillFailing, currentFailedIds } = splitCurrentFailing(currentFailing, baselineFailed);
13908
14076
  const { fixed, noLongerRun } = splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses);
13909
- for (const bucket of [newFailures, stillFailing, fixed, noLongerRun]) {
14077
+ const baselineFlakyIds = new Set((baseline.flakyTests ?? []).map((t) => t.id));
14078
+ const newlyFlaky = [];
14079
+ const seenFlaky = /* @__PURE__ */ new Set();
14080
+ for (const item of currentFlaky) {
14081
+ if (seenFlaky.has(item.id) || baselineFlakyIds.has(item.id)) continue;
14082
+ seenFlaky.add(item.id);
14083
+ newlyFlaky.push(item);
14084
+ }
14085
+ for (const bucket of [newFailures, stillFailing, fixed, noLongerRun, newlyFlaky]) {
13910
14086
  bucket.sort((a, b) => a.title.localeCompare(b.title));
13911
14087
  }
13912
14088
  return {
@@ -13914,6 +14090,7 @@ function computeRunDiff(args) {
13914
14090
  stillFailing,
13915
14091
  fixed,
13916
14092
  noLongerRun,
14093
+ newlyFlaky,
13917
14094
  baselineRunId: baseline.runId,
13918
14095
  baselineTimestamp: baseline.timestamp,
13919
14096
  baselineTruncated: baseline.failedTestsTruncated === true
@@ -13922,10 +14099,11 @@ function computeRunDiff(args) {
13922
14099
 
13923
14100
  // src/history/FlakinessAnalyser.ts
13924
14101
  init_esm_shims();
13925
- function computeTopN(entries, topN) {
13926
- if (topN === 0) return [];
14102
+ var QUARANTINE_MIN_RUNS = 3;
14103
+ function computeTopN(entries, topN, quarantineThreshold = 0) {
14104
+ if (topN === 0) return { rows: [], quarantineTotal: 0 };
13927
14105
  const qualifying = entries.filter((e) => Array.isArray(e.flakyTests));
13928
- if (qualifying.length === 0) return [];
14106
+ if (qualifying.length === 0) return { rows: [], quarantineTotal: 0 };
13929
14107
  const map = /* @__PURE__ */ new Map();
13930
14108
  qualifying.forEach((entry, idx) => {
13931
14109
  const seen = /* @__PURE__ */ new Set();
@@ -13943,17 +14121,26 @@ function computeTopN(entries, topN) {
13943
14121
  const originalIdx = qualifying.length - 1 - reversedIdx;
13944
14122
  return hits.has(originalIdx);
13945
14123
  });
14124
+ let streak = 0;
14125
+ while (streak < totalRuns && hits.has(streak)) streak++;
14126
+ const flakeRate = Math.round(flakeCount / totalRuns * 100);
13946
14127
  return {
13947
14128
  title,
13948
- flakeRate: Math.round(flakeCount / totalRuns * 100),
14129
+ flakeRate,
13949
14130
  flakeCount,
13950
14131
  totalRuns,
13951
- sparkline
14132
+ sparkline,
14133
+ streak,
14134
+ quarantine: quarantineThreshold > 0 && totalRuns >= QUARANTINE_MIN_RUNS && flakeRate >= quarantineThreshold
13952
14135
  };
13953
14136
  });
13954
- return rows.sort(
13955
- (a, b) => b.flakeRate - a.flakeRate || b.flakeCount - a.flakeCount || a.title.localeCompare(b.title)
13956
- ).slice(0, topN);
14137
+ const quarantineTotal = rows.filter((r) => r.quarantine).length;
14138
+ return {
14139
+ rows: rows.sort(
14140
+ (a, b) => b.flakeRate - a.flakeRate || b.flakeCount - a.flakeCount || a.title.localeCompare(b.title)
14141
+ ).slice(0, topN),
14142
+ quarantineTotal
14143
+ };
13957
14144
  }
13958
14145
 
13959
14146
  // src/notify/index.ts
@@ -14396,6 +14583,9 @@ async function runReportPipeline(hooks) {
14396
14583
  }
14397
14584
  }
14398
14585
  const reportData = buildReportData(collected, licenseInfo, hooks);
14586
+ if (options.evidenceUrlTemplate) {
14587
+ applyEvidenceUrls(reportData, options.evidenceUrlTemplate, hooks.cwd);
14588
+ }
14399
14589
  runFailureAnalysis(reportData, options, hooks.cwd);
14400
14590
  reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
14401
14591
  if (hooks.redactor) {
@@ -14444,7 +14634,7 @@ async function appendTrend(collected, licenseInfo, hooks) {
14444
14634
  branch: collected.environment.branch
14445
14635
  };
14446
14636
  const localEntries = hm.append(entry, options.historySize);
14447
- collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch);
14637
+ collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch, flakyTests);
14448
14638
  let trendEntries = localEntries;
14449
14639
  if (options.remoteHistory && licenseInfo.jwt) {
14450
14640
  const serverUrl = options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL;
@@ -14477,9 +14667,17 @@ function populateHistoryCharts(trendEntries, localEntries, collected, options) {
14477
14667
  };
14478
14668
  }
14479
14669
  if (options.flakinessTopN > 0) {
14480
- const table = computeTopN(localEntries, options.flakinessTopN);
14481
- if (table.length > 0) {
14482
- collected.charts.flakinessTable = table;
14670
+ const { rows, quarantineTotal } = computeTopN(
14671
+ localEntries,
14672
+ options.flakinessTopN,
14673
+ options.quarantineThreshold
14674
+ );
14675
+ if (rows.length > 0) {
14676
+ collected.charts.flakinessTable = rows;
14677
+ if (quarantineTotal > 0) {
14678
+ collected.charts.flakinessQuarantineTotal = quarantineTotal;
14679
+ collected.charts.flakinessQuarantineThreshold = options.quarantineThreshold;
14680
+ }
14483
14681
  }
14484
14682
  }
14485
14683
  }
@@ -14506,12 +14704,13 @@ function buildReportData(collected, licenseInfo, hooks) {
14506
14704
  failures,
14507
14705
  environment,
14508
14706
  charts,
14509
- runDiff
14707
+ runDiff,
14708
+ browserMatrix: buildBrowserMatrix(projects)
14510
14709
  };
14511
14710
  }
14512
14711
  function resolveProjectNameFromPkg(cwd) {
14513
14712
  try {
14514
- const pkgPath = path15.resolve(cwd, "package.json");
14713
+ const pkgPath = path16.resolve(cwd, "package.json");
14515
14714
  const pkg = __require(pkgPath);
14516
14715
  return pkg.name ?? "Playwright Tests";
14517
14716
  } catch {
@@ -14560,9 +14759,9 @@ function deriveVerdict(stats) {
14560
14759
  }
14561
14760
  function resolveHistoryPath(options, cwd) {
14562
14761
  if (options.historyFile) {
14563
- return path15.isAbsolute(options.historyFile) ? options.historyFile : path15.resolve(cwd, options.historyFile);
14762
+ return path16.isAbsolute(options.historyFile) ? options.historyFile : path16.resolve(cwd, options.historyFile);
14564
14763
  }
14565
- return path15.join(resolveProjectDir(options, cwd), "history.json");
14764
+ return path16.join(resolveProjectDir(options, cwd), "history.json");
14566
14765
  }
14567
14766
  function flattenSuiteNodes(suites) {
14568
14767
  return suites.flatMap((s) => [s, ...flattenSuiteNodes(s.suites)]);
@@ -14615,9 +14814,10 @@ function buildFailedTestsFields(projects, redactor) {
14615
14814
  const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
14616
14815
  return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
14617
14816
  }
14618
- function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
14817
+ function resolveRunDiff(projects, currentFailing, priorEntries, branch, currentFlaky) {
14619
14818
  return computeRunDiff({
14620
14819
  currentFailing,
14820
+ currentFlaky,
14621
14821
  currentStatuses: collectCurrentStatuses(projects),
14622
14822
  priorEntries,
14623
14823
  branch
@@ -14981,7 +15181,7 @@ var MAX_ERR_SNIPPET = 4096;
14981
15181
  var isHook = (c) => c === "hook" || c === "fixture";
14982
15182
  function flatten2(steps) {
14983
15183
  const out = [];
14984
- const walk = (nodes, depth, ancestors) => {
15184
+ const walk2 = (nodes, depth, ancestors) => {
14985
15185
  for (const s of nodes ?? []) {
14986
15186
  const index = out.length;
14987
15187
  out.push({
@@ -14991,10 +15191,10 @@ function flatten2(steps) {
14991
15191
  hasError: s.error != null,
14992
15192
  ancestors
14993
15193
  });
14994
- if (s.steps?.length) walk(s.steps, depth + 1, [...ancestors, index]);
15194
+ if (s.steps?.length) walk2(s.steps, depth + 1, [...ancestors, index]);
14995
15195
  }
14996
15196
  };
14997
- walk(steps ?? [], 0, []);
15197
+ walk2(steps ?? [], 0, []);
14998
15198
  return out;
14999
15199
  }
15000
15200
  var inHookSubtree2 = (flat, f) => isHook(f.category) || f.ancestors.some((a) => isHook(flat[a].category));
@@ -15137,9 +15337,9 @@ var MAX_FILE_CHARS = 300;
15137
15337
  var MAX_PROJECT_CHARS = 100;
15138
15338
  var MAX_SUITE_SEGMENTS = 10;
15139
15339
  function suiteIdentity(test) {
15140
- const path16 = typeof test.titlePath === "function" ? test.titlePath() : [];
15141
- if (!Array.isArray(path16) || path16.length < 4) return {};
15142
- const [, project, file, ...rest] = path16;
15340
+ const path17 = typeof test.titlePath === "function" ? test.titlePath() : [];
15341
+ if (!Array.isArray(path17) || path17.length < 4) return {};
15342
+ const [, project, file, ...rest] = path17;
15143
15343
  const describes = rest.slice(0, -1);
15144
15344
  const out = {};
15145
15345
  if (typeof file === "string" && file) {
@@ -15258,7 +15458,7 @@ var PdfReporter = class {
15258
15458
  if (this.options.redact.enabled) {
15259
15459
  this.redactor = new Redactor(this.options.redact);
15260
15460
  }
15261
- this.version = true ? "0.28.0" : "0.x";
15461
+ this.version = true ? "0.30.0" : "0.x";
15262
15462
  logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
15263
15463
  this.licenseClient = new LicenseClient({
15264
15464
  licenseKey: this.options.licenseKey,