@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 +12 -0
- package/dist/demo/cli.js +34 -5
- package/dist/index.js +35 -6
- package/dist/index.mjs +35 -6
- package/package.json +1 -1
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" ?
|
|
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(
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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" ?
|
|
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(
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
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" ?
|
|
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(
|
|
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.
|
|
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.
|
|
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",
|