@reportforge/playwright-pdf 0.20.2 → 0.22.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 +8 -0
- package/README.md +19 -6
- package/dist/cli/index.js +68 -53
- package/dist/index.d.mts +21 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +122 -50
- package/dist/index.mjs +122 -50
- package/dist/templates/partials/requirements-matrix.hbs +20 -4
- package/dist/templates/styles/base.css +23 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.21.0] - 2026-07-12
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`remoteHistory` option — the pass-rate trend now survives ephemeral CI runners.** The local history file is wiped with the CI workspace, so the trend chart never accumulated past one data point. With `remoteHistory: true`, the reporter sends one request per run over the existing license connection carrying **aggregate numbers only** (pass rate, counts, duration, verdict, an opaque run id — no test titles, no error text; the branch name is sent as a hash). The server keeps the last 100 runs per project + branch and returns them for the chart. Any failure falls back to the local file. The flakiness table stays local-only by design (it needs test titles, which never leave your machine).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
6
14
|
## [0.20.2] - 2026-07-12
|
|
7
15
|
|
|
8
16
|
### Fixed
|
package/README.md
CHANGED
|
@@ -205,8 +205,10 @@ All options are the second element of the reporter tuple.
|
|
|
205
205
|
| `historyFile` | `string` | `~/.reportforge/{key}/history.json` | Path to the history JSON file. Relative paths resolve from `cwd`. Enables pass-rate trending charts in the `detailed` template. |
|
|
206
206
|
| `historySize` | `number` | `10` | Maximum number of test runs to keep in the history file (integer ≥ 2). Older runs are pruned on append. |
|
|
207
207
|
| `showTrend` | `boolean` | `true` | Show the pass-rate sparkline and delta badge in the `detailed` template. Set to `false` to disable history tracking entirely. |
|
|
208
|
+
| `remoteHistory` | `boolean` | `false` | Opt-in server-side trend store so the pass-rate trend survives ephemeral CI runners (the local history file is wiped with the workspace, leaving the chart stuck at one data point). One request per run over the existing license connection carrying aggregate numbers only — no test titles, no error text; the branch name is sent as a hash. Falls back to the local history file on any failure. See the History docs. |
|
|
208
209
|
| `flakinessTopN` | `number` | `5` | Maximum flaky tests to show in the flakiness table (`detailed` template). Set to `0` to disable the table entirely. |
|
|
209
210
|
| `slowTestThreshold` | `number` | `10` | Minimum timed-test count before `SLOW` badges appear in the suite breakdown: below this, every test is trivially the "slowest" so badges stay hidden. On larger runs only genuine outliers (at least 2× the median test duration) are badged, so the badge stays rare. Set to `0` to drop the run-size gate (outliers still required). |
|
|
211
|
+
| `requirementTagPattern` | `string` | `'^@?[A-Z][A-Z0-9]*-\\d+$'` | Regex deciding which tags count as requirement IDs in the Requirements Traceability section (`detailed` template). Matching tags (ticket shapes like `@ODP-5328`, `REQ-001`) get the traceability matrix; everything else (`@regression`, `@sanity`) collapses into a compact tag summary instead of repeating identical rows. Set to `''` to disable the split and list every tag in the matrix. |
|
|
210
212
|
| `templatePath` | `string \| string[]` | n/a | Path to a custom Handlebars (`.hbs`) template file. Takes precedence over `template`. Pass an array to generate one PDF per custom template. See the Custom Templates docs. |
|
|
211
213
|
| `sections` | `object` | per-template | Override which report sections appear, on top of the chosen template's defaults. Flat keys are the baseline for every chosen template; per-template keys (`minimal`\|`detailed`\|`executive`) override per template. Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`, `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`, `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`. Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`, `stackTraces`. See the Report Sections docs. |
|
|
212
214
|
<!-- AUTOGEN:options-table END -->
|
|
@@ -319,7 +321,18 @@ After each run the reporter appends a summary entry to a local JSON file and ren
|
|
|
319
321
|
|
|
320
322
|
**Local dev (zero config)**: history is written automatically to `~/.reportforge/{projectKey}/history.json`. The sparkline appears once two or more runs have been recorded.
|
|
321
323
|
|
|
322
|
-
**CI with ephemeral runners**:
|
|
324
|
+
**CI with ephemeral runners (recommended)**: enable the server-side trend store; the local file is wiped with the workspace, leaving the chart stuck at one data point:
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
reporter: [['@reportforge/playwright-pdf', {
|
|
328
|
+
template: 'detailed',
|
|
329
|
+
remoteHistory: true,
|
|
330
|
+
}]]
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
One request per run over the existing license connection, carrying aggregate numbers only (pass rate, counts, duration, verdict; no test titles, no error text — the branch name is sent as a hash). The server keeps the last 100 runs per project + branch and returns them for the chart; any failure falls back to the local file. The flakiness table stays local-only by design (it needs test titles, which never leave your machine). Bitbucket Pipelines users especially: Bitbucket caches are immutable once written, so `remoteHistory` is the practical path there.
|
|
334
|
+
|
|
335
|
+
**CI caching alternative** (keeps even aggregate numbers off the server): set `historyFile` to a project-relative path and cache it between runs:
|
|
323
336
|
|
|
324
337
|
```typescript
|
|
325
338
|
// playwright.config.ts
|
|
@@ -430,7 +443,7 @@ reporter: [['@reportforge/playwright-pdf', {
|
|
|
430
443
|
| `summary` | KPI strip (total · passed · failed · timed-out · skipped · flaky), verdict, duration, pass rate, flaky callout |
|
|
431
444
|
| `charts` | Pass-rate doughnut + stacked suite-results bar |
|
|
432
445
|
| `trend` | Pass-rate trend line, run-history table, and flakiness table (rendered inside `charts`) |
|
|
433
|
-
| `requirementsMatrix` |
|
|
446
|
+
| `requirementsMatrix` | Requirement-ID tags as a traceability matrix with pass-rate bars, plus a category tag summary |
|
|
434
447
|
| `ciEnvironment` | Branch, commit, browsers, CI provider, OS, Node, Playwright, projects, workers |
|
|
435
448
|
| `suiteBreakdown` | Per-suite → per-test table (status, duration, tags, retries). Failed tests carry their failure detail, root-cause chip, and a `SLOW` badge inline (the three keys below) |
|
|
436
449
|
| `failureDeepDive` | Inline failure detail under each failed test in the breakdown: error message, stack trace, screenshot, retry history |
|
|
@@ -627,14 +640,14 @@ Full lifecycle + offline failure modes: [reportforge.org/docs/licensing](https:/
|
|
|
627
640
|
|
|
628
641
|
## Requirements Traceability
|
|
629
642
|
|
|
630
|
-
The `detailed` template generates a requirements matrix from test tags. Tag tests to link them to requirements:
|
|
643
|
+
The `detailed` template generates a requirements matrix from test tags. Tag tests with ticket-shaped IDs to link them to requirements:
|
|
631
644
|
|
|
632
645
|
```ts
|
|
633
|
-
test('
|
|
634
|
-
test.describe('@
|
|
646
|
+
test('user can login', { tag: ['@REQ-101', '@smoke'] }, async ({ page }) => { /* ... */ });
|
|
647
|
+
test.describe('payment', { tag: '@PAY-204' }, () => { /* ... */ });
|
|
635
648
|
```
|
|
636
649
|
|
|
637
|
-
|
|
650
|
+
Tags matching `requirementTagPattern` (default: ticket shapes like `@REQ-101`, `@ODP-5328`, `JIRA-42`) each get a matrix row — test count, pass/fail counts, and a pass-rate bar — sorted by ID. Category tags (`@smoke`, `@regression`) collapse into a compact tag summary below the matrix instead of repeating one near-identical row per tag. Set `requirementTagPattern: ''` to disable the split and list every tag in the matrix.
|
|
638
651
|
|
|
639
652
|
---
|
|
640
653
|
|
package/dist/cli/index.js
CHANGED
|
@@ -95,7 +95,7 @@ var init_stats_utils = __esm({
|
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
// src/demo/fixture.ts
|
|
98
|
-
function makeTest(id, title, status, duration, retries = 0) {
|
|
98
|
+
function makeTest(id, title, status, duration, retries = 0, tags = []) {
|
|
99
99
|
return {
|
|
100
100
|
id,
|
|
101
101
|
title,
|
|
@@ -103,7 +103,7 @@ function makeTest(id, title, status, duration, retries = 0) {
|
|
|
103
103
|
status,
|
|
104
104
|
duration,
|
|
105
105
|
retryCount: retries,
|
|
106
|
-
tags
|
|
106
|
+
tags,
|
|
107
107
|
annotations: []
|
|
108
108
|
};
|
|
109
109
|
}
|
|
@@ -381,20 +381,20 @@ var init_fixture = __esm({
|
|
|
381
381
|
machineLimit: 25
|
|
382
382
|
};
|
|
383
383
|
DEMO_AUTH_TESTS = [
|
|
384
|
-
makeTest("auth-1", "should login with valid credentials", "passed", 1240),
|
|
385
|
-
makeTest("auth-2", "should show error for wrong password", "passed", 890),
|
|
386
|
-
makeTest("auth-3", "should require email confirmation", "passed", 1100),
|
|
387
|
-
makeTest("auth-4", "should handle session timeout gracefully", "flaky", 3200, 1),
|
|
388
|
-
makeTest("auth-5", "should block brute-force attempts after 5 tries", "failed", 15e3, 2),
|
|
389
|
-
makeTest("auth-6", "should logout and clear session cookies", "passed", 780)
|
|
384
|
+
makeTest("auth-1", "should login with valid credentials", "passed", 1240, 0, ["@auth", "@smoke", "@SHOP-1201"]),
|
|
385
|
+
makeTest("auth-2", "should show error for wrong password", "passed", 890, 0, ["@auth", "@smoke", "@SHOP-1201"]),
|
|
386
|
+
makeTest("auth-3", "should require email confirmation", "passed", 1100, 0, ["@auth", "@regression", "@SHOP-1188"]),
|
|
387
|
+
makeTest("auth-4", "should handle session timeout gracefully", "flaky", 3200, 1, ["@auth", "@regression"]),
|
|
388
|
+
makeTest("auth-5", "should block brute-force attempts after 5 tries", "failed", 15e3, 2, ["@auth", "@regression", "@SHOP-1349"]),
|
|
389
|
+
makeTest("auth-6", "should logout and clear session cookies", "passed", 780, 0, ["@auth", "@smoke"])
|
|
390
390
|
];
|
|
391
391
|
DEMO_CHECKOUT_TESTS = [
|
|
392
|
-
makeTest("co-1", "should add product to cart", "passed", 1050),
|
|
393
|
-
makeTest("co-2", "should apply valid promo code", "passed", 2100),
|
|
394
|
-
makeTest("co-3", "should reject expired promo code", "failed", 8e3, 2),
|
|
395
|
-
makeTest("co-4", "should calculate shipping by region", "passed", 1800),
|
|
396
|
-
makeTest("co-5", "should complete Stripe checkout", "passed", 4200),
|
|
397
|
-
makeTest("co-6", "should send order confirmation email", "skipped", 0)
|
|
392
|
+
makeTest("co-1", "should add product to cart", "passed", 1050, 0, ["@checkout", "@smoke", "@SHOP-1042"]),
|
|
393
|
+
makeTest("co-2", "should apply valid promo code", "passed", 2100, 0, ["@checkout", "@regression", "@SHOP-1277"]),
|
|
394
|
+
makeTest("co-3", "should reject expired promo code", "failed", 8e3, 2, ["@checkout", "@regression", "@SHOP-1277"]),
|
|
395
|
+
makeTest("co-4", "should calculate shipping by region", "passed", 1800, 0, ["@checkout", "@regression"]),
|
|
396
|
+
makeTest("co-5", "should complete Stripe checkout", "passed", 4200, 0, ["@checkout", "@smoke", "@SHOP-1042"]),
|
|
397
|
+
makeTest("co-6", "should send order confirmation email", "skipped", 0, 0, ["@checkout"])
|
|
398
398
|
];
|
|
399
399
|
DEMO_CATALOG_TESTS = [
|
|
400
400
|
makeTest("cat-1", "should display product grid with filters", "passed", 980),
|
|
@@ -6362,7 +6362,10 @@ var init_defaults = __esm({
|
|
|
6362
6362
|
historySize: 10,
|
|
6363
6363
|
showTrend: true,
|
|
6364
6364
|
flakinessTopN: 5,
|
|
6365
|
-
slowTestThreshold: 10
|
|
6365
|
+
slowTestThreshold: 10,
|
|
6366
|
+
// Ticket-ID shape: @ODP-5328, REQ-001, JIRA-123. Tags matching this land in
|
|
6367
|
+
// the Requirements Traceability matrix; the rest fall to the tag summary.
|
|
6368
|
+
requirementTagPattern: "^@?[A-Z][A-Z0-9]*-\\d+$"
|
|
6366
6369
|
};
|
|
6367
6370
|
}
|
|
6368
6371
|
});
|
|
@@ -6620,7 +6623,7 @@ function deriveOptions(s) {
|
|
|
6620
6623
|
showFullFailures: s.fullFailures
|
|
6621
6624
|
};
|
|
6622
6625
|
}
|
|
6623
|
-
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;
|
|
6626
|
+
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, NUMERIC_COLLATOR, SLOW_MEDIAN_FACTOR, TemplateEngine;
|
|
6624
6627
|
var init_engine = __esm({
|
|
6625
6628
|
"src/templates/engine.ts"() {
|
|
6626
6629
|
"use strict";
|
|
@@ -6640,6 +6643,7 @@ var init_engine = __esm({
|
|
|
6640
6643
|
SUITE_CHART_ROW_PX = 18;
|
|
6641
6644
|
SUITE_CHART_PAD_PX = 56;
|
|
6642
6645
|
PAGE_CONTENT_HEIGHT_PX = (A4_HEIGHT_MM - 2 * PAGE_MARGIN_Y_MM) * PX_PER_MM;
|
|
6646
|
+
NUMERIC_COLLATOR = new Intl.Collator(void 0, { numeric: true });
|
|
6643
6647
|
SLOW_MEDIAN_FACTOR = 2;
|
|
6644
6648
|
TemplateEngine = class {
|
|
6645
6649
|
constructor() {
|
|
@@ -6666,17 +6670,22 @@ var init_engine = __esm({
|
|
|
6666
6670
|
setChartjsBundle(inline) {
|
|
6667
6671
|
this.chartjsInline = inline;
|
|
6668
6672
|
}
|
|
6669
|
-
render(data, template,
|
|
6673
|
+
render(data, template, opts = {}) {
|
|
6670
6674
|
const layoutPath = import_path.default.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
6671
6675
|
if (!import_fs.default.existsSync(layoutPath)) {
|
|
6672
6676
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
6673
6677
|
}
|
|
6674
6678
|
const layoutSource = import_fs.default.readFileSync(layoutPath, "utf8");
|
|
6675
6679
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
6676
|
-
const ctx = this.
|
|
6680
|
+
const ctx = this.assembleContext(
|
|
6681
|
+
data,
|
|
6682
|
+
resolveSections(template, opts.sections),
|
|
6683
|
+
this.readStyle(`${template}.css`),
|
|
6684
|
+
opts
|
|
6685
|
+
);
|
|
6677
6686
|
return compiledLayout(ctx);
|
|
6678
6687
|
}
|
|
6679
|
-
renderFromPath(data, filePath,
|
|
6688
|
+
renderFromPath(data, filePath, opts = {}) {
|
|
6680
6689
|
if (!this.customTemplateCache.has(filePath)) {
|
|
6681
6690
|
if (!import_fs.default.existsSync(filePath)) {
|
|
6682
6691
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
@@ -6685,46 +6694,27 @@ var init_engine = __esm({
|
|
|
6685
6694
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
6686
6695
|
}
|
|
6687
6696
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
6688
|
-
return compiledFn(this.
|
|
6697
|
+
return compiledFn(this.assembleContext(data, allSectionsOn(), "", opts));
|
|
6689
6698
|
}
|
|
6690
|
-
|
|
6691
|
-
const sections = allSectionsOn();
|
|
6692
|
-
const slow = this.buildSlowTests(data);
|
|
6693
|
-
return {
|
|
6694
|
-
...data,
|
|
6695
|
-
projects: this.enrichProjects(data, slow, sections, slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold),
|
|
6696
|
-
options: deriveOptions(sections),
|
|
6697
|
-
sections,
|
|
6698
|
-
tagGroups: this.buildTagGroups(data),
|
|
6699
|
-
slowTests: slow.ranked,
|
|
6700
|
-
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
6701
|
-
templateCSS: "",
|
|
6702
|
-
chartjsScript: this.buildChartjsScript(),
|
|
6703
|
-
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
6704
|
-
releaseGateTally: buildFailureTally(data.analysis),
|
|
6705
|
-
suiteChartLarge: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX,
|
|
6706
|
-
suiteChartChunks: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX ? buildSuiteChartChunks(data.charts.suiteResults) : []
|
|
6707
|
-
};
|
|
6708
|
-
}
|
|
6709
|
-
buildContext(data, template, userSections, slowTestThreshold) {
|
|
6710
|
-
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
6711
|
-
const templateCSS = this.readStyle(`${template}.css`);
|
|
6712
|
-
const sections = resolveSections(template, userSections);
|
|
6699
|
+
assembleContext(data, sections, templateCSS, opts) {
|
|
6713
6700
|
const options = deriveOptions(sections);
|
|
6714
6701
|
const slow = this.buildSlowTests(data);
|
|
6702
|
+
const tags = this.buildTagGroups(data, opts.requirementTagPattern);
|
|
6715
6703
|
return {
|
|
6716
6704
|
...data,
|
|
6717
6705
|
projects: this.enrichProjects(
|
|
6718
6706
|
data,
|
|
6719
6707
|
slow,
|
|
6720
6708
|
sections,
|
|
6721
|
-
slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
6709
|
+
opts.slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
6722
6710
|
),
|
|
6723
6711
|
options,
|
|
6724
6712
|
sections,
|
|
6725
|
-
tagGroups:
|
|
6713
|
+
tagGroups: tags.all,
|
|
6714
|
+
requirementTagGroups: tags.requirements,
|
|
6715
|
+
categoryTagGroups: tags.categories,
|
|
6726
6716
|
slowTests: slow.ranked,
|
|
6727
|
-
baseCSS,
|
|
6717
|
+
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
6728
6718
|
templateCSS,
|
|
6729
6719
|
chartjsScript: this.buildChartjsScript(),
|
|
6730
6720
|
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
@@ -6736,7 +6726,7 @@ var init_engine = __esm({
|
|
|
6736
6726
|
buildChartjsScript() {
|
|
6737
6727
|
return this.chartjsInline ? `<script>${this.chartjsInline}</script>` : "<!-- Chart.js not bundled; charts disabled --><script>window.__chartsReady=true;</script>";
|
|
6738
6728
|
}
|
|
6739
|
-
buildTagGroups(data) {
|
|
6729
|
+
buildTagGroups(data, requirementTagPattern) {
|
|
6740
6730
|
const tagMap = /* @__PURE__ */ new Map();
|
|
6741
6731
|
for (const project of data.projects) {
|
|
6742
6732
|
for (const suite of this.flattenSuites(project.suites)) {
|
|
@@ -6751,11 +6741,20 @@ var init_engine = __esm({
|
|
|
6751
6741
|
}
|
|
6752
6742
|
}
|
|
6753
6743
|
}
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6744
|
+
const regex = new RegExp(requirementTagPattern ?? DEFAULT_OPTIONS.requirementTagPattern);
|
|
6745
|
+
const requirements = [];
|
|
6746
|
+
const categories = [];
|
|
6747
|
+
for (const [tag, counts] of tagMap) {
|
|
6748
|
+
const row = {
|
|
6749
|
+
tag,
|
|
6750
|
+
...counts,
|
|
6751
|
+
coveragePct: counts.total > 0 ? Math.round(counts.passed / counts.total * 100) : 0
|
|
6752
|
+
};
|
|
6753
|
+
(regex.test(tag) ? requirements : categories).push(row);
|
|
6754
|
+
}
|
|
6755
|
+
requirements.sort((a, b) => NUMERIC_COLLATOR.compare(a.tag, b.tag));
|
|
6756
|
+
categories.sort((a, b) => b.total - a.total || a.tag.localeCompare(b.tag));
|
|
6757
|
+
return { all: [...requirements, ...categories], requirements, categories };
|
|
6759
6758
|
}
|
|
6760
6759
|
buildSlowTests(data) {
|
|
6761
6760
|
const tests = [];
|
|
@@ -7858,7 +7857,12 @@ var init_PdfGenerator = __esm({
|
|
|
7858
7857
|
}
|
|
7859
7858
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
7860
7859
|
logger.info(`Rendering template: ${templateLabel}`);
|
|
7861
|
-
const
|
|
7860
|
+
const renderOpts = {
|
|
7861
|
+
sections: options.sections,
|
|
7862
|
+
slowTestThreshold: options.slowTestThreshold,
|
|
7863
|
+
requirementTagPattern: options.requirementTagPattern
|
|
7864
|
+
};
|
|
7865
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, renderOpts) : this.templateEngine.renderFromPath(renderData, source.absolutePath, renderOpts);
|
|
7862
7866
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
7863
7867
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
7864
7868
|
try {
|
|
@@ -12128,8 +12132,19 @@ var init_schema = __esm({
|
|
|
12128
12132
|
historyFile: external_exports.string().optional(),
|
|
12129
12133
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
12130
12134
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
12135
|
+
// Opt-in server-side trend store: survives ephemeral CI runners where the
|
|
12136
|
+
// local history file is wiped every run. Aggregate numbers only.
|
|
12137
|
+
remoteHistory: external_exports.boolean().default(false),
|
|
12131
12138
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
12132
12139
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
12140
|
+
requirementTagPattern: external_exports.string().refine((v) => {
|
|
12141
|
+
try {
|
|
12142
|
+
new RegExp(v);
|
|
12143
|
+
return true;
|
|
12144
|
+
} catch {
|
|
12145
|
+
return false;
|
|
12146
|
+
}
|
|
12147
|
+
}, "requirementTagPattern must be a valid regular expression").optional().default(DEFAULT_OPTIONS.requirementTagPattern),
|
|
12133
12148
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
12134
12149
|
capture: captureConfigSchema,
|
|
12135
12150
|
live: liveConfigSchema,
|
package/dist/index.d.mts
CHANGED
|
@@ -332,6 +332,16 @@ interface ReporterOptions {
|
|
|
332
332
|
* @default true
|
|
333
333
|
*/
|
|
334
334
|
showTrend?: boolean;
|
|
335
|
+
/**
|
|
336
|
+
* Store the pass-rate trend on the ReportForge server so it survives
|
|
337
|
+
* ephemeral CI runners (where the local history file is wiped every run).
|
|
338
|
+
* One request per run over the existing license connection, carrying
|
|
339
|
+
* AGGREGATE NUMBERS ONLY — pass rate, counts, duration, verdict. No test
|
|
340
|
+
* titles, no error text; even the branch name is sent as a hash. Falls back
|
|
341
|
+
* to the local history file on any failure.
|
|
342
|
+
* @default false
|
|
343
|
+
*/
|
|
344
|
+
remoteHistory?: boolean;
|
|
335
345
|
/**
|
|
336
346
|
* Number of flaky tests to display in the flakiness table (detailed template).
|
|
337
347
|
* Set to 0 to disable. Must be a non-negative integer.
|
|
@@ -347,6 +357,17 @@ interface ReporterOptions {
|
|
|
347
357
|
* @default 10
|
|
348
358
|
*/
|
|
349
359
|
slowTestThreshold?: number;
|
|
360
|
+
/**
|
|
361
|
+
* Regex (string) deciding which tags count as requirement IDs in the
|
|
362
|
+
* Requirements Traceability section. Matching tags get the traceability
|
|
363
|
+
* matrix; the rest collapse into a compact tag summary, so suite-level
|
|
364
|
+
* category tags (`@regression`, `@sanity`) no longer repeat identical
|
|
365
|
+
* rows. The default matches ticket-ID shapes such as `@ODP-5328`,
|
|
366
|
+
* `REQ-001`, or `JIRA-123` (uppercase key, dash, digits). Set to `''` to
|
|
367
|
+
* disable the split and list every tag in the matrix (legacy behavior).
|
|
368
|
+
* @default '^@?[A-Z][A-Z0-9]*-\\d+$'
|
|
369
|
+
*/
|
|
370
|
+
requirementTagPattern?: string;
|
|
350
371
|
/**
|
|
351
372
|
* Path to a custom Handlebars (.hbs) template file. When set, takes
|
|
352
373
|
* precedence over `template`. Relative paths resolve from `process.cwd()`.
|
package/dist/index.d.ts
CHANGED
|
@@ -332,6 +332,16 @@ interface ReporterOptions {
|
|
|
332
332
|
* @default true
|
|
333
333
|
*/
|
|
334
334
|
showTrend?: boolean;
|
|
335
|
+
/**
|
|
336
|
+
* Store the pass-rate trend on the ReportForge server so it survives
|
|
337
|
+
* ephemeral CI runners (where the local history file is wiped every run).
|
|
338
|
+
* One request per run over the existing license connection, carrying
|
|
339
|
+
* AGGREGATE NUMBERS ONLY — pass rate, counts, duration, verdict. No test
|
|
340
|
+
* titles, no error text; even the branch name is sent as a hash. Falls back
|
|
341
|
+
* to the local history file on any failure.
|
|
342
|
+
* @default false
|
|
343
|
+
*/
|
|
344
|
+
remoteHistory?: boolean;
|
|
335
345
|
/**
|
|
336
346
|
* Number of flaky tests to display in the flakiness table (detailed template).
|
|
337
347
|
* Set to 0 to disable. Must be a non-negative integer.
|
|
@@ -347,6 +357,17 @@ interface ReporterOptions {
|
|
|
347
357
|
* @default 10
|
|
348
358
|
*/
|
|
349
359
|
slowTestThreshold?: number;
|
|
360
|
+
/**
|
|
361
|
+
* Regex (string) deciding which tags count as requirement IDs in the
|
|
362
|
+
* Requirements Traceability section. Matching tags get the traceability
|
|
363
|
+
* matrix; the rest collapse into a compact tag summary, so suite-level
|
|
364
|
+
* category tags (`@regression`, `@sanity`) no longer repeat identical
|
|
365
|
+
* rows. The default matches ticket-ID shapes such as `@ODP-5328`,
|
|
366
|
+
* `REQ-001`, or `JIRA-123` (uppercase key, dash, digits). Set to `''` to
|
|
367
|
+
* disable the split and list every tag in the matrix (legacy behavior).
|
|
368
|
+
* @default '^@?[A-Z][A-Z0-9]*-\\d+$'
|
|
369
|
+
*/
|
|
370
|
+
requirementTagPattern?: string;
|
|
350
371
|
/**
|
|
351
372
|
* Path to a custom Handlebars (.hbs) template file. When set, takes
|
|
352
373
|
* precedence over `template`. Relative paths resolve from `process.cwd()`.
|
package/dist/index.js
CHANGED
|
@@ -10330,7 +10330,10 @@ var DEFAULT_OPTIONS = {
|
|
|
10330
10330
|
historySize: 10,
|
|
10331
10331
|
showTrend: true,
|
|
10332
10332
|
flakinessTopN: 5,
|
|
10333
|
-
slowTestThreshold: 10
|
|
10333
|
+
slowTestThreshold: 10,
|
|
10334
|
+
// Ticket-ID shape: @ODP-5328, REQ-001, JIRA-123. Tags matching this land in
|
|
10335
|
+
// the Requirements Traceability matrix; the rest fall to the tag summary.
|
|
10336
|
+
requirementTagPattern: "^@?[A-Z][A-Z0-9]*-\\d+$"
|
|
10334
10337
|
};
|
|
10335
10338
|
|
|
10336
10339
|
// src/templates/sections.ts
|
|
@@ -10554,8 +10557,19 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10554
10557
|
historyFile: external_exports.string().optional(),
|
|
10555
10558
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
10556
10559
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
10560
|
+
// Opt-in server-side trend store: survives ephemeral CI runners where the
|
|
10561
|
+
// local history file is wiped every run. Aggregate numbers only.
|
|
10562
|
+
remoteHistory: external_exports.boolean().default(false),
|
|
10557
10563
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10558
10564
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10565
|
+
requirementTagPattern: external_exports.string().refine((v) => {
|
|
10566
|
+
try {
|
|
10567
|
+
new RegExp(v);
|
|
10568
|
+
return true;
|
|
10569
|
+
} catch {
|
|
10570
|
+
return false;
|
|
10571
|
+
}
|
|
10572
|
+
}, "requirementTagPattern must be a valid regular expression").optional().default(DEFAULT_OPTIONS.requirementTagPattern),
|
|
10559
10573
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
10560
10574
|
capture: captureConfigSchema,
|
|
10561
10575
|
live: liveConfigSchema,
|
|
@@ -12635,6 +12649,7 @@ var PAGE_CONTENT_HEIGHT_PX = (A4_HEIGHT_MM - 2 * PAGE_MARGIN_Y_MM) * PX_PER_MM;
|
|
|
12635
12649
|
function isUnknownValue(v) {
|
|
12636
12650
|
return !v || v === "unknown";
|
|
12637
12651
|
}
|
|
12652
|
+
var NUMERIC_COLLATOR = new Intl.Collator(void 0, { numeric: true });
|
|
12638
12653
|
var SLOW_MEDIAN_FACTOR = 2;
|
|
12639
12654
|
function buildSuiteChartChunks(rows) {
|
|
12640
12655
|
const chunks = [];
|
|
@@ -12684,17 +12699,22 @@ var TemplateEngine = class {
|
|
|
12684
12699
|
setChartjsBundle(inline) {
|
|
12685
12700
|
this.chartjsInline = inline;
|
|
12686
12701
|
}
|
|
12687
|
-
render(data, template,
|
|
12702
|
+
render(data, template, opts = {}) {
|
|
12688
12703
|
const layoutPath = import_path3.default.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
12689
12704
|
if (!import_fs4.default.existsSync(layoutPath)) {
|
|
12690
12705
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
12691
12706
|
}
|
|
12692
12707
|
const layoutSource = import_fs4.default.readFileSync(layoutPath, "utf8");
|
|
12693
12708
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
12694
|
-
const ctx = this.
|
|
12709
|
+
const ctx = this.assembleContext(
|
|
12710
|
+
data,
|
|
12711
|
+
resolveSections(template, opts.sections),
|
|
12712
|
+
this.readStyle(`${template}.css`),
|
|
12713
|
+
opts
|
|
12714
|
+
);
|
|
12695
12715
|
return compiledLayout(ctx);
|
|
12696
12716
|
}
|
|
12697
|
-
renderFromPath(data, filePath,
|
|
12717
|
+
renderFromPath(data, filePath, opts = {}) {
|
|
12698
12718
|
if (!this.customTemplateCache.has(filePath)) {
|
|
12699
12719
|
if (!import_fs4.default.existsSync(filePath)) {
|
|
12700
12720
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
@@ -12703,46 +12723,27 @@ var TemplateEngine = class {
|
|
|
12703
12723
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
12704
12724
|
}
|
|
12705
12725
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
12706
|
-
return compiledFn(this.
|
|
12707
|
-
}
|
|
12708
|
-
buildCustomContext(data, slowTestThreshold) {
|
|
12709
|
-
const sections = allSectionsOn();
|
|
12710
|
-
const slow = this.buildSlowTests(data);
|
|
12711
|
-
return {
|
|
12712
|
-
...data,
|
|
12713
|
-
projects: this.enrichProjects(data, slow, sections, slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold),
|
|
12714
|
-
options: deriveOptions(sections),
|
|
12715
|
-
sections,
|
|
12716
|
-
tagGroups: this.buildTagGroups(data),
|
|
12717
|
-
slowTests: slow.ranked,
|
|
12718
|
-
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12719
|
-
templateCSS: "",
|
|
12720
|
-
chartjsScript: this.buildChartjsScript(),
|
|
12721
|
-
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12722
|
-
releaseGateTally: buildFailureTally(data.analysis),
|
|
12723
|
-
suiteChartLarge: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX,
|
|
12724
|
-
suiteChartChunks: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX ? buildSuiteChartChunks(data.charts.suiteResults) : []
|
|
12725
|
-
};
|
|
12726
|
+
return compiledFn(this.assembleContext(data, allSectionsOn(), "", opts));
|
|
12726
12727
|
}
|
|
12727
|
-
|
|
12728
|
-
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
12729
|
-
const templateCSS = this.readStyle(`${template}.css`);
|
|
12730
|
-
const sections = resolveSections(template, userSections);
|
|
12728
|
+
assembleContext(data, sections, templateCSS, opts) {
|
|
12731
12729
|
const options = deriveOptions(sections);
|
|
12732
12730
|
const slow = this.buildSlowTests(data);
|
|
12731
|
+
const tags = this.buildTagGroups(data, opts.requirementTagPattern);
|
|
12733
12732
|
return {
|
|
12734
12733
|
...data,
|
|
12735
12734
|
projects: this.enrichProjects(
|
|
12736
12735
|
data,
|
|
12737
12736
|
slow,
|
|
12738
12737
|
sections,
|
|
12739
|
-
slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
12738
|
+
opts.slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
12740
12739
|
),
|
|
12741
12740
|
options,
|
|
12742
12741
|
sections,
|
|
12743
|
-
tagGroups:
|
|
12742
|
+
tagGroups: tags.all,
|
|
12743
|
+
requirementTagGroups: tags.requirements,
|
|
12744
|
+
categoryTagGroups: tags.categories,
|
|
12744
12745
|
slowTests: slow.ranked,
|
|
12745
|
-
baseCSS,
|
|
12746
|
+
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12746
12747
|
templateCSS,
|
|
12747
12748
|
chartjsScript: this.buildChartjsScript(),
|
|
12748
12749
|
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
@@ -12754,7 +12755,7 @@ var TemplateEngine = class {
|
|
|
12754
12755
|
buildChartjsScript() {
|
|
12755
12756
|
return this.chartjsInline ? `<script>${this.chartjsInline}</script>` : "<!-- Chart.js not bundled; charts disabled --><script>window.__chartsReady=true;</script>";
|
|
12756
12757
|
}
|
|
12757
|
-
buildTagGroups(data) {
|
|
12758
|
+
buildTagGroups(data, requirementTagPattern) {
|
|
12758
12759
|
const tagMap = /* @__PURE__ */ new Map();
|
|
12759
12760
|
for (const project of data.projects) {
|
|
12760
12761
|
for (const suite of this.flattenSuites(project.suites)) {
|
|
@@ -12769,11 +12770,20 @@ var TemplateEngine = class {
|
|
|
12769
12770
|
}
|
|
12770
12771
|
}
|
|
12771
12772
|
}
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12773
|
+
const regex = new RegExp(requirementTagPattern ?? DEFAULT_OPTIONS.requirementTagPattern);
|
|
12774
|
+
const requirements = [];
|
|
12775
|
+
const categories = [];
|
|
12776
|
+
for (const [tag, counts] of tagMap) {
|
|
12777
|
+
const row = {
|
|
12778
|
+
tag,
|
|
12779
|
+
...counts,
|
|
12780
|
+
coveragePct: counts.total > 0 ? Math.round(counts.passed / counts.total * 100) : 0
|
|
12781
|
+
};
|
|
12782
|
+
(regex.test(tag) ? requirements : categories).push(row);
|
|
12783
|
+
}
|
|
12784
|
+
requirements.sort((a, b) => NUMERIC_COLLATOR.compare(a.tag, b.tag));
|
|
12785
|
+
categories.sort((a, b) => b.total - a.total || a.tag.localeCompare(b.tag));
|
|
12786
|
+
return { all: [...requirements, ...categories], requirements, categories };
|
|
12777
12787
|
}
|
|
12778
12788
|
buildSlowTests(data) {
|
|
12779
12789
|
const tests = [];
|
|
@@ -13530,7 +13540,12 @@ var PdfGenerator = class {
|
|
|
13530
13540
|
}
|
|
13531
13541
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
13532
13542
|
logger.info(`Rendering template: ${templateLabel}`);
|
|
13533
|
-
const
|
|
13543
|
+
const renderOpts = {
|
|
13544
|
+
sections: options.sections,
|
|
13545
|
+
slowTestThreshold: options.slowTestThreshold,
|
|
13546
|
+
requirementTagPattern: options.requirementTagPattern
|
|
13547
|
+
};
|
|
13548
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, renderOpts) : this.templateEngine.renderFromPath(renderData, source.absolutePath, renderOpts);
|
|
13534
13549
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
13535
13550
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
13536
13551
|
try {
|
|
@@ -13595,6 +13610,48 @@ var HistoryManager = class {
|
|
|
13595
13610
|
}
|
|
13596
13611
|
};
|
|
13597
13612
|
|
|
13613
|
+
// src/history/RemoteHistory.ts
|
|
13614
|
+
init_cjs_shims();
|
|
13615
|
+
var import_node_crypto3 = __toESM(require("crypto"));
|
|
13616
|
+
var NETWORK_TIMEOUT_MS2 = 5e3;
|
|
13617
|
+
function buildProjectId(projectName, outputFile) {
|
|
13618
|
+
return import_node_crypto3.default.createHash("sha256").update(`${projectName ?? ""}:${outputFile}`).digest("hex").slice(0, 16);
|
|
13619
|
+
}
|
|
13620
|
+
function buildBranchHash(branch) {
|
|
13621
|
+
return import_node_crypto3.default.createHash("sha256").update(branch || "unknown").digest("hex").slice(0, 12);
|
|
13622
|
+
}
|
|
13623
|
+
async function appendRemoteHistory(opts) {
|
|
13624
|
+
const numericEntry = { ...opts.entry };
|
|
13625
|
+
delete numericEntry.flakyTests;
|
|
13626
|
+
const controller = new AbortController();
|
|
13627
|
+
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS2);
|
|
13628
|
+
try {
|
|
13629
|
+
const res = await fetch(`${opts.serverUrl}/api/history/append`, {
|
|
13630
|
+
method: "POST",
|
|
13631
|
+
headers: {
|
|
13632
|
+
"content-type": "application/json",
|
|
13633
|
+
authorization: `Bearer ${opts.jwt}`
|
|
13634
|
+
},
|
|
13635
|
+
body: JSON.stringify({ projectId: opts.projectId, branchHash: opts.branchHash, entry: numericEntry }),
|
|
13636
|
+
signal: controller.signal
|
|
13637
|
+
});
|
|
13638
|
+
if (!res.ok) {
|
|
13639
|
+
await res.body?.cancel().catch(() => {
|
|
13640
|
+
});
|
|
13641
|
+
logger.debug(`remote history append failed: HTTP ${res.status} \u2014 using local history`);
|
|
13642
|
+
return null;
|
|
13643
|
+
}
|
|
13644
|
+
const data = await res.json();
|
|
13645
|
+
if (!Array.isArray(data.entries) || data.entries.length === 0) return null;
|
|
13646
|
+
return data.entries;
|
|
13647
|
+
} catch (err) {
|
|
13648
|
+
logger.debug(`remote history append failed: ${err.message} \u2014 using local history`);
|
|
13649
|
+
return null;
|
|
13650
|
+
} finally {
|
|
13651
|
+
clearTimeout(timeout);
|
|
13652
|
+
}
|
|
13653
|
+
}
|
|
13654
|
+
|
|
13598
13655
|
// src/history/FlakinessAnalyser.ts
|
|
13599
13656
|
init_cjs_shims();
|
|
13600
13657
|
function computeTopN(entries, topN) {
|
|
@@ -14009,7 +14066,7 @@ var NotificationSender = class {
|
|
|
14009
14066
|
|
|
14010
14067
|
// src/live/LiveStreamer.ts
|
|
14011
14068
|
init_cjs_shims();
|
|
14012
|
-
var
|
|
14069
|
+
var NETWORK_TIMEOUT_MS3 = 8e3;
|
|
14013
14070
|
var LIVE_DRAIN_DEADLINE_MS = 1e4;
|
|
14014
14071
|
var MAX_FLUSH_MS = 3e4;
|
|
14015
14072
|
var WARN_THROTTLE_MS = 3e4;
|
|
@@ -14182,7 +14239,7 @@ var LiveStreamer = class {
|
|
|
14182
14239
|
async send(body) {
|
|
14183
14240
|
if (!this.jwt) return;
|
|
14184
14241
|
const controller = new AbortController();
|
|
14185
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
14242
|
+
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS3);
|
|
14186
14243
|
try {
|
|
14187
14244
|
const res = await fetch(`${this.serverUrl}/api/live/ingest`, {
|
|
14188
14245
|
method: "POST",
|
|
@@ -14356,7 +14413,7 @@ var PdfReporter = class {
|
|
|
14356
14413
|
this.options = parseOptions(rawOptions);
|
|
14357
14414
|
setLogLevel(this.options.logLevel);
|
|
14358
14415
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14359
|
-
this.version = true ? "0.
|
|
14416
|
+
this.version = true ? "0.22.0" : "0.x";
|
|
14360
14417
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
14361
14418
|
this.licenseClient = new LicenseClient({
|
|
14362
14419
|
licenseKey: this.options.licenseKey,
|
|
@@ -14439,7 +14496,7 @@ var PdfReporter = class {
|
|
|
14439
14496
|
}
|
|
14440
14497
|
if (this.options.showTrend && collected.stats.total > 0) {
|
|
14441
14498
|
try {
|
|
14442
|
-
this._appendTrend(collected);
|
|
14499
|
+
await this._appendTrend(collected, licenseInfo);
|
|
14443
14500
|
} catch (err) {
|
|
14444
14501
|
logger.warn(`History write failed \u2014 trend data skipped: ${err.message}`);
|
|
14445
14502
|
}
|
|
@@ -14474,7 +14531,7 @@ var PdfReporter = class {
|
|
|
14474
14531
|
}
|
|
14475
14532
|
return this.dataCollector.finalize(result);
|
|
14476
14533
|
}
|
|
14477
|
-
_appendTrend(collected) {
|
|
14534
|
+
async _appendTrend(collected, licenseInfo) {
|
|
14478
14535
|
const historyPath = resolveHistoryPath(this.options, this.cwd);
|
|
14479
14536
|
const hm = new HistoryManager(historyPath);
|
|
14480
14537
|
const suffix = Math.random().toString(36).padEnd(8, "0").slice(2, 6);
|
|
@@ -14491,13 +14548,28 @@ var PdfReporter = class {
|
|
|
14491
14548
|
verdict: deriveVerdict(collected.stats),
|
|
14492
14549
|
flakyTests
|
|
14493
14550
|
};
|
|
14494
|
-
const
|
|
14495
|
-
|
|
14551
|
+
const localEntries = hm.append(entry, this.options.historySize);
|
|
14552
|
+
let trendEntries = localEntries;
|
|
14553
|
+
if (this.options.remoteHistory && licenseInfo.jwt) {
|
|
14554
|
+
const serverUrl = this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL2;
|
|
14555
|
+
const remote = await appendRemoteHistory({
|
|
14556
|
+
serverUrl,
|
|
14557
|
+
jwt: licenseInfo.jwt,
|
|
14558
|
+
projectId: buildProjectId(this.options.projectName, this.options.outputFile),
|
|
14559
|
+
branchHash: buildBranchHash(collected.environment.branch),
|
|
14560
|
+
entry
|
|
14561
|
+
});
|
|
14562
|
+
if (remote) {
|
|
14563
|
+
trendEntries = remote;
|
|
14564
|
+
logger.debug(`remote history: ${remote.length} entr${remote.length === 1 ? "y" : "ies"} for trend`);
|
|
14565
|
+
}
|
|
14566
|
+
}
|
|
14567
|
+
this._populateHistoryCharts(trendEntries, localEntries, collected);
|
|
14496
14568
|
}
|
|
14497
|
-
_populateHistoryCharts(
|
|
14498
|
-
if (
|
|
14569
|
+
_populateHistoryCharts(trendEntries, localEntries, collected) {
|
|
14570
|
+
if (trendEntries.length >= 2) {
|
|
14499
14571
|
collected.charts.trend = {
|
|
14500
|
-
entries:
|
|
14572
|
+
entries: trendEntries.map((e) => ({
|
|
14501
14573
|
runId: e.runId,
|
|
14502
14574
|
timestamp: e.timestamp,
|
|
14503
14575
|
passRate: e.passRate,
|
|
@@ -14505,11 +14577,11 @@ var PdfReporter = class {
|
|
|
14505
14577
|
duration: e.duration,
|
|
14506
14578
|
verdict: e.verdict
|
|
14507
14579
|
})),
|
|
14508
|
-
delta:
|
|
14580
|
+
delta: trendEntries[0].passRate - trendEntries[1].passRate
|
|
14509
14581
|
};
|
|
14510
14582
|
}
|
|
14511
14583
|
if (this.options.flakinessTopN > 0) {
|
|
14512
|
-
const table = computeTopN(
|
|
14584
|
+
const table = computeTopN(localEntries, this.options.flakinessTopN);
|
|
14513
14585
|
if (table.length > 0) {
|
|
14514
14586
|
collected.charts.flakinessTable = table;
|
|
14515
14587
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -10331,7 +10331,10 @@ var DEFAULT_OPTIONS = {
|
|
|
10331
10331
|
historySize: 10,
|
|
10332
10332
|
showTrend: true,
|
|
10333
10333
|
flakinessTopN: 5,
|
|
10334
|
-
slowTestThreshold: 10
|
|
10334
|
+
slowTestThreshold: 10,
|
|
10335
|
+
// Ticket-ID shape: @ODP-5328, REQ-001, JIRA-123. Tags matching this land in
|
|
10336
|
+
// the Requirements Traceability matrix; the rest fall to the tag summary.
|
|
10337
|
+
requirementTagPattern: "^@?[A-Z][A-Z0-9]*-\\d+$"
|
|
10335
10338
|
};
|
|
10336
10339
|
|
|
10337
10340
|
// src/templates/sections.ts
|
|
@@ -10555,8 +10558,19 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10555
10558
|
historyFile: external_exports.string().optional(),
|
|
10556
10559
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
10557
10560
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
10561
|
+
// Opt-in server-side trend store: survives ephemeral CI runners where the
|
|
10562
|
+
// local history file is wiped every run. Aggregate numbers only.
|
|
10563
|
+
remoteHistory: external_exports.boolean().default(false),
|
|
10558
10564
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10559
10565
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10566
|
+
requirementTagPattern: external_exports.string().refine((v) => {
|
|
10567
|
+
try {
|
|
10568
|
+
new RegExp(v);
|
|
10569
|
+
return true;
|
|
10570
|
+
} catch {
|
|
10571
|
+
return false;
|
|
10572
|
+
}
|
|
10573
|
+
}, "requirementTagPattern must be a valid regular expression").optional().default(DEFAULT_OPTIONS.requirementTagPattern),
|
|
10560
10574
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
10561
10575
|
capture: captureConfigSchema,
|
|
10562
10576
|
live: liveConfigSchema,
|
|
@@ -12636,6 +12650,7 @@ var PAGE_CONTENT_HEIGHT_PX = (A4_HEIGHT_MM - 2 * PAGE_MARGIN_Y_MM) * PX_PER_MM;
|
|
|
12636
12650
|
function isUnknownValue(v) {
|
|
12637
12651
|
return !v || v === "unknown";
|
|
12638
12652
|
}
|
|
12653
|
+
var NUMERIC_COLLATOR = new Intl.Collator(void 0, { numeric: true });
|
|
12639
12654
|
var SLOW_MEDIAN_FACTOR = 2;
|
|
12640
12655
|
function buildSuiteChartChunks(rows) {
|
|
12641
12656
|
const chunks = [];
|
|
@@ -12685,17 +12700,22 @@ var TemplateEngine = class {
|
|
|
12685
12700
|
setChartjsBundle(inline) {
|
|
12686
12701
|
this.chartjsInline = inline;
|
|
12687
12702
|
}
|
|
12688
|
-
render(data, template,
|
|
12703
|
+
render(data, template, opts = {}) {
|
|
12689
12704
|
const layoutPath = path9.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
12690
12705
|
if (!fs7.existsSync(layoutPath)) {
|
|
12691
12706
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
12692
12707
|
}
|
|
12693
12708
|
const layoutSource = fs7.readFileSync(layoutPath, "utf8");
|
|
12694
12709
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
12695
|
-
const ctx = this.
|
|
12710
|
+
const ctx = this.assembleContext(
|
|
12711
|
+
data,
|
|
12712
|
+
resolveSections(template, opts.sections),
|
|
12713
|
+
this.readStyle(`${template}.css`),
|
|
12714
|
+
opts
|
|
12715
|
+
);
|
|
12696
12716
|
return compiledLayout(ctx);
|
|
12697
12717
|
}
|
|
12698
|
-
renderFromPath(data, filePath,
|
|
12718
|
+
renderFromPath(data, filePath, opts = {}) {
|
|
12699
12719
|
if (!this.customTemplateCache.has(filePath)) {
|
|
12700
12720
|
if (!fs7.existsSync(filePath)) {
|
|
12701
12721
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
@@ -12704,46 +12724,27 @@ var TemplateEngine = class {
|
|
|
12704
12724
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
12705
12725
|
}
|
|
12706
12726
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
12707
|
-
return compiledFn(this.
|
|
12708
|
-
}
|
|
12709
|
-
buildCustomContext(data, slowTestThreshold) {
|
|
12710
|
-
const sections = allSectionsOn();
|
|
12711
|
-
const slow = this.buildSlowTests(data);
|
|
12712
|
-
return {
|
|
12713
|
-
...data,
|
|
12714
|
-
projects: this.enrichProjects(data, slow, sections, slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold),
|
|
12715
|
-
options: deriveOptions(sections),
|
|
12716
|
-
sections,
|
|
12717
|
-
tagGroups: this.buildTagGroups(data),
|
|
12718
|
-
slowTests: slow.ranked,
|
|
12719
|
-
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12720
|
-
templateCSS: "",
|
|
12721
|
-
chartjsScript: this.buildChartjsScript(),
|
|
12722
|
-
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
12723
|
-
releaseGateTally: buildFailureTally(data.analysis),
|
|
12724
|
-
suiteChartLarge: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX,
|
|
12725
|
-
suiteChartChunks: data.charts.suiteResults.length > SUITE_CHART_SMALL_MAX ? buildSuiteChartChunks(data.charts.suiteResults) : []
|
|
12726
|
-
};
|
|
12727
|
+
return compiledFn(this.assembleContext(data, allSectionsOn(), "", opts));
|
|
12727
12728
|
}
|
|
12728
|
-
|
|
12729
|
-
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
12730
|
-
const templateCSS = this.readStyle(`${template}.css`);
|
|
12731
|
-
const sections = resolveSections(template, userSections);
|
|
12729
|
+
assembleContext(data, sections, templateCSS, opts) {
|
|
12732
12730
|
const options = deriveOptions(sections);
|
|
12733
12731
|
const slow = this.buildSlowTests(data);
|
|
12732
|
+
const tags = this.buildTagGroups(data, opts.requirementTagPattern);
|
|
12734
12733
|
return {
|
|
12735
12734
|
...data,
|
|
12736
12735
|
projects: this.enrichProjects(
|
|
12737
12736
|
data,
|
|
12738
12737
|
slow,
|
|
12739
12738
|
sections,
|
|
12740
|
-
slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
12739
|
+
opts.slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
12741
12740
|
),
|
|
12742
12741
|
options,
|
|
12743
12742
|
sections,
|
|
12744
|
-
tagGroups:
|
|
12743
|
+
tagGroups: tags.all,
|
|
12744
|
+
requirementTagGroups: tags.requirements,
|
|
12745
|
+
categoryTagGroups: tags.categories,
|
|
12745
12746
|
slowTests: slow.ranked,
|
|
12746
|
-
baseCSS,
|
|
12747
|
+
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12747
12748
|
templateCSS,
|
|
12748
12749
|
chartjsScript: this.buildChartjsScript(),
|
|
12749
12750
|
watermarkPageHeightPx: PAGE_CONTENT_HEIGHT_PX,
|
|
@@ -12755,7 +12756,7 @@ var TemplateEngine = class {
|
|
|
12755
12756
|
buildChartjsScript() {
|
|
12756
12757
|
return this.chartjsInline ? `<script>${this.chartjsInline}</script>` : "<!-- Chart.js not bundled; charts disabled --><script>window.__chartsReady=true;</script>";
|
|
12757
12758
|
}
|
|
12758
|
-
buildTagGroups(data) {
|
|
12759
|
+
buildTagGroups(data, requirementTagPattern) {
|
|
12759
12760
|
const tagMap = /* @__PURE__ */ new Map();
|
|
12760
12761
|
for (const project of data.projects) {
|
|
12761
12762
|
for (const suite of this.flattenSuites(project.suites)) {
|
|
@@ -12770,11 +12771,20 @@ var TemplateEngine = class {
|
|
|
12770
12771
|
}
|
|
12771
12772
|
}
|
|
12772
12773
|
}
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12774
|
+
const regex = new RegExp(requirementTagPattern ?? DEFAULT_OPTIONS.requirementTagPattern);
|
|
12775
|
+
const requirements = [];
|
|
12776
|
+
const categories = [];
|
|
12777
|
+
for (const [tag, counts] of tagMap) {
|
|
12778
|
+
const row = {
|
|
12779
|
+
tag,
|
|
12780
|
+
...counts,
|
|
12781
|
+
coveragePct: counts.total > 0 ? Math.round(counts.passed / counts.total * 100) : 0
|
|
12782
|
+
};
|
|
12783
|
+
(regex.test(tag) ? requirements : categories).push(row);
|
|
12784
|
+
}
|
|
12785
|
+
requirements.sort((a, b) => NUMERIC_COLLATOR.compare(a.tag, b.tag));
|
|
12786
|
+
categories.sort((a, b) => b.total - a.total || a.tag.localeCompare(b.tag));
|
|
12787
|
+
return { all: [...requirements, ...categories], requirements, categories };
|
|
12778
12788
|
}
|
|
12779
12789
|
buildSlowTests(data) {
|
|
12780
12790
|
const tests = [];
|
|
@@ -13531,7 +13541,12 @@ var PdfGenerator = class {
|
|
|
13531
13541
|
}
|
|
13532
13542
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
13533
13543
|
logger.info(`Rendering template: ${templateLabel}`);
|
|
13534
|
-
const
|
|
13544
|
+
const renderOpts = {
|
|
13545
|
+
sections: options.sections,
|
|
13546
|
+
slowTestThreshold: options.slowTestThreshold,
|
|
13547
|
+
requirementTagPattern: options.requirementTagPattern
|
|
13548
|
+
};
|
|
13549
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, renderOpts) : this.templateEngine.renderFromPath(renderData, source.absolutePath, renderOpts);
|
|
13535
13550
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
13536
13551
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
13537
13552
|
try {
|
|
@@ -13596,6 +13611,48 @@ var HistoryManager = class {
|
|
|
13596
13611
|
}
|
|
13597
13612
|
};
|
|
13598
13613
|
|
|
13614
|
+
// src/history/RemoteHistory.ts
|
|
13615
|
+
init_esm_shims();
|
|
13616
|
+
import crypto4 from "crypto";
|
|
13617
|
+
var NETWORK_TIMEOUT_MS2 = 5e3;
|
|
13618
|
+
function buildProjectId(projectName, outputFile) {
|
|
13619
|
+
return crypto4.createHash("sha256").update(`${projectName ?? ""}:${outputFile}`).digest("hex").slice(0, 16);
|
|
13620
|
+
}
|
|
13621
|
+
function buildBranchHash(branch) {
|
|
13622
|
+
return crypto4.createHash("sha256").update(branch || "unknown").digest("hex").slice(0, 12);
|
|
13623
|
+
}
|
|
13624
|
+
async function appendRemoteHistory(opts) {
|
|
13625
|
+
const numericEntry = { ...opts.entry };
|
|
13626
|
+
delete numericEntry.flakyTests;
|
|
13627
|
+
const controller = new AbortController();
|
|
13628
|
+
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS2);
|
|
13629
|
+
try {
|
|
13630
|
+
const res = await fetch(`${opts.serverUrl}/api/history/append`, {
|
|
13631
|
+
method: "POST",
|
|
13632
|
+
headers: {
|
|
13633
|
+
"content-type": "application/json",
|
|
13634
|
+
authorization: `Bearer ${opts.jwt}`
|
|
13635
|
+
},
|
|
13636
|
+
body: JSON.stringify({ projectId: opts.projectId, branchHash: opts.branchHash, entry: numericEntry }),
|
|
13637
|
+
signal: controller.signal
|
|
13638
|
+
});
|
|
13639
|
+
if (!res.ok) {
|
|
13640
|
+
await res.body?.cancel().catch(() => {
|
|
13641
|
+
});
|
|
13642
|
+
logger.debug(`remote history append failed: HTTP ${res.status} \u2014 using local history`);
|
|
13643
|
+
return null;
|
|
13644
|
+
}
|
|
13645
|
+
const data = await res.json();
|
|
13646
|
+
if (!Array.isArray(data.entries) || data.entries.length === 0) return null;
|
|
13647
|
+
return data.entries;
|
|
13648
|
+
} catch (err) {
|
|
13649
|
+
logger.debug(`remote history append failed: ${err.message} \u2014 using local history`);
|
|
13650
|
+
return null;
|
|
13651
|
+
} finally {
|
|
13652
|
+
clearTimeout(timeout);
|
|
13653
|
+
}
|
|
13654
|
+
}
|
|
13655
|
+
|
|
13599
13656
|
// src/history/FlakinessAnalyser.ts
|
|
13600
13657
|
init_esm_shims();
|
|
13601
13658
|
function computeTopN(entries, topN) {
|
|
@@ -14010,7 +14067,7 @@ var NotificationSender = class {
|
|
|
14010
14067
|
|
|
14011
14068
|
// src/live/LiveStreamer.ts
|
|
14012
14069
|
init_esm_shims();
|
|
14013
|
-
var
|
|
14070
|
+
var NETWORK_TIMEOUT_MS3 = 8e3;
|
|
14014
14071
|
var LIVE_DRAIN_DEADLINE_MS = 1e4;
|
|
14015
14072
|
var MAX_FLUSH_MS = 3e4;
|
|
14016
14073
|
var WARN_THROTTLE_MS = 3e4;
|
|
@@ -14183,7 +14240,7 @@ var LiveStreamer = class {
|
|
|
14183
14240
|
async send(body) {
|
|
14184
14241
|
if (!this.jwt) return;
|
|
14185
14242
|
const controller = new AbortController();
|
|
14186
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
14243
|
+
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS3);
|
|
14187
14244
|
try {
|
|
14188
14245
|
const res = await fetch(`${this.serverUrl}/api/live/ingest`, {
|
|
14189
14246
|
method: "POST",
|
|
@@ -14357,7 +14414,7 @@ var PdfReporter = class {
|
|
|
14357
14414
|
this.options = parseOptions(rawOptions);
|
|
14358
14415
|
setLogLevel(this.options.logLevel);
|
|
14359
14416
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14360
|
-
this.version = true ? "0.
|
|
14417
|
+
this.version = true ? "0.22.0" : "0.x";
|
|
14361
14418
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
14362
14419
|
this.licenseClient = new LicenseClient({
|
|
14363
14420
|
licenseKey: this.options.licenseKey,
|
|
@@ -14440,7 +14497,7 @@ var PdfReporter = class {
|
|
|
14440
14497
|
}
|
|
14441
14498
|
if (this.options.showTrend && collected.stats.total > 0) {
|
|
14442
14499
|
try {
|
|
14443
|
-
this._appendTrend(collected);
|
|
14500
|
+
await this._appendTrend(collected, licenseInfo);
|
|
14444
14501
|
} catch (err) {
|
|
14445
14502
|
logger.warn(`History write failed \u2014 trend data skipped: ${err.message}`);
|
|
14446
14503
|
}
|
|
@@ -14475,7 +14532,7 @@ var PdfReporter = class {
|
|
|
14475
14532
|
}
|
|
14476
14533
|
return this.dataCollector.finalize(result);
|
|
14477
14534
|
}
|
|
14478
|
-
_appendTrend(collected) {
|
|
14535
|
+
async _appendTrend(collected, licenseInfo) {
|
|
14479
14536
|
const historyPath = resolveHistoryPath(this.options, this.cwd);
|
|
14480
14537
|
const hm = new HistoryManager(historyPath);
|
|
14481
14538
|
const suffix = Math.random().toString(36).padEnd(8, "0").slice(2, 6);
|
|
@@ -14492,13 +14549,28 @@ var PdfReporter = class {
|
|
|
14492
14549
|
verdict: deriveVerdict(collected.stats),
|
|
14493
14550
|
flakyTests
|
|
14494
14551
|
};
|
|
14495
|
-
const
|
|
14496
|
-
|
|
14552
|
+
const localEntries = hm.append(entry, this.options.historySize);
|
|
14553
|
+
let trendEntries = localEntries;
|
|
14554
|
+
if (this.options.remoteHistory && licenseInfo.jwt) {
|
|
14555
|
+
const serverUrl = this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL2;
|
|
14556
|
+
const remote = await appendRemoteHistory({
|
|
14557
|
+
serverUrl,
|
|
14558
|
+
jwt: licenseInfo.jwt,
|
|
14559
|
+
projectId: buildProjectId(this.options.projectName, this.options.outputFile),
|
|
14560
|
+
branchHash: buildBranchHash(collected.environment.branch),
|
|
14561
|
+
entry
|
|
14562
|
+
});
|
|
14563
|
+
if (remote) {
|
|
14564
|
+
trendEntries = remote;
|
|
14565
|
+
logger.debug(`remote history: ${remote.length} entr${remote.length === 1 ? "y" : "ies"} for trend`);
|
|
14566
|
+
}
|
|
14567
|
+
}
|
|
14568
|
+
this._populateHistoryCharts(trendEntries, localEntries, collected);
|
|
14497
14569
|
}
|
|
14498
|
-
_populateHistoryCharts(
|
|
14499
|
-
if (
|
|
14570
|
+
_populateHistoryCharts(trendEntries, localEntries, collected) {
|
|
14571
|
+
if (trendEntries.length >= 2) {
|
|
14500
14572
|
collected.charts.trend = {
|
|
14501
|
-
entries:
|
|
14573
|
+
entries: trendEntries.map((e) => ({
|
|
14502
14574
|
runId: e.runId,
|
|
14503
14575
|
timestamp: e.timestamp,
|
|
14504
14576
|
passRate: e.passRate,
|
|
@@ -14506,11 +14578,11 @@ var PdfReporter = class {
|
|
|
14506
14578
|
duration: e.duration,
|
|
14507
14579
|
verdict: e.verdict
|
|
14508
14580
|
})),
|
|
14509
|
-
delta:
|
|
14581
|
+
delta: trendEntries[0].passRate - trendEntries[1].passRate
|
|
14510
14582
|
};
|
|
14511
14583
|
}
|
|
14512
14584
|
if (this.options.flakinessTopN > 0) {
|
|
14513
|
-
const table = computeTopN(
|
|
14585
|
+
const table = computeTopN(localEntries, this.options.flakinessTopN);
|
|
14514
14586
|
if (table.length > 0) {
|
|
14515
14587
|
collected.charts.flakinessTable = table;
|
|
14516
14588
|
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
<div class="section">
|
|
3
3
|
<h2>Requirements Traceability</h2>
|
|
4
4
|
<p style="font-size:10px;color:var(--text-3);margin-bottom:12px;">
|
|
5
|
-
Tests grouped by
|
|
5
|
+
{{#if requirementTagGroups.length}}Tests grouped by requirement ID tags.{{else}}No requirement ID tags found.{{/if}}
|
|
6
|
+
Add <code>{ tag: '@REQ-001' }</code> to link tests to requirements.
|
|
6
7
|
</p>
|
|
8
|
+
{{#if requirementTagGroups.length}}
|
|
7
9
|
<table>
|
|
8
10
|
<thead>
|
|
9
11
|
<tr>
|
|
@@ -11,13 +13,13 @@
|
|
|
11
13
|
<th style="width:60px;">Tests</th>
|
|
12
14
|
<th style="width:60px;">Passed</th>
|
|
13
15
|
<th style="width:60px;">Failed</th>
|
|
14
|
-
<th style="width:120px;">
|
|
16
|
+
<th style="width:120px;">Pass Rate</th>
|
|
15
17
|
</tr>
|
|
16
18
|
</thead>
|
|
17
19
|
<tbody>
|
|
18
|
-
{{#each
|
|
20
|
+
{{#each requirementTagGroups}}
|
|
19
21
|
<tr class="no-break">
|
|
20
|
-
<td><code style="font-size:9.5px;">{{tag}}</code></td>
|
|
22
|
+
<td><code class="req-matrix__tag" style="font-size:9.5px;">{{tag}}</code></td>
|
|
21
23
|
<td>{{total}}</td>
|
|
22
24
|
<td style="color:var(--pass);font-weight:600;">{{passed}}</td>
|
|
23
25
|
<td style="color:{{#if failed}}var(--fail){{else}}var(--pass){{/if}};font-weight:600;">{{failed}}</td>
|
|
@@ -31,5 +33,19 @@
|
|
|
31
33
|
{{/each}}
|
|
32
34
|
</tbody>
|
|
33
35
|
</table>
|
|
36
|
+
{{/if}}
|
|
37
|
+
{{#if categoryTagGroups.length}}
|
|
38
|
+
<div class="tag-summary no-break">
|
|
39
|
+
<div class="tag-summary__label">Tag Summary</div>
|
|
40
|
+
<div class="tag-summary__items">
|
|
41
|
+
{{#each categoryTagGroups}}
|
|
42
|
+
<span class="tag-summary__item{{#if failed}} tag-summary__item--fail{{/if}}">
|
|
43
|
+
<code class="tag-summary__tag">{{tag}}</code>
|
|
44
|
+
<span class="tag-summary__counts">{{passed}}/{{total}}</span>
|
|
45
|
+
</span>
|
|
46
|
+
{{/each}}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
{{/if}}
|
|
34
50
|
</div>
|
|
35
51
|
{{/if}}
|
|
@@ -891,6 +891,29 @@ code {
|
|
|
891
891
|
.coverage-bar__fill--mid { background: var(--timeout); }
|
|
892
892
|
.coverage-bar__fill--high { background: var(--pass); }
|
|
893
893
|
|
|
894
|
+
/* ── Requirements traceability: category tag summary ─────────────────────── */
|
|
895
|
+
/* Non-requirement tags (suite-level categories like @regression) collapse
|
|
896
|
+
into one compact row list instead of repeating identical matrix rows. */
|
|
897
|
+
.tag-summary {
|
|
898
|
+
margin-top: 10px; padding-top: 10px;
|
|
899
|
+
border-top: 1px solid var(--border);
|
|
900
|
+
}
|
|
901
|
+
.tag-summary__label {
|
|
902
|
+
font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
|
|
903
|
+
font-size: 8.5px; letter-spacing: 0.8px; text-transform: uppercase;
|
|
904
|
+
color: var(--text-3); margin-bottom: 4px;
|
|
905
|
+
}
|
|
906
|
+
.tag-summary__items { max-width: 340px; }
|
|
907
|
+
.tag-summary__item {
|
|
908
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
909
|
+
padding: 3.5px 2px;
|
|
910
|
+
border-bottom: 1px solid var(--border);
|
|
911
|
+
}
|
|
912
|
+
.tag-summary__item:last-child { border-bottom: none; }
|
|
913
|
+
.tag-summary__tag { font-size: 9.5px; color: var(--text-1); background: none; border: none; padding: 0; }
|
|
914
|
+
.tag-summary__counts { font-size: 8.5px; font-weight: 600; color: var(--pass); }
|
|
915
|
+
.tag-summary__item--fail .tag-summary__counts { color: var(--fail); }
|
|
916
|
+
|
|
894
917
|
/* ── Environment table — full ci-environment mode (fullEnvironment), shared ─── */
|
|
895
918
|
.env-table {
|
|
896
919
|
background: var(--surface);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Playwright Test reporter that generates designed PDF reports: minimal, detailed, and executive templates with CI/CD integrations",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"author": "ReportForge",
|