@reportforge/playwright-pdf 0.15.1 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +93 -19
- package/README.md +5 -3
- 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 +2 -2
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.1] - 2026-07-09
|
|
7
|
+
|
|
8
|
+
### Changed
|
|
9
|
+
|
|
10
|
+
- **Relicensed from MIT to the Elastic License 2.0** (source-available). The source stays readable, copyable, and modifiable; the license does not allow offering the software as a hosted service, bypassing or removing the license-key check, or stripping license/copyright notices. Surfaces updated together: `LICENSE`, `package.json` license metadata, README badge + License section, docs badge, marketing footer, pricing FAQ, and legal terms (§2 License grant, §7 IP, `lastUpdated`). Versions 0.16.0 and earlier were published under MIT and remain MIT.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## [0.16.0] - 2026-07-07
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **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.
|
|
19
|
+
- **Per-suite totals on the bar chart**: each bar ends with its muted test count, so counts read directly instead of via gridlines.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **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.
|
|
28
|
+
- **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).
|
|
29
|
+
- `timedOut` counts are now carried through the suite-chart data type and the demo fixture's suite entries.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
6
33
|
## [0.15.1] - 2026-07-07
|
|
7
34
|
|
|
8
35
|
### Changed
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,95 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
1
|
Copyright (c) 2026 ReportForge
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+
Elastic License 2.0
|
|
4
|
+
|
|
5
|
+
URL: https://www.elastic.co/licensing/elastic-license
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
By using the software, you agree to all of the terms and conditions below.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a non-exclusive, royalty-free, worldwide,
|
|
14
|
+
non-sublicensable, non-transferable license to use, copy, distribute, make
|
|
15
|
+
available, and prepare derivative works of the software, in each case subject to
|
|
16
|
+
the limitations and conditions below.
|
|
17
|
+
|
|
18
|
+
## Limitations
|
|
19
|
+
|
|
20
|
+
You may not provide the software to third parties as a hosted or managed
|
|
21
|
+
service, where the service provides users with access to any substantial set of
|
|
22
|
+
the features or functionality of the software.
|
|
23
|
+
|
|
24
|
+
You may not move, change, disable, or circumvent the license key functionality
|
|
25
|
+
in the software, and you may not remove or obscure any functionality in the
|
|
26
|
+
software that is protected by the license key.
|
|
27
|
+
|
|
28
|
+
You may not alter, remove, or obscure any licensing, copyright, or other notices
|
|
29
|
+
of the licensor in the software. Any use of the licensor’s trademarks is subject
|
|
30
|
+
to applicable law.
|
|
31
|
+
|
|
32
|
+
## Patents
|
|
33
|
+
|
|
34
|
+
The licensor grants you a license, under any patent claims the licensor can
|
|
35
|
+
license, or becomes able to license, to make, have made, use, sell, offer for
|
|
36
|
+
sale, import and have imported the software, in each case subject to the
|
|
37
|
+
limitations and conditions in this license. This license does not cover any
|
|
38
|
+
patent claims that you cause to be infringed by modifications or additions to
|
|
39
|
+
the software. If you or your company make any written claim that the software
|
|
40
|
+
infringes or contributes to infringement of any patent, your patent license for
|
|
41
|
+
the software granted under these terms ends immediately. If your company makes
|
|
42
|
+
such a claim, your patent license ends immediately for work on behalf of your
|
|
43
|
+
company.
|
|
44
|
+
|
|
45
|
+
## Notices
|
|
46
|
+
|
|
47
|
+
You must ensure that anyone who gets a copy of any part of the software from you
|
|
48
|
+
also gets a copy of these terms.
|
|
49
|
+
|
|
50
|
+
If you modify the software, you must include in any modified copies of the
|
|
51
|
+
software prominent notices stating that you have modified the software.
|
|
52
|
+
|
|
53
|
+
## No Other Rights
|
|
54
|
+
|
|
55
|
+
These terms do not imply any licenses other than those expressly granted in
|
|
56
|
+
these terms.
|
|
57
|
+
|
|
58
|
+
## Termination
|
|
59
|
+
|
|
60
|
+
If you use the software in violation of these terms, such use is not licensed,
|
|
61
|
+
and your licenses will automatically terminate. If the licensor provides you
|
|
62
|
+
with a notice of your violation, and you cease all violation of this license no
|
|
63
|
+
later than 30 days after you receive that notice, your licenses will be
|
|
64
|
+
reinstated retroactively. However, if you violate these terms after such
|
|
65
|
+
reinstatement, any additional violation of these terms will cause your licenses
|
|
66
|
+
to terminate automatically and permanently.
|
|
67
|
+
|
|
68
|
+
## No Liability
|
|
69
|
+
|
|
70
|
+
*As far as the law allows, the software comes as is, without any warranty or
|
|
71
|
+
condition, and the licensor will not be liable to you for any damages arising
|
|
72
|
+
out of these terms or the use or nature of the software, under any kind of
|
|
73
|
+
legal claim.*
|
|
74
|
+
|
|
75
|
+
## Definitions
|
|
76
|
+
|
|
77
|
+
The **licensor** is the entity offering these terms, and the **software** is the
|
|
78
|
+
software the licensor makes available under these terms, including any portion
|
|
79
|
+
of it.
|
|
80
|
+
|
|
81
|
+
**you** refers to the individual or entity agreeing to these terms.
|
|
82
|
+
|
|
83
|
+
**your company** is any legal entity, sole proprietorship, or other kind of
|
|
84
|
+
organization that you work for, plus all organizations that have control over,
|
|
85
|
+
are under the control of, or are under common control with that
|
|
86
|
+
organization. **control** means ownership of substantially all the assets of an
|
|
87
|
+
entity, or the power to direct its management and policies by vote, contract, or
|
|
88
|
+
otherwise. Control can be direct or indirect.
|
|
89
|
+
|
|
90
|
+
**your licenses** are all the licenses granted to you for the software under
|
|
91
|
+
these terms.
|
|
92
|
+
|
|
93
|
+
**use** means anything you do with the software requiring one of your licenses.
|
|
94
|
+
|
|
95
|
+
**trademark** means trademarks, service marks, and similar rights.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Drop it into any Playwright project and get a PDF report on every CI run. No cha
|
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/@reportforge/playwright-pdf)
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
|
-
[](./LICENSE)
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://reportforge.org/pricing">
|
|
@@ -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
|
|
@@ -678,4 +678,6 @@ Questions and bug reports: email [support@reportforge.org](mailto:support@report
|
|
|
678
678
|
|
|
679
679
|
## License
|
|
680
680
|
|
|
681
|
-
|
|
681
|
+
[Elastic License 2.0](./LICENSE), source-available. Read, copy, and modify the source freely. Three things the license does not allow: offering the software as a hosted service, bypassing or removing the license-key check, and stripping license or copyright notices. Generating PDF reports requires an active subscription.
|
|
682
|
+
|
|
683
|
+
Versions 0.16.0 and earlier shipped under MIT and stay MIT.
|
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.1" : "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.1" : "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,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Playwright Test reporter that generates designed PDF reports: minimal, detailed, and executive templates with CI/CD integrations",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Elastic-2.0",
|
|
6
6
|
"author": "ReportForge",
|
|
7
7
|
"homepage": "https://reportforge.org",
|
|
8
8
|
"repository": {
|