@reportforge/playwright-pdf 0.13.1 → 0.13.2
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 +14 -0
- package/dist/cli/index.js +34 -21
- package/dist/index.js +94 -64
- package/dist/index.mjs +94 -64
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
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.13.2] - 2026-07-04
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **License refresh no longer wipes a valid cache on rate-limit**: an HTTP 429/408 from the license server was misclassified as a denial, clearing a still-valid cached license and skipping PDF generation. Now treated as transient — falls back to the cache.
|
|
11
|
+
- **`pdfPassword` encryption now actually works**: a malformed qpdf command meant every password-protected PDF shipped unencrypted while the run reported success. qpdf failures are now fatal instead of silent, and the password is no longer passed via process arguments.
|
|
12
|
+
- **`test.fail()` expected failures no longer miscounted**: an expected failure was counted as a failure in the KPI stats and failure list even though the suite tree correctly showed it as passed.
|
|
13
|
+
- **Sharded runs: describe blocks split across shards now merge correctly** instead of rendering as duplicate, half-populated nodes.
|
|
14
|
+
- **Sharded runs: failure severity now reflects tags/annotations** instead of always showing "medium".
|
|
15
|
+
- **Custom templates sharing a basename no longer overwrite each other's output.**
|
|
16
|
+
- **PDF size-cap retuning now actually reduces the inline failure count** when the report has fewer real failures than the configured cap.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
6
20
|
## [0.13.1] - 2026-07-03
|
|
7
21
|
|
|
8
22
|
### Changed
|
package/dist/cli/index.js
CHANGED
|
@@ -7325,25 +7325,18 @@ var init_PdfEncryptor = __esm({
|
|
|
7325
7325
|
return;
|
|
7326
7326
|
}
|
|
7327
7327
|
const tmpPath = import_path3.default.join(import_os2.default.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
|
|
7328
|
-
const
|
|
7328
|
+
const argFile = import_path3.default.join(import_os2.default.tmpdir(), `rf-qpdf-args-${import_crypto2.default.randomBytes(8).toString("hex")}`);
|
|
7329
7329
|
try {
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
(0, import_child_process2.execFileSync)("qpdf", [
|
|
7333
|
-
`--password-file=${pwFile}`,
|
|
7334
|
-
"--encrypt",
|
|
7335
|
-
"256",
|
|
7336
|
-
"--",
|
|
7337
|
-
pdfPath,
|
|
7338
|
-
tmpPath
|
|
7339
|
-
], { stdio: "pipe" });
|
|
7330
|
+
const args = ["--encrypt", password, password, "256", "--", pdfPath, tmpPath];
|
|
7331
|
+
import_fs5.default.writeFileSync(argFile, args.join("\n"), { mode: 384 });
|
|
7332
|
+
(0, import_child_process2.execFileSync)("qpdf", [`@${argFile}`], { stdio: "pipe" });
|
|
7340
7333
|
import_fs5.default.renameSync(tmpPath, pdfPath);
|
|
7341
7334
|
logger.info(`PDF encrypted with password.`);
|
|
7342
7335
|
} catch (e) {
|
|
7343
|
-
|
|
7336
|
+
throw new Error(`qpdf encryption failed: ${e.message}`);
|
|
7344
7337
|
} finally {
|
|
7345
7338
|
if (import_fs5.default.existsSync(tmpPath)) import_fs5.default.unlinkSync(tmpPath);
|
|
7346
|
-
import_fs5.default.rmSync(
|
|
7339
|
+
import_fs5.default.rmSync(argFile, { force: true });
|
|
7347
7340
|
}
|
|
7348
7341
|
}
|
|
7349
7342
|
isQpdfAvailable() {
|
|
@@ -7668,7 +7661,8 @@ var init_PdfGenerator = __esm({
|
|
|
7668
7661
|
if (capBytes > 0) {
|
|
7669
7662
|
const sizeBytes = (await import_fs8.default.promises.stat(outputPath)).size;
|
|
7670
7663
|
if (sizeBytes > capBytes) {
|
|
7671
|
-
const
|
|
7664
|
+
const inlinedCount = Math.min(templateData.failures.length, compression.maxInlineFailures);
|
|
7665
|
+
const tighter = this.tightenForCap(compression, sizeBytes, capBytes, inlinedCount);
|
|
7672
7666
|
logger.warn(
|
|
7673
7667
|
`PDF size ${fmtMb(sizeBytes)} exceeds cap ${fmtMb(capBytes)} \u2014 re-rendering (jpeg-q=${tighter.quality}, max-w=${tighter.maxWidth}px, inline-failures\u2264${tighter.maxInlineFailures}).`
|
|
7674
7668
|
);
|
|
@@ -7697,11 +7691,30 @@ var init_PdfGenerator = __esm({
|
|
|
7697
7691
|
}
|
|
7698
7692
|
resolveTemplateSources(options) {
|
|
7699
7693
|
if (options.templatePath && options.templatePath.length > 0) {
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
absolutePath
|
|
7703
|
-
|
|
7704
|
-
}
|
|
7694
|
+
const byAbsolutePath = /* @__PURE__ */ new Map();
|
|
7695
|
+
for (const p of options.templatePath) {
|
|
7696
|
+
const absolutePath = import_path6.default.isAbsolute(p) ? p : import_path6.default.resolve(process.cwd(), p);
|
|
7697
|
+
if (!byAbsolutePath.has(absolutePath)) byAbsolutePath.set(absolutePath, p);
|
|
7698
|
+
}
|
|
7699
|
+
const labelCounts = /* @__PURE__ */ new Map();
|
|
7700
|
+
for (const rawPath of byAbsolutePath.values()) {
|
|
7701
|
+
const label = import_path6.default.basename(rawPath, ".hbs");
|
|
7702
|
+
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
|
7703
|
+
}
|
|
7704
|
+
const seenPerLabel = /* @__PURE__ */ new Map();
|
|
7705
|
+
return Array.from(byAbsolutePath.entries()).map(([absolutePath, rawPath]) => {
|
|
7706
|
+
const baseLabel = import_path6.default.basename(rawPath, ".hbs");
|
|
7707
|
+
let label = baseLabel;
|
|
7708
|
+
if ((labelCounts.get(baseLabel) ?? 0) > 1) {
|
|
7709
|
+
const n = (seenPerLabel.get(baseLabel) ?? 0) + 1;
|
|
7710
|
+
seenPerLabel.set(baseLabel, n);
|
|
7711
|
+
label = `${baseLabel}-${n}`;
|
|
7712
|
+
logger.warn(
|
|
7713
|
+
`Multiple templatePath entries resolve to the basename "${baseLabel}" \u2014 disambiguating output filenames with a numeric suffix ("${label}").`
|
|
7714
|
+
);
|
|
7715
|
+
}
|
|
7716
|
+
return { kind: "custom", absolutePath, label };
|
|
7717
|
+
});
|
|
7705
7718
|
}
|
|
7706
7719
|
const rawIds = Array.isArray(options.template) ? options.template : [options.template];
|
|
7707
7720
|
return [...new Set(rawIds)].map((id) => ({ kind: "builtin", id }));
|
|
@@ -7723,8 +7736,8 @@ var init_PdfGenerator = __esm({
|
|
|
7723
7736
|
* Also tightens JPEG quality and width to claw back bytes from each
|
|
7724
7737
|
* remaining image.
|
|
7725
7738
|
*/
|
|
7726
|
-
tightenForCap(prev, actualBytes, capBytes) {
|
|
7727
|
-
const observedPerFailure = actualBytes / Math.max(1,
|
|
7739
|
+
tightenForCap(prev, actualBytes, capBytes, actualInlinedCount) {
|
|
7740
|
+
const observedPerFailure = actualBytes / Math.max(1, actualInlinedCount);
|
|
7728
7741
|
const targetInline = Math.max(20, Math.floor(capBytes * 0.85 / observedPerFailure));
|
|
7729
7742
|
return {
|
|
7730
7743
|
effective: "max",
|
package/dist/index.js
CHANGED
|
@@ -11061,6 +11061,11 @@ var LicenseClient = class {
|
|
|
11061
11061
|
body: JSON.stringify(body),
|
|
11062
11062
|
signal: controller.signal
|
|
11063
11063
|
});
|
|
11064
|
+
if (res.status === 429 || res.status === 408) {
|
|
11065
|
+
await res.body?.cancel().catch(() => {
|
|
11066
|
+
});
|
|
11067
|
+
throw new Error(`HTTP ${res.status}`);
|
|
11068
|
+
}
|
|
11064
11069
|
if (res.status >= 400 && res.status < 500) {
|
|
11065
11070
|
const text = await res.text().catch(() => "");
|
|
11066
11071
|
throw new Error(`license-denied: ${text || `HTTP ${res.status}`}`);
|
|
@@ -11619,6 +11624,17 @@ init_cjs_shims();
|
|
|
11619
11624
|
// src/collector/stats-utils.ts
|
|
11620
11625
|
init_cjs_shims();
|
|
11621
11626
|
var TOP_SUITES_FOR_CHART = 10;
|
|
11627
|
+
function resolveTestStatus(test, result) {
|
|
11628
|
+
if (!result) return "skipped";
|
|
11629
|
+
const expected = test.expectedStatus ?? "passed";
|
|
11630
|
+
const actual = result.status;
|
|
11631
|
+
if (actual === "skipped") return "skipped";
|
|
11632
|
+
if (actual === "timedOut") return "timedOut";
|
|
11633
|
+
if (actual === "passed" && result.retry > 0) return "flaky";
|
|
11634
|
+
if (actual === "passed" && expected === "passed") return "passed";
|
|
11635
|
+
if (actual === "failed" && expected === "failed") return "passed";
|
|
11636
|
+
return "failed";
|
|
11637
|
+
}
|
|
11622
11638
|
function statsFromTests(tests) {
|
|
11623
11639
|
return {
|
|
11624
11640
|
total: tests.length,
|
|
@@ -11694,7 +11710,7 @@ var SuiteWalker = class {
|
|
|
11694
11710
|
}
|
|
11695
11711
|
buildTest(test, resultMap) {
|
|
11696
11712
|
const result = resultMap.get(test.id);
|
|
11697
|
-
const status =
|
|
11713
|
+
const status = resolveTestStatus(test, result);
|
|
11698
11714
|
return {
|
|
11699
11715
|
id: test.id,
|
|
11700
11716
|
title: test.title,
|
|
@@ -11707,17 +11723,6 @@ var SuiteWalker = class {
|
|
|
11707
11723
|
location: test.location ? `${test.location.file}:${test.location.line}` : void 0
|
|
11708
11724
|
};
|
|
11709
11725
|
}
|
|
11710
|
-
resolveStatus(test, result) {
|
|
11711
|
-
if (!result) return "skipped";
|
|
11712
|
-
const expected = test.expectedStatus ?? "passed";
|
|
11713
|
-
const actual = result.status;
|
|
11714
|
-
if (actual === "skipped") return "skipped";
|
|
11715
|
-
if (actual === "timedOut") return "timedOut";
|
|
11716
|
-
if (actual === "passed" && result.retry > 0) return "flaky";
|
|
11717
|
-
if (actual === "passed" && expected === "passed") return "passed";
|
|
11718
|
-
if (actual === "failed" && expected === "failed") return "passed";
|
|
11719
|
-
return "failed";
|
|
11720
|
-
}
|
|
11721
11726
|
};
|
|
11722
11727
|
|
|
11723
11728
|
// src/collector/severity.ts
|
|
@@ -11731,6 +11736,16 @@ var SEVERITY_RANK = {
|
|
|
11731
11736
|
function sortBySeverity(failures) {
|
|
11732
11737
|
return [...failures].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
11733
11738
|
}
|
|
11739
|
+
function inferSeverity(annotations, tags) {
|
|
11740
|
+
const allLabels = [
|
|
11741
|
+
...annotations.map((a) => a.type.toLowerCase()),
|
|
11742
|
+
...tags.map((t) => t.toLowerCase())
|
|
11743
|
+
];
|
|
11744
|
+
if (allLabels.some((l) => l.includes("critical") || l.includes("blocker"))) return "critical";
|
|
11745
|
+
if (allLabels.some((l) => l.includes("high") || l.includes("major"))) return "high";
|
|
11746
|
+
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
11747
|
+
return "medium";
|
|
11748
|
+
}
|
|
11734
11749
|
|
|
11735
11750
|
// src/utils/env.ts
|
|
11736
11751
|
init_cjs_shims();
|
|
@@ -11855,6 +11870,10 @@ var DataCollector = class {
|
|
|
11855
11870
|
constructor(captureOpts = {}) {
|
|
11856
11871
|
this.captureOpts = captureOpts;
|
|
11857
11872
|
this.resultMap = /* @__PURE__ */ new Map();
|
|
11873
|
+
// Parallel to resultMap — needed so computeStats() (which only sees
|
|
11874
|
+
// TestResult, not TestCase) can reconcile against expectedStatus the same
|
|
11875
|
+
// way resolveTestStatus does for the suite tree and the failure list.
|
|
11876
|
+
this.expectedStatusMap = /* @__PURE__ */ new Map();
|
|
11858
11877
|
this.failureRecords = [];
|
|
11859
11878
|
this.startTime = Date.now();
|
|
11860
11879
|
}
|
|
@@ -11865,7 +11884,10 @@ var DataCollector = class {
|
|
|
11865
11884
|
}
|
|
11866
11885
|
onTestEnd(test, result) {
|
|
11867
11886
|
this.resultMap.set(test.id, result);
|
|
11868
|
-
|
|
11887
|
+
this.expectedStatusMap.set(test.id, test.expectedStatus);
|
|
11888
|
+
const resolved = resolveTestStatus(test, result);
|
|
11889
|
+
const isRealFailure = resolved === "failed" || resolved === "timedOut";
|
|
11890
|
+
if (isRealFailure) {
|
|
11869
11891
|
const existing = this.failureRecords.findIndex((f) => f.testId === test.id);
|
|
11870
11892
|
const record = this.buildFailureRecord(test, result);
|
|
11871
11893
|
if (existing >= 0) {
|
|
@@ -11905,7 +11927,7 @@ var DataCollector = class {
|
|
|
11905
11927
|
(a) => a.name.toLowerCase().includes("screenshot") && a.contentType.startsWith("image/")
|
|
11906
11928
|
);
|
|
11907
11929
|
const suitePath = test.titlePath().slice(0, -1);
|
|
11908
|
-
const severity =
|
|
11930
|
+
const severity = inferSeverity(test.annotations ?? [], test.tags ?? []);
|
|
11909
11931
|
return {
|
|
11910
11932
|
testId: test.id,
|
|
11911
11933
|
testTitle: test.title,
|
|
@@ -11928,28 +11950,18 @@ var DataCollector = class {
|
|
|
11928
11950
|
execution: executionFromResult(result, this.captureOpts)
|
|
11929
11951
|
};
|
|
11930
11952
|
}
|
|
11931
|
-
inferSeverity(test) {
|
|
11932
|
-
const annotations = test.annotations ?? [];
|
|
11933
|
-
const tags = test.tags ?? [];
|
|
11934
|
-
const allLabels = [
|
|
11935
|
-
...annotations.map((a) => a.type.toLowerCase()),
|
|
11936
|
-
...tags.map((t) => t.toLowerCase())
|
|
11937
|
-
];
|
|
11938
|
-
if (allLabels.some((l) => l.includes("critical") || l.includes("blocker"))) return "critical";
|
|
11939
|
-
if (allLabels.some((l) => l.includes("high") || l.includes("major"))) return "high";
|
|
11940
|
-
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
11941
|
-
return "medium";
|
|
11942
|
-
}
|
|
11943
11953
|
computeStats(fullResult) {
|
|
11944
11954
|
let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
|
|
11945
|
-
for (const [, result] of this.resultMap) {
|
|
11955
|
+
for (const [testId, result] of this.resultMap) {
|
|
11946
11956
|
total++;
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
// push passRate over 100%.
|
|
11957
|
+
const expectedStatus = this.expectedStatusMap.get(testId);
|
|
11958
|
+
const resolved = resolveTestStatus({ expectedStatus }, result);
|
|
11959
|
+
switch (resolved) {
|
|
11951
11960
|
case "passed":
|
|
11952
|
-
|
|
11961
|
+
passed++;
|
|
11962
|
+
break;
|
|
11963
|
+
case "flaky":
|
|
11964
|
+
flaky++;
|
|
11953
11965
|
break;
|
|
11954
11966
|
case "failed":
|
|
11955
11967
|
failed++;
|
|
@@ -11961,9 +11973,6 @@ var DataCollector = class {
|
|
|
11961
11973
|
case "skipped":
|
|
11962
11974
|
skipped++;
|
|
11963
11975
|
break;
|
|
11964
|
-
case "interrupted":
|
|
11965
|
-
failed++;
|
|
11966
|
-
break;
|
|
11967
11976
|
}
|
|
11968
11977
|
}
|
|
11969
11978
|
const duration = fullResult.duration ?? Date.now() - this.startTime;
|
|
@@ -12123,18 +12132,26 @@ var ShardMerger = class {
|
|
|
12123
12132
|
});
|
|
12124
12133
|
}
|
|
12125
12134
|
buildDescribeSuites(jsonSuites, projectName, parentPath) {
|
|
12126
|
-
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12135
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12136
|
+
for (const suite of jsonSuites) {
|
|
12137
|
+
if (!groups.has(suite.title)) groups.set(suite.title, []);
|
|
12138
|
+
groups.get(suite.title).push(suite);
|
|
12139
|
+
}
|
|
12140
|
+
return Array.from(groups.values()).map((group) => {
|
|
12141
|
+
const ref = group[0];
|
|
12142
|
+
const suitePath = [...parentPath, ref.title];
|
|
12143
|
+
const allSpecs = group.flatMap((s) => s.specs);
|
|
12144
|
+
const allChildSuites = group.flatMap((s) => s.suites ?? []);
|
|
12145
|
+
const tests = allSpecs.map((spec) => this.specToTestNode(spec, projectName, suitePath));
|
|
12146
|
+
const childSuites = this.buildDescribeSuites(allChildSuites, projectName, suitePath);
|
|
12130
12147
|
const stats = aggregateStats([
|
|
12131
12148
|
...childSuites.map((s) => s.stats),
|
|
12132
12149
|
statsFromTests(tests)
|
|
12133
12150
|
]);
|
|
12134
12151
|
return {
|
|
12135
|
-
title:
|
|
12152
|
+
title: ref.title,
|
|
12136
12153
|
type: "describe",
|
|
12137
|
-
location:
|
|
12154
|
+
location: ref.file ? `${ref.file}:${ref.line ?? 0}` : void 0,
|
|
12138
12155
|
suites: childSuites,
|
|
12139
12156
|
tests,
|
|
12140
12157
|
stats
|
|
@@ -12240,7 +12257,7 @@ var ShardMerger = class {
|
|
|
12240
12257
|
duration: r.duration
|
|
12241
12258
|
})),
|
|
12242
12259
|
durationMs: lastResult?.duration ?? 0,
|
|
12243
|
-
severity:
|
|
12260
|
+
severity: inferSeverity(test.annotations ?? [], spec.tags ?? []),
|
|
12244
12261
|
execution: lastResult ? buildExecution(
|
|
12245
12262
|
{
|
|
12246
12263
|
// Guard each field — a partial / non-conformant / older shard JSON can
|
|
@@ -12816,25 +12833,18 @@ var PdfEncryptor = class {
|
|
|
12816
12833
|
return;
|
|
12817
12834
|
}
|
|
12818
12835
|
const tmpPath = import_path5.default.join(import_os5.default.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
|
|
12819
|
-
const
|
|
12836
|
+
const argFile = import_path5.default.join(import_os5.default.tmpdir(), `rf-qpdf-args-${import_crypto7.default.randomBytes(8).toString("hex")}`);
|
|
12820
12837
|
try {
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
(0, import_child_process3.execFileSync)("qpdf", [
|
|
12824
|
-
`--password-file=${pwFile}`,
|
|
12825
|
-
"--encrypt",
|
|
12826
|
-
"256",
|
|
12827
|
-
"--",
|
|
12828
|
-
pdfPath,
|
|
12829
|
-
tmpPath
|
|
12830
|
-
], { stdio: "pipe" });
|
|
12838
|
+
const args = ["--encrypt", password, password, "256", "--", pdfPath, tmpPath];
|
|
12839
|
+
import_fs8.default.writeFileSync(argFile, args.join("\n"), { mode: 384 });
|
|
12840
|
+
(0, import_child_process3.execFileSync)("qpdf", [`@${argFile}`], { stdio: "pipe" });
|
|
12831
12841
|
import_fs8.default.renameSync(tmpPath, pdfPath);
|
|
12832
12842
|
logger.info(`PDF encrypted with password.`);
|
|
12833
12843
|
} catch (e) {
|
|
12834
|
-
|
|
12844
|
+
throw new Error(`qpdf encryption failed: ${e.message}`);
|
|
12835
12845
|
} finally {
|
|
12836
12846
|
if (import_fs8.default.existsSync(tmpPath)) import_fs8.default.unlinkSync(tmpPath);
|
|
12837
|
-
import_fs8.default.rmSync(
|
|
12847
|
+
import_fs8.default.rmSync(argFile, { force: true });
|
|
12838
12848
|
}
|
|
12839
12849
|
}
|
|
12840
12850
|
isQpdfAvailable() {
|
|
@@ -13083,7 +13093,8 @@ var PdfGenerator = class {
|
|
|
13083
13093
|
if (capBytes > 0) {
|
|
13084
13094
|
const sizeBytes = (await import_fs11.default.promises.stat(outputPath)).size;
|
|
13085
13095
|
if (sizeBytes > capBytes) {
|
|
13086
|
-
const
|
|
13096
|
+
const inlinedCount = Math.min(templateData.failures.length, compression.maxInlineFailures);
|
|
13097
|
+
const tighter = this.tightenForCap(compression, sizeBytes, capBytes, inlinedCount);
|
|
13087
13098
|
logger.warn(
|
|
13088
13099
|
`PDF size ${fmtMb(sizeBytes)} exceeds cap ${fmtMb(capBytes)} \u2014 re-rendering (jpeg-q=${tighter.quality}, max-w=${tighter.maxWidth}px, inline-failures\u2264${tighter.maxInlineFailures}).`
|
|
13089
13100
|
);
|
|
@@ -13112,11 +13123,30 @@ var PdfGenerator = class {
|
|
|
13112
13123
|
}
|
|
13113
13124
|
resolveTemplateSources(options) {
|
|
13114
13125
|
if (options.templatePath && options.templatePath.length > 0) {
|
|
13115
|
-
|
|
13116
|
-
|
|
13117
|
-
absolutePath
|
|
13118
|
-
|
|
13119
|
-
}
|
|
13126
|
+
const byAbsolutePath = /* @__PURE__ */ new Map();
|
|
13127
|
+
for (const p of options.templatePath) {
|
|
13128
|
+
const absolutePath = import_path8.default.isAbsolute(p) ? p : import_path8.default.resolve(process.cwd(), p);
|
|
13129
|
+
if (!byAbsolutePath.has(absolutePath)) byAbsolutePath.set(absolutePath, p);
|
|
13130
|
+
}
|
|
13131
|
+
const labelCounts = /* @__PURE__ */ new Map();
|
|
13132
|
+
for (const rawPath of byAbsolutePath.values()) {
|
|
13133
|
+
const label = import_path8.default.basename(rawPath, ".hbs");
|
|
13134
|
+
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
|
13135
|
+
}
|
|
13136
|
+
const seenPerLabel = /* @__PURE__ */ new Map();
|
|
13137
|
+
return Array.from(byAbsolutePath.entries()).map(([absolutePath, rawPath]) => {
|
|
13138
|
+
const baseLabel = import_path8.default.basename(rawPath, ".hbs");
|
|
13139
|
+
let label = baseLabel;
|
|
13140
|
+
if ((labelCounts.get(baseLabel) ?? 0) > 1) {
|
|
13141
|
+
const n = (seenPerLabel.get(baseLabel) ?? 0) + 1;
|
|
13142
|
+
seenPerLabel.set(baseLabel, n);
|
|
13143
|
+
label = `${baseLabel}-${n}`;
|
|
13144
|
+
logger.warn(
|
|
13145
|
+
`Multiple templatePath entries resolve to the basename "${baseLabel}" \u2014 disambiguating output filenames with a numeric suffix ("${label}").`
|
|
13146
|
+
);
|
|
13147
|
+
}
|
|
13148
|
+
return { kind: "custom", absolutePath, label };
|
|
13149
|
+
});
|
|
13120
13150
|
}
|
|
13121
13151
|
const rawIds = Array.isArray(options.template) ? options.template : [options.template];
|
|
13122
13152
|
return [...new Set(rawIds)].map((id) => ({ kind: "builtin", id }));
|
|
@@ -13138,8 +13168,8 @@ var PdfGenerator = class {
|
|
|
13138
13168
|
* Also tightens JPEG quality and width to claw back bytes from each
|
|
13139
13169
|
* remaining image.
|
|
13140
13170
|
*/
|
|
13141
|
-
tightenForCap(prev, actualBytes, capBytes) {
|
|
13142
|
-
const observedPerFailure = actualBytes / Math.max(1,
|
|
13171
|
+
tightenForCap(prev, actualBytes, capBytes, actualInlinedCount) {
|
|
13172
|
+
const observedPerFailure = actualBytes / Math.max(1, actualInlinedCount);
|
|
13143
13173
|
const targetInline = Math.max(20, Math.floor(capBytes * 0.85 / observedPerFailure));
|
|
13144
13174
|
return {
|
|
13145
13175
|
effective: "max",
|
|
@@ -14001,7 +14031,7 @@ var PdfReporter = class {
|
|
|
14001
14031
|
this.liveConsole = false;
|
|
14002
14032
|
this.options = parseOptions(rawOptions);
|
|
14003
14033
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14004
|
-
const version = true ? "0.13.
|
|
14034
|
+
const version = true ? "0.13.2" : "0.x";
|
|
14005
14035
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
14006
14036
|
this.licenseClient = new LicenseClient({
|
|
14007
14037
|
licenseKey: this.options.licenseKey,
|
package/dist/index.mjs
CHANGED
|
@@ -11062,6 +11062,11 @@ var LicenseClient = class {
|
|
|
11062
11062
|
body: JSON.stringify(body),
|
|
11063
11063
|
signal: controller.signal
|
|
11064
11064
|
});
|
|
11065
|
+
if (res.status === 429 || res.status === 408) {
|
|
11066
|
+
await res.body?.cancel().catch(() => {
|
|
11067
|
+
});
|
|
11068
|
+
throw new Error(`HTTP ${res.status}`);
|
|
11069
|
+
}
|
|
11065
11070
|
if (res.status >= 400 && res.status < 500) {
|
|
11066
11071
|
const text = await res.text().catch(() => "");
|
|
11067
11072
|
throw new Error(`license-denied: ${text || `HTTP ${res.status}`}`);
|
|
@@ -11620,6 +11625,17 @@ init_esm_shims();
|
|
|
11620
11625
|
// src/collector/stats-utils.ts
|
|
11621
11626
|
init_esm_shims();
|
|
11622
11627
|
var TOP_SUITES_FOR_CHART = 10;
|
|
11628
|
+
function resolveTestStatus(test, result) {
|
|
11629
|
+
if (!result) return "skipped";
|
|
11630
|
+
const expected = test.expectedStatus ?? "passed";
|
|
11631
|
+
const actual = result.status;
|
|
11632
|
+
if (actual === "skipped") return "skipped";
|
|
11633
|
+
if (actual === "timedOut") return "timedOut";
|
|
11634
|
+
if (actual === "passed" && result.retry > 0) return "flaky";
|
|
11635
|
+
if (actual === "passed" && expected === "passed") return "passed";
|
|
11636
|
+
if (actual === "failed" && expected === "failed") return "passed";
|
|
11637
|
+
return "failed";
|
|
11638
|
+
}
|
|
11623
11639
|
function statsFromTests(tests) {
|
|
11624
11640
|
return {
|
|
11625
11641
|
total: tests.length,
|
|
@@ -11695,7 +11711,7 @@ var SuiteWalker = class {
|
|
|
11695
11711
|
}
|
|
11696
11712
|
buildTest(test, resultMap) {
|
|
11697
11713
|
const result = resultMap.get(test.id);
|
|
11698
|
-
const status =
|
|
11714
|
+
const status = resolveTestStatus(test, result);
|
|
11699
11715
|
return {
|
|
11700
11716
|
id: test.id,
|
|
11701
11717
|
title: test.title,
|
|
@@ -11708,17 +11724,6 @@ var SuiteWalker = class {
|
|
|
11708
11724
|
location: test.location ? `${test.location.file}:${test.location.line}` : void 0
|
|
11709
11725
|
};
|
|
11710
11726
|
}
|
|
11711
|
-
resolveStatus(test, result) {
|
|
11712
|
-
if (!result) return "skipped";
|
|
11713
|
-
const expected = test.expectedStatus ?? "passed";
|
|
11714
|
-
const actual = result.status;
|
|
11715
|
-
if (actual === "skipped") return "skipped";
|
|
11716
|
-
if (actual === "timedOut") return "timedOut";
|
|
11717
|
-
if (actual === "passed" && result.retry > 0) return "flaky";
|
|
11718
|
-
if (actual === "passed" && expected === "passed") return "passed";
|
|
11719
|
-
if (actual === "failed" && expected === "failed") return "passed";
|
|
11720
|
-
return "failed";
|
|
11721
|
-
}
|
|
11722
11727
|
};
|
|
11723
11728
|
|
|
11724
11729
|
// src/collector/severity.ts
|
|
@@ -11732,6 +11737,16 @@ var SEVERITY_RANK = {
|
|
|
11732
11737
|
function sortBySeverity(failures) {
|
|
11733
11738
|
return [...failures].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
11734
11739
|
}
|
|
11740
|
+
function inferSeverity(annotations, tags) {
|
|
11741
|
+
const allLabels = [
|
|
11742
|
+
...annotations.map((a) => a.type.toLowerCase()),
|
|
11743
|
+
...tags.map((t) => t.toLowerCase())
|
|
11744
|
+
];
|
|
11745
|
+
if (allLabels.some((l) => l.includes("critical") || l.includes("blocker"))) return "critical";
|
|
11746
|
+
if (allLabels.some((l) => l.includes("high") || l.includes("major"))) return "high";
|
|
11747
|
+
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
11748
|
+
return "medium";
|
|
11749
|
+
}
|
|
11735
11750
|
|
|
11736
11751
|
// src/utils/env.ts
|
|
11737
11752
|
init_esm_shims();
|
|
@@ -11856,6 +11871,10 @@ var DataCollector = class {
|
|
|
11856
11871
|
constructor(captureOpts = {}) {
|
|
11857
11872
|
this.captureOpts = captureOpts;
|
|
11858
11873
|
this.resultMap = /* @__PURE__ */ new Map();
|
|
11874
|
+
// Parallel to resultMap — needed so computeStats() (which only sees
|
|
11875
|
+
// TestResult, not TestCase) can reconcile against expectedStatus the same
|
|
11876
|
+
// way resolveTestStatus does for the suite tree and the failure list.
|
|
11877
|
+
this.expectedStatusMap = /* @__PURE__ */ new Map();
|
|
11859
11878
|
this.failureRecords = [];
|
|
11860
11879
|
this.startTime = Date.now();
|
|
11861
11880
|
}
|
|
@@ -11866,7 +11885,10 @@ var DataCollector = class {
|
|
|
11866
11885
|
}
|
|
11867
11886
|
onTestEnd(test, result) {
|
|
11868
11887
|
this.resultMap.set(test.id, result);
|
|
11869
|
-
|
|
11888
|
+
this.expectedStatusMap.set(test.id, test.expectedStatus);
|
|
11889
|
+
const resolved = resolveTestStatus(test, result);
|
|
11890
|
+
const isRealFailure = resolved === "failed" || resolved === "timedOut";
|
|
11891
|
+
if (isRealFailure) {
|
|
11870
11892
|
const existing = this.failureRecords.findIndex((f) => f.testId === test.id);
|
|
11871
11893
|
const record = this.buildFailureRecord(test, result);
|
|
11872
11894
|
if (existing >= 0) {
|
|
@@ -11906,7 +11928,7 @@ var DataCollector = class {
|
|
|
11906
11928
|
(a) => a.name.toLowerCase().includes("screenshot") && a.contentType.startsWith("image/")
|
|
11907
11929
|
);
|
|
11908
11930
|
const suitePath = test.titlePath().slice(0, -1);
|
|
11909
|
-
const severity =
|
|
11931
|
+
const severity = inferSeverity(test.annotations ?? [], test.tags ?? []);
|
|
11910
11932
|
return {
|
|
11911
11933
|
testId: test.id,
|
|
11912
11934
|
testTitle: test.title,
|
|
@@ -11929,28 +11951,18 @@ var DataCollector = class {
|
|
|
11929
11951
|
execution: executionFromResult(result, this.captureOpts)
|
|
11930
11952
|
};
|
|
11931
11953
|
}
|
|
11932
|
-
inferSeverity(test) {
|
|
11933
|
-
const annotations = test.annotations ?? [];
|
|
11934
|
-
const tags = test.tags ?? [];
|
|
11935
|
-
const allLabels = [
|
|
11936
|
-
...annotations.map((a) => a.type.toLowerCase()),
|
|
11937
|
-
...tags.map((t) => t.toLowerCase())
|
|
11938
|
-
];
|
|
11939
|
-
if (allLabels.some((l) => l.includes("critical") || l.includes("blocker"))) return "critical";
|
|
11940
|
-
if (allLabels.some((l) => l.includes("high") || l.includes("major"))) return "high";
|
|
11941
|
-
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
11942
|
-
return "medium";
|
|
11943
|
-
}
|
|
11944
11954
|
computeStats(fullResult) {
|
|
11945
11955
|
let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
|
|
11946
|
-
for (const [, result] of this.resultMap) {
|
|
11956
|
+
for (const [testId, result] of this.resultMap) {
|
|
11947
11957
|
total++;
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
// push passRate over 100%.
|
|
11958
|
+
const expectedStatus = this.expectedStatusMap.get(testId);
|
|
11959
|
+
const resolved = resolveTestStatus({ expectedStatus }, result);
|
|
11960
|
+
switch (resolved) {
|
|
11952
11961
|
case "passed":
|
|
11953
|
-
|
|
11962
|
+
passed++;
|
|
11963
|
+
break;
|
|
11964
|
+
case "flaky":
|
|
11965
|
+
flaky++;
|
|
11954
11966
|
break;
|
|
11955
11967
|
case "failed":
|
|
11956
11968
|
failed++;
|
|
@@ -11962,9 +11974,6 @@ var DataCollector = class {
|
|
|
11962
11974
|
case "skipped":
|
|
11963
11975
|
skipped++;
|
|
11964
11976
|
break;
|
|
11965
|
-
case "interrupted":
|
|
11966
|
-
failed++;
|
|
11967
|
-
break;
|
|
11968
11977
|
}
|
|
11969
11978
|
}
|
|
11970
11979
|
const duration = fullResult.duration ?? Date.now() - this.startTime;
|
|
@@ -12124,18 +12133,26 @@ var ShardMerger = class {
|
|
|
12124
12133
|
});
|
|
12125
12134
|
}
|
|
12126
12135
|
buildDescribeSuites(jsonSuites, projectName, parentPath) {
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12136
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12137
|
+
for (const suite of jsonSuites) {
|
|
12138
|
+
if (!groups.has(suite.title)) groups.set(suite.title, []);
|
|
12139
|
+
groups.get(suite.title).push(suite);
|
|
12140
|
+
}
|
|
12141
|
+
return Array.from(groups.values()).map((group) => {
|
|
12142
|
+
const ref = group[0];
|
|
12143
|
+
const suitePath = [...parentPath, ref.title];
|
|
12144
|
+
const allSpecs = group.flatMap((s) => s.specs);
|
|
12145
|
+
const allChildSuites = group.flatMap((s) => s.suites ?? []);
|
|
12146
|
+
const tests = allSpecs.map((spec) => this.specToTestNode(spec, projectName, suitePath));
|
|
12147
|
+
const childSuites = this.buildDescribeSuites(allChildSuites, projectName, suitePath);
|
|
12131
12148
|
const stats = aggregateStats([
|
|
12132
12149
|
...childSuites.map((s) => s.stats),
|
|
12133
12150
|
statsFromTests(tests)
|
|
12134
12151
|
]);
|
|
12135
12152
|
return {
|
|
12136
|
-
title:
|
|
12153
|
+
title: ref.title,
|
|
12137
12154
|
type: "describe",
|
|
12138
|
-
location:
|
|
12155
|
+
location: ref.file ? `${ref.file}:${ref.line ?? 0}` : void 0,
|
|
12139
12156
|
suites: childSuites,
|
|
12140
12157
|
tests,
|
|
12141
12158
|
stats
|
|
@@ -12241,7 +12258,7 @@ var ShardMerger = class {
|
|
|
12241
12258
|
duration: r.duration
|
|
12242
12259
|
})),
|
|
12243
12260
|
durationMs: lastResult?.duration ?? 0,
|
|
12244
|
-
severity:
|
|
12261
|
+
severity: inferSeverity(test.annotations ?? [], spec.tags ?? []),
|
|
12245
12262
|
execution: lastResult ? buildExecution(
|
|
12246
12263
|
{
|
|
12247
12264
|
// Guard each field — a partial / non-conformant / older shard JSON can
|
|
@@ -12817,25 +12834,18 @@ var PdfEncryptor = class {
|
|
|
12817
12834
|
return;
|
|
12818
12835
|
}
|
|
12819
12836
|
const tmpPath = path9.join(os8.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
|
|
12820
|
-
const
|
|
12837
|
+
const argFile = path9.join(os8.tmpdir(), `rf-qpdf-args-${crypto3.randomBytes(8).toString("hex")}`);
|
|
12821
12838
|
try {
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
execFileSync("qpdf", [
|
|
12825
|
-
`--password-file=${pwFile}`,
|
|
12826
|
-
"--encrypt",
|
|
12827
|
-
"256",
|
|
12828
|
-
"--",
|
|
12829
|
-
pdfPath,
|
|
12830
|
-
tmpPath
|
|
12831
|
-
], { stdio: "pipe" });
|
|
12839
|
+
const args = ["--encrypt", password, password, "256", "--", pdfPath, tmpPath];
|
|
12840
|
+
fs9.writeFileSync(argFile, args.join("\n"), { mode: 384 });
|
|
12841
|
+
execFileSync("qpdf", [`@${argFile}`], { stdio: "pipe" });
|
|
12832
12842
|
fs9.renameSync(tmpPath, pdfPath);
|
|
12833
12843
|
logger.info(`PDF encrypted with password.`);
|
|
12834
12844
|
} catch (e) {
|
|
12835
|
-
|
|
12845
|
+
throw new Error(`qpdf encryption failed: ${e.message}`);
|
|
12836
12846
|
} finally {
|
|
12837
12847
|
if (fs9.existsSync(tmpPath)) fs9.unlinkSync(tmpPath);
|
|
12838
|
-
fs9.rmSync(
|
|
12848
|
+
fs9.rmSync(argFile, { force: true });
|
|
12839
12849
|
}
|
|
12840
12850
|
}
|
|
12841
12851
|
isQpdfAvailable() {
|
|
@@ -13084,7 +13094,8 @@ var PdfGenerator = class {
|
|
|
13084
13094
|
if (capBytes > 0) {
|
|
13085
13095
|
const sizeBytes = (await fs12.promises.stat(outputPath)).size;
|
|
13086
13096
|
if (sizeBytes > capBytes) {
|
|
13087
|
-
const
|
|
13097
|
+
const inlinedCount = Math.min(templateData.failures.length, compression.maxInlineFailures);
|
|
13098
|
+
const tighter = this.tightenForCap(compression, sizeBytes, capBytes, inlinedCount);
|
|
13088
13099
|
logger.warn(
|
|
13089
13100
|
`PDF size ${fmtMb(sizeBytes)} exceeds cap ${fmtMb(capBytes)} \u2014 re-rendering (jpeg-q=${tighter.quality}, max-w=${tighter.maxWidth}px, inline-failures\u2264${tighter.maxInlineFailures}).`
|
|
13090
13101
|
);
|
|
@@ -13113,11 +13124,30 @@ var PdfGenerator = class {
|
|
|
13113
13124
|
}
|
|
13114
13125
|
resolveTemplateSources(options) {
|
|
13115
13126
|
if (options.templatePath && options.templatePath.length > 0) {
|
|
13116
|
-
|
|
13117
|
-
|
|
13118
|
-
absolutePath
|
|
13119
|
-
|
|
13120
|
-
}
|
|
13127
|
+
const byAbsolutePath = /* @__PURE__ */ new Map();
|
|
13128
|
+
for (const p of options.templatePath) {
|
|
13129
|
+
const absolutePath = path12.isAbsolute(p) ? p : path12.resolve(process.cwd(), p);
|
|
13130
|
+
if (!byAbsolutePath.has(absolutePath)) byAbsolutePath.set(absolutePath, p);
|
|
13131
|
+
}
|
|
13132
|
+
const labelCounts = /* @__PURE__ */ new Map();
|
|
13133
|
+
for (const rawPath of byAbsolutePath.values()) {
|
|
13134
|
+
const label = path12.basename(rawPath, ".hbs");
|
|
13135
|
+
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
|
13136
|
+
}
|
|
13137
|
+
const seenPerLabel = /* @__PURE__ */ new Map();
|
|
13138
|
+
return Array.from(byAbsolutePath.entries()).map(([absolutePath, rawPath]) => {
|
|
13139
|
+
const baseLabel = path12.basename(rawPath, ".hbs");
|
|
13140
|
+
let label = baseLabel;
|
|
13141
|
+
if ((labelCounts.get(baseLabel) ?? 0) > 1) {
|
|
13142
|
+
const n = (seenPerLabel.get(baseLabel) ?? 0) + 1;
|
|
13143
|
+
seenPerLabel.set(baseLabel, n);
|
|
13144
|
+
label = `${baseLabel}-${n}`;
|
|
13145
|
+
logger.warn(
|
|
13146
|
+
`Multiple templatePath entries resolve to the basename "${baseLabel}" \u2014 disambiguating output filenames with a numeric suffix ("${label}").`
|
|
13147
|
+
);
|
|
13148
|
+
}
|
|
13149
|
+
return { kind: "custom", absolutePath, label };
|
|
13150
|
+
});
|
|
13121
13151
|
}
|
|
13122
13152
|
const rawIds = Array.isArray(options.template) ? options.template : [options.template];
|
|
13123
13153
|
return [...new Set(rawIds)].map((id) => ({ kind: "builtin", id }));
|
|
@@ -13139,8 +13169,8 @@ var PdfGenerator = class {
|
|
|
13139
13169
|
* Also tightens JPEG quality and width to claw back bytes from each
|
|
13140
13170
|
* remaining image.
|
|
13141
13171
|
*/
|
|
13142
|
-
tightenForCap(prev, actualBytes, capBytes) {
|
|
13143
|
-
const observedPerFailure = actualBytes / Math.max(1,
|
|
13172
|
+
tightenForCap(prev, actualBytes, capBytes, actualInlinedCount) {
|
|
13173
|
+
const observedPerFailure = actualBytes / Math.max(1, actualInlinedCount);
|
|
13144
13174
|
const targetInline = Math.max(20, Math.floor(capBytes * 0.85 / observedPerFailure));
|
|
13145
13175
|
return {
|
|
13146
13176
|
effective: "max",
|
|
@@ -14002,7 +14032,7 @@ var PdfReporter = class {
|
|
|
14002
14032
|
this.liveConsole = false;
|
|
14003
14033
|
this.options = parseOptions(rawOptions);
|
|
14004
14034
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14005
|
-
const version = true ? "0.13.
|
|
14035
|
+
const version = true ? "0.13.2" : "0.x";
|
|
14006
14036
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
14007
14037
|
this.licenseClient = new LicenseClient({
|
|
14008
14038
|
licenseKey: this.options.licenseKey,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.2",
|
|
4
4
|
"description": "Playwright Test reporter that generates designed PDF reports: minimal, detailed, and executive templates with CI/CD integrations",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ReportForge",
|