@reportforge/playwright-pdf 0.5.0 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.5.1] — 2026-06-19
7
+
8
+ ### Security
9
+
10
+ - **Chart labels escaped for inline-script safety** — a suite / describe / file title containing `</script>` can no longer break out of the inline Chart.js data block in the rendered report (relevant when the reporter runs over untrusted test code, e.g. external-contributor CI).
11
+
12
+ ### Changed
13
+
14
+ - **Sharper failure-analysis classification** — high-precision deterministic rules now label the error shapes the offline model struggles with: timed-out web-first assertions (`toHaveClass`/`toHaveText`/… with the element resolved) as **assertions**, connection/DNS/SSL errors as **network**, aborted/redirected/interrupted navigations as **navigation**, and resolved-to-0 / strict-mode locators as **locator-not-found**. Rules match the error header only, so call-log context and asserted values no longer cause mislabels (a navigation-wait timeout stays a timeout, a refused `page.goto` is network).
15
+
16
+ ---
17
+
6
18
  ## [0.5.0] — 2026-06-19
7
19
 
8
20
  ### Added
package/dist/demo/cli.js CHANGED
@@ -6532,6 +6532,9 @@ function dominantCategory(clusters) {
6532
6532
  }
6533
6533
 
6534
6534
  // src/templates/engine.ts
6535
+ function jsonForInlineScript(value) {
6536
+ return JSON.stringify(value).replace(/<\//g, "<\\/");
6537
+ }
6535
6538
  var TemplateEngine = class {
6536
6539
  constructor() {
6537
6540
  this.chartjsInline = "";
@@ -6747,7 +6750,7 @@ var TemplateEngine = class {
6747
6750
  if (!Array.isArray(arr)) return "";
6748
6751
  const values = arr.map((item) => {
6749
6752
  const v = item[key];
6750
- return typeof v === "string" ? JSON.stringify(v) : String(v ?? 0);
6753
+ return typeof v === "string" ? jsonForInlineScript(v) : String(v ?? 0);
6751
6754
  });
6752
6755
  return new import_handlebars.default.SafeString(values.join(","));
6753
6756
  }
@@ -6758,7 +6761,7 @@ var TemplateEngine = class {
6758
6761
  this.handlebars.registerHelper("absFixed", (n) => Math.abs(n).toFixed(0));
6759
6762
  this.handlebars.registerHelper(
6760
6763
  "safeJsonData",
6761
- (value) => new import_handlebars.default.SafeString(JSON.stringify(value).replace(/<\//g, "<\\/"))
6764
+ (value) => new import_handlebars.default.SafeString(jsonForInlineScript(value))
6762
6765
  );
6763
6766
  this.handlebars.registerHelper(
6764
6767
  "reverse",
@@ -11684,11 +11687,33 @@ var STRENGTH = {
11684
11687
  strongMinMargin: 0.25
11685
11688
  };
11686
11689
  var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
11687
- var FLAKY_MARGIN = 999;
11690
+ var OVERRIDE_MARGIN = 999;
11688
11691
 
11689
11692
  // src/analysis/FailureClusterer.ts
11690
11693
  var MAX_TESTS_PER_CLUSTER = 20;
11691
11694
  var MAX_MESSAGE_CHARS = 200;
11695
+ function errorHeader(message) {
11696
+ const beforeCallLog = message.split(/\n\s*Call log:/i)[0];
11697
+ return beforeCallLog.split("\n").filter((line) => !/^\s*[-+]?\s*(?:Expected|Received)\b/i.test(line)).join("\n");
11698
+ }
11699
+ function isAssertionTimeout(message) {
11700
+ return /Expect "[^"]+" with timeout/.test(message) && /resolved to [1-9]\d* element/.test(message);
11701
+ }
11702
+ function isNetwork(message) {
11703
+ 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));
11704
+ }
11705
+ function isNavigation(message) {
11706
+ 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));
11707
+ }
11708
+ function isLocatorNotFound(message) {
11709
+ 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));
11710
+ }
11711
+ var MESSAGE_RULES = [
11712
+ [isAssertionTimeout, "assertion"],
11713
+ [isNavigation, "navigation"],
11714
+ [isNetwork, "network"],
11715
+ [isLocatorNotFound, "locator-not-found"]
11716
+ ];
11692
11717
  function strengthOf(margin) {
11693
11718
  if (margin >= STRENGTH.strongMinMargin) return "strong";
11694
11719
  if (margin >= STRENGTH.moderateMinMargin) return "moderate";
@@ -11711,11 +11736,15 @@ function representativeMessage(message) {
11711
11736
  return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
11712
11737
  }
11713
11738
  function classifyOne(f, model) {
11739
+ const frame = topFrame(f.error.stack);
11714
11740
  if (f.retryHistory.some((r) => r.status === "passed")) {
11715
- return { category: "flaky-by-retry", margin: FLAKY_MARGIN, failure: f, frame: topFrame(f.error.stack) };
11741
+ return { category: "flaky-by-retry", margin: OVERRIDE_MARGIN, failure: f, frame };
11742
+ }
11743
+ for (const [match, category] of MESSAGE_RULES) {
11744
+ if (match(f.error.message)) return { category, margin: OVERRIDE_MARGIN, failure: f, frame };
11716
11745
  }
11717
11746
  const { label, margin } = classify(model, classifyInput(f));
11718
- return { category: label, margin, failure: f, frame: topFrame(f.error.stack) };
11747
+ return { category: label, margin, failure: f, frame };
11719
11748
  }
11720
11749
  function classifyAll(failures, model) {
11721
11750
  return failures.map((f) => classifyOne(f, model));
package/dist/index.js CHANGED
@@ -10993,11 +10993,33 @@ var STRENGTH = {
10993
10993
  strongMinMargin: 0.25
10994
10994
  };
10995
10995
  var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
10996
- var FLAKY_MARGIN = 999;
10996
+ var OVERRIDE_MARGIN = 999;
10997
10997
 
10998
10998
  // src/analysis/FailureClusterer.ts
10999
10999
  var MAX_TESTS_PER_CLUSTER = 20;
11000
11000
  var MAX_MESSAGE_CHARS = 200;
11001
+ function errorHeader(message) {
11002
+ const beforeCallLog = message.split(/\n\s*Call log:/i)[0];
11003
+ return beforeCallLog.split("\n").filter((line) => !/^\s*[-+]?\s*(?:Expected|Received)\b/i.test(line)).join("\n");
11004
+ }
11005
+ function isAssertionTimeout(message) {
11006
+ return /Expect "[^"]+" with timeout/.test(message) && /resolved to [1-9]\d* element/.test(message);
11007
+ }
11008
+ function isNetwork(message) {
11009
+ 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));
11010
+ }
11011
+ function isNavigation(message) {
11012
+ 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));
11013
+ }
11014
+ function isLocatorNotFound(message) {
11015
+ 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));
11016
+ }
11017
+ var MESSAGE_RULES = [
11018
+ [isAssertionTimeout, "assertion"],
11019
+ [isNavigation, "navigation"],
11020
+ [isNetwork, "network"],
11021
+ [isLocatorNotFound, "locator-not-found"]
11022
+ ];
11001
11023
  function strengthOf(margin) {
11002
11024
  if (margin >= STRENGTH.strongMinMargin) return "strong";
11003
11025
  if (margin >= STRENGTH.moderateMinMargin) return "moderate";
@@ -11020,11 +11042,15 @@ function representativeMessage(message) {
11020
11042
  return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
11021
11043
  }
11022
11044
  function classifyOne(f, model) {
11045
+ const frame = topFrame(f.error.stack);
11023
11046
  if (f.retryHistory.some((r) => r.status === "passed")) {
11024
- return { category: "flaky-by-retry", margin: FLAKY_MARGIN, failure: f, frame: topFrame(f.error.stack) };
11047
+ return { category: "flaky-by-retry", margin: OVERRIDE_MARGIN, failure: f, frame };
11048
+ }
11049
+ for (const [match, category] of MESSAGE_RULES) {
11050
+ if (match(f.error.message)) return { category, margin: OVERRIDE_MARGIN, failure: f, frame };
11025
11051
  }
11026
11052
  const { label, margin } = classify(model, classifyInput(f));
11027
- return { category: label, margin, failure: f, frame: topFrame(f.error.stack) };
11053
+ return { category: label, margin, failure: f, frame };
11028
11054
  }
11029
11055
  function classifyAll(failures, model) {
11030
11056
  return failures.map((f) => classifyOne(f, model));
@@ -11884,6 +11910,9 @@ function formatDuration(ms) {
11884
11910
  }
11885
11911
 
11886
11912
  // src/templates/engine.ts
11913
+ function jsonForInlineScript(value) {
11914
+ return JSON.stringify(value).replace(/<\//g, "<\\/");
11915
+ }
11887
11916
  var TemplateEngine = class {
11888
11917
  constructor() {
11889
11918
  this.chartjsInline = "";
@@ -12099,7 +12128,7 @@ var TemplateEngine = class {
12099
12128
  if (!Array.isArray(arr)) return "";
12100
12129
  const values = arr.map((item) => {
12101
12130
  const v = item[key];
12102
- return typeof v === "string" ? JSON.stringify(v) : String(v ?? 0);
12131
+ return typeof v === "string" ? jsonForInlineScript(v) : String(v ?? 0);
12103
12132
  });
12104
12133
  return new import_handlebars.default.SafeString(values.join(","));
12105
12134
  }
@@ -12110,7 +12139,7 @@ var TemplateEngine = class {
12110
12139
  this.handlebars.registerHelper("absFixed", (n) => Math.abs(n).toFixed(0));
12111
12140
  this.handlebars.registerHelper(
12112
12141
  "safeJsonData",
12113
- (value) => new import_handlebars.default.SafeString(JSON.stringify(value).replace(/<\//g, "<\\/"))
12142
+ (value) => new import_handlebars.default.SafeString(jsonForInlineScript(value))
12114
12143
  );
12115
12144
  this.handlebars.registerHelper(
12116
12145
  "reverse",
@@ -13097,7 +13126,7 @@ var PdfReporter = class {
13097
13126
  this.dataCollector = new DataCollector();
13098
13127
  this.pdfGenerator = new PdfGenerator();
13099
13128
  this.options = parseOptions(rawOptions);
13100
- const version = true ? "0.5.0" : "0.x";
13129
+ const version = true ? "0.5.1" : "0.x";
13101
13130
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
13102
13131
  this.licenseClient = new LicenseClient({
13103
13132
  licenseKey: this.options.licenseKey,
package/dist/index.mjs CHANGED
@@ -10994,11 +10994,33 @@ var STRENGTH = {
10994
10994
  strongMinMargin: 0.25
10995
10995
  };
10996
10996
  var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
10997
- var FLAKY_MARGIN = 999;
10997
+ var OVERRIDE_MARGIN = 999;
10998
10998
 
10999
10999
  // src/analysis/FailureClusterer.ts
11000
11000
  var MAX_TESTS_PER_CLUSTER = 20;
11001
11001
  var MAX_MESSAGE_CHARS = 200;
11002
+ function errorHeader(message) {
11003
+ const beforeCallLog = message.split(/\n\s*Call log:/i)[0];
11004
+ return beforeCallLog.split("\n").filter((line) => !/^\s*[-+]?\s*(?:Expected|Received)\b/i.test(line)).join("\n");
11005
+ }
11006
+ function isAssertionTimeout(message) {
11007
+ return /Expect "[^"]+" with timeout/.test(message) && /resolved to [1-9]\d* element/.test(message);
11008
+ }
11009
+ function isNetwork(message) {
11010
+ 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));
11011
+ }
11012
+ function isNavigation(message) {
11013
+ 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));
11014
+ }
11015
+ function isLocatorNotFound(message) {
11016
+ 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));
11017
+ }
11018
+ var MESSAGE_RULES = [
11019
+ [isAssertionTimeout, "assertion"],
11020
+ [isNavigation, "navigation"],
11021
+ [isNetwork, "network"],
11022
+ [isLocatorNotFound, "locator-not-found"]
11023
+ ];
11002
11024
  function strengthOf(margin) {
11003
11025
  if (margin >= STRENGTH.strongMinMargin) return "strong";
11004
11026
  if (margin >= STRENGTH.moderateMinMargin) return "moderate";
@@ -11021,11 +11043,15 @@ function representativeMessage(message) {
11021
11043
  return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
11022
11044
  }
11023
11045
  function classifyOne(f, model) {
11046
+ const frame = topFrame(f.error.stack);
11024
11047
  if (f.retryHistory.some((r) => r.status === "passed")) {
11025
- return { category: "flaky-by-retry", margin: FLAKY_MARGIN, failure: f, frame: topFrame(f.error.stack) };
11048
+ return { category: "flaky-by-retry", margin: OVERRIDE_MARGIN, failure: f, frame };
11049
+ }
11050
+ for (const [match, category] of MESSAGE_RULES) {
11051
+ if (match(f.error.message)) return { category, margin: OVERRIDE_MARGIN, failure: f, frame };
11026
11052
  }
11027
11053
  const { label, margin } = classify(model, classifyInput(f));
11028
- return { category: label, margin, failure: f, frame: topFrame(f.error.stack) };
11054
+ return { category: label, margin, failure: f, frame };
11029
11055
  }
11030
11056
  function classifyAll(failures, model) {
11031
11057
  return failures.map((f) => classifyOne(f, model));
@@ -11885,6 +11911,9 @@ function formatDuration(ms) {
11885
11911
  }
11886
11912
 
11887
11913
  // src/templates/engine.ts
11914
+ function jsonForInlineScript(value) {
11915
+ return JSON.stringify(value).replace(/<\//g, "<\\/");
11916
+ }
11888
11917
  var TemplateEngine = class {
11889
11918
  constructor() {
11890
11919
  this.chartjsInline = "";
@@ -12100,7 +12129,7 @@ var TemplateEngine = class {
12100
12129
  if (!Array.isArray(arr)) return "";
12101
12130
  const values = arr.map((item) => {
12102
12131
  const v = item[key];
12103
- return typeof v === "string" ? JSON.stringify(v) : String(v ?? 0);
12132
+ return typeof v === "string" ? jsonForInlineScript(v) : String(v ?? 0);
12104
12133
  });
12105
12134
  return new import_handlebars.default.SafeString(values.join(","));
12106
12135
  }
@@ -12111,7 +12140,7 @@ var TemplateEngine = class {
12111
12140
  this.handlebars.registerHelper("absFixed", (n) => Math.abs(n).toFixed(0));
12112
12141
  this.handlebars.registerHelper(
12113
12142
  "safeJsonData",
12114
- (value) => new import_handlebars.default.SafeString(JSON.stringify(value).replace(/<\//g, "<\\/"))
12143
+ (value) => new import_handlebars.default.SafeString(jsonForInlineScript(value))
12115
12144
  );
12116
12145
  this.handlebars.registerHelper(
12117
12146
  "reverse",
@@ -13098,7 +13127,7 @@ var PdfReporter = class {
13098
13127
  this.dataCollector = new DataCollector();
13099
13128
  this.pdfGenerator = new PdfGenerator();
13100
13129
  this.options = parseOptions(rawOptions);
13101
- const version = true ? "0.5.0" : "0.x";
13130
+ const version = true ? "0.5.1" : "0.x";
13102
13131
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
13103
13132
  this.licenseClient = new LicenseClient({
13104
13133
  licenseKey: this.options.licenseKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportforge/playwright-pdf",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Enterprise-ready PDF reports for Playwright Test — minimal, detailed, and executive templates with CI/CD integrations",
5
5
  "license": "MIT",
6
6
  "author": "ReportForge",