@reportforge/playwright-pdf 0.25.0 → 0.26.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/README.md +6 -3
- package/dist/cli/index.js +24 -1
- package/dist/index.d.mts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +178 -36
- package/dist/index.mjs +178 -36
- package/dist/templates/layouts/detailed.hbs +2 -0
- package/dist/templates/layouts/executive.hbs +2 -0
- package/dist/templates/layouts/minimal.hbs +2 -0
- package/dist/templates/partials/run-diff.hbs +41 -0
- package/dist/templates/styles/base.css +19 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,7 @@ Three templates, one reporter. Every PDF is fully offline: fonts, charts, and sc
|
|
|
58
58
|
- **Notifications**: post pass/fail summaries to Slack, Teams, Discord, or email after each run; configurable trigger (`always` / `failure` / `success`) per channel; email and Discord support optional PDF attachment via `attachPdf`
|
|
59
59
|
- **Flakiness trend**: top-N flakiest tests table with per-test dot sparkline across stored history runs in the `detailed` template; `flakinessTopN` option (default 5, `0` disables)
|
|
60
60
|
- **Pass-rate trend**: cross-run trend chart from stored history; opt-in `remoteHistory` keeps the trend alive on ephemeral CI runners by storing aggregate numbers only (pass rate, counts, duration, verdict; no test titles or error text, and the branch name never travels in plaintext), with no extra network calls
|
|
61
|
+
- **Run-over-run diff**: every report shows what changed since the previous run on the same branch: new failures, fixed tests, and still-failing tests, and the same summary rides along in Slack, Teams, Discord, and email notifications
|
|
61
62
|
- **Offline failure analysis**: sorts each failure into one of 7 root-cause buckets and renders a root-cause chip under each failed test in the `detailed` PDF (dominant-cause one-liner in `executive`). No network, no AI service: a small embedded classifier with automatic updates; pin it any time. Full details at [reportforge.org/docs/advanced/failure-analysis](https://reportforge.org/docs/advanced/failure-analysis).
|
|
62
63
|
- **On-device training**: teach the classifier your team's failures without any data leaving your machine. Label locally-collected samples (`npx @reportforge/playwright-pdf label-feedback`), then `train-model` trains and cross-validates a personal layer on-device; it activates only when it measurably beats the base model on your own labeled set (`evaluate-model` shows the comparison any time). Commit the model file to your repo via `failureAnalysis.localModelPath` to share it with CI. Training data, the trained model, and evaluation are all local files; the analysis code cannot even name a network primitive (enforced by an automated source audit).
|
|
63
64
|
- `npx @reportforge/playwright-pdf export-feedback`: merges your locally-collected feedback (tokenized + redacted) into one CSV for review or moving between machines. Local file only; there is no upload.
|
|
@@ -213,7 +214,7 @@ All options are the second element of the reporter tuple.
|
|
|
213
214
|
| `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). |
|
|
214
215
|
| `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. |
|
|
215
216
|
| `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. |
|
|
216
|
-
| `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. |
|
|
217
|
+
| `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. 20 keys total. Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`, `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`, `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`, `runDiff`. Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`, `stackTraces`. `runDiff` (block toggle, on by default in all three templates) shows new failures, fixed tests, and still-failing tests compared with the previous run on the same branch. See the Report Sections docs. |
|
|
217
218
|
<!-- AUTOGEN:options-table END -->
|
|
218
219
|
|
|
219
220
|
Full reference: [reportforge.org/docs/configuration](https://reportforge.org/docs/configuration).
|
|
@@ -316,7 +317,7 @@ The `on` trigger controls when the message fires:
|
|
|
316
317
|
|
|
317
318
|
**PDF attachment** (`attachPdf: true`) is available on `email` and `discord`: Slack and Teams webhooks cannot carry file uploads. On Discord the PDF is uploaded with the message; if the upload fails or the PDF exceeds Discord's file-size cap, the summary still posts without the attachment. With multiple templates, the first PDF is attached.
|
|
318
319
|
|
|
319
|
-
The summary includes pass rate, test counts, duration, and the report filename. Notifications require a valid license and fire after PDF generation (or after a PDF failure; you still get the ping).
|
|
320
|
+
The summary includes pass rate, test counts, duration, and the report filename. When a previous run on the same branch exists, the message also carries a run-over-run diff line: new failures, fixed tests, and tests still failing. Notifications require a valid license and fire after PDF generation (or after a PDF failure; you still get the ping).
|
|
320
321
|
|
|
321
322
|
### Data Protection
|
|
322
323
|
|
|
@@ -364,6 +365,8 @@ Full details at [reportforge.org/docs/advanced/security](https://reportforge.org
|
|
|
364
365
|
|
|
365
366
|
After each run the reporter appends a summary entry to a local JSON file and renders a pass-rate sparkline + verdict row in the `detailed` template.
|
|
366
367
|
|
|
368
|
+
**Since Last Run**: every report also gets a section (`runDiff`, on by default in all three templates) comparing the current run against the previous run on the same branch: new failures, fixed tests, and tests still failing, with the same summary line carried into Slack, Teams, Discord, and email notifications. The first run after upgrading has no comparable prior run to diff against, so the section starts appearing from the second run onward. It reads only the local history file, so test names never leave your machine; see the ephemeral-CI note below for keeping that file around between runs. Like the flakiness table, it rides history tracking (`showTrend`, on by default), and the notification line follows history availability rather than the `runDiff` section toggle.
|
|
369
|
+
|
|
367
370
|
**Local dev (zero config)**: history is written automatically to `~/.reportforge/{projectKey}/history.json`. The sparkline appears once two or more runs have been recorded.
|
|
368
371
|
|
|
369
372
|
**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:
|
|
@@ -375,7 +378,7 @@ reporter: [['@reportforge/playwright-pdf', {
|
|
|
375
378
|
}]]
|
|
376
379
|
```
|
|
377
380
|
|
|
378
|
-
One small authenticated request per run, carrying aggregate numbers only (pass rate, counts, duration, verdict; no test titles, no error text, and the branch name never travels in plaintext). The server keeps roughly the last 100 runs per project + branch, expiring after 180 days, and returns them for the chart; any failure falls back to the local file.
|
|
381
|
+
One small authenticated request per run, carrying aggregate numbers only (pass rate, counts, duration, verdict; no test titles, no error text, and the branch name never travels in plaintext). The server keeps roughly the last 100 runs per project + branch, expiring after 180 days, and returns them for the chart; any failure falls back to the local file. This feeds the trend chart only: the flakiness table and the Since Last Run diff both stay local-only by design (they need test titles, which never leave your machine); to keep either alive on ephemeral runners, use the CI caching alternative below to persist `historyFile` instead. Bitbucket Pipelines users especially: Bitbucket caches are immutable once written, so `remoteHistory` is the practical path for the trend chart there.
|
|
379
382
|
|
|
380
383
|
**CI caching alternative** (keeps even aggregate numbers off the server): set `historyFile` to a project-relative path and cache it between runs:
|
|
381
384
|
|
package/dist/cli/index.js
CHANGED
|
@@ -311,6 +311,24 @@ function buildDemoCharts(passed, failed, skipped, flaky, total) {
|
|
|
311
311
|
]
|
|
312
312
|
};
|
|
313
313
|
}
|
|
314
|
+
function buildDemoRunDiff() {
|
|
315
|
+
return {
|
|
316
|
+
newFailures: [
|
|
317
|
+
{ id: "auth-5", title: "should block brute-force attempts after 5 tries" },
|
|
318
|
+
{ id: "cat-7", title: "should lazy-load images on scroll" }
|
|
319
|
+
],
|
|
320
|
+
stillFailing: [
|
|
321
|
+
{ id: "co-3", title: "should reject expired promo code" }
|
|
322
|
+
],
|
|
323
|
+
fixed: [
|
|
324
|
+
{ id: "co-5", title: "should complete Stripe checkout" }
|
|
325
|
+
],
|
|
326
|
+
noLongerRun: [],
|
|
327
|
+
baselineRunId: "demo-1d",
|
|
328
|
+
baselineTimestamp: DEMO_BASE_TS - 864e5,
|
|
329
|
+
baselineTruncated: false
|
|
330
|
+
};
|
|
331
|
+
}
|
|
314
332
|
function buildDemoReportData(template = "detailed") {
|
|
315
333
|
const projects = [
|
|
316
334
|
makeProject("chromium", [
|
|
@@ -362,7 +380,8 @@ function buildDemoReportData(template = "detailed") {
|
|
|
362
380
|
projectCount: 2,
|
|
363
381
|
workerCount: 4
|
|
364
382
|
},
|
|
365
|
-
charts
|
|
383
|
+
charts,
|
|
384
|
+
runDiff: buildDemoRunDiff()
|
|
366
385
|
};
|
|
367
386
|
}
|
|
368
387
|
var DEMO_BASE_TS, DEMO_LICENSE_INFO, DEMO_AUTH_TESTS, DEMO_CHECKOUT_TESTS, DEMO_CATALOG_TESTS;
|
|
@@ -6498,6 +6517,7 @@ var init_sections = __esm({
|
|
|
6498
6517
|
"slowTests",
|
|
6499
6518
|
"defectLog",
|
|
6500
6519
|
"briefBand",
|
|
6520
|
+
"runDiff",
|
|
6501
6521
|
// display modifiers
|
|
6502
6522
|
"passRate",
|
|
6503
6523
|
"fullEnvironment",
|
|
@@ -6521,6 +6541,7 @@ var init_sections = __esm({
|
|
|
6521
6541
|
slowTests: false,
|
|
6522
6542
|
defectLog: false,
|
|
6523
6543
|
briefBand: false,
|
|
6544
|
+
runDiff: true,
|
|
6524
6545
|
passRate: true,
|
|
6525
6546
|
fullEnvironment: false,
|
|
6526
6547
|
retries: true,
|
|
@@ -6542,6 +6563,7 @@ var init_sections = __esm({
|
|
|
6542
6563
|
slowTests: true,
|
|
6543
6564
|
defectLog: true,
|
|
6544
6565
|
briefBand: false,
|
|
6566
|
+
runDiff: true,
|
|
6545
6567
|
passRate: true,
|
|
6546
6568
|
fullEnvironment: true,
|
|
6547
6569
|
retries: true,
|
|
@@ -6568,6 +6590,7 @@ var init_sections = __esm({
|
|
|
6568
6590
|
slowTests: true,
|
|
6569
6591
|
defectLog: false,
|
|
6570
6592
|
briefBand: true,
|
|
6593
|
+
runDiff: true,
|
|
6571
6594
|
passRate: true,
|
|
6572
6595
|
fullEnvironment: false,
|
|
6573
6596
|
retries: false,
|
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Reporter, FullConfig, Suite, TestCase, TestResult, TestStep, FullResult } from '@playwright/test/reporter';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Canonical section catalog.
|
|
4
|
+
* Canonical section catalog. 15 block toggles (section present/absent) followed
|
|
5
5
|
* by 5 display modifiers (tweak a section that is on). Single source of truth —
|
|
6
6
|
* the Zod schema in src/config/schema.ts builds its flag shape from this array.
|
|
7
7
|
*/
|
|
8
|
-
declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "briefBand", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
|
|
8
|
+
declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "briefBand", "runDiff", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
|
|
9
9
|
type SectionKey = (typeof SECTION_KEYS)[number];
|
|
10
10
|
/** A partial map of section flags — used both as the flat baseline and per-template. */
|
|
11
11
|
type SectionFlags = Partial<Record<SectionKey, boolean>>;
|
|
@@ -406,12 +406,18 @@ interface ReporterOptions {
|
|
|
406
406
|
* every chosen template; per-template keys (`minimal` / `detailed` /
|
|
407
407
|
* `executive`) override the baseline for that template only.
|
|
408
408
|
*
|
|
409
|
+
* 20 keys total: 15 block toggles, 5 display modifiers.
|
|
410
|
+
*
|
|
409
411
|
* Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
|
|
410
412
|
* `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
|
|
411
|
-
* `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand
|
|
413
|
+
* `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`,
|
|
414
|
+
* `runDiff`.
|
|
412
415
|
* Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`,
|
|
413
416
|
* `stackTraces`.
|
|
414
417
|
*
|
|
418
|
+
* `runDiff` shows new failures, fixed tests, and still-failing tests against
|
|
419
|
+
* the previous run on the same branch; on by default in all three templates.
|
|
420
|
+
*
|
|
415
421
|
* `trend` renders only when `charts` is also on and history data exists (`showTrend` enabled). Sections gated on data
|
|
416
422
|
* (`failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog`) appear
|
|
417
423
|
* only when matching data exists — this option never changes data collection.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { Reporter, FullConfig, Suite, TestCase, TestResult, TestStep, FullResult } from '@playwright/test/reporter';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Canonical section catalog.
|
|
4
|
+
* Canonical section catalog. 15 block toggles (section present/absent) followed
|
|
5
5
|
* by 5 display modifiers (tweak a section that is on). Single source of truth —
|
|
6
6
|
* the Zod schema in src/config/schema.ts builds its flag shape from this array.
|
|
7
7
|
*/
|
|
8
|
-
declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "briefBand", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
|
|
8
|
+
declare const SECTION_KEYS: readonly ["coverPage", "analysisOneliner", "releaseGate", "summary", "charts", "trend", "requirementsMatrix", "ciEnvironment", "suiteBreakdown", "failureDeepDive", "failureAnalysis", "slowTests", "defectLog", "briefBand", "runDiff", "passRate", "fullEnvironment", "retries", "fullFailures", "stackTraces"];
|
|
9
9
|
type SectionKey = (typeof SECTION_KEYS)[number];
|
|
10
10
|
/** A partial map of section flags — used both as the flat baseline and per-template. */
|
|
11
11
|
type SectionFlags = Partial<Record<SectionKey, boolean>>;
|
|
@@ -406,12 +406,18 @@ interface ReporterOptions {
|
|
|
406
406
|
* every chosen template; per-template keys (`minimal` / `detailed` /
|
|
407
407
|
* `executive`) override the baseline for that template only.
|
|
408
408
|
*
|
|
409
|
+
* 20 keys total: 15 block toggles, 5 display modifiers.
|
|
410
|
+
*
|
|
409
411
|
* Block toggles: `coverPage`, `analysisOneliner`, `releaseGate`, `summary`,
|
|
410
412
|
* `charts`, `trend`, `requirementsMatrix`, `ciEnvironment`, `suiteBreakdown`,
|
|
411
|
-
* `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand
|
|
413
|
+
* `failureDeepDive`, `failureAnalysis`, `slowTests`, `defectLog`, `briefBand`,
|
|
414
|
+
* `runDiff`.
|
|
412
415
|
* Display modifiers: `passRate`, `fullEnvironment`, `retries`, `fullFailures`,
|
|
413
416
|
* `stackTraces`.
|
|
414
417
|
*
|
|
418
|
+
* `runDiff` shows new failures, fixed tests, and still-failing tests against
|
|
419
|
+
* the previous run on the same branch; on by default in all three templates.
|
|
420
|
+
*
|
|
415
421
|
* `trend` renders only when `charts` is also on and history data exists (`showTrend` enabled). Sections gated on data
|
|
416
422
|
* (`failureAnalysis`, `requirementsMatrix`, `slowTests`, `defectLog`) appear
|
|
417
423
|
* only when matching data exists — this option never changes data collection.
|
package/dist/index.js
CHANGED
|
@@ -10354,6 +10354,7 @@ var SECTION_KEYS = [
|
|
|
10354
10354
|
"slowTests",
|
|
10355
10355
|
"defectLog",
|
|
10356
10356
|
"briefBand",
|
|
10357
|
+
"runDiff",
|
|
10357
10358
|
// display modifiers
|
|
10358
10359
|
"passRate",
|
|
10359
10360
|
"fullEnvironment",
|
|
@@ -10377,6 +10378,7 @@ var SECTION_DEFAULTS = {
|
|
|
10377
10378
|
slowTests: false,
|
|
10378
10379
|
defectLog: false,
|
|
10379
10380
|
briefBand: false,
|
|
10381
|
+
runDiff: true,
|
|
10380
10382
|
passRate: true,
|
|
10381
10383
|
fullEnvironment: false,
|
|
10382
10384
|
retries: true,
|
|
@@ -10398,6 +10400,7 @@ var SECTION_DEFAULTS = {
|
|
|
10398
10400
|
slowTests: true,
|
|
10399
10401
|
defectLog: true,
|
|
10400
10402
|
briefBand: false,
|
|
10403
|
+
runDiff: true,
|
|
10401
10404
|
passRate: true,
|
|
10402
10405
|
fullEnvironment: true,
|
|
10403
10406
|
retries: true,
|
|
@@ -10424,6 +10427,7 @@ var SECTION_DEFAULTS = {
|
|
|
10424
10427
|
slowTests: true,
|
|
10425
10428
|
defectLog: false,
|
|
10426
10429
|
briefBand: true,
|
|
10430
|
+
runDiff: true,
|
|
10427
10431
|
passRate: true,
|
|
10428
10432
|
fullEnvironment: false,
|
|
10429
10433
|
retries: false,
|
|
@@ -13783,6 +13787,9 @@ function buildBranchHash(branch) {
|
|
|
13783
13787
|
async function appendRemoteHistory(opts) {
|
|
13784
13788
|
const numericEntry = { ...opts.entry };
|
|
13785
13789
|
delete numericEntry.flakyTests;
|
|
13790
|
+
delete numericEntry.failedTests;
|
|
13791
|
+
delete numericEntry.failedTestsTruncated;
|
|
13792
|
+
delete numericEntry.branch;
|
|
13786
13793
|
const controller = new AbortController();
|
|
13787
13794
|
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS2);
|
|
13788
13795
|
try {
|
|
@@ -13812,6 +13819,58 @@ async function appendRemoteHistory(opts) {
|
|
|
13812
13819
|
}
|
|
13813
13820
|
}
|
|
13814
13821
|
|
|
13822
|
+
// src/history/types.ts
|
|
13823
|
+
init_cjs_shims();
|
|
13824
|
+
var FAILED_TESTS_CAP = 300;
|
|
13825
|
+
|
|
13826
|
+
// src/history/RunDiff.ts
|
|
13827
|
+
init_cjs_shims();
|
|
13828
|
+
function splitCurrentFailing(currentFailing, baselineFailed) {
|
|
13829
|
+
const currentFailedIds = /* @__PURE__ */ new Set();
|
|
13830
|
+
const newFailures = [];
|
|
13831
|
+
const stillFailing = [];
|
|
13832
|
+
for (const item of currentFailing) {
|
|
13833
|
+
if (currentFailedIds.has(item.id)) continue;
|
|
13834
|
+
currentFailedIds.add(item.id);
|
|
13835
|
+
(baselineFailed.has(item.id) ? stillFailing : newFailures).push(item);
|
|
13836
|
+
}
|
|
13837
|
+
return { newFailures, stillFailing, currentFailedIds };
|
|
13838
|
+
}
|
|
13839
|
+
function splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses) {
|
|
13840
|
+
const fixed = [];
|
|
13841
|
+
const noLongerRun = [];
|
|
13842
|
+
for (const [id, title] of baselineFailed) {
|
|
13843
|
+
if (currentFailedIds.has(id)) continue;
|
|
13844
|
+
const status = currentStatuses.get(id);
|
|
13845
|
+
const bucket = status === "passed" || status === "flaky" ? fixed : noLongerRun;
|
|
13846
|
+
bucket.push({ id, title });
|
|
13847
|
+
}
|
|
13848
|
+
return { fixed, noLongerRun };
|
|
13849
|
+
}
|
|
13850
|
+
function computeRunDiff(args) {
|
|
13851
|
+
const { currentFailing, currentStatuses, priorEntries, branch } = args;
|
|
13852
|
+
const baseline = priorEntries.find((e) => e.branch === branch && Array.isArray(e.failedTests));
|
|
13853
|
+
if (!baseline) return null;
|
|
13854
|
+
const baselineFailed = /* @__PURE__ */ new Map();
|
|
13855
|
+
for (const item of baseline.failedTests) {
|
|
13856
|
+
if (!baselineFailed.has(item.id)) baselineFailed.set(item.id, item.title);
|
|
13857
|
+
}
|
|
13858
|
+
const { newFailures, stillFailing, currentFailedIds } = splitCurrentFailing(currentFailing, baselineFailed);
|
|
13859
|
+
const { fixed, noLongerRun } = splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses);
|
|
13860
|
+
for (const bucket of [newFailures, stillFailing, fixed, noLongerRun]) {
|
|
13861
|
+
bucket.sort((a, b) => a.title.localeCompare(b.title));
|
|
13862
|
+
}
|
|
13863
|
+
return {
|
|
13864
|
+
newFailures,
|
|
13865
|
+
stillFailing,
|
|
13866
|
+
fixed,
|
|
13867
|
+
noLongerRun,
|
|
13868
|
+
baselineRunId: baseline.runId,
|
|
13869
|
+
baselineTimestamp: baseline.timestamp,
|
|
13870
|
+
baselineTruncated: baseline.failedTestsTruncated === true
|
|
13871
|
+
};
|
|
13872
|
+
}
|
|
13873
|
+
|
|
13815
13874
|
// src/history/FlakinessAnalyser.ts
|
|
13816
13875
|
init_cjs_shims();
|
|
13817
13876
|
function computeTopN(entries, topN) {
|
|
@@ -13888,11 +13947,32 @@ function reportName(pdfPaths, reportTitle) {
|
|
|
13888
13947
|
if (pdfPaths.length > 0) return (0, import_path9.basename)(pdfPaths[0]);
|
|
13889
13948
|
return reportTitle || "ReportForge";
|
|
13890
13949
|
}
|
|
13950
|
+
function formatDiffLine(diff) {
|
|
13951
|
+
if (!diff) return null;
|
|
13952
|
+
const newFailures = diff.newFailures.length;
|
|
13953
|
+
const fixed = diff.fixed.length;
|
|
13954
|
+
const stillFailing = diff.stillFailing.length;
|
|
13955
|
+
const noLongerRun = diff.noLongerRun.length;
|
|
13956
|
+
const parts = [
|
|
13957
|
+
`${newFailures} new ${newFailures === 1 ? "failure" : "failures"}`,
|
|
13958
|
+
`${fixed} fixed`,
|
|
13959
|
+
`${stillFailing} still failing`
|
|
13960
|
+
];
|
|
13961
|
+
if (noLongerRun > 0) parts.push(`${noLongerRun} no longer run`);
|
|
13962
|
+
return parts.join(" \xB7 ");
|
|
13963
|
+
}
|
|
13891
13964
|
|
|
13892
13965
|
// src/notify/formatters/slack.ts
|
|
13893
|
-
function buildSlackPayload(stats, pdfPaths, reportTitle) {
|
|
13966
|
+
function buildSlackPayload(stats, pdfPaths, reportTitle, runDiff) {
|
|
13894
13967
|
const label = statusLabel(stats.verdict);
|
|
13895
13968
|
const name = reportName(pdfPaths, reportTitle);
|
|
13969
|
+
const diffLine = formatDiffLine(runDiff);
|
|
13970
|
+
const lines = [
|
|
13971
|
+
`*${label}* \xB7 ${stats.passRate}% passed`,
|
|
13972
|
+
countsLine(stats),
|
|
13973
|
+
...diffLine ? [diffLine] : [],
|
|
13974
|
+
`_${name}_`
|
|
13975
|
+
];
|
|
13896
13976
|
return {
|
|
13897
13977
|
text: `${label} \xB7 ${stats.passRate}% passed`,
|
|
13898
13978
|
blocks: [
|
|
@@ -13900,9 +13980,7 @@ function buildSlackPayload(stats, pdfPaths, reportTitle) {
|
|
|
13900
13980
|
type: "section",
|
|
13901
13981
|
text: {
|
|
13902
13982
|
type: "mrkdwn",
|
|
13903
|
-
text:
|
|
13904
|
-
${countsLine(stats)}
|
|
13905
|
-
_${name}_`
|
|
13983
|
+
text: lines.join("\n")
|
|
13906
13984
|
}
|
|
13907
13985
|
}
|
|
13908
13986
|
]
|
|
@@ -13927,9 +14005,37 @@ function buildTeamsSimpleMessage(text) {
|
|
|
13927
14005
|
]
|
|
13928
14006
|
};
|
|
13929
14007
|
}
|
|
13930
|
-
function buildTeamsPayload(stats, pdfPaths, reportTitle) {
|
|
14008
|
+
function buildTeamsPayload(stats, pdfPaths, reportTitle, runDiff) {
|
|
13931
14009
|
const label = statusLabel(stats.verdict);
|
|
13932
14010
|
const name = reportName(pdfPaths, reportTitle);
|
|
14011
|
+
const diffLine = formatDiffLine(runDiff);
|
|
14012
|
+
const body = [
|
|
14013
|
+
{
|
|
14014
|
+
type: "TextBlock",
|
|
14015
|
+
text: `${label} \xB7 ${stats.passRate}% passed`,
|
|
14016
|
+
weight: "bolder",
|
|
14017
|
+
size: "medium"
|
|
14018
|
+
},
|
|
14019
|
+
{
|
|
14020
|
+
type: "TextBlock",
|
|
14021
|
+
text: countsLine(stats),
|
|
14022
|
+
isSubtle: true
|
|
14023
|
+
},
|
|
14024
|
+
{
|
|
14025
|
+
type: "TextBlock",
|
|
14026
|
+
text: name,
|
|
14027
|
+
isSubtle: true,
|
|
14028
|
+
spacing: "none"
|
|
14029
|
+
}
|
|
14030
|
+
];
|
|
14031
|
+
if (diffLine) {
|
|
14032
|
+
body.push({
|
|
14033
|
+
type: "TextBlock",
|
|
14034
|
+
text: diffLine,
|
|
14035
|
+
isSubtle: true,
|
|
14036
|
+
spacing: "none"
|
|
14037
|
+
});
|
|
14038
|
+
}
|
|
13933
14039
|
return {
|
|
13934
14040
|
type: "message",
|
|
13935
14041
|
attachments: [
|
|
@@ -13939,25 +14045,7 @@ function buildTeamsPayload(stats, pdfPaths, reportTitle) {
|
|
|
13939
14045
|
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
13940
14046
|
type: "AdaptiveCard",
|
|
13941
14047
|
version: "1.4",
|
|
13942
|
-
body
|
|
13943
|
-
{
|
|
13944
|
-
type: "TextBlock",
|
|
13945
|
-
text: `${label} \xB7 ${stats.passRate}% passed`,
|
|
13946
|
-
weight: "bolder",
|
|
13947
|
-
size: "medium"
|
|
13948
|
-
},
|
|
13949
|
-
{
|
|
13950
|
-
type: "TextBlock",
|
|
13951
|
-
text: countsLine(stats),
|
|
13952
|
-
isSubtle: true
|
|
13953
|
-
},
|
|
13954
|
-
{
|
|
13955
|
-
type: "TextBlock",
|
|
13956
|
-
text: name,
|
|
13957
|
-
isSubtle: true,
|
|
13958
|
-
spacing: "none"
|
|
13959
|
-
}
|
|
13960
|
-
]
|
|
14048
|
+
body
|
|
13961
14049
|
}
|
|
13962
14050
|
}
|
|
13963
14051
|
]
|
|
@@ -13966,16 +14054,17 @@ function buildTeamsPayload(stats, pdfPaths, reportTitle) {
|
|
|
13966
14054
|
|
|
13967
14055
|
// src/notify/formatters/discord.ts
|
|
13968
14056
|
init_cjs_shims();
|
|
13969
|
-
function buildDiscordPayload(stats, pdfPaths, reportTitle) {
|
|
14057
|
+
function buildDiscordPayload(stats, pdfPaths, reportTitle, runDiff) {
|
|
13970
14058
|
const label = statusLabel(stats.verdict);
|
|
13971
14059
|
const name = reportName(pdfPaths, reportTitle);
|
|
13972
14060
|
const color = stats.verdict === "passed" ? 3066993 : 15158332;
|
|
14061
|
+
const diffLine = formatDiffLine(runDiff);
|
|
14062
|
+
const descriptionLines = [countsLine(stats), ...diffLine ? [diffLine] : [], name];
|
|
13973
14063
|
return {
|
|
13974
14064
|
embeds: [
|
|
13975
14065
|
{
|
|
13976
14066
|
title: `${label} \xB7 ${stats.passRate}% passed`,
|
|
13977
|
-
description:
|
|
13978
|
-
${name}`,
|
|
14067
|
+
description: descriptionLines.join("\n"),
|
|
13979
14068
|
color
|
|
13980
14069
|
}
|
|
13981
14070
|
]
|
|
@@ -13989,10 +14078,16 @@ var import_path10 = require("path");
|
|
|
13989
14078
|
|
|
13990
14079
|
// src/notify/formatters/email.ts
|
|
13991
14080
|
init_cjs_shims();
|
|
13992
|
-
function buildEmailContent(stats, pdfPaths, reportTitle) {
|
|
14081
|
+
function buildEmailContent(stats, pdfPaths, reportTitle, runDiff) {
|
|
13993
14082
|
const label = statusLabel(stats.verdict);
|
|
13994
|
-
const subject = `[ReportForge] ${label} \
|
|
14083
|
+
const subject = `[ReportForge] ${label} \xB7 ${stats.passRate}% passed`;
|
|
13995
14084
|
const name = reportName(pdfPaths, reportTitle);
|
|
14085
|
+
const diffLine = formatDiffLine(runDiff);
|
|
14086
|
+
const diffRow = diffLine ? `
|
|
14087
|
+
<tr>
|
|
14088
|
+
<td style="padding:8px 12px;border-top:1px solid #e5e5e5">Since last run</td>
|
|
14089
|
+
<td style="padding:8px 12px;border-top:1px solid #e5e5e5;text-align:right">${escHtml(diffLine)}</td>
|
|
14090
|
+
</tr>` : "";
|
|
13996
14091
|
const html = `<!DOCTYPE html>
|
|
13997
14092
|
<html lang="en">
|
|
13998
14093
|
<head><meta charset="utf-8"><title>${escHtml(subject)}</title></head>
|
|
@@ -14027,7 +14122,7 @@ function buildEmailContent(stats, pdfPaths, reportTitle) {
|
|
|
14027
14122
|
<tr style="background:#f9f9f9">
|
|
14028
14123
|
<td style="padding:8px 12px;border-top:1px solid #e5e5e5">Duration</td>
|
|
14029
14124
|
<td style="padding:8px 12px;border-top:1px solid #e5e5e5;text-align:right">${escHtml(countsLine(stats).split(" \xB7 ").pop() ?? "")}</td>
|
|
14030
|
-
</tr
|
|
14125
|
+
</tr>${diffRow}
|
|
14031
14126
|
</table>
|
|
14032
14127
|
<p style="margin:24px 0 0;color:#888;font-size:12px">Sent by ReportForge \xB7 <a href="https://reportforge.org" style="color:#888">reportforge.org</a></p>
|
|
14033
14128
|
</body>
|
|
@@ -14052,7 +14147,8 @@ var EmailSender = class {
|
|
|
14052
14147
|
const { subject, html } = buildEmailContent(
|
|
14053
14148
|
reportData.stats,
|
|
14054
14149
|
pdfPaths,
|
|
14055
|
-
reportData.meta.title ?? ""
|
|
14150
|
+
reportData.meta.title ?? "",
|
|
14151
|
+
reportData.runDiff
|
|
14056
14152
|
);
|
|
14057
14153
|
const body = { from, to: config.to, subject, html };
|
|
14058
14154
|
if (config.attachPdf && pdfPaths.length > 0) {
|
|
@@ -14116,7 +14212,7 @@ var NotificationSender = class {
|
|
|
14116
14212
|
result.skipped.push(name);
|
|
14117
14213
|
return Promise.resolve();
|
|
14118
14214
|
}
|
|
14119
|
-
const payload = FORMATTERS[name](reportData.stats, pdfPaths, reportTitle);
|
|
14215
|
+
const payload = FORMATTERS[name](reportData.stats, pdfPaths, reportTitle, reportData.runDiff);
|
|
14120
14216
|
if (name === "discord" && notifyConfig.discord?.attachPdf && pdfPaths.length > 0) {
|
|
14121
14217
|
return this.postDiscordWithPdf(channel.url, payload, pdfPaths[0], result);
|
|
14122
14218
|
}
|
|
@@ -14859,7 +14955,7 @@ var PdfReporter = class {
|
|
|
14859
14955
|
if (this.options.redact.enabled) {
|
|
14860
14956
|
this.redactor = new Redactor(this.options.redact);
|
|
14861
14957
|
}
|
|
14862
|
-
this.version = true ? "0.
|
|
14958
|
+
this.version = true ? "0.26.1" : "0.x";
|
|
14863
14959
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
14864
14960
|
this.licenseClient = new LicenseClient({
|
|
14865
14961
|
licenseKey: this.options.licenseKey,
|
|
@@ -14988,6 +15084,7 @@ var PdfReporter = class {
|
|
|
14988
15084
|
const flakyTests = collectFlakyTests(collected.projects).map(
|
|
14989
15085
|
(t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
|
|
14990
15086
|
);
|
|
15087
|
+
const { redactedFailing, ...failedTestsFields } = buildFailedTestsFields(collected.projects, redactor);
|
|
14991
15088
|
const entry = {
|
|
14992
15089
|
runId: `${(/* @__PURE__ */ new Date()).toISOString()}-${suffix}`,
|
|
14993
15090
|
timestamp: Date.now(),
|
|
@@ -14998,9 +15095,12 @@ var PdfReporter = class {
|
|
|
14998
15095
|
flaky: collected.stats.flaky,
|
|
14999
15096
|
duration: collected.stats.duration,
|
|
15000
15097
|
verdict: deriveVerdict(collected.stats),
|
|
15001
|
-
flakyTests
|
|
15098
|
+
flakyTests,
|
|
15099
|
+
...failedTestsFields,
|
|
15100
|
+
branch: collected.environment.branch
|
|
15002
15101
|
};
|
|
15003
15102
|
const localEntries = hm.append(entry, this.options.historySize);
|
|
15103
|
+
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch);
|
|
15004
15104
|
let trendEntries = localEntries;
|
|
15005
15105
|
if (this.options.remoteHistory && licenseInfo.jwt) {
|
|
15006
15106
|
const serverUrl = this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL2;
|
|
@@ -15040,7 +15140,7 @@ var PdfReporter = class {
|
|
|
15040
15140
|
}
|
|
15041
15141
|
}
|
|
15042
15142
|
_buildReportData(collected, licenseInfo) {
|
|
15043
|
-
const { projects, stats, failures, environment, charts } = collected;
|
|
15143
|
+
const { projects, stats, failures, environment, charts, runDiff } = collected;
|
|
15044
15144
|
const projectName = this.options.projectName ?? this.resolveProjectName();
|
|
15045
15145
|
const firstTemplate = this.options.templatePath?.length ? "detailed" : Array.isArray(this.options.template) ? this.options.template[0] : this.options.template;
|
|
15046
15146
|
return {
|
|
@@ -15060,7 +15160,8 @@ var PdfReporter = class {
|
|
|
15060
15160
|
projects,
|
|
15061
15161
|
failures,
|
|
15062
15162
|
environment,
|
|
15063
|
-
charts
|
|
15163
|
+
charts,
|
|
15164
|
+
runDiff
|
|
15064
15165
|
};
|
|
15065
15166
|
}
|
|
15066
15167
|
printsToStdio() {
|
|
@@ -15264,6 +15365,47 @@ function collectFlakyTests(projects) {
|
|
|
15264
15365
|
}
|
|
15265
15366
|
return result;
|
|
15266
15367
|
}
|
|
15368
|
+
function collectFailedTests(projects) {
|
|
15369
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15370
|
+
const result = [];
|
|
15371
|
+
for (const project of projects) {
|
|
15372
|
+
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
15373
|
+
for (const test of suite.tests) {
|
|
15374
|
+
if ((test.status === "failed" || test.status === "timedOut") && !seen.has(test.id)) {
|
|
15375
|
+
seen.add(test.id);
|
|
15376
|
+
result.push({ id: test.id, title: test.fullTitle });
|
|
15377
|
+
}
|
|
15378
|
+
}
|
|
15379
|
+
}
|
|
15380
|
+
}
|
|
15381
|
+
return result;
|
|
15382
|
+
}
|
|
15383
|
+
function collectCurrentStatuses(projects) {
|
|
15384
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
15385
|
+
for (const project of projects) {
|
|
15386
|
+
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
15387
|
+
for (const test of suite.tests) {
|
|
15388
|
+
statuses.set(test.id, test.status);
|
|
15389
|
+
}
|
|
15390
|
+
}
|
|
15391
|
+
}
|
|
15392
|
+
return statuses;
|
|
15393
|
+
}
|
|
15394
|
+
function buildFailedTestsFields(projects, redactor) {
|
|
15395
|
+
const redactedFailing = collectFailedTests(projects).map(
|
|
15396
|
+
(t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
|
|
15397
|
+
);
|
|
15398
|
+
const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
|
|
15399
|
+
return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
|
|
15400
|
+
}
|
|
15401
|
+
function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
|
|
15402
|
+
return computeRunDiff({
|
|
15403
|
+
currentFailing,
|
|
15404
|
+
currentStatuses: collectCurrentStatuses(projects),
|
|
15405
|
+
priorEntries,
|
|
15406
|
+
branch
|
|
15407
|
+
}) ?? void 0;
|
|
15408
|
+
}
|
|
15267
15409
|
|
|
15268
15410
|
// src/index.ts
|
|
15269
15411
|
function defineReporterConfig(options) {
|
package/dist/index.mjs
CHANGED
|
@@ -10355,6 +10355,7 @@ var SECTION_KEYS = [
|
|
|
10355
10355
|
"slowTests",
|
|
10356
10356
|
"defectLog",
|
|
10357
10357
|
"briefBand",
|
|
10358
|
+
"runDiff",
|
|
10358
10359
|
// display modifiers
|
|
10359
10360
|
"passRate",
|
|
10360
10361
|
"fullEnvironment",
|
|
@@ -10378,6 +10379,7 @@ var SECTION_DEFAULTS = {
|
|
|
10378
10379
|
slowTests: false,
|
|
10379
10380
|
defectLog: false,
|
|
10380
10381
|
briefBand: false,
|
|
10382
|
+
runDiff: true,
|
|
10381
10383
|
passRate: true,
|
|
10382
10384
|
fullEnvironment: false,
|
|
10383
10385
|
retries: true,
|
|
@@ -10399,6 +10401,7 @@ var SECTION_DEFAULTS = {
|
|
|
10399
10401
|
slowTests: true,
|
|
10400
10402
|
defectLog: true,
|
|
10401
10403
|
briefBand: false,
|
|
10404
|
+
runDiff: true,
|
|
10402
10405
|
passRate: true,
|
|
10403
10406
|
fullEnvironment: true,
|
|
10404
10407
|
retries: true,
|
|
@@ -10425,6 +10428,7 @@ var SECTION_DEFAULTS = {
|
|
|
10425
10428
|
slowTests: true,
|
|
10426
10429
|
defectLog: false,
|
|
10427
10430
|
briefBand: true,
|
|
10431
|
+
runDiff: true,
|
|
10428
10432
|
passRate: true,
|
|
10429
10433
|
fullEnvironment: false,
|
|
10430
10434
|
retries: false,
|
|
@@ -13784,6 +13788,9 @@ function buildBranchHash(branch) {
|
|
|
13784
13788
|
async function appendRemoteHistory(opts) {
|
|
13785
13789
|
const numericEntry = { ...opts.entry };
|
|
13786
13790
|
delete numericEntry.flakyTests;
|
|
13791
|
+
delete numericEntry.failedTests;
|
|
13792
|
+
delete numericEntry.failedTestsTruncated;
|
|
13793
|
+
delete numericEntry.branch;
|
|
13787
13794
|
const controller = new AbortController();
|
|
13788
13795
|
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS2);
|
|
13789
13796
|
try {
|
|
@@ -13813,6 +13820,58 @@ async function appendRemoteHistory(opts) {
|
|
|
13813
13820
|
}
|
|
13814
13821
|
}
|
|
13815
13822
|
|
|
13823
|
+
// src/history/types.ts
|
|
13824
|
+
init_esm_shims();
|
|
13825
|
+
var FAILED_TESTS_CAP = 300;
|
|
13826
|
+
|
|
13827
|
+
// src/history/RunDiff.ts
|
|
13828
|
+
init_esm_shims();
|
|
13829
|
+
function splitCurrentFailing(currentFailing, baselineFailed) {
|
|
13830
|
+
const currentFailedIds = /* @__PURE__ */ new Set();
|
|
13831
|
+
const newFailures = [];
|
|
13832
|
+
const stillFailing = [];
|
|
13833
|
+
for (const item of currentFailing) {
|
|
13834
|
+
if (currentFailedIds.has(item.id)) continue;
|
|
13835
|
+
currentFailedIds.add(item.id);
|
|
13836
|
+
(baselineFailed.has(item.id) ? stillFailing : newFailures).push(item);
|
|
13837
|
+
}
|
|
13838
|
+
return { newFailures, stillFailing, currentFailedIds };
|
|
13839
|
+
}
|
|
13840
|
+
function splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses) {
|
|
13841
|
+
const fixed = [];
|
|
13842
|
+
const noLongerRun = [];
|
|
13843
|
+
for (const [id, title] of baselineFailed) {
|
|
13844
|
+
if (currentFailedIds.has(id)) continue;
|
|
13845
|
+
const status = currentStatuses.get(id);
|
|
13846
|
+
const bucket = status === "passed" || status === "flaky" ? fixed : noLongerRun;
|
|
13847
|
+
bucket.push({ id, title });
|
|
13848
|
+
}
|
|
13849
|
+
return { fixed, noLongerRun };
|
|
13850
|
+
}
|
|
13851
|
+
function computeRunDiff(args) {
|
|
13852
|
+
const { currentFailing, currentStatuses, priorEntries, branch } = args;
|
|
13853
|
+
const baseline = priorEntries.find((e) => e.branch === branch && Array.isArray(e.failedTests));
|
|
13854
|
+
if (!baseline) return null;
|
|
13855
|
+
const baselineFailed = /* @__PURE__ */ new Map();
|
|
13856
|
+
for (const item of baseline.failedTests) {
|
|
13857
|
+
if (!baselineFailed.has(item.id)) baselineFailed.set(item.id, item.title);
|
|
13858
|
+
}
|
|
13859
|
+
const { newFailures, stillFailing, currentFailedIds } = splitCurrentFailing(currentFailing, baselineFailed);
|
|
13860
|
+
const { fixed, noLongerRun } = splitBaselineResolved(baselineFailed, currentFailedIds, currentStatuses);
|
|
13861
|
+
for (const bucket of [newFailures, stillFailing, fixed, noLongerRun]) {
|
|
13862
|
+
bucket.sort((a, b) => a.title.localeCompare(b.title));
|
|
13863
|
+
}
|
|
13864
|
+
return {
|
|
13865
|
+
newFailures,
|
|
13866
|
+
stillFailing,
|
|
13867
|
+
fixed,
|
|
13868
|
+
noLongerRun,
|
|
13869
|
+
baselineRunId: baseline.runId,
|
|
13870
|
+
baselineTimestamp: baseline.timestamp,
|
|
13871
|
+
baselineTruncated: baseline.failedTestsTruncated === true
|
|
13872
|
+
};
|
|
13873
|
+
}
|
|
13874
|
+
|
|
13816
13875
|
// src/history/FlakinessAnalyser.ts
|
|
13817
13876
|
init_esm_shims();
|
|
13818
13877
|
function computeTopN(entries, topN) {
|
|
@@ -13889,11 +13948,32 @@ function reportName(pdfPaths, reportTitle) {
|
|
|
13889
13948
|
if (pdfPaths.length > 0) return basename(pdfPaths[0]);
|
|
13890
13949
|
return reportTitle || "ReportForge";
|
|
13891
13950
|
}
|
|
13951
|
+
function formatDiffLine(diff) {
|
|
13952
|
+
if (!diff) return null;
|
|
13953
|
+
const newFailures = diff.newFailures.length;
|
|
13954
|
+
const fixed = diff.fixed.length;
|
|
13955
|
+
const stillFailing = diff.stillFailing.length;
|
|
13956
|
+
const noLongerRun = diff.noLongerRun.length;
|
|
13957
|
+
const parts = [
|
|
13958
|
+
`${newFailures} new ${newFailures === 1 ? "failure" : "failures"}`,
|
|
13959
|
+
`${fixed} fixed`,
|
|
13960
|
+
`${stillFailing} still failing`
|
|
13961
|
+
];
|
|
13962
|
+
if (noLongerRun > 0) parts.push(`${noLongerRun} no longer run`);
|
|
13963
|
+
return parts.join(" \xB7 ");
|
|
13964
|
+
}
|
|
13892
13965
|
|
|
13893
13966
|
// src/notify/formatters/slack.ts
|
|
13894
|
-
function buildSlackPayload(stats, pdfPaths, reportTitle) {
|
|
13967
|
+
function buildSlackPayload(stats, pdfPaths, reportTitle, runDiff) {
|
|
13895
13968
|
const label = statusLabel(stats.verdict);
|
|
13896
13969
|
const name = reportName(pdfPaths, reportTitle);
|
|
13970
|
+
const diffLine = formatDiffLine(runDiff);
|
|
13971
|
+
const lines = [
|
|
13972
|
+
`*${label}* \xB7 ${stats.passRate}% passed`,
|
|
13973
|
+
countsLine(stats),
|
|
13974
|
+
...diffLine ? [diffLine] : [],
|
|
13975
|
+
`_${name}_`
|
|
13976
|
+
];
|
|
13897
13977
|
return {
|
|
13898
13978
|
text: `${label} \xB7 ${stats.passRate}% passed`,
|
|
13899
13979
|
blocks: [
|
|
@@ -13901,9 +13981,7 @@ function buildSlackPayload(stats, pdfPaths, reportTitle) {
|
|
|
13901
13981
|
type: "section",
|
|
13902
13982
|
text: {
|
|
13903
13983
|
type: "mrkdwn",
|
|
13904
|
-
text:
|
|
13905
|
-
${countsLine(stats)}
|
|
13906
|
-
_${name}_`
|
|
13984
|
+
text: lines.join("\n")
|
|
13907
13985
|
}
|
|
13908
13986
|
}
|
|
13909
13987
|
]
|
|
@@ -13928,9 +14006,37 @@ function buildTeamsSimpleMessage(text) {
|
|
|
13928
14006
|
]
|
|
13929
14007
|
};
|
|
13930
14008
|
}
|
|
13931
|
-
function buildTeamsPayload(stats, pdfPaths, reportTitle) {
|
|
14009
|
+
function buildTeamsPayload(stats, pdfPaths, reportTitle, runDiff) {
|
|
13932
14010
|
const label = statusLabel(stats.verdict);
|
|
13933
14011
|
const name = reportName(pdfPaths, reportTitle);
|
|
14012
|
+
const diffLine = formatDiffLine(runDiff);
|
|
14013
|
+
const body = [
|
|
14014
|
+
{
|
|
14015
|
+
type: "TextBlock",
|
|
14016
|
+
text: `${label} \xB7 ${stats.passRate}% passed`,
|
|
14017
|
+
weight: "bolder",
|
|
14018
|
+
size: "medium"
|
|
14019
|
+
},
|
|
14020
|
+
{
|
|
14021
|
+
type: "TextBlock",
|
|
14022
|
+
text: countsLine(stats),
|
|
14023
|
+
isSubtle: true
|
|
14024
|
+
},
|
|
14025
|
+
{
|
|
14026
|
+
type: "TextBlock",
|
|
14027
|
+
text: name,
|
|
14028
|
+
isSubtle: true,
|
|
14029
|
+
spacing: "none"
|
|
14030
|
+
}
|
|
14031
|
+
];
|
|
14032
|
+
if (diffLine) {
|
|
14033
|
+
body.push({
|
|
14034
|
+
type: "TextBlock",
|
|
14035
|
+
text: diffLine,
|
|
14036
|
+
isSubtle: true,
|
|
14037
|
+
spacing: "none"
|
|
14038
|
+
});
|
|
14039
|
+
}
|
|
13934
14040
|
return {
|
|
13935
14041
|
type: "message",
|
|
13936
14042
|
attachments: [
|
|
@@ -13940,25 +14046,7 @@ function buildTeamsPayload(stats, pdfPaths, reportTitle) {
|
|
|
13940
14046
|
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
13941
14047
|
type: "AdaptiveCard",
|
|
13942
14048
|
version: "1.4",
|
|
13943
|
-
body
|
|
13944
|
-
{
|
|
13945
|
-
type: "TextBlock",
|
|
13946
|
-
text: `${label} \xB7 ${stats.passRate}% passed`,
|
|
13947
|
-
weight: "bolder",
|
|
13948
|
-
size: "medium"
|
|
13949
|
-
},
|
|
13950
|
-
{
|
|
13951
|
-
type: "TextBlock",
|
|
13952
|
-
text: countsLine(stats),
|
|
13953
|
-
isSubtle: true
|
|
13954
|
-
},
|
|
13955
|
-
{
|
|
13956
|
-
type: "TextBlock",
|
|
13957
|
-
text: name,
|
|
13958
|
-
isSubtle: true,
|
|
13959
|
-
spacing: "none"
|
|
13960
|
-
}
|
|
13961
|
-
]
|
|
14049
|
+
body
|
|
13962
14050
|
}
|
|
13963
14051
|
}
|
|
13964
14052
|
]
|
|
@@ -13967,16 +14055,17 @@ function buildTeamsPayload(stats, pdfPaths, reportTitle) {
|
|
|
13967
14055
|
|
|
13968
14056
|
// src/notify/formatters/discord.ts
|
|
13969
14057
|
init_esm_shims();
|
|
13970
|
-
function buildDiscordPayload(stats, pdfPaths, reportTitle) {
|
|
14058
|
+
function buildDiscordPayload(stats, pdfPaths, reportTitle, runDiff) {
|
|
13971
14059
|
const label = statusLabel(stats.verdict);
|
|
13972
14060
|
const name = reportName(pdfPaths, reportTitle);
|
|
13973
14061
|
const color = stats.verdict === "passed" ? 3066993 : 15158332;
|
|
14062
|
+
const diffLine = formatDiffLine(runDiff);
|
|
14063
|
+
const descriptionLines = [countsLine(stats), ...diffLine ? [diffLine] : [], name];
|
|
13974
14064
|
return {
|
|
13975
14065
|
embeds: [
|
|
13976
14066
|
{
|
|
13977
14067
|
title: `${label} \xB7 ${stats.passRate}% passed`,
|
|
13978
|
-
description:
|
|
13979
|
-
${name}`,
|
|
14068
|
+
description: descriptionLines.join("\n"),
|
|
13980
14069
|
color
|
|
13981
14070
|
}
|
|
13982
14071
|
]
|
|
@@ -13990,10 +14079,16 @@ import { basename as basename2 } from "path";
|
|
|
13990
14079
|
|
|
13991
14080
|
// src/notify/formatters/email.ts
|
|
13992
14081
|
init_esm_shims();
|
|
13993
|
-
function buildEmailContent(stats, pdfPaths, reportTitle) {
|
|
14082
|
+
function buildEmailContent(stats, pdfPaths, reportTitle, runDiff) {
|
|
13994
14083
|
const label = statusLabel(stats.verdict);
|
|
13995
|
-
const subject = `[ReportForge] ${label} \
|
|
14084
|
+
const subject = `[ReportForge] ${label} \xB7 ${stats.passRate}% passed`;
|
|
13996
14085
|
const name = reportName(pdfPaths, reportTitle);
|
|
14086
|
+
const diffLine = formatDiffLine(runDiff);
|
|
14087
|
+
const diffRow = diffLine ? `
|
|
14088
|
+
<tr>
|
|
14089
|
+
<td style="padding:8px 12px;border-top:1px solid #e5e5e5">Since last run</td>
|
|
14090
|
+
<td style="padding:8px 12px;border-top:1px solid #e5e5e5;text-align:right">${escHtml(diffLine)}</td>
|
|
14091
|
+
</tr>` : "";
|
|
13997
14092
|
const html = `<!DOCTYPE html>
|
|
13998
14093
|
<html lang="en">
|
|
13999
14094
|
<head><meta charset="utf-8"><title>${escHtml(subject)}</title></head>
|
|
@@ -14028,7 +14123,7 @@ function buildEmailContent(stats, pdfPaths, reportTitle) {
|
|
|
14028
14123
|
<tr style="background:#f9f9f9">
|
|
14029
14124
|
<td style="padding:8px 12px;border-top:1px solid #e5e5e5">Duration</td>
|
|
14030
14125
|
<td style="padding:8px 12px;border-top:1px solid #e5e5e5;text-align:right">${escHtml(countsLine(stats).split(" \xB7 ").pop() ?? "")}</td>
|
|
14031
|
-
</tr
|
|
14126
|
+
</tr>${diffRow}
|
|
14032
14127
|
</table>
|
|
14033
14128
|
<p style="margin:24px 0 0;color:#888;font-size:12px">Sent by ReportForge \xB7 <a href="https://reportforge.org" style="color:#888">reportforge.org</a></p>
|
|
14034
14129
|
</body>
|
|
@@ -14053,7 +14148,8 @@ var EmailSender = class {
|
|
|
14053
14148
|
const { subject, html } = buildEmailContent(
|
|
14054
14149
|
reportData.stats,
|
|
14055
14150
|
pdfPaths,
|
|
14056
|
-
reportData.meta.title ?? ""
|
|
14151
|
+
reportData.meta.title ?? "",
|
|
14152
|
+
reportData.runDiff
|
|
14057
14153
|
);
|
|
14058
14154
|
const body = { from, to: config.to, subject, html };
|
|
14059
14155
|
if (config.attachPdf && pdfPaths.length > 0) {
|
|
@@ -14117,7 +14213,7 @@ var NotificationSender = class {
|
|
|
14117
14213
|
result.skipped.push(name);
|
|
14118
14214
|
return Promise.resolve();
|
|
14119
14215
|
}
|
|
14120
|
-
const payload = FORMATTERS[name](reportData.stats, pdfPaths, reportTitle);
|
|
14216
|
+
const payload = FORMATTERS[name](reportData.stats, pdfPaths, reportTitle, reportData.runDiff);
|
|
14121
14217
|
if (name === "discord" && notifyConfig.discord?.attachPdf && pdfPaths.length > 0) {
|
|
14122
14218
|
return this.postDiscordWithPdf(channel.url, payload, pdfPaths[0], result);
|
|
14123
14219
|
}
|
|
@@ -14860,7 +14956,7 @@ var PdfReporter = class {
|
|
|
14860
14956
|
if (this.options.redact.enabled) {
|
|
14861
14957
|
this.redactor = new Redactor(this.options.redact);
|
|
14862
14958
|
}
|
|
14863
|
-
this.version = true ? "0.
|
|
14959
|
+
this.version = true ? "0.26.1" : "0.x";
|
|
14864
14960
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
14865
14961
|
this.licenseClient = new LicenseClient({
|
|
14866
14962
|
licenseKey: this.options.licenseKey,
|
|
@@ -14989,6 +15085,7 @@ var PdfReporter = class {
|
|
|
14989
15085
|
const flakyTests = collectFlakyTests(collected.projects).map(
|
|
14990
15086
|
(t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
|
|
14991
15087
|
);
|
|
15088
|
+
const { redactedFailing, ...failedTestsFields } = buildFailedTestsFields(collected.projects, redactor);
|
|
14992
15089
|
const entry = {
|
|
14993
15090
|
runId: `${(/* @__PURE__ */ new Date()).toISOString()}-${suffix}`,
|
|
14994
15091
|
timestamp: Date.now(),
|
|
@@ -14999,9 +15096,12 @@ var PdfReporter = class {
|
|
|
14999
15096
|
flaky: collected.stats.flaky,
|
|
15000
15097
|
duration: collected.stats.duration,
|
|
15001
15098
|
verdict: deriveVerdict(collected.stats),
|
|
15002
|
-
flakyTests
|
|
15099
|
+
flakyTests,
|
|
15100
|
+
...failedTestsFields,
|
|
15101
|
+
branch: collected.environment.branch
|
|
15003
15102
|
};
|
|
15004
15103
|
const localEntries = hm.append(entry, this.options.historySize);
|
|
15104
|
+
collected.runDiff = resolveRunDiff(collected.projects, redactedFailing, localEntries.slice(1), collected.environment.branch);
|
|
15005
15105
|
let trendEntries = localEntries;
|
|
15006
15106
|
if (this.options.remoteHistory && licenseInfo.jwt) {
|
|
15007
15107
|
const serverUrl = this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL2;
|
|
@@ -15041,7 +15141,7 @@ var PdfReporter = class {
|
|
|
15041
15141
|
}
|
|
15042
15142
|
}
|
|
15043
15143
|
_buildReportData(collected, licenseInfo) {
|
|
15044
|
-
const { projects, stats, failures, environment, charts } = collected;
|
|
15144
|
+
const { projects, stats, failures, environment, charts, runDiff } = collected;
|
|
15045
15145
|
const projectName = this.options.projectName ?? this.resolveProjectName();
|
|
15046
15146
|
const firstTemplate = this.options.templatePath?.length ? "detailed" : Array.isArray(this.options.template) ? this.options.template[0] : this.options.template;
|
|
15047
15147
|
return {
|
|
@@ -15061,7 +15161,8 @@ var PdfReporter = class {
|
|
|
15061
15161
|
projects,
|
|
15062
15162
|
failures,
|
|
15063
15163
|
environment,
|
|
15064
|
-
charts
|
|
15164
|
+
charts,
|
|
15165
|
+
runDiff
|
|
15065
15166
|
};
|
|
15066
15167
|
}
|
|
15067
15168
|
printsToStdio() {
|
|
@@ -15265,6 +15366,47 @@ function collectFlakyTests(projects) {
|
|
|
15265
15366
|
}
|
|
15266
15367
|
return result;
|
|
15267
15368
|
}
|
|
15369
|
+
function collectFailedTests(projects) {
|
|
15370
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15371
|
+
const result = [];
|
|
15372
|
+
for (const project of projects) {
|
|
15373
|
+
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
15374
|
+
for (const test of suite.tests) {
|
|
15375
|
+
if ((test.status === "failed" || test.status === "timedOut") && !seen.has(test.id)) {
|
|
15376
|
+
seen.add(test.id);
|
|
15377
|
+
result.push({ id: test.id, title: test.fullTitle });
|
|
15378
|
+
}
|
|
15379
|
+
}
|
|
15380
|
+
}
|
|
15381
|
+
}
|
|
15382
|
+
return result;
|
|
15383
|
+
}
|
|
15384
|
+
function collectCurrentStatuses(projects) {
|
|
15385
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
15386
|
+
for (const project of projects) {
|
|
15387
|
+
for (const suite of flattenSuiteNodes(project.suites)) {
|
|
15388
|
+
for (const test of suite.tests) {
|
|
15389
|
+
statuses.set(test.id, test.status);
|
|
15390
|
+
}
|
|
15391
|
+
}
|
|
15392
|
+
}
|
|
15393
|
+
return statuses;
|
|
15394
|
+
}
|
|
15395
|
+
function buildFailedTestsFields(projects, redactor) {
|
|
15396
|
+
const redactedFailing = collectFailedTests(projects).map(
|
|
15397
|
+
(t) => redactor ? { ...t, title: redactor.redactText(t.title) } : t
|
|
15398
|
+
);
|
|
15399
|
+
const failedTests = redactedFailing.slice(0, FAILED_TESTS_CAP);
|
|
15400
|
+
return redactedFailing.length > FAILED_TESTS_CAP ? { redactedFailing, failedTests, failedTestsTruncated: true } : { redactedFailing, failedTests };
|
|
15401
|
+
}
|
|
15402
|
+
function resolveRunDiff(projects, currentFailing, priorEntries, branch) {
|
|
15403
|
+
return computeRunDiff({
|
|
15404
|
+
currentFailing,
|
|
15405
|
+
currentStatuses: collectCurrentStatuses(projects),
|
|
15406
|
+
priorEntries,
|
|
15407
|
+
branch
|
|
15408
|
+
}) ?? void 0;
|
|
15409
|
+
}
|
|
15268
15410
|
|
|
15269
15411
|
// src/index.ts
|
|
15270
15412
|
function defineReporterConfig(options) {
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
|
|
25
25
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend) }}{{/if}}
|
|
26
26
|
|
|
27
|
+
{{#if sections.runDiff}}{{> run-diff variant='full'}}{{/if}}
|
|
28
|
+
|
|
27
29
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
28
30
|
|
|
29
31
|
{{#if sections.ciEnvironment}}{{> ci-environment options=(obj showFullEnvironment=sections.fullEnvironment) }}{{/if}}
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
{{#if sections.briefBand}}{{> brief-band}}{{/if}}
|
|
22
22
|
|
|
23
|
+
{{#if sections.runDiff}}{{> run-diff variant='compact'}}{{/if}}
|
|
24
|
+
|
|
23
25
|
{{#if sections.summary}}{{> executive-hero-summary options=(obj showPassRate=sections.passRate releaseGateShown=sections.releaseGate) }}{{/if}}
|
|
24
26
|
|
|
25
27
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend heroTrend=true) }}{{/if}}
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
{{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate releaseGateShown=sections.releaseGate) }}{{/if}}
|
|
24
24
|
|
|
25
|
+
{{#if sections.runDiff}}{{> run-diff variant='strip'}}{{/if}}
|
|
26
|
+
|
|
25
27
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend) }}{{/if}}
|
|
26
28
|
|
|
27
29
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{{#if runDiff}}
|
|
2
|
+
<div class="section section--run-diff">
|
|
3
|
+
<h2>Since Last Run</h2>
|
|
4
|
+
<div class="run-diff__head">
|
|
5
|
+
<div class="run-diff__strip">
|
|
6
|
+
<span class="run-diff__count run-diff__count--new"><strong>{{runDiff.newFailures.length}}</strong> {{pluralise runDiff.newFailures.length "new failure" "new failures"}}</span> · <span class="run-diff__count run-diff__count--fixed"><strong>{{runDiff.fixed.length}}</strong> fixed</span> · <span class="run-diff__count run-diff__count--still"><strong>{{runDiff.stillFailing.length}}</strong> still failing</span>{{#if runDiff.noLongerRun.length}} · <span class="run-diff__count run-diff__count--neutral"><strong>{{runDiff.noLongerRun.length}}</strong> no longer run</span>{{/if}}
|
|
7
|
+
</div>
|
|
8
|
+
<div class="run-diff__baseline">vs. previous run on {{formatDate runDiff.baselineTimestamp}}</div>
|
|
9
|
+
{{#if runDiff.baselineTruncated}}
|
|
10
|
+
<div class="overflow-banner run-diff__caveat">Baseline list was capped; fixed and no-longer-run may be incomplete.</div>
|
|
11
|
+
{{/if}}
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
{{#if (eq variant 'full')}}
|
|
15
|
+
{{> run-diff-list items=runDiff.newFailures label="New Failures" }}
|
|
16
|
+
{{> run-diff-list items=runDiff.fixed label="Fixed" }}
|
|
17
|
+
{{> run-diff-list items=runDiff.stillFailing label="Still Failing" }}
|
|
18
|
+
{{#if runDiff.noLongerRun.length}}
|
|
19
|
+
<div class="run-diff__footnote">No longer run: {{#each (take runDiff.noLongerRun 5)}}{{title}}{{#unless @last}} · {{/unless}}{{/each}}{{#if (gt runDiff.noLongerRun.length 5)}} · + {{subtract runDiff.noLongerRun.length 5}} more{{/if}}</div>
|
|
20
|
+
{{/if}}
|
|
21
|
+
{{/if}}
|
|
22
|
+
|
|
23
|
+
{{#if (eq variant 'compact')}}
|
|
24
|
+
{{> run-diff-list items=runDiff.newFailures label="New Failures" }}
|
|
25
|
+
{{/if}}
|
|
26
|
+
</div>
|
|
27
|
+
{{/if}}
|
|
28
|
+
|
|
29
|
+
{{#*inline "run-diff-list"}}
|
|
30
|
+
{{#if items.length}}
|
|
31
|
+
<div class="run-diff__list">
|
|
32
|
+
<div class="run-diff__list-label">{{label}}</div>
|
|
33
|
+
{{#each (take items 20)}}
|
|
34
|
+
<div class="run-diff__list-item">- {{title}}</div>
|
|
35
|
+
{{/each}}
|
|
36
|
+
{{#if (gt items.length 20)}}
|
|
37
|
+
<div class="run-diff__list-more">+ {{subtract items.length 20}} more</div>
|
|
38
|
+
{{/if}}
|
|
39
|
+
</div>
|
|
40
|
+
{{/if}}
|
|
41
|
+
{{/inline}}
|
|
@@ -188,7 +188,7 @@ tr:last-child td { border-bottom: none; }
|
|
|
188
188
|
blocks below stay glued together, and headings stick to the content beneath. */
|
|
189
189
|
.section { margin-bottom: 24px; }
|
|
190
190
|
.kpi-strip, .summary-row, .charts-grid, .release-gate, .flaky-callout,
|
|
191
|
-
.trend-card, .flakiness-card, .env-grid {
|
|
191
|
+
.trend-card, .flakiness-card, .env-grid, .run-diff__head {
|
|
192
192
|
page-break-inside: avoid; break-inside: avoid;
|
|
193
193
|
}
|
|
194
194
|
h2, h3 { page-break-after: avoid; break-after: avoid; }
|
|
@@ -684,6 +684,24 @@ code {
|
|
|
684
684
|
color: var(--text-1); line-height: 1.55;
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
+
/* ── Run diff — since-last-run counts + optional lists (sections.runDiff) ── */
|
|
688
|
+
.run-diff__strip { font-size: 10.5px; }
|
|
689
|
+
.run-diff__count strong { font-weight: 700; }
|
|
690
|
+
.run-diff__count--new strong { color: var(--fail); }
|
|
691
|
+
.run-diff__count--fixed strong { color: var(--pass); }
|
|
692
|
+
.run-diff__count--still strong,
|
|
693
|
+
.run-diff__count--neutral strong { color: var(--text-3); }
|
|
694
|
+
.run-diff__baseline { font-size: 9.5px; color: var(--text-3); margin-top: 4px; }
|
|
695
|
+
.run-diff__caveat { margin-top: 8px; }
|
|
696
|
+
.run-diff__list { margin-top: 10px; }
|
|
697
|
+
.run-diff__list-label {
|
|
698
|
+
font-size: 9px; font-weight: 600; letter-spacing: 0.6px; text-transform: uppercase;
|
|
699
|
+
color: var(--text-2); margin-bottom: 4px;
|
|
700
|
+
}
|
|
701
|
+
.run-diff__list-item { font-size: 10px; color: var(--text-1); padding: 1px 0; }
|
|
702
|
+
.run-diff__list-more { font-size: 9.5px; color: var(--text-3); padding: 1px 0; }
|
|
703
|
+
.run-diff__footnote { font-size: 9.5px; color: var(--text-3); margin-top: 8px; }
|
|
704
|
+
|
|
687
705
|
/* Charts grid + cards (available to any template via sections.charts) */
|
|
688
706
|
.charts-grid {
|
|
689
707
|
display: grid; grid-template-columns: 210px 1fr;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
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",
|