@reportforge/playwright-pdf 0.24.0 → 0.26.1

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.
Files changed (31) hide show
  1. package/README.md +9 -6
  2. package/ci-templates/azure-devops/azure-pipelines.yml +58 -58
  3. package/ci-templates/github-actions/basic-workflow.yml +44 -44
  4. package/ci-templates/gitlab-ci/.gitlab-ci.yml +42 -42
  5. package/ci-templates/jenkins/Jenkinsfile +54 -54
  6. package/dist/cli/index.js +24 -1
  7. package/dist/index.d.mts +9 -3
  8. package/dist/index.d.ts +9 -3
  9. package/dist/index.js +407 -44
  10. package/dist/index.mjs +407 -44
  11. package/dist/templates/layouts/detailed.hbs +43 -41
  12. package/dist/templates/layouts/executive.hbs +43 -41
  13. package/dist/templates/layouts/minimal.hbs +43 -41
  14. package/dist/templates/partials/analysis-oneliner.hbs +3 -3
  15. package/dist/templates/partials/chart-summary-grid.hbs +40 -40
  16. package/dist/templates/partials/chart-trend-card.hbs +16 -16
  17. package/dist/templates/partials/charts.hbs +433 -433
  18. package/dist/templates/partials/ci-environment.hbs +56 -56
  19. package/dist/templates/partials/cover-page.hbs +48 -48
  20. package/dist/templates/partials/defect-log.hbs +79 -79
  21. package/dist/templates/partials/executive-hero-summary.hbs +66 -66
  22. package/dist/templates/partials/executive-summary.hbs +78 -78
  23. package/dist/templates/partials/failure-card.hbs +35 -35
  24. package/dist/templates/partials/head.hbs +17 -17
  25. package/dist/templates/partials/release-gate.hbs +84 -84
  26. package/dist/templates/partials/requirements-matrix.hbs +51 -51
  27. package/dist/templates/partials/run-diff.hbs +41 -0
  28. package/dist/templates/partials/suite-breakdown.hbs +89 -89
  29. package/dist/templates/partials/watermark.hbs +42 -42
  30. package/dist/templates/styles/base.css +19 -1
  31. package/package.json +1 -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,
@@ -13783,6 +13787,9 @@ function buildBranchHash(branch) {
13783
13787
  async function appendRemoteHistory(opts) {
13784
13788
  const numericEntry = { ...opts.entry };
13785
13789
  delete numericEntry.flakyTests;
13790
+ delete numericEntry.failedTests;
13791
+ delete numericEntry.failedTestsTruncated;
13792
+ delete numericEntry.branch;
13786
13793
  const controller = new AbortController();
13787
13794
  const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS2);
13788
13795
  try {
@@ -13812,6 +13819,58 @@ async function appendRemoteHistory(opts) {
13812
13819
  }
13813
13820
  }
13814
13821
 
13822
+ // src/history/types.ts
13823
+ init_cjs_shims();
13824
+ var FAILED_TESTS_CAP = 300;
13825
+
13826
+ // src/history/RunDiff.ts
13827
+ init_cjs_shims();
13828
+ function splitCurrentFailing(currentFailing, baselineFailed) {
13829
+ const currentFailedIds = /* @__PURE__ */ new Set();
13830
+ const newFailures = [];
13831
+ const stillFailing = [];
13832
+ for (const item of currentFailing) {
13833
+ if (currentFailedIds.has(item.id)) continue;
13834
+ currentFailedIds.add(item.id);
13835
+ (baselineFailed.has(item.id) ? stillFailing : newFailures).push(item);
13836
+ }
13837
+ return { newFailures, stillFailing, currentFailedIds };
13838
+ }
13839
+ function splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses) {
13840
+ const fixed = [];
13841
+ const noLongerRun = [];
13842
+ for (const [id, title] of baselineFailed) {
13843
+ if (currentFailedIds.has(id)) continue;
13844
+ const status = currentStatuses.get(id);
13845
+ const bucket = status === "passed" || status === "flaky" ? fixed : noLongerRun;
13846
+ bucket.push({ id, title });
13847
+ }
13848
+ return { fixed, noLongerRun };
13849
+ }
13850
+ function computeRunDiff(args) {
13851
+ const { currentFailing, currentStatuses, priorEntries, branch } = args;
13852
+ const baseline = priorEntries.find((e) => e.branch === branch && Array.isArray(e.failedTests));
13853
+ if (!baseline) return null;
13854
+ const baselineFailed = /* @__PURE__ */ new Map();
13855
+ for (const item of baseline.failedTests) {
13856
+ if (!baselineFailed.has(item.id)) baselineFailed.set(item.id, item.title);
13857
+ }
13858
+ const { newFailures, stillFailing, currentFailedIds } = splitCurrentFailing(currentFailing, baselineFailed);
13859
+ const { fixed, noLongerRun } = splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses);
13860
+ for (const bucket of [newFailures, stillFailing, fixed, noLongerRun]) {
13861
+ bucket.sort((a, b) => a.title.localeCompare(b.title));
13862
+ }
13863
+ return {
13864
+ newFailures,
13865
+ stillFailing,
13866
+ fixed,
13867
+ noLongerRun,
13868
+ baselineRunId: baseline.runId,
13869
+ baselineTimestamp: baseline.timestamp,
13870
+ baselineTruncated: baseline.failedTestsTruncated === true
13871
+ };
13872
+ }
13873
+
13815
13874
  // src/history/FlakinessAnalyser.ts
13816
13875
  init_cjs_shims();
13817
13876
  function computeTopN(entries, topN) {
@@ -13888,11 +13947,32 @@ function reportName(pdfPaths, reportTitle) {
13888
13947
  if (pdfPaths.length > 0) return (0, import_path9.basename)(pdfPaths[0]);
13889
13948
  return reportTitle || "ReportForge";
13890
13949
  }
13950
+ function formatDiffLine(diff) {
13951
+ if (!diff) return null;
13952
+ const newFailures = diff.newFailures.length;
13953
+ const fixed = diff.fixed.length;
13954
+ const stillFailing = diff.stillFailing.length;
13955
+ const noLongerRun = diff.noLongerRun.length;
13956
+ const parts = [
13957
+ `${newFailures} new ${newFailures === 1 ? "failure" : "failures"}`,
13958
+ `${fixed} fixed`,
13959
+ `${stillFailing} still failing`
13960
+ ];
13961
+ if (noLongerRun > 0) parts.push(`${noLongerRun} no longer run`);
13962
+ return parts.join(" \xB7 ");
13963
+ }
13891
13964
 
13892
13965
  // src/notify/formatters/slack.ts
13893
- function buildSlackPayload(stats, pdfPaths, reportTitle) {
13966
+ function buildSlackPayload(stats, pdfPaths, reportTitle, runDiff) {
13894
13967
  const label = statusLabel(stats.verdict);
13895
13968
  const name = reportName(pdfPaths, reportTitle);
13969
+ const diffLine = formatDiffLine(runDiff);
13970
+ const lines = [
13971
+ `*${label}* \xB7 ${stats.passRate}% passed`,
13972
+ countsLine(stats),
13973
+ ...diffLine ? [diffLine] : [],
13974
+ `_${name}_`
13975
+ ];
13896
13976
  return {
13897
13977
  text: `${label} \xB7 ${stats.passRate}% passed`,
13898
13978
  blocks: [
@@ -13900,9 +13980,7 @@ function buildSlackPayload(stats, pdfPaths, reportTitle) {
13900
13980
  type: "section",
13901
13981
  text: {
13902
13982
  type: "mrkdwn",
13903
- text: `*${label}* \xB7 ${stats.passRate}% passed
13904
- ${countsLine(stats)}
13905
- _${name}_`
13983
+ text: lines.join("\n")
13906
13984
  }
13907
13985
  }
13908
13986
  ]
@@ -13927,9 +14005,37 @@ function buildTeamsSimpleMessage(text) {
13927
14005
  ]
13928
14006
  };
13929
14007
  }
13930
- function buildTeamsPayload(stats, pdfPaths, reportTitle) {
14008
+ function buildTeamsPayload(stats, pdfPaths, reportTitle, runDiff) {
13931
14009
  const label = statusLabel(stats.verdict);
13932
14010
  const name = reportName(pdfPaths, reportTitle);
14011
+ const diffLine = formatDiffLine(runDiff);
14012
+ const body = [
14013
+ {
14014
+ type: "TextBlock",
14015
+ text: `${label} \xB7 ${stats.passRate}% passed`,
14016
+ weight: "bolder",
14017
+ size: "medium"
14018
+ },
14019
+ {
14020
+ type: "TextBlock",
14021
+ text: countsLine(stats),
14022
+ isSubtle: true
14023
+ },
14024
+ {
14025
+ type: "TextBlock",
14026
+ text: name,
14027
+ isSubtle: true,
14028
+ spacing: "none"
14029
+ }
14030
+ ];
14031
+ if (diffLine) {
14032
+ body.push({
14033
+ type: "TextBlock",
14034
+ text: diffLine,
14035
+ isSubtle: true,
14036
+ spacing: "none"
14037
+ });
14038
+ }
13933
14039
  return {
13934
14040
  type: "message",
13935
14041
  attachments: [
@@ -13939,25 +14045,7 @@ function buildTeamsPayload(stats, pdfPaths, reportTitle) {
13939
14045
  $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
13940
14046
  type: "AdaptiveCard",
13941
14047
  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
- ]
14048
+ body
13961
14049
  }
13962
14050
  }
13963
14051
  ]
@@ -13966,16 +14054,17 @@ function buildTeamsPayload(stats, pdfPaths, reportTitle) {
13966
14054
 
13967
14055
  // src/notify/formatters/discord.ts
13968
14056
  init_cjs_shims();
13969
- function buildDiscordPayload(stats, pdfPaths, reportTitle) {
14057
+ function buildDiscordPayload(stats, pdfPaths, reportTitle, runDiff) {
13970
14058
  const label = statusLabel(stats.verdict);
13971
14059
  const name = reportName(pdfPaths, reportTitle);
13972
14060
  const color = stats.verdict === "passed" ? 3066993 : 15158332;
14061
+ const diffLine = formatDiffLine(runDiff);
14062
+ const descriptionLines = [countsLine(stats), ...diffLine ? [diffLine] : [], name];
13973
14063
  return {
13974
14064
  embeds: [
13975
14065
  {
13976
14066
  title: `${label} \xB7 ${stats.passRate}% passed`,
13977
- description: `${countsLine(stats)}
13978
- ${name}`,
14067
+ description: descriptionLines.join("\n"),
13979
14068
  color
13980
14069
  }
13981
14070
  ]
@@ -13989,10 +14078,16 @@ var import_path10 = require("path");
13989
14078
 
13990
14079
  // src/notify/formatters/email.ts
13991
14080
  init_cjs_shims();
13992
- function buildEmailContent(stats, pdfPaths, reportTitle) {
14081
+ function buildEmailContent(stats, pdfPaths, reportTitle, runDiff) {
13993
14082
  const label = statusLabel(stats.verdict);
13994
- const subject = `[ReportForge] ${label} \u2014 ${stats.passRate}% passed`;
14083
+ const subject = `[ReportForge] ${label} \xB7 ${stats.passRate}% passed`;
13995
14084
  const name = reportName(pdfPaths, reportTitle);
14085
+ const diffLine = formatDiffLine(runDiff);
14086
+ const diffRow = diffLine ? `
14087
+ <tr>
14088
+ <td style="padding:8px 12px;border-top:1px solid #e5e5e5">Since last run</td>
14089
+ <td style="padding:8px 12px;border-top:1px solid #e5e5e5;text-align:right">${escHtml(diffLine)}</td>
14090
+ </tr>` : "";
13996
14091
  const html = `<!DOCTYPE html>
13997
14092
  <html lang="en">
13998
14093
  <head><meta charset="utf-8"><title>${escHtml(subject)}</title></head>
@@ -14027,7 +14122,7 @@ function buildEmailContent(stats, pdfPaths, reportTitle) {
14027
14122
  <tr style="background:#f9f9f9">
14028
14123
  <td style="padding:8px 12px;border-top:1px solid #e5e5e5">Duration</td>
14029
14124
  <td style="padding:8px 12px;border-top:1px solid #e5e5e5;text-align:right">${escHtml(countsLine(stats).split(" \xB7 ").pop() ?? "")}</td>
14030
- </tr>
14125
+ </tr>${diffRow}
14031
14126
  </table>
14032
14127
  <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
14128
  </body>
@@ -14052,7 +14147,8 @@ var EmailSender = class {
14052
14147
  const { subject, html } = buildEmailContent(
14053
14148
  reportData.stats,
14054
14149
  pdfPaths,
14055
- reportData.meta.title ?? ""
14150
+ reportData.meta.title ?? "",
14151
+ reportData.runDiff
14056
14152
  );
14057
14153
  const body = { from, to: config.to, subject, html };
14058
14154
  if (config.attachPdf && pdfPaths.length > 0) {
@@ -14116,7 +14212,7 @@ var NotificationSender = class {
14116
14212
  result.skipped.push(name);
14117
14213
  return Promise.resolve();
14118
14214
  }
14119
- const payload = FORMATTERS[name](reportData.stats, pdfPaths, reportTitle);
14215
+ const payload = FORMATTERS[name](reportData.stats, pdfPaths, reportTitle, reportData.runDiff);
14120
14216
  if (name === "discord" && notifyConfig.discord?.attachPdf && pdfPaths.length > 0) {
14121
14217
  return this.postDiscordWithPdf(channel.url, payload, pdfPaths[0], result);
14122
14218
  }
@@ -14230,11 +14326,35 @@ var NETWORK_TIMEOUT_MS3 = 8e3;
14230
14326
  var LIVE_DRAIN_DEADLINE_MS = 1e4;
14231
14327
  var MAX_FLUSH_MS = 3e4;
14232
14328
  var WARN_THROTTLE_MS = 3e4;
14329
+ var TREE_UPDATE_WEIGHT = 25;
14330
+ function redactTestUpdate(update, redact) {
14331
+ if (update.title !== void 0) update.title = redact(update.title);
14332
+ if (update.currentStep !== void 0) update.currentStep = redact(update.currentStep);
14333
+ if (update.file !== void 0) update.file = redact(update.file);
14334
+ if (update.suitePath) update.suitePath = update.suitePath.map(redact);
14335
+ if (update.projectName !== void 0) update.projectName = redact(update.projectName);
14336
+ if (update.steps) redactStepNodes(update.steps, redact);
14337
+ if (update.errors) update.errors.forEach((e) => redactError(e, redact));
14338
+ }
14339
+ function redactStepNodes(nodes, redact) {
14340
+ for (const node of nodes) {
14341
+ node.t = redact(node.t);
14342
+ if (node.err) redactError(node.err, redact);
14343
+ if (node.ch) redactStepNodes(node.ch, redact);
14344
+ }
14345
+ }
14346
+ function redactError(err, redact) {
14347
+ err.message = redact(err.message);
14348
+ if (err.stack !== void 0) err.stack = redact(err.stack);
14349
+ if (err.snippet !== void 0) err.snippet = redact(err.snippet);
14350
+ }
14233
14351
  var LiveStreamer = class {
14234
14352
  constructor(config) {
14235
14353
  this.config = config;
14236
14354
  this.testBuffer = [];
14237
14355
  this.eventBuffer = [];
14356
+ /** Weighted size of the buffers (tree-bearing updates count TREE_UPDATE_WEIGHT). */
14357
+ this.bufferWeight = 0;
14238
14358
  /** Monotonic per-shard event counter → stable `{shardKey}:{seq}` event ids. */
14239
14359
  this.eventSeq = 0;
14240
14360
  /**
@@ -14260,13 +14380,11 @@ var LiveStreamer = class {
14260
14380
  }
14261
14381
  enqueueTest(update) {
14262
14382
  if (!this.enabled) return;
14263
- if (this.config.redact) {
14264
- if (update.title !== void 0) update.title = this.config.redact(update.title);
14265
- if (update.currentStep !== void 0) update.currentStep = this.config.redact(update.currentStep);
14266
- }
14383
+ if (this.config.redact) redactTestUpdate(update, this.config.redact);
14267
14384
  const prevRetry = this.testStates.get(update.id)?.retry;
14268
14385
  this.testStates.set(update.id, { status: update.status, retry: update.retry ?? prevRetry });
14269
14386
  this.testBuffer.push(update);
14387
+ this.bufferWeight += update.steps ? TREE_UPDATE_WEIGHT : 1;
14270
14388
  this.capPreLicenseBuffer();
14271
14389
  this.scheduleFlush();
14272
14390
  }
@@ -14278,6 +14396,7 @@ var LiveStreamer = class {
14278
14396
  }
14279
14397
  event.id = `${this.config.shardKey}:${this.eventSeq++}`;
14280
14398
  this.eventBuffer.push(event);
14399
+ this.bufferWeight += 1;
14281
14400
  this.capPreLicenseBuffer();
14282
14401
  this.scheduleFlush();
14283
14402
  }
@@ -14395,9 +14514,13 @@ var LiveStreamer = class {
14395
14514
  if (dropEvents > 0) this.eventBuffer.splice(0, dropEvents);
14396
14515
  overflow -= dropEvents;
14397
14516
  if (overflow > 0) this.testBuffer.splice(0, overflow);
14517
+ this.bufferWeight = this.currentBufferWeight();
14518
+ }
14519
+ currentBufferWeight() {
14520
+ return this.eventBuffer.length + this.testBuffer.reduce((w, t) => w + (t.steps ? TREE_UPDATE_WEIGHT : 1), 0);
14398
14521
  }
14399
14522
  scheduleFlush() {
14400
- if (this.testBuffer.length + this.eventBuffer.length >= this.flushMaxBatch) {
14523
+ if (this.bufferWeight >= this.flushMaxBatch) {
14401
14524
  void this.flush("running");
14402
14525
  return;
14403
14526
  }
@@ -14418,6 +14541,7 @@ var LiveStreamer = class {
14418
14541
  if (status !== "finished" && tests.length === 0 && events.length === 0) return;
14419
14542
  this.testBuffer = [];
14420
14543
  this.eventBuffer = [];
14544
+ this.bufferWeight = 0;
14421
14545
  const body = {
14422
14546
  runId: this.config.runId,
14423
14547
  shardKey: this.config.shardKey,
@@ -14452,6 +14576,7 @@ var LiveStreamer = class {
14452
14576
  this.enabled = false;
14453
14577
  this.testBuffer = [];
14454
14578
  this.eventBuffer = [];
14579
+ this.bufferWeight = 0;
14455
14580
  logger.debug("Live streaming disabled \u2014 no active license or live entitlement.");
14456
14581
  return false;
14457
14582
  }
@@ -14538,25 +14663,213 @@ function shardKeyOf(shard) {
14538
14663
 
14539
14664
  // src/live/event-map.ts
14540
14665
  init_cjs_shims();
14666
+
14667
+ // src/live/step-tree.ts
14668
+ init_cjs_shims();
14669
+ var MAX_TREE_DEPTH = 10;
14670
+ var MAX_TREE_NODES = 150;
14671
+ var MAX_TREE_JSON = 24e3;
14672
+ var MAX_NODE_TITLE = 200;
14673
+ var MAX_ERRORS = 3;
14674
+ var MAX_ERR_MESSAGE = 2e3;
14675
+ var MAX_ERR_STACK = 8192;
14676
+ var MAX_ERR_SNIPPET = 4096;
14677
+ var isHook = (c) => c === "hook" || c === "fixture";
14678
+ function flatten2(steps) {
14679
+ const out = [];
14680
+ const walk = (nodes, depth, ancestors) => {
14681
+ for (const s of nodes ?? []) {
14682
+ const index = out.length;
14683
+ out.push({
14684
+ raw: s,
14685
+ category: s.category ?? "",
14686
+ depth,
14687
+ hasError: s.error != null,
14688
+ ancestors
14689
+ });
14690
+ if (s.steps?.length) walk(s.steps, depth + 1, [...ancestors, index]);
14691
+ }
14692
+ };
14693
+ walk(steps ?? [], 0, []);
14694
+ return out;
14695
+ }
14696
+ var inHookSubtree2 = (flat, f) => isHook(f.category) || f.ancestors.some((a) => isHook(flat[a].category));
14697
+ function pickFailingIdx2(flat) {
14698
+ const pick = (allowTeardown) => {
14699
+ let idx = -1;
14700
+ for (let i = 0; i < flat.length; i++) {
14701
+ const f = flat[i];
14702
+ if (!f.hasError || !allowTeardown && inHookSubtree2(flat, f)) continue;
14703
+ if (idx === -1 || f.depth >= flat[idx].depth) idx = i;
14704
+ }
14705
+ return idx;
14706
+ };
14707
+ const body = pick(false);
14708
+ return body >= 0 ? body : pick(true);
14709
+ }
14710
+ function mapCategory(category) {
14711
+ if (category === "test.step") return "step";
14712
+ if (category === "expect") return "expect";
14713
+ if (isHook(category)) return "hook";
14714
+ return "api";
14715
+ }
14716
+ function buildErrorInfo(err) {
14717
+ if (!err?.message) return void 0;
14718
+ return {
14719
+ message: clampWithEllipsis(stripAnsi(err.message), MAX_ERR_MESSAGE),
14720
+ ...err.stack ? { stack: clampWithEllipsis(stripAnsi(err.stack), MAX_ERR_STACK) } : {},
14721
+ ...err.snippet ? { snippet: clampWithEllipsis(stripAnsi(err.snippet), MAX_ERR_SNIPPET) } : {}
14722
+ };
14723
+ }
14724
+ function mapResultErrors(errors) {
14725
+ if (!errors?.length) return void 0;
14726
+ const out = errors.slice(0, MAX_ERRORS).map((e) => buildErrorInfo(e)).filter((e) => e !== void 0);
14727
+ return out.length > 0 ? out : void 0;
14728
+ }
14729
+ function buildLiveStepTree(steps) {
14730
+ if (!steps?.length) return void 0;
14731
+ const flat = flatten2(steps);
14732
+ if (flat.length === 0) return void 0;
14733
+ const failingIdx = pickFailingIdx2(flat);
14734
+ const failingPath = /* @__PURE__ */ new Set();
14735
+ if (failingIdx >= 0) {
14736
+ failingPath.add(flat[failingIdx].raw);
14737
+ for (const a of flat[failingIdx].ancestors) failingPath.add(flat[a].raw);
14738
+ }
14739
+ const failingLeaf = failingIdx >= 0 ? flat[failingIdx].raw : void 0;
14740
+ for (const level of [0, 1, 2, 3]) {
14741
+ const budget = { left: MAX_TREE_NODES };
14742
+ const tree = buildChildren(steps, {
14743
+ level,
14744
+ depth: 0,
14745
+ underHook: false,
14746
+ underApiOrExpect: false,
14747
+ failingPath,
14748
+ failingLeaf,
14749
+ budget
14750
+ });
14751
+ if (tree.length === 0) return void 0;
14752
+ if (budget.left >= 0 && JSON.stringify(tree).length <= MAX_TREE_JSON) return tree;
14753
+ }
14754
+ return void 0;
14755
+ }
14756
+ function keepNode(s, ctx) {
14757
+ if (ctx.failingPath.has(s)) return true;
14758
+ if (!s.title) return false;
14759
+ const c = s.category ?? "";
14760
+ if (isHook(c) || ctx.underHook) return false;
14761
+ if (c === "test.step") return true;
14762
+ if (ctx.underApiOrExpect) return false;
14763
+ if (c === "expect") return ctx.level < 2;
14764
+ if (c === "pw:api") return ctx.level < 1;
14765
+ return false;
14766
+ }
14767
+ function containsFailing(s, leaf) {
14768
+ if (!leaf) return false;
14769
+ if (s === leaf) return true;
14770
+ return (s.steps ?? []).some((child) => containsFailing(child, leaf));
14771
+ }
14772
+ function buildChildren(nodes, ctx) {
14773
+ const out = [];
14774
+ for (const s of nodes ?? []) {
14775
+ if (ctx.budget.left <= 0) {
14776
+ ctx.budget.left = -1;
14777
+ break;
14778
+ }
14779
+ const c = s.category ?? "";
14780
+ if (!keepNode(s, ctx)) {
14781
+ if (containsFailing(s, ctx.failingLeaf)) {
14782
+ out.push(
14783
+ ...buildChildren(s.steps, {
14784
+ ...ctx,
14785
+ underHook: ctx.underHook || isHook(c),
14786
+ underApiOrExpect: ctx.underApiOrExpect || c === "pw:api" || c === "expect"
14787
+ })
14788
+ );
14789
+ }
14790
+ continue;
14791
+ }
14792
+ ctx.budget.left -= 1;
14793
+ const node = {
14794
+ t: clampWithEllipsis(stripAnsi(s.title ?? ""), MAX_NODE_TITLE),
14795
+ c: mapCategory(c),
14796
+ ...Number.isFinite(s.duration) && s.duration >= 0 ? { d: Math.round(s.duration) } : {},
14797
+ ...s.error != null ? { s: "failed" } : {},
14798
+ ...s === ctx.failingLeaf ? errField(s) : {}
14799
+ };
14800
+ if (s.steps?.length) {
14801
+ if (ctx.depth + 1 >= MAX_TREE_DEPTH) {
14802
+ if (!node.err && containsFailing(s, ctx.failingLeaf) && s !== ctx.failingLeaf) {
14803
+ const hoisted = errField(ctx.failingLeaf);
14804
+ if (hoisted.err) {
14805
+ node.err = hoisted.err;
14806
+ node.s = "failed";
14807
+ }
14808
+ }
14809
+ } else {
14810
+ const ch = buildChildren(s.steps, {
14811
+ ...ctx,
14812
+ depth: ctx.depth + 1,
14813
+ underHook: ctx.underHook || isHook(c),
14814
+ underApiOrExpect: ctx.underApiOrExpect || c === "pw:api" || c === "expect"
14815
+ });
14816
+ if (ch.length > 0) node.ch = ch;
14817
+ }
14818
+ }
14819
+ out.push(node);
14820
+ }
14821
+ return out;
14822
+ }
14823
+ function errField(s) {
14824
+ const err = buildErrorInfo(s.error);
14825
+ return err ? { err } : {};
14826
+ }
14827
+
14828
+ // src/live/event-map.ts
14541
14829
  var MAX_TITLE_CHARS2 = 200;
14542
14830
  var MAX_STEP_TITLE_CHARS = 200;
14543
14831
  var MAX_CONSOLE_CHARS = 2048;
14832
+ var MAX_FILE_CHARS = 300;
14833
+ var MAX_PROJECT_CHARS = 100;
14834
+ var MAX_SUITE_SEGMENTS = 10;
14835
+ function suiteIdentity(test) {
14836
+ const path15 = typeof test.titlePath === "function" ? test.titlePath() : [];
14837
+ if (!Array.isArray(path15) || path15.length < 4) return {};
14838
+ const [, project, file, ...rest] = path15;
14839
+ const describes = rest.slice(0, -1);
14840
+ const out = {};
14841
+ if (typeof file === "string" && file) {
14842
+ out.file = clampWithEllipsis(file.replace(/\\/g, "/"), MAX_FILE_CHARS);
14843
+ }
14844
+ const segs = describes.filter((s) => typeof s === "string" && s.length > 0).slice(0, MAX_SUITE_SEGMENTS).map((s) => clampWithEllipsis(s, MAX_STEP_TITLE_CHARS));
14845
+ if (segs.length > 0) out.suitePath = segs;
14846
+ if (typeof project === "string" && project) {
14847
+ out.projectName = clampWithEllipsis(project, MAX_PROJECT_CHARS);
14848
+ }
14849
+ return out;
14850
+ }
14544
14851
  function mapTestBegin(test, shardKey) {
14545
14852
  return {
14546
14853
  id: test.id,
14547
14854
  title: clampWithEllipsis(test.title ?? "", MAX_TITLE_CHARS2),
14548
14855
  status: "running",
14549
- shardKey
14856
+ shardKey,
14857
+ ...suiteIdentity(test)
14550
14858
  };
14551
14859
  }
14552
14860
  function mapTestEnd(test, result, shardKey) {
14861
+ const steps = buildLiveStepTree(result.steps);
14862
+ const errors = mapResultErrors(result.errors);
14553
14863
  return {
14554
14864
  id: test.id,
14555
14865
  title: clampWithEllipsis(test.title ?? "", MAX_TITLE_CHARS2),
14556
14866
  status: result.status,
14557
14867
  durationMs: result.duration,
14558
14868
  retry: result.retry,
14559
- shardKey
14869
+ shardKey,
14870
+ ...suiteIdentity(test),
14871
+ ...steps ? { steps } : {},
14872
+ ...errors ? { errors } : {}
14560
14873
  };
14561
14874
  }
14562
14875
  function isLifecycleStep(step) {
@@ -14606,7 +14919,11 @@ function mapStep(testId, step, phase, shardKey) {
14606
14919
  startedAt: step.startTime?.getTime(),
14607
14920
  // Nesting depth (test.step ancestors) so the page indents children under their
14608
14921
  // parent step. 0 → flat, so a test without test.steps renders as a flat trail.
14609
- depth: testStepDepth2(step)
14922
+ depth: testStepDepth2(step),
14923
+ // Category so the trail styles pw:api actions and expects distinctly. Hooks
14924
+ // and fixtures never reach here non-errored (shouldEmitStep drops them); an
14925
+ // errored one maps to 'api' — the trail has no dedicated hook style.
14926
+ category: step.category === "test.step" ? "step" : step.category === "expect" ? "expect" : "api"
14610
14927
  };
14611
14928
  }
14612
14929
  function mapConsole(kind, chunk, testId, shardKey) {
@@ -14638,7 +14955,7 @@ var PdfReporter = class {
14638
14955
  if (this.options.redact.enabled) {
14639
14956
  this.redactor = new Redactor(this.options.redact);
14640
14957
  }
14641
- this.version = true ? "0.24.0" : "0.x";
14958
+ this.version = true ? "0.26.1" : "0.x";
14642
14959
  logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
14643
14960
  this.licenseClient = new LicenseClient({
14644
14961
  licenseKey: this.options.licenseKey,
@@ -14767,6 +15084,7 @@ var PdfReporter = class {
14767
15084
  const flakyTests = collectFlakyTests(collected.projects).map(
14768
15085
  (t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
14769
15086
  );
15087
+ const { redactedFailing, ...failedTestsFields } = buildFailedTestsFields(collected.projects, redactor);
14770
15088
  const entry = {
14771
15089
  runId: `${(/* @__PURE__ */ new Date()).toISOString()}-${suffix}`,
14772
15090
  timestamp: Date.now(),
@@ -14777,9 +15095,12 @@ var PdfReporter = class {
14777
15095
  flaky: collected.stats.flaky,
14778
15096
  duration: collected.stats.duration,
14779
15097
  verdict: deriveVerdict(collected.stats),
14780
- flakyTests
15098
+ flakyTests,
15099
+ ...failedTestsFields,
15100
+ branch: collected.environment.branch
14781
15101
  };
14782
15102
  const localEntries = hm.append(entry, this.options.historySize);
15103
+ collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch);
14783
15104
  let trendEntries = localEntries;
14784
15105
  if (this.options.remoteHistory && licenseInfo.jwt) {
14785
15106
  const serverUrl = this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL2;
@@ -14819,7 +15140,7 @@ var PdfReporter = class {
14819
15140
  }
14820
15141
  }
14821
15142
  _buildReportData(collected, licenseInfo) {
14822
- const { projects, stats, failures, environment, charts } = collected;
15143
+ const { projects, stats, failures, environment, charts, runDiff } = collected;
14823
15144
  const projectName = this.options.projectName ?? this.resolveProjectName();
14824
15145
  const firstTemplate = this.options.templatePath?.length ? "detailed" : Array.isArray(this.options.template) ? this.options.template[0] : this.options.template;
14825
15146
  return {
@@ -14839,7 +15160,8 @@ var PdfReporter = class {
14839
15160
  projects,
14840
15161
  failures,
14841
15162
  environment,
14842
- charts
15163
+ charts,
15164
+ runDiff
14843
15165
  };
14844
15166
  }
14845
15167
  printsToStdio() {
@@ -15043,6 +15365,47 @@ function collectFlakyTests(projects) {
15043
15365
  }
15044
15366
  return result;
15045
15367
  }
15368
+ function collectFailedTests(projects) {
15369
+ const seen = /* @__PURE__ */ new Set();
15370
+ const result = [];
15371
+ for (const project of projects) {
15372
+ for (const suite of flattenSuiteNodes(project.suites)) {
15373
+ for (const test of suite.tests) {
15374
+ if ((test.status === "failed" || test.status === "timedOut") && !seen.has(test.id)) {
15375
+ seen.add(test.id);
15376
+ result.push({ id: test.id, title: test.fullTitle });
15377
+ }
15378
+ }
15379
+ }
15380
+ }
15381
+ return result;
15382
+ }
15383
+ function collectCurrentStatuses(projects) {
15384
+ const statuses = /* @__PURE__ */ new Map();
15385
+ for (const project of projects) {
15386
+ for (const suite of flattenSuiteNodes(project.suites)) {
15387
+ for (const test of suite.tests) {
15388
+ statuses.set(test.id, test.status);
15389
+ }
15390
+ }
15391
+ }
15392
+ return statuses;
15393
+ }
15394
+ function buildFailedTestsFields(projects, redactor) {
15395
+ const redactedFailing = collectFailedTests(projects).map(
15396
+ (t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
15397
+ );
15398
+ const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
15399
+ return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
15400
+ }
15401
+ function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
15402
+ return computeRunDiff({
15403
+ currentFailing,
15404
+ currentStatuses: collectCurrentStatuses(projects),
15405
+ priorEntries,
15406
+ branch
15407
+ }) ?? void 0;
15408
+ }
15046
15409
 
15047
15410
  // src/index.ts
15048
15411
  function defineReporterConfig(options) {