@reportforge/playwright-pdf 0.15.1 → 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 +19 -0
- package/README.md +1 -1
- package/dist/cli/index.js +25 -4
- package/dist/index.js +57 -10
- package/dist/index.mjs +57 -10
- package/dist/templates/partials/chart-summary-grid.hbs +23 -1
- package/dist/templates/partials/charts.hbs +143 -56
- package/dist/templates/styles/base.css +17 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
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
|
+
|
|
6
25
|
## [0.15.1] - 2026-07-07
|
|
7
26
|
|
|
8
27
|
### 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 [
|
|
@@ -6587,6 +6588,18 @@ function jsonForInlineScript(value) {
|
|
|
6587
6588
|
function isUnknownValue(v) {
|
|
6588
6589
|
return !v || v === "unknown";
|
|
6589
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
|
+
}
|
|
6590
6603
|
function deriveOptions(s) {
|
|
6591
6604
|
return {
|
|
6592
6605
|
showCharts: s.charts,
|
|
@@ -6598,7 +6611,7 @@ function deriveOptions(s) {
|
|
|
6598
6611
|
showFullFailures: s.fullFailures
|
|
6599
6612
|
};
|
|
6600
6613
|
}
|
|
6601
|
-
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;
|
|
6602
6615
|
var init_engine = __esm({
|
|
6603
6616
|
"src/templates/engine.ts"() {
|
|
6604
6617
|
"use strict";
|
|
@@ -6613,6 +6626,10 @@ var init_engine = __esm({
|
|
|
6613
6626
|
A4_HEIGHT_MM = 297;
|
|
6614
6627
|
PAGE_MARGIN_Y_MM = 16;
|
|
6615
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;
|
|
6616
6633
|
PAGE_CONTENT_HEIGHT_PX = (A4_HEIGHT_MM - 2 * PAGE_MARGIN_Y_MM) * PX_PER_MM;
|
|
6617
6634
|
SLOW_MEDIAN_FACTOR = 2;
|
|
6618
6635
|
TemplateEngine = class {
|
|
@@ -6675,7 +6692,9 @@ var init_engine = __esm({
|
|
|
6675
6692
|
templateCSS: "",
|
|
6676
6693
|
chartjsScript: this.buildChartjsScript(),
|
|
6677
6694
|
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
6678
|
-
releaseGateTally: buildFailureTally(data.analysis)
|
|
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) : []
|
|
6679
6698
|
};
|
|
6680
6699
|
}
|
|
6681
6700
|
buildContext(data, template, userSections, slowTestThreshold) {
|
|
@@ -6700,7 +6719,9 @@ var init_engine = __esm({
|
|
|
6700
6719
|
templateCSS,
|
|
6701
6720
|
chartjsScript: this.buildChartjsScript(),
|
|
6702
6721
|
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
6703
|
-
releaseGateTally: buildFailureTally(data.analysis)
|
|
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) : []
|
|
6704
6725
|
};
|
|
6705
6726
|
}
|
|
6706
6727
|
buildChartjsScript() {
|
package/dist/index.js
CHANGED
|
@@ -11656,7 +11656,7 @@ init_cjs_shims();
|
|
|
11656
11656
|
|
|
11657
11657
|
// src/collector/stats-utils.ts
|
|
11658
11658
|
init_cjs_shims();
|
|
11659
|
-
var
|
|
11659
|
+
var SUITE_CHART_MAX_ROWS = 150;
|
|
11660
11660
|
function resolveTestStatus(test, result) {
|
|
11661
11661
|
if (!result) return "skipped";
|
|
11662
11662
|
const expected = test.expectedStatus ?? "passed";
|
|
@@ -11700,15 +11700,40 @@ function toRow(s) {
|
|
|
11700
11700
|
skipped: s.stats.skipped
|
|
11701
11701
|
};
|
|
11702
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
|
+
}
|
|
11703
11728
|
function buildSuiteResults(projects, limit) {
|
|
11704
11729
|
const top = projects.flatMap((p) => p.suites);
|
|
11705
11730
|
if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
|
|
11706
11731
|
const file = top[0];
|
|
11707
11732
|
const fs14 = statsFromTests(file.tests);
|
|
11708
11733
|
const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs14.passed, failed: fs14.failed, timedOut: fs14.timedOut, skipped: fs14.skipped }] : [];
|
|
11709
|
-
return [...rootRow, ...file.suites.map(toRow)]
|
|
11734
|
+
return capRows([...rootRow, ...file.suites.map(toRow)], limit);
|
|
11710
11735
|
}
|
|
11711
|
-
return top.map(toRow)
|
|
11736
|
+
return capRows(top.map(toRow), limit);
|
|
11712
11737
|
}
|
|
11713
11738
|
|
|
11714
11739
|
// src/collector/SuiteWalker.ts
|
|
@@ -12053,7 +12078,7 @@ var DataCollector = class {
|
|
|
12053
12078
|
};
|
|
12054
12079
|
}
|
|
12055
12080
|
buildChartData(stats, projects) {
|
|
12056
|
-
const suiteResults = buildSuiteResults(projects,
|
|
12081
|
+
const { rows: suiteResults, overflowNote: suiteResultsOverflowNote } = buildSuiteResults(projects, SUITE_CHART_MAX_ROWS);
|
|
12057
12082
|
return {
|
|
12058
12083
|
passRate: {
|
|
12059
12084
|
passed: stats.passed,
|
|
@@ -12062,7 +12087,8 @@ var DataCollector = class {
|
|
|
12062
12087
|
skipped: stats.skipped,
|
|
12063
12088
|
flaky: stats.flaky
|
|
12064
12089
|
},
|
|
12065
|
-
suiteResults
|
|
12090
|
+
suiteResults,
|
|
12091
|
+
suiteResultsOverflowNote
|
|
12066
12092
|
};
|
|
12067
12093
|
}
|
|
12068
12094
|
};
|
|
@@ -12332,7 +12358,7 @@ var ShardMerger = class {
|
|
|
12332
12358
|
};
|
|
12333
12359
|
}
|
|
12334
12360
|
buildCharts(stats, projects) {
|
|
12335
|
-
const suiteResults = buildSuiteResults(projects,
|
|
12361
|
+
const { rows: suiteResults, overflowNote: suiteResultsOverflowNote } = buildSuiteResults(projects, SUITE_CHART_MAX_ROWS);
|
|
12336
12362
|
return {
|
|
12337
12363
|
passRate: {
|
|
12338
12364
|
passed: stats.passed,
|
|
@@ -12342,7 +12368,8 @@ var ShardMerger = class {
|
|
|
12342
12368
|
skipped: stats.skipped,
|
|
12343
12369
|
flaky: stats.flaky
|
|
12344
12370
|
},
|
|
12345
|
-
suiteResults
|
|
12371
|
+
suiteResults,
|
|
12372
|
+
suiteResultsOverflowNote
|
|
12346
12373
|
};
|
|
12347
12374
|
}
|
|
12348
12375
|
};
|
|
@@ -12376,11 +12403,27 @@ function jsonForInlineScript(value) {
|
|
|
12376
12403
|
var A4_HEIGHT_MM = 297;
|
|
12377
12404
|
var PAGE_MARGIN_Y_MM = 16;
|
|
12378
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;
|
|
12379
12410
|
var PAGE_CONTENT_HEIGHT_PX = (A4_HEIGHT_MM - 2 * PAGE_MARGIN_Y_MM) * PX_PER_MM;
|
|
12380
12411
|
function isUnknownValue(v) {
|
|
12381
12412
|
return !v || v === "unknown";
|
|
12382
12413
|
}
|
|
12383
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
|
+
}
|
|
12384
12427
|
function deriveOptions(s) {
|
|
12385
12428
|
return {
|
|
12386
12429
|
showCharts: s.charts,
|
|
@@ -12452,7 +12495,9 @@ var TemplateEngine = class {
|
|
|
12452
12495
|
templateCSS: "",
|
|
12453
12496
|
chartjsScript: this.buildChartjsScript(),
|
|
12454
12497
|
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12455
|
-
releaseGateTally: buildFailureTally(data.analysis)
|
|
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) : []
|
|
12456
12501
|
};
|
|
12457
12502
|
}
|
|
12458
12503
|
buildContext(data, template, userSections, slowTestThreshold) {
|
|
@@ -12477,7 +12522,9 @@ var TemplateEngine = class {
|
|
|
12477
12522
|
templateCSS,
|
|
12478
12523
|
chartjsScript: this.buildChartjsScript(),
|
|
12479
12524
|
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12480
|
-
releaseGateTally: buildFailureTally(data.analysis)
|
|
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) : []
|
|
12481
12528
|
};
|
|
12482
12529
|
}
|
|
12483
12530
|
buildChartjsScript() {
|
|
@@ -14072,7 +14119,7 @@ var PdfReporter = class {
|
|
|
14072
14119
|
this.liveConsole = false;
|
|
14073
14120
|
this.options = parseOptions(rawOptions);
|
|
14074
14121
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14075
|
-
const version = true ? "0.
|
|
14122
|
+
const version = true ? "0.16.0" : "0.x";
|
|
14076
14123
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
14077
14124
|
this.licenseClient = new LicenseClient({
|
|
14078
14125
|
licenseKey: this.options.licenseKey,
|
package/dist/index.mjs
CHANGED
|
@@ -11657,7 +11657,7 @@ init_esm_shims();
|
|
|
11657
11657
|
|
|
11658
11658
|
// src/collector/stats-utils.ts
|
|
11659
11659
|
init_esm_shims();
|
|
11660
|
-
var
|
|
11660
|
+
var SUITE_CHART_MAX_ROWS = 150;
|
|
11661
11661
|
function resolveTestStatus(test, result) {
|
|
11662
11662
|
if (!result) return "skipped";
|
|
11663
11663
|
const expected = test.expectedStatus ?? "passed";
|
|
@@ -11701,15 +11701,40 @@ function toRow(s) {
|
|
|
11701
11701
|
skipped: s.stats.skipped
|
|
11702
11702
|
};
|
|
11703
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
|
+
}
|
|
11704
11729
|
function buildSuiteResults(projects, limit) {
|
|
11705
11730
|
const top = projects.flatMap((p) => p.suites);
|
|
11706
11731
|
if (top.length <= 1 && top[0] && top[0].suites.length > 0) {
|
|
11707
11732
|
const file = top[0];
|
|
11708
11733
|
const fs14 = statsFromTests(file.tests);
|
|
11709
11734
|
const rootRow = file.tests.length > 0 ? [{ label: file.title, passed: fs14.passed, failed: fs14.failed, timedOut: fs14.timedOut, skipped: fs14.skipped }] : [];
|
|
11710
|
-
return [...rootRow, ...file.suites.map(toRow)]
|
|
11735
|
+
return capRows([...rootRow, ...file.suites.map(toRow)], limit);
|
|
11711
11736
|
}
|
|
11712
|
-
return top.map(toRow)
|
|
11737
|
+
return capRows(top.map(toRow), limit);
|
|
11713
11738
|
}
|
|
11714
11739
|
|
|
11715
11740
|
// src/collector/SuiteWalker.ts
|
|
@@ -12054,7 +12079,7 @@ var DataCollector = class {
|
|
|
12054
12079
|
};
|
|
12055
12080
|
}
|
|
12056
12081
|
buildChartData(stats, projects) {
|
|
12057
|
-
const suiteResults = buildSuiteResults(projects,
|
|
12082
|
+
const { rows: suiteResults, overflowNote: suiteResultsOverflowNote } = buildSuiteResults(projects, SUITE_CHART_MAX_ROWS);
|
|
12058
12083
|
return {
|
|
12059
12084
|
passRate: {
|
|
12060
12085
|
passed: stats.passed,
|
|
@@ -12063,7 +12088,8 @@ var DataCollector = class {
|
|
|
12063
12088
|
skipped: stats.skipped,
|
|
12064
12089
|
flaky: stats.flaky
|
|
12065
12090
|
},
|
|
12066
|
-
suiteResults
|
|
12091
|
+
suiteResults,
|
|
12092
|
+
suiteResultsOverflowNote
|
|
12067
12093
|
};
|
|
12068
12094
|
}
|
|
12069
12095
|
};
|
|
@@ -12333,7 +12359,7 @@ var ShardMerger = class {
|
|
|
12333
12359
|
};
|
|
12334
12360
|
}
|
|
12335
12361
|
buildCharts(stats, projects) {
|
|
12336
|
-
const suiteResults = buildSuiteResults(projects,
|
|
12362
|
+
const { rows: suiteResults, overflowNote: suiteResultsOverflowNote } = buildSuiteResults(projects, SUITE_CHART_MAX_ROWS);
|
|
12337
12363
|
return {
|
|
12338
12364
|
passRate: {
|
|
12339
12365
|
passed: stats.passed,
|
|
@@ -12343,7 +12369,8 @@ var ShardMerger = class {
|
|
|
12343
12369
|
skipped: stats.skipped,
|
|
12344
12370
|
flaky: stats.flaky
|
|
12345
12371
|
},
|
|
12346
|
-
suiteResults
|
|
12372
|
+
suiteResults,
|
|
12373
|
+
suiteResultsOverflowNote
|
|
12347
12374
|
};
|
|
12348
12375
|
}
|
|
12349
12376
|
};
|
|
@@ -12377,11 +12404,27 @@ function jsonForInlineScript(value) {
|
|
|
12377
12404
|
var A4_HEIGHT_MM = 297;
|
|
12378
12405
|
var PAGE_MARGIN_Y_MM = 16;
|
|
12379
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;
|
|
12380
12411
|
var PAGE_CONTENT_HEIGHT_PX = (A4_HEIGHT_MM - 2 * PAGE_MARGIN_Y_MM) * PX_PER_MM;
|
|
12381
12412
|
function isUnknownValue(v) {
|
|
12382
12413
|
return !v || v === "unknown";
|
|
12383
12414
|
}
|
|
12384
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
|
+
}
|
|
12385
12428
|
function deriveOptions(s) {
|
|
12386
12429
|
return {
|
|
12387
12430
|
showCharts: s.charts,
|
|
@@ -12453,7 +12496,9 @@ var TemplateEngine = class {
|
|
|
12453
12496
|
templateCSS: "",
|
|
12454
12497
|
chartjsScript: this.buildChartjsScript(),
|
|
12455
12498
|
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12456
|
-
releaseGateTally: buildFailureTally(data.analysis)
|
|
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) : []
|
|
12457
12502
|
};
|
|
12458
12503
|
}
|
|
12459
12504
|
buildContext(data, template, userSections, slowTestThreshold) {
|
|
@@ -12478,7 +12523,9 @@ var TemplateEngine = class {
|
|
|
12478
12523
|
templateCSS,
|
|
12479
12524
|
chartjsScript: this.buildChartjsScript(),
|
|
12480
12525
|
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12481
|
-
releaseGateTally: buildFailureTally(data.analysis)
|
|
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) : []
|
|
12482
12529
|
};
|
|
12483
12530
|
}
|
|
12484
12531
|
buildChartjsScript() {
|
|
@@ -14073,7 +14120,7 @@ var PdfReporter = class {
|
|
|
14073
14120
|
this.liveConsole = false;
|
|
14074
14121
|
this.options = parseOptions(rawOptions);
|
|
14075
14122
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14076
|
-
const version = true ? "0.
|
|
14123
|
+
const version = true ? "0.16.0" : "0.x";
|
|
14077
14124
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
14078
14125
|
this.licenseClient = new LicenseClient({
|
|
14079
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}}
|
|
@@ -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",
|