@reportforge/playwright-pdf 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +1 -1
- package/dist/cli/index.js +38 -7
- package/dist/index.js +70 -13
- package/dist/index.mjs +70 -13
- package/dist/templates/partials/chart-summary-grid.hbs +23 -1
- package/dist/templates/partials/charts.hbs +143 -56
- package/dist/templates/partials/release-gate.hbs +8 -6
- package/dist/templates/styles/base.css +17 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
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.16.0] - 2026-07-07
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Every suite now gets its own bar in the Suite Results chart.** Runs with more than 10 suites switch from the compact side-by-side canvas to full-width charts (about 45 suites per page-sized canvas, "continued" across pages, declaration order preserved) — a run-wide per-suite coverage map instead of a silently truncated top-10. The Pass Rate doughnut takes the freed slot at 230px. A 150-suite safety ceiling (failures kept first, remainder summarised in a "+ N more suites" note under the chart) guards pathological runs only. Applies to every template that renders the charts section.
|
|
11
|
+
- **Per-suite totals on the bar chart**: each bar ends with its muted test count, so counts read directly instead of via gridlines.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Chart polish**: the pass-rate doughnut renders as a gauge (thicker ring, rounded and gapped segments), legends use colour dots, bars have rounded caps, and suite labels went from 9px to 10px with taller rows.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Chart axis labels no longer clip their first character.** Chart.js measured tick labels before the report's embedded Inter font finished loading and painted them after, so wide-glyph labels overflowed their measured gutter. Charts now initialise after `document.fonts.ready`, making measurement match paint exactly.
|
|
20
|
+
- **X-axis no longer draws one tick per test** on suites with large test counts (`stepSize: 1` replaced with auto steps capped at 12, integers only).
|
|
21
|
+
- `timedOut` counts are now carried through the suite-chart data type and the demo fixture's suite entries.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## [0.15.1] - 2026-07-07
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Release Recommendation copy is dash-free and data-driven.** The banner verdict reads `HOLD: N FAILURES` and the detail line now carries the per-run root-cause tally from failure analysis, e.g. "3 of 21 tests failed (2 assertion, 1 timeout). Each failure may be a product defect or an automation script issue. Review and verify manually before shipping." Falls back to the plain count when analysis is off; `flaky-by-retry` is excluded from the tally (flaky tests keep their own suffix). Timed-out variant matches.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
6
33
|
## [0.15.0] - 2026-07-07
|
|
7
34
|
|
|
8
35
|
### Changed
|
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Three templates, one reporter. Every PDF is fully offline: fonts, charts, and sc
|
|
|
47
47
|
|
|
48
48
|
- **3 templates**: `minimal` (developer), `detailed` (QA team), `executive` (stakeholders)
|
|
49
49
|
- **Detailed reports**: KPI dashboard, suite breakdown with inline failure detail (error, screenshot, root-cause chip) under each failed test, CI/CD environment
|
|
50
|
-
- **Embedded Chart.js**: pass-rate doughnut + per-suite bar chart, bundled inline (no CDN)
|
|
50
|
+
- **Embedded Chart.js**: pass-rate doughnut + per-suite bar chart, bundled inline (no CDN); every suite gets its own bar — large runs switch to full-width suite charts that continue across pages, so per-suite coverage and failures stay visible run-wide
|
|
51
51
|
- **Self-contained PDFs**: screenshots, fonts, logos all embedded; share or archive with confidence
|
|
52
52
|
- **Custom branding**: logo, primary/accent colours, watermark, PDF password encryption
|
|
53
53
|
- **Dynamic filenames**: `{date}`, `{branch}`, `{status}` tokens in the output path
|
package/dist/cli/index.js
CHANGED
|
@@ -120,8 +120,9 @@ function makeProject(name, suites) {
|
|
|
120
120
|
function suiteEntry(label, tests) {
|
|
121
121
|
const passed = tests.filter((t) => t.status === "passed" || t.status === "flaky").length;
|
|
122
122
|
const failed = tests.filter((t) => t.status === "failed").length;
|
|
123
|
+
const timedOut = tests.filter((t) => t.status === "timedOut").length;
|
|
123
124
|
const skipped = tests.filter((t) => t.status === "skipped").length;
|
|
124
|
-
return { label, passed, failed, skipped };
|
|
125
|
+
return { label, passed, failed, timedOut, skipped };
|
|
125
126
|
}
|
|
126
127
|
function buildDemoFailures() {
|
|
127
128
|
return [
|
|
@@ -6375,16 +6376,24 @@ var init_duration = __esm({
|
|
|
6375
6376
|
});
|
|
6376
6377
|
|
|
6377
6378
|
// src/analysis/SummaryWriter.ts
|
|
6378
|
-
function
|
|
6379
|
-
if (clusters.length === 0) return "";
|
|
6379
|
+
function tallyEntries(clusters) {
|
|
6380
6380
|
const byCategory = /* @__PURE__ */ new Map();
|
|
6381
6381
|
for (const c of clusters) {
|
|
6382
6382
|
byCategory.set(c.category, (byCategory.get(c.category) ?? 0) + c.count);
|
|
6383
6383
|
}
|
|
6384
|
+
return [...byCategory.entries()].sort((a, b) => b[1] - a[1]);
|
|
6385
|
+
}
|
|
6386
|
+
function buildOneLiner(clusters) {
|
|
6387
|
+
if (clusters.length === 0) return "";
|
|
6384
6388
|
const head = `${clusters.length} root ${clusters.length === 1 ? "cause" : "causes"}`;
|
|
6385
|
-
const tally =
|
|
6389
|
+
const tally = tallyEntries(clusters).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`);
|
|
6386
6390
|
return [head, ...tally].join(" \xB7 ");
|
|
6387
6391
|
}
|
|
6392
|
+
function buildFailureTally(analysis) {
|
|
6393
|
+
if (!analysis) return "";
|
|
6394
|
+
const failureClusters = analysis.clusters.filter((c) => c.category !== "flaky-by-retry");
|
|
6395
|
+
return tallyEntries(failureClusters).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`).join(", ");
|
|
6396
|
+
}
|
|
6388
6397
|
function dominantCategory(clusters) {
|
|
6389
6398
|
return clusters[0]?.category ?? "unknown";
|
|
6390
6399
|
}
|
|
@@ -6579,6 +6588,18 @@ function jsonForInlineScript(value) {
|
|
|
6579
6588
|
function isUnknownValue(v) {
|
|
6580
6589
|
return !v || v === "unknown";
|
|
6581
6590
|
}
|
|
6591
|
+
function buildSuiteChartChunks(rows) {
|
|
6592
|
+
const chunks = [];
|
|
6593
|
+
for (let i = 0; i < rows.length; i += SUITE_CHART_ROWS_PER_CANVAS) {
|
|
6594
|
+
const slice = rows.slice(i, i + SUITE_CHART_ROWS_PER_CANVAS);
|
|
6595
|
+
chunks.push({
|
|
6596
|
+
canvasId: `suitesBarChart${chunks.length}`,
|
|
6597
|
+
heightPx: slice.length * SUITE_CHART_ROW_PX + SUITE_CHART_PAD_PX,
|
|
6598
|
+
rows: slice
|
|
6599
|
+
});
|
|
6600
|
+
}
|
|
6601
|
+
return chunks;
|
|
6602
|
+
}
|
|
6582
6603
|
function deriveOptions(s) {
|
|
6583
6604
|
return {
|
|
6584
6605
|
showCharts: s.charts,
|
|
@@ -6590,7 +6611,7 @@ function deriveOptions(s) {
|
|
|
6590
6611
|
showFullFailures: s.fullFailures
|
|
6591
6612
|
};
|
|
6592
6613
|
}
|
|
6593
|
-
var import_handlebars, import_fs, import_path, A4_HEIGHT_MM, PAGE_MARGIN_Y_MM, PX_PER_MM, PAGE_CONTENT_HEIGHT_PX, SLOW_MEDIAN_FACTOR, TemplateEngine;
|
|
6614
|
+
var import_handlebars, import_fs, import_path, A4_HEIGHT_MM, PAGE_MARGIN_Y_MM, PX_PER_MM, SUITE_CHART_SMALL_MAX, SUITE_CHART_ROWS_PER_CANVAS, SUITE_CHART_ROW_PX, SUITE_CHART_PAD_PX, PAGE_CONTENT_HEIGHT_PX, SLOW_MEDIAN_FACTOR, TemplateEngine;
|
|
6594
6615
|
var init_engine = __esm({
|
|
6595
6616
|
"src/templates/engine.ts"() {
|
|
6596
6617
|
"use strict";
|
|
@@ -6605,6 +6626,10 @@ var init_engine = __esm({
|
|
|
6605
6626
|
A4_HEIGHT_MM = 297;
|
|
6606
6627
|
PAGE_MARGIN_Y_MM = 16;
|
|
6607
6628
|
PX_PER_MM = 96 / 25.4;
|
|
6629
|
+
SUITE_CHART_SMALL_MAX = 10;
|
|
6630
|
+
SUITE_CHART_ROWS_PER_CANVAS = 45;
|
|
6631
|
+
SUITE_CHART_ROW_PX = 18;
|
|
6632
|
+
SUITE_CHART_PAD_PX = 56;
|
|
6608
6633
|
PAGE_CONTENT_HEIGHT_PX = (A4_HEIGHT_MM - 2 * PAGE_MARGIN_Y_MM) * PX_PER_MM;
|
|
6609
6634
|
SLOW_MEDIAN_FACTOR = 2;
|
|
6610
6635
|
TemplateEngine = class {
|
|
@@ -6666,7 +6691,10 @@ var init_engine = __esm({
|
|
|
6666
6691
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
6667
6692
|
templateCSS: "",
|
|
6668
6693
|
chartjsScript: this.buildChartjsScript(),
|
|
6669
|
-
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX
|
|
6694
|
+
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
6695
|
+
releaseGateTally: buildFailureTally(data.analysis),
|
|
6696
|
+
suiteChartLarge: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX,
|
|
6697
|
+
suiteChartChunks: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX ? buildSuiteChartChunks(data.charts.suiteResults) : []
|
|
6670
6698
|
};
|
|
6671
6699
|
}
|
|
6672
6700
|
buildContext(data, template, userSections, slowTestThreshold) {
|
|
@@ -6690,7 +6718,10 @@ var init_engine = __esm({
|
|
|
6690
6718
|
baseCSS,
|
|
6691
6719
|
templateCSS,
|
|
6692
6720
|
chartjsScript: this.buildChartjsScript(),
|
|
6693
|
-
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX
|
|
6721
|
+
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
6722
|
+
releaseGateTally: buildFailureTally(data.analysis),
|
|
6723
|
+
suiteChartLarge: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX,
|
|
6724
|
+
suiteChartChunks: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX ? buildSuiteChartChunks(data.charts.suiteResults) : []
|
|
6694
6725
|
};
|
|
6695
6726
|
}
|
|
6696
6727
|
buildChartjsScript() {
|
package/dist/index.js
CHANGED
|
@@ -11291,16 +11291,24 @@ var SHORT_LABEL = {
|
|
|
11291
11291
|
"flaky-by-retry": "flaky",
|
|
11292
11292
|
unknown: "unclassified"
|
|
11293
11293
|
};
|
|
11294
|
-
function
|
|
11295
|
-
if (clusters.length === 0) return "";
|
|
11294
|
+
function tallyEntries(clusters) {
|
|
11296
11295
|
const byCategory = /* @__PURE__ */ new Map();
|
|
11297
11296
|
for (const c of clusters) {
|
|
11298
11297
|
byCategory.set(c.category, (byCategory.get(c.category) ?? 0) + c.count);
|
|
11299
11298
|
}
|
|
11299
|
+
return [...byCategory.entries()].sort((a, b) => b[1] - a[1]);
|
|
11300
|
+
}
|
|
11301
|
+
function buildOneLiner(clusters) {
|
|
11302
|
+
if (clusters.length === 0) return "";
|
|
11300
11303
|
const head = `${clusters.length} root ${clusters.length === 1 ? "cause" : "causes"}`;
|
|
11301
|
-
const tally =
|
|
11304
|
+
const tally = tallyEntries(clusters).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`);
|
|
11302
11305
|
return [head, ...tally].join(" \xB7 ");
|
|
11303
11306
|
}
|
|
11307
|
+
function buildFailureTally(analysis) {
|
|
11308
|
+
if (!analysis) return "";
|
|
11309
|
+
const failureClusters = analysis.clusters.filter((c) => c.category !== "flaky-by-retry");
|
|
11310
|
+
return tallyEntries(failureClusters).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`).join(", ");
|
|
11311
|
+
}
|
|
11304
11312
|
function dominantCategory(clusters) {
|
|
11305
11313
|
return clusters[0]?.category ?? "unknown";
|
|
11306
11314
|
}
|
|
@@ -11648,7 +11656,7 @@ init_cjs_shims();
|
|
|
11648
11656
|
|
|
11649
11657
|
// src/collector/stats-utils.ts
|
|
11650
11658
|
init_cjs_shims();
|
|
11651
|
-
var
|
|
11659
|
+
var SUITE_CHART_MAX_ROWS = 150;
|
|
11652
11660
|
function resolveTestStatus(test, result) {
|
|
11653
11661
|
if (!result) return "skipped";
|
|
11654
11662
|
const expected = test.expectedStatus ?? "passed";
|
|
@@ -11692,15 +11700,40 @@ function toRow(s) {
|
|
|
11692
11700
|
skipped: s.stats.skipped
|
|
11693
11701
|
};
|
|
11694
11702
|
}
|
|
11703
|
+
function capRows(rows, limit) {
|
|
11704
|
+
if (rows.length <= limit) return { rows };
|
|
11705
|
+
const size = (r) => r.passed + r.failed + r.timedOut + r.skipped;
|
|
11706
|
+
const scored = [...rows].sort(
|
|
11707
|
+
(a, b) => b.failed + b.timedOut - (a.failed + a.timedOut) || size(b) - size(a)
|
|
11708
|
+
);
|
|
11709
|
+
const rest = scored.slice(limit);
|
|
11710
|
+
const sums = rest.reduce(
|
|
11711
|
+
(acc, r) => ({
|
|
11712
|
+
passed: acc.passed + r.passed,
|
|
11713
|
+
failed: acc.failed + r.failed,
|
|
11714
|
+
timedOut: acc.timedOut + r.timedOut,
|
|
11715
|
+
skipped: acc.skipped + r.skipped
|
|
11716
|
+
}),
|
|
11717
|
+
{ passed: 0, failed: 0, timedOut: 0, skipped: 0 }
|
|
11718
|
+
);
|
|
11719
|
+
const parts = [
|
|
11720
|
+
sums.failed > 0 ? `${sums.failed} failed` : "",
|
|
11721
|
+
sums.timedOut > 0 ? `${sums.timedOut} timed out` : "",
|
|
11722
|
+
sums.passed > 0 ? `${sums.passed} passed` : "",
|
|
11723
|
+
sums.skipped > 0 ? `${sums.skipped} skipped` : ""
|
|
11724
|
+
].filter(Boolean);
|
|
11725
|
+
const overflowNote = `+ ${rest.length} more suites` + (parts.length > 0 ? `: ${parts.join(" \xB7 ")}` : "");
|
|
11726
|
+
return { rows: scored.slice(0, limit), overflowNote };
|
|
11727
|
+
}
|
|
11695
11728
|
function buildSuiteResults(projects, limit) {
|
|
11696
11729
|
const top = projects.flatMap((p) => p.suites);
|
|
11697
11730
|
if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
|
|
11698
11731
|
const file = top[0];
|
|
11699
11732
|
const fs14 = statsFromTests(file.tests);
|
|
11700
11733
|
const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs14.passed, failed: fs14.failed, timedOut: fs14.timedOut, skipped: fs14.skipped }] : [];
|
|
11701
|
-
return [...rootRow, ...file.suites.map(toRow)]
|
|
11734
|
+
return capRows([...rootRow, ...file.suites.map(toRow)], limit);
|
|
11702
11735
|
}
|
|
11703
|
-
return top.map(toRow)
|
|
11736
|
+
return capRows(top.map(toRow), limit);
|
|
11704
11737
|
}
|
|
11705
11738
|
|
|
11706
11739
|
// src/collector/SuiteWalker.ts
|
|
@@ -12045,7 +12078,7 @@ var DataCollector = class {
|
|
|
12045
12078
|
};
|
|
12046
12079
|
}
|
|
12047
12080
|
buildChartData(stats, projects) {
|
|
12048
|
-
const suiteResults = buildSuiteResults(projects,
|
|
12081
|
+
const { rows: suiteResults, overflowNote: suiteResultsOverflowNote } = buildSuiteResults(projects, SUITE_CHART_MAX_ROWS);
|
|
12049
12082
|
return {
|
|
12050
12083
|
passRate: {
|
|
12051
12084
|
passed: stats.passed,
|
|
@@ -12054,7 +12087,8 @@ var DataCollector = class {
|
|
|
12054
12087
|
skipped: stats.skipped,
|
|
12055
12088
|
flaky: stats.flaky
|
|
12056
12089
|
},
|
|
12057
|
-
suiteResults
|
|
12090
|
+
suiteResults,
|
|
12091
|
+
suiteResultsOverflowNote
|
|
12058
12092
|
};
|
|
12059
12093
|
}
|
|
12060
12094
|
};
|
|
@@ -12324,7 +12358,7 @@ var ShardMerger = class {
|
|
|
12324
12358
|
};
|
|
12325
12359
|
}
|
|
12326
12360
|
buildCharts(stats, projects) {
|
|
12327
|
-
const suiteResults = buildSuiteResults(projects,
|
|
12361
|
+
const { rows: suiteResults, overflowNote: suiteResultsOverflowNote } = buildSuiteResults(projects, SUITE_CHART_MAX_ROWS);
|
|
12328
12362
|
return {
|
|
12329
12363
|
passRate: {
|
|
12330
12364
|
passed: stats.passed,
|
|
@@ -12334,7 +12368,8 @@ var ShardMerger = class {
|
|
|
12334
12368
|
skipped: stats.skipped,
|
|
12335
12369
|
flaky: stats.flaky
|
|
12336
12370
|
},
|
|
12337
|
-
suiteResults
|
|
12371
|
+
suiteResults,
|
|
12372
|
+
suiteResultsOverflowNote
|
|
12338
12373
|
};
|
|
12339
12374
|
}
|
|
12340
12375
|
};
|
|
@@ -12368,11 +12403,27 @@ function jsonForInlineScript(value) {
|
|
|
12368
12403
|
var A4_HEIGHT_MM = 297;
|
|
12369
12404
|
var PAGE_MARGIN_Y_MM = 16;
|
|
12370
12405
|
var PX_PER_MM = 96 / 25.4;
|
|
12406
|
+
var SUITE_CHART_SMALL_MAX = 10;
|
|
12407
|
+
var SUITE_CHART_ROWS_PER_CANVAS = 45;
|
|
12408
|
+
var SUITE_CHART_ROW_PX = 18;
|
|
12409
|
+
var SUITE_CHART_PAD_PX = 56;
|
|
12371
12410
|
var PAGE_CONTENT_HEIGHT_PX = (A4_HEIGHT_MM - 2 * PAGE_MARGIN_Y_MM) * PX_PER_MM;
|
|
12372
12411
|
function isUnknownValue(v) {
|
|
12373
12412
|
return !v || v === "unknown";
|
|
12374
12413
|
}
|
|
12375
12414
|
var SLOW_MEDIAN_FACTOR = 2;
|
|
12415
|
+
function buildSuiteChartChunks(rows) {
|
|
12416
|
+
const chunks = [];
|
|
12417
|
+
for (let i = 0; i < rows.length; i += SUITE_CHART_ROWS_PER_CANVAS) {
|
|
12418
|
+
const slice = rows.slice(i, i + SUITE_CHART_ROWS_PER_CANVAS);
|
|
12419
|
+
chunks.push({
|
|
12420
|
+
canvasId: `suitesBarChart${chunks.length}`,
|
|
12421
|
+
heightPx: slice.length * SUITE_CHART_ROW_PX + SUITE_CHART_PAD_PX,
|
|
12422
|
+
rows: slice
|
|
12423
|
+
});
|
|
12424
|
+
}
|
|
12425
|
+
return chunks;
|
|
12426
|
+
}
|
|
12376
12427
|
function deriveOptions(s) {
|
|
12377
12428
|
return {
|
|
12378
12429
|
showCharts: s.charts,
|
|
@@ -12443,7 +12494,10 @@ var TemplateEngine = class {
|
|
|
12443
12494
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12444
12495
|
templateCSS: "",
|
|
12445
12496
|
chartjsScript: this.buildChartjsScript(),
|
|
12446
|
-
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX
|
|
12497
|
+
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12498
|
+
releaseGateTally: buildFailureTally(data.analysis),
|
|
12499
|
+
suiteChartLarge: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX,
|
|
12500
|
+
suiteChartChunks: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX ? buildSuiteChartChunks(data.charts.suiteResults) : []
|
|
12447
12501
|
};
|
|
12448
12502
|
}
|
|
12449
12503
|
buildContext(data, template, userSections, slowTestThreshold) {
|
|
@@ -12467,7 +12521,10 @@ var TemplateEngine = class {
|
|
|
12467
12521
|
baseCSS,
|
|
12468
12522
|
templateCSS,
|
|
12469
12523
|
chartjsScript: this.buildChartjsScript(),
|
|
12470
|
-
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX
|
|
12524
|
+
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12525
|
+
releaseGateTally: buildFailureTally(data.analysis),
|
|
12526
|
+
suiteChartLarge: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX,
|
|
12527
|
+
suiteChartChunks: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX ? buildSuiteChartChunks(data.charts.suiteResults) : []
|
|
12471
12528
|
};
|
|
12472
12529
|
}
|
|
12473
12530
|
buildChartjsScript() {
|
|
@@ -14062,7 +14119,7 @@ var PdfReporter = class {
|
|
|
14062
14119
|
this.liveConsole = false;
|
|
14063
14120
|
this.options = parseOptions(rawOptions);
|
|
14064
14121
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14065
|
-
const version = true ? "0.
|
|
14122
|
+
const version = true ? "0.16.0" : "0.x";
|
|
14066
14123
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
14067
14124
|
this.licenseClient = new LicenseClient({
|
|
14068
14125
|
licenseKey: this.options.licenseKey,
|
package/dist/index.mjs
CHANGED
|
@@ -11292,16 +11292,24 @@ var SHORT_LABEL = {
|
|
|
11292
11292
|
"flaky-by-retry": "flaky",
|
|
11293
11293
|
unknown: "unclassified"
|
|
11294
11294
|
};
|
|
11295
|
-
function
|
|
11296
|
-
if (clusters.length === 0) return "";
|
|
11295
|
+
function tallyEntries(clusters) {
|
|
11297
11296
|
const byCategory = /* @__PURE__ */ new Map();
|
|
11298
11297
|
for (const c of clusters) {
|
|
11299
11298
|
byCategory.set(c.category, (byCategory.get(c.category) ?? 0) + c.count);
|
|
11300
11299
|
}
|
|
11300
|
+
return [...byCategory.entries()].sort((a, b) => b[1] - a[1]);
|
|
11301
|
+
}
|
|
11302
|
+
function buildOneLiner(clusters) {
|
|
11303
|
+
if (clusters.length === 0) return "";
|
|
11301
11304
|
const head = `${clusters.length} root ${clusters.length === 1 ? "cause" : "causes"}`;
|
|
11302
|
-
const tally =
|
|
11305
|
+
const tally = tallyEntries(clusters).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`);
|
|
11303
11306
|
return [head, ...tally].join(" \xB7 ");
|
|
11304
11307
|
}
|
|
11308
|
+
function buildFailureTally(analysis) {
|
|
11309
|
+
if (!analysis) return "";
|
|
11310
|
+
const failureClusters = analysis.clusters.filter((c) => c.category !== "flaky-by-retry");
|
|
11311
|
+
return tallyEntries(failureClusters).map(([cat, count]) => `${count} ${SHORT_LABEL[cat]}`).join(", ");
|
|
11312
|
+
}
|
|
11305
11313
|
function dominantCategory(clusters) {
|
|
11306
11314
|
return clusters[0]?.category ?? "unknown";
|
|
11307
11315
|
}
|
|
@@ -11649,7 +11657,7 @@ init_esm_shims();
|
|
|
11649
11657
|
|
|
11650
11658
|
// src/collector/stats-utils.ts
|
|
11651
11659
|
init_esm_shims();
|
|
11652
|
-
var
|
|
11660
|
+
var SUITE_CHART_MAX_ROWS = 150;
|
|
11653
11661
|
function resolveTestStatus(test, result) {
|
|
11654
11662
|
if (!result) return "skipped";
|
|
11655
11663
|
const expected = test.expectedStatus ?? "passed";
|
|
@@ -11693,15 +11701,40 @@ function toRow(s) {
|
|
|
11693
11701
|
skipped: s.stats.skipped
|
|
11694
11702
|
};
|
|
11695
11703
|
}
|
|
11704
|
+
function capRows(rows, limit) {
|
|
11705
|
+
if (rows.length <= limit) return { rows };
|
|
11706
|
+
const size = (r) => r.passed + r.failed + r.timedOut + r.skipped;
|
|
11707
|
+
const scored = [...rows].sort(
|
|
11708
|
+
(a, b) => b.failed + b.timedOut - (a.failed + a.timedOut) || size(b) - size(a)
|
|
11709
|
+
);
|
|
11710
|
+
const rest = scored.slice(limit);
|
|
11711
|
+
const sums = rest.reduce(
|
|
11712
|
+
(acc, r) => ({
|
|
11713
|
+
passed: acc.passed + r.passed,
|
|
11714
|
+
failed: acc.failed + r.failed,
|
|
11715
|
+
timedOut: acc.timedOut + r.timedOut,
|
|
11716
|
+
skipped: acc.skipped + r.skipped
|
|
11717
|
+
}),
|
|
11718
|
+
{ passed: 0, failed: 0, timedOut: 0, skipped: 0 }
|
|
11719
|
+
);
|
|
11720
|
+
const parts = [
|
|
11721
|
+
sums.failed > 0 ? `${sums.failed} failed` : "",
|
|
11722
|
+
sums.timedOut > 0 ? `${sums.timedOut} timed out` : "",
|
|
11723
|
+
sums.passed > 0 ? `${sums.passed} passed` : "",
|
|
11724
|
+
sums.skipped > 0 ? `${sums.skipped} skipped` : ""
|
|
11725
|
+
].filter(Boolean);
|
|
11726
|
+
const overflowNote = `+ ${rest.length} more suites` + (parts.length > 0 ? `: ${parts.join(" \xB7 ")}` : "");
|
|
11727
|
+
return { rows: scored.slice(0, limit), overflowNote };
|
|
11728
|
+
}
|
|
11696
11729
|
function buildSuiteResults(projects, limit) {
|
|
11697
11730
|
const top = projects.flatMap((p) => p.suites);
|
|
11698
11731
|
if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
|
|
11699
11732
|
const file = top[0];
|
|
11700
11733
|
const fs14 = statsFromTests(file.tests);
|
|
11701
11734
|
const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs14.passed, failed: fs14.failed, timedOut: fs14.timedOut, skipped: fs14.skipped }] : [];
|
|
11702
|
-
return [...rootRow, ...file.suites.map(toRow)]
|
|
11735
|
+
return capRows([...rootRow, ...file.suites.map(toRow)], limit);
|
|
11703
11736
|
}
|
|
11704
|
-
return top.map(toRow)
|
|
11737
|
+
return capRows(top.map(toRow), limit);
|
|
11705
11738
|
}
|
|
11706
11739
|
|
|
11707
11740
|
// src/collector/SuiteWalker.ts
|
|
@@ -12046,7 +12079,7 @@ var DataCollector = class {
|
|
|
12046
12079
|
};
|
|
12047
12080
|
}
|
|
12048
12081
|
buildChartData(stats, projects) {
|
|
12049
|
-
const suiteResults = buildSuiteResults(projects,
|
|
12082
|
+
const { rows: suiteResults, overflowNote: suiteResultsOverflowNote } = buildSuiteResults(projects, SUITE_CHART_MAX_ROWS);
|
|
12050
12083
|
return {
|
|
12051
12084
|
passRate: {
|
|
12052
12085
|
passed: stats.passed,
|
|
@@ -12055,7 +12088,8 @@ var DataCollector = class {
|
|
|
12055
12088
|
skipped: stats.skipped,
|
|
12056
12089
|
flaky: stats.flaky
|
|
12057
12090
|
},
|
|
12058
|
-
suiteResults
|
|
12091
|
+
suiteResults,
|
|
12092
|
+
suiteResultsOverflowNote
|
|
12059
12093
|
};
|
|
12060
12094
|
}
|
|
12061
12095
|
};
|
|
@@ -12325,7 +12359,7 @@ var ShardMerger = class {
|
|
|
12325
12359
|
};
|
|
12326
12360
|
}
|
|
12327
12361
|
buildCharts(stats, projects) {
|
|
12328
|
-
const suiteResults = buildSuiteResults(projects,
|
|
12362
|
+
const { rows: suiteResults, overflowNote: suiteResultsOverflowNote } = buildSuiteResults(projects, SUITE_CHART_MAX_ROWS);
|
|
12329
12363
|
return {
|
|
12330
12364
|
passRate: {
|
|
12331
12365
|
passed: stats.passed,
|
|
@@ -12335,7 +12369,8 @@ var ShardMerger = class {
|
|
|
12335
12369
|
skipped: stats.skipped,
|
|
12336
12370
|
flaky: stats.flaky
|
|
12337
12371
|
},
|
|
12338
|
-
suiteResults
|
|
12372
|
+
suiteResults,
|
|
12373
|
+
suiteResultsOverflowNote
|
|
12339
12374
|
};
|
|
12340
12375
|
}
|
|
12341
12376
|
};
|
|
@@ -12369,11 +12404,27 @@ function jsonForInlineScript(value) {
|
|
|
12369
12404
|
var A4_HEIGHT_MM = 297;
|
|
12370
12405
|
var PAGE_MARGIN_Y_MM = 16;
|
|
12371
12406
|
var PX_PER_MM = 96 / 25.4;
|
|
12407
|
+
var SUITE_CHART_SMALL_MAX = 10;
|
|
12408
|
+
var SUITE_CHART_ROWS_PER_CANVAS = 45;
|
|
12409
|
+
var SUITE_CHART_ROW_PX = 18;
|
|
12410
|
+
var SUITE_CHART_PAD_PX = 56;
|
|
12372
12411
|
var PAGE_CONTENT_HEIGHT_PX = (A4_HEIGHT_MM - 2 * PAGE_MARGIN_Y_MM) * PX_PER_MM;
|
|
12373
12412
|
function isUnknownValue(v) {
|
|
12374
12413
|
return !v || v === "unknown";
|
|
12375
12414
|
}
|
|
12376
12415
|
var SLOW_MEDIAN_FACTOR = 2;
|
|
12416
|
+
function buildSuiteChartChunks(rows) {
|
|
12417
|
+
const chunks = [];
|
|
12418
|
+
for (let i = 0; i < rows.length; i += SUITE_CHART_ROWS_PER_CANVAS) {
|
|
12419
|
+
const slice = rows.slice(i, i + SUITE_CHART_ROWS_PER_CANVAS);
|
|
12420
|
+
chunks.push({
|
|
12421
|
+
canvasId: `suitesBarChart${chunks.length}`,
|
|
12422
|
+
heightPx: slice.length * SUITE_CHART_ROW_PX + SUITE_CHART_PAD_PX,
|
|
12423
|
+
rows: slice
|
|
12424
|
+
});
|
|
12425
|
+
}
|
|
12426
|
+
return chunks;
|
|
12427
|
+
}
|
|
12377
12428
|
function deriveOptions(s) {
|
|
12378
12429
|
return {
|
|
12379
12430
|
showCharts: s.charts,
|
|
@@ -12444,7 +12495,10 @@ var TemplateEngine = class {
|
|
|
12444
12495
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12445
12496
|
templateCSS: "",
|
|
12446
12497
|
chartjsScript: this.buildChartjsScript(),
|
|
12447
|
-
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX
|
|
12498
|
+
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12499
|
+
releaseGateTally: buildFailureTally(data.analysis),
|
|
12500
|
+
suiteChartLarge: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX,
|
|
12501
|
+
suiteChartChunks: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX ? buildSuiteChartChunks(data.charts.suiteResults) : []
|
|
12448
12502
|
};
|
|
12449
12503
|
}
|
|
12450
12504
|
buildContext(data, template, userSections, slowTestThreshold) {
|
|
@@ -12468,7 +12522,10 @@ var TemplateEngine = class {
|
|
|
12468
12522
|
baseCSS,
|
|
12469
12523
|
templateCSS,
|
|
12470
12524
|
chartjsScript: this.buildChartjsScript(),
|
|
12471
|
-
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX
|
|
12525
|
+
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12526
|
+
releaseGateTally: buildFailureTally(data.analysis),
|
|
12527
|
+
suiteChartLarge: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX,
|
|
12528
|
+
suiteChartChunks: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX ? buildSuiteChartChunks(data.charts.suiteResults) : []
|
|
12472
12529
|
};
|
|
12473
12530
|
}
|
|
12474
12531
|
buildChartjsScript() {
|
|
@@ -14063,7 +14120,7 @@ var PdfReporter = class {
|
|
|
14063
14120
|
this.liveConsole = false;
|
|
14064
14121
|
this.options = parseOptions(rawOptions);
|
|
14065
14122
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14066
|
-
const version = true ? "0.
|
|
14123
|
+
const version = true ? "0.16.0" : "0.x";
|
|
14067
14124
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
14068
14125
|
this.licenseClient = new LicenseClient({
|
|
14069
14126
|
licenseKey: this.options.licenseKey,
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
<div class="section charts-grid{{#if options.heroTrend}} charts-grid--secondary{{/if}}">
|
|
1
|
+
<div class="section charts-grid{{#if options.heroTrend}} charts-grid--secondary{{/if}}{{#if suiteChartLarge}} charts-grid--solo{{/if}}">
|
|
2
2
|
<div class="chart-card">
|
|
3
3
|
<h3>Pass Rate</h3>
|
|
4
|
+
{{#if suiteChartLarge}}
|
|
5
|
+
{{! Solo card (suite chart moved below) — use the freed space: bigger doughnut. }}
|
|
6
|
+
<canvas id="passRateChart" width="230" height="230"></canvas>
|
|
7
|
+
{{else}}
|
|
4
8
|
{{#if options.heroTrend}}
|
|
5
9
|
<canvas id="passRateChart" width="150" height="150"></canvas>
|
|
6
10
|
{{else}}
|
|
7
11
|
<canvas id="passRateChart" width="186" height="186"></canvas>
|
|
8
12
|
{{/if}}
|
|
13
|
+
{{/if}}
|
|
9
14
|
</div>
|
|
15
|
+
{{#unless suiteChartLarge}}
|
|
10
16
|
<div class="chart-card">
|
|
11
17
|
<h3>Suite Results</h3>
|
|
12
18
|
{{#if options.heroTrend}}
|
|
@@ -15,4 +21,20 @@
|
|
|
15
21
|
<canvas id="suitesBarChart" width="390" height="186"></canvas>
|
|
16
22
|
{{/if}}
|
|
17
23
|
</div>
|
|
24
|
+
{{/unless}}
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{{#if suiteChartLarge}}
|
|
28
|
+
{{! Large runs: every suite keeps its own bar. The chart leaves the side-by-side
|
|
29
|
+
grid, goes full width, and splits into page-sized canvases that flow across
|
|
30
|
+
pages — a continuous per-suite coverage map instead of a squashed canvas. }}
|
|
31
|
+
{{#each suiteChartChunks}}
|
|
32
|
+
<div class="section chart-card chart-card--wide">
|
|
33
|
+
<h3>Suite Results{{#unless @first}} · continued{{/unless}}</h3>
|
|
34
|
+
<canvas id="{{this.canvasId}}" width="640" height="{{this.heightPx}}"></canvas>
|
|
35
|
+
{{#if @last}}{{#if ../charts.suiteResultsOverflowNote}}
|
|
36
|
+
<div class="chart-note">{{../charts.suiteResultsOverflowNote}}</div>
|
|
37
|
+
{{/if}}{{/if}}
|
|
18
38
|
</div>
|
|
39
|
+
{{/each}}
|
|
40
|
+
{{/if}}
|
|
@@ -45,6 +45,17 @@
|
|
|
45
45
|
|
|
46
46
|
<script>
|
|
47
47
|
(function() {
|
|
48
|
+
// Chart.js measures axis labels with whatever font is available at chart
|
|
49
|
+
// construction. The embedded Inter loads asynchronously (even from a data:
|
|
50
|
+
// URI), so charts built immediately measure ticks with narrower fallback
|
|
51
|
+
// metrics and then PAINT with Inter — wide-glyph labels overflowed their
|
|
52
|
+
// measured gutter by a character. Build every chart only after the document
|
|
53
|
+
// fonts settle; the timeout fallback guards a hung fonts.ready so
|
|
54
|
+
// __chartsReady can never be starved.
|
|
55
|
+
var inited = false;
|
|
56
|
+
function initCharts() {
|
|
57
|
+
if (inited) return;
|
|
58
|
+
inited = true;
|
|
48
59
|
var remaining = 0;
|
|
49
60
|
function chartDone() { if (--remaining === 0) window.__chartsReady = true; }
|
|
50
61
|
|
|
@@ -90,6 +101,9 @@
|
|
|
90
101
|
Chart.register(centerTextPlugin);
|
|
91
102
|
|
|
92
103
|
/* ── Pass rate doughnut ──────────────────────────────────────────────── */
|
|
104
|
+
// Rounded, slightly separated segments + a thicker ring read as a designed
|
|
105
|
+
// gauge rather than a default pie; the centre % (drawn by centerTextPlugin)
|
|
106
|
+
// scales off the inner radius automatically.
|
|
93
107
|
remaining += 1;
|
|
94
108
|
new Chart(document.getElementById('passRateChart'), {
|
|
95
109
|
type: 'doughnut',
|
|
@@ -99,19 +113,22 @@
|
|
|
99
113
|
data: [passed, failed, timedOut, skipped, flaky],
|
|
100
114
|
backgroundColor: ['#3F7D58', '#C3553C', '#B5803A', '#7A786E', '#CA8A04'],
|
|
101
115
|
borderColor: ['#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5'],
|
|
102
|
-
borderWidth:
|
|
116
|
+
borderWidth: 1,
|
|
117
|
+
borderRadius: 5,
|
|
118
|
+
spacing: 2,
|
|
103
119
|
hoverOffset: 4
|
|
104
120
|
}]
|
|
105
121
|
},
|
|
106
122
|
options: {
|
|
107
123
|
responsive: false,
|
|
108
|
-
cutout: '
|
|
124
|
+
cutout: '64%',
|
|
109
125
|
plugins: {
|
|
110
126
|
legend: {
|
|
111
127
|
position: 'bottom',
|
|
112
128
|
labels: {
|
|
113
129
|
font: { size: 9, family: 'Inter, sans-serif' },
|
|
114
|
-
color: '#5A5A52', padding: 10,
|
|
130
|
+
color: '#5A5A52', padding: 10,
|
|
131
|
+
usePointStyle: true, pointStyle: 'circle', boxWidth: 7, boxHeight: 7
|
|
115
132
|
}
|
|
116
133
|
}
|
|
117
134
|
},
|
|
@@ -119,64 +136,126 @@
|
|
|
119
136
|
}
|
|
120
137
|
});
|
|
121
138
|
|
|
122
|
-
/* ── Horizontal suite results bar
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
var
|
|
127
|
-
|
|
139
|
+
/* ── Horizontal suite results bar(s) ─────────────────────────────────── */
|
|
140
|
+
// Muted per-suite total at each bar's end — the count is scannable without
|
|
141
|
+
// tracing gridlines. Guarded by canvas id prefix so it fires on every suite
|
|
142
|
+
// chunk canvas and nothing else.
|
|
143
|
+
var suiteTotalsPlugin = {
|
|
144
|
+
id: 'suiteBarTotals',
|
|
145
|
+
afterDatasetsDraw: function(chart) {
|
|
146
|
+
if (chart.canvas.id.indexOf('suitesBarChart') !== 0) return;
|
|
147
|
+
var ctx = chart.ctx;
|
|
148
|
+
for (var i = 0; i < chart.data.labels.length; i++) {
|
|
149
|
+
var total = 0;
|
|
150
|
+
chart.data.datasets.forEach(function(ds) { total += ds.data[i] || 0; });
|
|
151
|
+
if (!total) continue;
|
|
152
|
+
var y = chart.scales.y.getPixelForValue(i);
|
|
153
|
+
var x = chart.scales.x.getPixelForValue(total);
|
|
154
|
+
ctx.save();
|
|
155
|
+
ctx.font = '600 8.5px Inter, sans-serif';
|
|
156
|
+
ctx.fillStyle = '#8C8B82';
|
|
157
|
+
ctx.textAlign = 'left';
|
|
158
|
+
ctx.textBaseline = 'middle';
|
|
159
|
+
ctx.fillText(String(total), x + 5, y);
|
|
160
|
+
ctx.restore();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
Chart.register(suiteTotalsPlugin);
|
|
128
165
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
166
|
+
// Shared config so the compact small-run card and the chunked large-run
|
|
167
|
+
// canvases render identically bar-for-bar.
|
|
168
|
+
function suiteChartConfig(labels, passed, failed, timedOut, skipped) {
|
|
169
|
+
return {
|
|
170
|
+
type: 'bar',
|
|
171
|
+
data: {
|
|
172
|
+
labels: labels,
|
|
173
|
+
datasets: [
|
|
174
|
+
{ label: 'Passed', data: passed, backgroundColor: '#3F7D58', borderRadius: 4 },
|
|
175
|
+
{ label: 'Failed', data: failed, backgroundColor: '#C3553C', borderRadius: 4 },
|
|
176
|
+
{ label: 'Timed Out', data: timedOut, backgroundColor: '#B5803A', borderRadius: 4 },
|
|
177
|
+
{ label: 'Skipped', data: skipped, backgroundColor: '#7A786E', borderRadius: 4 }
|
|
178
|
+
]
|
|
179
|
+
},
|
|
180
|
+
options: {
|
|
181
|
+
indexAxis: 'y',
|
|
182
|
+
responsive: false,
|
|
183
|
+
// Left: small gutter margin for the y labels (enlarging scale.width in
|
|
184
|
+
// afterFit is not honoured by the layout pass); exact fit comes from
|
|
185
|
+
// building charts after fonts.ready so tick measurement uses Inter.
|
|
186
|
+
// Right: room for the per-suite total drawn past the bar end.
|
|
187
|
+
layout: { padding: { left: 16, right: 22 } },
|
|
188
|
+
scales: {
|
|
189
|
+
x: {
|
|
190
|
+
stacked: true,
|
|
191
|
+
// maxTicksLimit instead of stepSize:1 — a suite with 40+ tests would
|
|
192
|
+
// otherwise spray 40+ ticks across the axis. precision:0 keeps the
|
|
193
|
+
// auto-chosen steps integral.
|
|
194
|
+
ticks: {
|
|
195
|
+
font: { size: 9, family: 'Inter, sans-serif' }, color: '#5A5A52',
|
|
196
|
+
maxTicksLimit: 12, precision: 0
|
|
197
|
+
},
|
|
198
|
+
grid: { color: 'rgba(31,31,28,0.06)' },
|
|
199
|
+
border: { color: '#D6D4C8' }
|
|
200
|
+
},
|
|
201
|
+
y: {
|
|
202
|
+
stacked: true,
|
|
203
|
+
ticks: {
|
|
204
|
+
font: { size: 10, family: 'Inter, sans-serif' }, color: '#1F1F1C', autoSkip: false,
|
|
205
|
+
// Truncate only very long suite / describe labels; measurement runs
|
|
206
|
+
// post-fonts.ready so the gutter fits exactly what gets painted.
|
|
207
|
+
callback: function(value) {
|
|
208
|
+
var l = this.getLabelForValue(value);
|
|
209
|
+
return l.length > 24 ? l.slice(0, 23) + '…' : l;
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
grid: { display: false },
|
|
213
|
+
border: { color: '#D6D4C8' }
|
|
214
|
+
}
|
|
150
215
|
},
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
// lets most labels show in full instead of clipping at the canvas edge.
|
|
159
|
-
callback: function(value) {
|
|
160
|
-
var l = this.getLabelForValue(value);
|
|
161
|
-
return l.length > 24 ? l.slice(0, 23) + '…' : l;
|
|
216
|
+
plugins: {
|
|
217
|
+
legend: {
|
|
218
|
+
position: 'bottom',
|
|
219
|
+
labels: {
|
|
220
|
+
font: { size: 9, family: 'Inter, sans-serif' }, color: '#5A5A52',
|
|
221
|
+
padding: 12,
|
|
222
|
+
usePointStyle: true, pointStyle: 'circle', boxWidth: 7, boxHeight: 7
|
|
162
223
|
}
|
|
163
|
-
},
|
|
164
|
-
grid: { display: false },
|
|
165
|
-
border: { color: '#D6D4C8' }
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
plugins: {
|
|
169
|
-
legend: {
|
|
170
|
-
position: 'bottom',
|
|
171
|
-
labels: {
|
|
172
|
-
font: { size: 9, family: 'Inter, sans-serif' }, color: '#5A5A52',
|
|
173
|
-
padding: 12, boxWidth: 10, boxHeight: 10
|
|
174
224
|
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
225
|
+
},
|
|
226
|
+
animation: { onComplete: chartDone }
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
{{#if suiteChartLarge}}
|
|
232
|
+
// Large run: one canvas per page-sized chunk — every suite keeps its own bar.
|
|
233
|
+
var suiteChunks = [
|
|
234
|
+
{{#each suiteChartChunks}}
|
|
235
|
+
{
|
|
236
|
+
id: '{{this.canvasId}}',
|
|
237
|
+
labels: [{{{jsonArray this.rows 'label'}}}],
|
|
238
|
+
passed: [{{{jsonArray this.rows 'passed'}}}],
|
|
239
|
+
failed: [{{{jsonArray this.rows 'failed'}}}],
|
|
240
|
+
timedOut: [{{{jsonArray this.rows 'timedOut'}}}],
|
|
241
|
+
skipped: [{{{jsonArray this.rows 'skipped'}}}]
|
|
242
|
+
}{{#unless @last}},{{/unless}}
|
|
243
|
+
{{/each}}
|
|
244
|
+
];
|
|
245
|
+
remaining += suiteChunks.length;
|
|
246
|
+
suiteChunks.forEach(function(c) {
|
|
247
|
+
new Chart(document.getElementById(c.id), suiteChartConfig(c.labels, c.passed, c.failed, c.timedOut, c.skipped));
|
|
179
248
|
});
|
|
249
|
+
{{else}}
|
|
250
|
+
remaining += 1;
|
|
251
|
+
new Chart(document.getElementById('suitesBarChart'), suiteChartConfig(
|
|
252
|
+
[{{{jsonArray charts.suiteResults 'label'}}}],
|
|
253
|
+
[{{{jsonArray charts.suiteResults 'passed'}}}],
|
|
254
|
+
[{{{jsonArray charts.suiteResults 'failed'}}}],
|
|
255
|
+
[{{{jsonArray charts.suiteResults 'timedOut'}}}],
|
|
256
|
+
[{{{jsonArray charts.suiteResults 'skipped'}}}]
|
|
257
|
+
));
|
|
258
|
+
{{/if}}
|
|
180
259
|
|
|
181
260
|
{{#if options.showTrend}}{{#if charts.trend}}
|
|
182
261
|
// entries are newest-first in storage; reverse for oldest-left (chronological) display
|
|
@@ -339,6 +418,14 @@
|
|
|
339
418
|
});
|
|
340
419
|
}
|
|
341
420
|
{{/if}}{{/if}}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (document.fonts && document.fonts.ready && typeof document.fonts.ready.then === 'function') {
|
|
424
|
+
document.fonts.ready.then(initCharts, initCharts);
|
|
425
|
+
setTimeout(initCharts, 3000);
|
|
426
|
+
} else {
|
|
427
|
+
initCharts();
|
|
428
|
+
}
|
|
342
429
|
})();
|
|
343
430
|
</script>
|
|
344
431
|
{{else}}
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
<div class="release-gate__body">
|
|
5
5
|
<div class="release-gate__label">Release Recommendation</div>
|
|
6
6
|
<div class="release-gate__verdict release-gate__verdict--fail">
|
|
7
|
-
HOLD
|
|
7
|
+
HOLD: {{stats.failed}} FAILURE{{#if (gt stats.failed 1)}}S{{/if}}
|
|
8
8
|
</div>
|
|
9
9
|
<div class="release-gate__detail">
|
|
10
|
-
{{stats.failed}} of {{stats.total}} tests failed
|
|
11
|
-
product defect or an automation script issue
|
|
10
|
+
{{stats.failed}} of {{stats.total}} tests failed{{#if releaseGateTally}} ({{releaseGateTally}}){{/if}}.
|
|
11
|
+
Each failure may be a product defect or an automation script issue.
|
|
12
|
+
Review and verify manually before shipping.
|
|
12
13
|
{{#if stats.flaky}} · {{stats.flaky}} flaky (additional risk).{{/if}}
|
|
13
14
|
</div>
|
|
14
15
|
</div>
|
|
@@ -24,11 +25,12 @@
|
|
|
24
25
|
<div class="release-gate__body">
|
|
25
26
|
<div class="release-gate__label">Release Recommendation</div>
|
|
26
27
|
<div class="release-gate__verdict release-gate__verdict--fail">
|
|
27
|
-
HOLD
|
|
28
|
+
HOLD: {{stats.timedOut}} TIMED OUT
|
|
28
29
|
</div>
|
|
29
30
|
<div class="release-gate__detail">
|
|
30
|
-
{{stats.timedOut}}
|
|
31
|
-
|
|
31
|
+
{{stats.timedOut}} test{{#if (gt stats.timedOut 1)}}s{{/if}} exceeded timeout.
|
|
32
|
+
Timeouts are often environment or automation issues.
|
|
33
|
+
Review and verify manually before shipping.
|
|
32
34
|
</div>
|
|
33
35
|
</div>
|
|
34
36
|
<div class="release-gate__meta">
|
|
@@ -688,9 +688,26 @@ code {
|
|
|
688
688
|
border: 1px solid var(--border-2);
|
|
689
689
|
border-radius: 6px; padding: 16px 14px; text-align: center;
|
|
690
690
|
}
|
|
691
|
+
/* Large runs: the doughnut sits alone in the grid (the suite chart moved to
|
|
692
|
+
full-width chunk cards below) — wider card + bigger canvas so it owns the
|
|
693
|
+
space instead of floating in it. Extra specificity so a template
|
|
694
|
+
stylesheet's --secondary column override can't re-widen it. */
|
|
695
|
+
.charts-grid.charts-grid--solo { grid-template-columns: 320px; justify-content: center; }
|
|
696
|
+
/* Full-width Suite Results cards, one per ~45 suites — each page-break-atomic
|
|
697
|
+
so the chart continues cleanly across pages. */
|
|
698
|
+
.chart-card--wide {
|
|
699
|
+
width: 100%; margin-bottom: 16px;
|
|
700
|
+
page-break-inside: avoid; break-inside: avoid;
|
|
701
|
+
}
|
|
691
702
|
/* canvas computes to display:block in Chromium, so text-align:center above
|
|
692
703
|
never centers it (that only affects inline-level boxes) — margin:auto does. */
|
|
693
704
|
.chart-card canvas { display: block; margin: 0 auto; }
|
|
705
|
+
/* Overflow note under the Suite Results chart ("+ N more suites: …") — rendered
|
|
706
|
+
when the run has more suites than the chart's bar cap. */
|
|
707
|
+
.chart-note {
|
|
708
|
+
margin-top: 6px; text-align: center;
|
|
709
|
+
font-size: 9px; color: var(--text-3); letter-spacing: 0.2px;
|
|
710
|
+
}
|
|
694
711
|
.chart-card h3 {
|
|
695
712
|
margin: 0 0 12px;
|
|
696
713
|
font-family: 'Inter', system-ui, sans-serif; font-size: 9px;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
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",
|