@reportforge/playwright-pdf 0.20.1 → 0.21.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 +17 -0
- package/README.md +13 -1
- package/dist/cli/index.js +5 -1
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +77 -13
- package/dist/index.mjs +77 -13
- package/dist/templates/partials/release-gate.hbs +21 -0
- package/dist/templates/styles/base.css +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,23 @@
|
|
|
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
|
+
|
|
14
|
+
## [0.20.2] - 2026-07-12
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- **A run that executed zero tests (e.g. a `--grep` that matched nothing) no longer renders "APPROVED TO SHIP".** The release gate now shows a neutral "NO TESTS RAN" card explaining that there is nothing to assess, and the reporter prints a warning pointing at test filters. Previously an empty run produced an approval verdict over "0 of 0 tests passed · 0%".
|
|
19
|
+
- Sub-second durations are rounded — a near-empty run prints `36ms`, not `36.343000000000075ms`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
6
23
|
## [0.20.1] - 2026-07-12
|
|
7
24
|
|
|
8
25
|
### Fixed
|
package/README.md
CHANGED
|
@@ -205,6 +205,7 @@ 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). |
|
|
210
211
|
| `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. |
|
|
@@ -319,7 +320,18 @@ After each run the reporter appends a summary entry to a local JSON file and ren
|
|
|
319
320
|
|
|
320
321
|
**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
322
|
|
|
322
|
-
**CI with ephemeral runners**:
|
|
323
|
+
**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:
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
reporter: [['@reportforge/playwright-pdf', {
|
|
327
|
+
template: 'detailed',
|
|
328
|
+
remoteHistory: true,
|
|
329
|
+
}]]
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
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.
|
|
333
|
+
|
|
334
|
+
**CI caching alternative** (keeps even aggregate numbers off the server): set `historyFile` to a project-relative path and cache it between runs:
|
|
323
335
|
|
|
324
336
|
```typescript
|
|
325
337
|
// playwright.config.ts
|
package/dist/cli/index.js
CHANGED
|
@@ -6369,7 +6369,8 @@ var init_defaults = __esm({
|
|
|
6369
6369
|
|
|
6370
6370
|
// src/utils/duration.ts
|
|
6371
6371
|
function formatDuration(ms) {
|
|
6372
|
-
|
|
6372
|
+
const rounded = Math.round(ms);
|
|
6373
|
+
if (rounded < 1e3) return `${rounded}ms`;
|
|
6373
6374
|
const totalSeconds = ms / 1e3;
|
|
6374
6375
|
if (totalSeconds < 60) return `${totalSeconds.toFixed(2)}s`;
|
|
6375
6376
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -12127,6 +12128,9 @@ var init_schema = __esm({
|
|
|
12127
12128
|
historyFile: external_exports.string().optional(),
|
|
12128
12129
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
12129
12130
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
12131
|
+
// Opt-in server-side trend store: survives ephemeral CI runners where the
|
|
12132
|
+
// local history file is wiped every run. Aggregate numbers only.
|
|
12133
|
+
remoteHistory: external_exports.boolean().default(false),
|
|
12130
12134
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
12131
12135
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
12132
12136
|
failureAnalysis: failureAnalysisConfigSchema,
|
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.
|
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.
|
package/dist/index.js
CHANGED
|
@@ -10554,6 +10554,9 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10554
10554
|
historyFile: external_exports.string().optional(),
|
|
10555
10555
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
10556
10556
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
10557
|
+
// Opt-in server-side trend store: survives ephemeral CI runners where the
|
|
10558
|
+
// local history file is wiped every run. Aggregate numbers only.
|
|
10559
|
+
remoteHistory: external_exports.boolean().default(false),
|
|
10557
10560
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10558
10561
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10559
10562
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
@@ -12611,7 +12614,8 @@ var import_path3 = __toESM(require("path"));
|
|
|
12611
12614
|
// src/utils/duration.ts
|
|
12612
12615
|
init_cjs_shims();
|
|
12613
12616
|
function formatDuration(ms) {
|
|
12614
|
-
|
|
12617
|
+
const rounded = Math.round(ms);
|
|
12618
|
+
if (rounded < 1e3) return `${rounded}ms`;
|
|
12615
12619
|
const totalSeconds = ms / 1e3;
|
|
12616
12620
|
if (totalSeconds < 60) return `${totalSeconds.toFixed(2)}s`;
|
|
12617
12621
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -13594,6 +13598,48 @@ var HistoryManager = class {
|
|
|
13594
13598
|
}
|
|
13595
13599
|
};
|
|
13596
13600
|
|
|
13601
|
+
// src/history/RemoteHistory.ts
|
|
13602
|
+
init_cjs_shims();
|
|
13603
|
+
var import_node_crypto3 = __toESM(require("crypto"));
|
|
13604
|
+
var NETWORK_TIMEOUT_MS2 = 5e3;
|
|
13605
|
+
function buildProjectId(projectName, outputFile) {
|
|
13606
|
+
return import_node_crypto3.default.createHash("sha256").update(`${projectName ?? ""}:${outputFile}`).digest("hex").slice(0, 16);
|
|
13607
|
+
}
|
|
13608
|
+
function buildBranchHash(branch) {
|
|
13609
|
+
return import_node_crypto3.default.createHash("sha256").update(branch || "unknown").digest("hex").slice(0, 12);
|
|
13610
|
+
}
|
|
13611
|
+
async function appendRemoteHistory(opts) {
|
|
13612
|
+
const numericEntry = { ...opts.entry };
|
|
13613
|
+
delete numericEntry.flakyTests;
|
|
13614
|
+
const controller = new AbortController();
|
|
13615
|
+
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS2);
|
|
13616
|
+
try {
|
|
13617
|
+
const res = await fetch(`${opts.serverUrl}/api/history/append`, {
|
|
13618
|
+
method: "POST",
|
|
13619
|
+
headers: {
|
|
13620
|
+
"content-type": "application/json",
|
|
13621
|
+
authorization: `Bearer ${opts.jwt}`
|
|
13622
|
+
},
|
|
13623
|
+
body: JSON.stringify({ projectId: opts.projectId, branchHash: opts.branchHash, entry: numericEntry }),
|
|
13624
|
+
signal: controller.signal
|
|
13625
|
+
});
|
|
13626
|
+
if (!res.ok) {
|
|
13627
|
+
await res.body?.cancel().catch(() => {
|
|
13628
|
+
});
|
|
13629
|
+
logger.debug(`remote history append failed: HTTP ${res.status} \u2014 using local history`);
|
|
13630
|
+
return null;
|
|
13631
|
+
}
|
|
13632
|
+
const data = await res.json();
|
|
13633
|
+
if (!Array.isArray(data.entries) || data.entries.length === 0) return null;
|
|
13634
|
+
return data.entries;
|
|
13635
|
+
} catch (err) {
|
|
13636
|
+
logger.debug(`remote history append failed: ${err.message} \u2014 using local history`);
|
|
13637
|
+
return null;
|
|
13638
|
+
} finally {
|
|
13639
|
+
clearTimeout(timeout);
|
|
13640
|
+
}
|
|
13641
|
+
}
|
|
13642
|
+
|
|
13597
13643
|
// src/history/FlakinessAnalyser.ts
|
|
13598
13644
|
init_cjs_shims();
|
|
13599
13645
|
function computeTopN(entries, topN) {
|
|
@@ -14008,7 +14054,7 @@ var NotificationSender = class {
|
|
|
14008
14054
|
|
|
14009
14055
|
// src/live/LiveStreamer.ts
|
|
14010
14056
|
init_cjs_shims();
|
|
14011
|
-
var
|
|
14057
|
+
var NETWORK_TIMEOUT_MS3 = 8e3;
|
|
14012
14058
|
var LIVE_DRAIN_DEADLINE_MS = 1e4;
|
|
14013
14059
|
var MAX_FLUSH_MS = 3e4;
|
|
14014
14060
|
var WARN_THROTTLE_MS = 3e4;
|
|
@@ -14181,7 +14227,7 @@ var LiveStreamer = class {
|
|
|
14181
14227
|
async send(body) {
|
|
14182
14228
|
if (!this.jwt) return;
|
|
14183
14229
|
const controller = new AbortController();
|
|
14184
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
14230
|
+
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS3);
|
|
14185
14231
|
try {
|
|
14186
14232
|
const res = await fetch(`${this.serverUrl}/api/live/ingest`, {
|
|
14187
14233
|
method: "POST",
|
|
@@ -14355,7 +14401,7 @@ var PdfReporter = class {
|
|
|
14355
14401
|
this.options = parseOptions(rawOptions);
|
|
14356
14402
|
setLogLevel(this.options.logLevel);
|
|
14357
14403
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14358
|
-
this.version = true ? "0.
|
|
14404
|
+
this.version = true ? "0.21.0" : "0.x";
|
|
14359
14405
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
14360
14406
|
this.licenseClient = new LicenseClient({
|
|
14361
14407
|
licenseKey: this.options.licenseKey,
|
|
@@ -14433,9 +14479,12 @@ var PdfReporter = class {
|
|
|
14433
14479
|
maybePrintUpdateNotice(licenseInfo, this.version);
|
|
14434
14480
|
return;
|
|
14435
14481
|
}
|
|
14482
|
+
if (collected.stats.total === 0) {
|
|
14483
|
+
logger.warn("No tests were executed \u2014 check your --grep / project filters. The report has nothing to assess.");
|
|
14484
|
+
}
|
|
14436
14485
|
if (this.options.showTrend && collected.stats.total > 0) {
|
|
14437
14486
|
try {
|
|
14438
|
-
this._appendTrend(collected);
|
|
14487
|
+
await this._appendTrend(collected, licenseInfo);
|
|
14439
14488
|
} catch (err) {
|
|
14440
14489
|
logger.warn(`History write failed \u2014 trend data skipped: ${err.message}`);
|
|
14441
14490
|
}
|
|
@@ -14470,7 +14519,7 @@ var PdfReporter = class {
|
|
|
14470
14519
|
}
|
|
14471
14520
|
return this.dataCollector.finalize(result);
|
|
14472
14521
|
}
|
|
14473
|
-
_appendTrend(collected) {
|
|
14522
|
+
async _appendTrend(collected, licenseInfo) {
|
|
14474
14523
|
const historyPath = resolveHistoryPath(this.options, this.cwd);
|
|
14475
14524
|
const hm = new HistoryManager(historyPath);
|
|
14476
14525
|
const suffix = Math.random().toString(36).padEnd(8, "0").slice(2, 6);
|
|
@@ -14487,13 +14536,28 @@ var PdfReporter = class {
|
|
|
14487
14536
|
verdict: deriveVerdict(collected.stats),
|
|
14488
14537
|
flakyTests
|
|
14489
14538
|
};
|
|
14490
|
-
const
|
|
14491
|
-
|
|
14539
|
+
const localEntries = hm.append(entry, this.options.historySize);
|
|
14540
|
+
let trendEntries = localEntries;
|
|
14541
|
+
if (this.options.remoteHistory && licenseInfo.jwt) {
|
|
14542
|
+
const serverUrl = this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL2;
|
|
14543
|
+
const remote = await appendRemoteHistory({
|
|
14544
|
+
serverUrl,
|
|
14545
|
+
jwt: licenseInfo.jwt,
|
|
14546
|
+
projectId: buildProjectId(this.options.projectName, this.options.outputFile),
|
|
14547
|
+
branchHash: buildBranchHash(collected.environment.branch),
|
|
14548
|
+
entry
|
|
14549
|
+
});
|
|
14550
|
+
if (remote) {
|
|
14551
|
+
trendEntries = remote;
|
|
14552
|
+
logger.debug(`remote history: ${remote.length} entr${remote.length === 1 ? "y" : "ies"} for trend`);
|
|
14553
|
+
}
|
|
14554
|
+
}
|
|
14555
|
+
this._populateHistoryCharts(trendEntries, localEntries, collected);
|
|
14492
14556
|
}
|
|
14493
|
-
_populateHistoryCharts(
|
|
14494
|
-
if (
|
|
14557
|
+
_populateHistoryCharts(trendEntries, localEntries, collected) {
|
|
14558
|
+
if (trendEntries.length >= 2) {
|
|
14495
14559
|
collected.charts.trend = {
|
|
14496
|
-
entries:
|
|
14560
|
+
entries: trendEntries.map((e) => ({
|
|
14497
14561
|
runId: e.runId,
|
|
14498
14562
|
timestamp: e.timestamp,
|
|
14499
14563
|
passRate: e.passRate,
|
|
@@ -14501,11 +14565,11 @@ var PdfReporter = class {
|
|
|
14501
14565
|
duration: e.duration,
|
|
14502
14566
|
verdict: e.verdict
|
|
14503
14567
|
})),
|
|
14504
|
-
delta:
|
|
14568
|
+
delta: trendEntries[0].passRate - trendEntries[1].passRate
|
|
14505
14569
|
};
|
|
14506
14570
|
}
|
|
14507
14571
|
if (this.options.flakinessTopN > 0) {
|
|
14508
|
-
const table = computeTopN(
|
|
14572
|
+
const table = computeTopN(localEntries, this.options.flakinessTopN);
|
|
14509
14573
|
if (table.length > 0) {
|
|
14510
14574
|
collected.charts.flakinessTable = table;
|
|
14511
14575
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -10555,6 +10555,9 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10555
10555
|
historyFile: external_exports.string().optional(),
|
|
10556
10556
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
10557
10557
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
10558
|
+
// Opt-in server-side trend store: survives ephemeral CI runners where the
|
|
10559
|
+
// local history file is wiped every run. Aggregate numbers only.
|
|
10560
|
+
remoteHistory: external_exports.boolean().default(false),
|
|
10558
10561
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10559
10562
|
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10560
10563
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
@@ -12612,7 +12615,8 @@ import path9 from "path";
|
|
|
12612
12615
|
// src/utils/duration.ts
|
|
12613
12616
|
init_esm_shims();
|
|
12614
12617
|
function formatDuration(ms) {
|
|
12615
|
-
|
|
12618
|
+
const rounded = Math.round(ms);
|
|
12619
|
+
if (rounded < 1e3) return `${rounded}ms`;
|
|
12616
12620
|
const totalSeconds = ms / 1e3;
|
|
12617
12621
|
if (totalSeconds < 60) return `${totalSeconds.toFixed(2)}s`;
|
|
12618
12622
|
const minutes = Math.floor(totalSeconds / 60);
|
|
@@ -13595,6 +13599,48 @@ var HistoryManager = class {
|
|
|
13595
13599
|
}
|
|
13596
13600
|
};
|
|
13597
13601
|
|
|
13602
|
+
// src/history/RemoteHistory.ts
|
|
13603
|
+
init_esm_shims();
|
|
13604
|
+
import crypto4 from "crypto";
|
|
13605
|
+
var NETWORK_TIMEOUT_MS2 = 5e3;
|
|
13606
|
+
function buildProjectId(projectName, outputFile) {
|
|
13607
|
+
return crypto4.createHash("sha256").update(`${projectName ?? ""}:${outputFile}`).digest("hex").slice(0, 16);
|
|
13608
|
+
}
|
|
13609
|
+
function buildBranchHash(branch) {
|
|
13610
|
+
return crypto4.createHash("sha256").update(branch || "unknown").digest("hex").slice(0, 12);
|
|
13611
|
+
}
|
|
13612
|
+
async function appendRemoteHistory(opts) {
|
|
13613
|
+
const numericEntry = { ...opts.entry };
|
|
13614
|
+
delete numericEntry.flakyTests;
|
|
13615
|
+
const controller = new AbortController();
|
|
13616
|
+
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS2);
|
|
13617
|
+
try {
|
|
13618
|
+
const res = await fetch(`${opts.serverUrl}/api/history/append`, {
|
|
13619
|
+
method: "POST",
|
|
13620
|
+
headers: {
|
|
13621
|
+
"content-type": "application/json",
|
|
13622
|
+
authorization: `Bearer ${opts.jwt}`
|
|
13623
|
+
},
|
|
13624
|
+
body: JSON.stringify({ projectId: opts.projectId, branchHash: opts.branchHash, entry: numericEntry }),
|
|
13625
|
+
signal: controller.signal
|
|
13626
|
+
});
|
|
13627
|
+
if (!res.ok) {
|
|
13628
|
+
await res.body?.cancel().catch(() => {
|
|
13629
|
+
});
|
|
13630
|
+
logger.debug(`remote history append failed: HTTP ${res.status} \u2014 using local history`);
|
|
13631
|
+
return null;
|
|
13632
|
+
}
|
|
13633
|
+
const data = await res.json();
|
|
13634
|
+
if (!Array.isArray(data.entries) || data.entries.length === 0) return null;
|
|
13635
|
+
return data.entries;
|
|
13636
|
+
} catch (err) {
|
|
13637
|
+
logger.debug(`remote history append failed: ${err.message} \u2014 using local history`);
|
|
13638
|
+
return null;
|
|
13639
|
+
} finally {
|
|
13640
|
+
clearTimeout(timeout);
|
|
13641
|
+
}
|
|
13642
|
+
}
|
|
13643
|
+
|
|
13598
13644
|
// src/history/FlakinessAnalyser.ts
|
|
13599
13645
|
init_esm_shims();
|
|
13600
13646
|
function computeTopN(entries, topN) {
|
|
@@ -14009,7 +14055,7 @@ var NotificationSender = class {
|
|
|
14009
14055
|
|
|
14010
14056
|
// src/live/LiveStreamer.ts
|
|
14011
14057
|
init_esm_shims();
|
|
14012
|
-
var
|
|
14058
|
+
var NETWORK_TIMEOUT_MS3 = 8e3;
|
|
14013
14059
|
var LIVE_DRAIN_DEADLINE_MS = 1e4;
|
|
14014
14060
|
var MAX_FLUSH_MS = 3e4;
|
|
14015
14061
|
var WARN_THROTTLE_MS = 3e4;
|
|
@@ -14182,7 +14228,7 @@ var LiveStreamer = class {
|
|
|
14182
14228
|
async send(body) {
|
|
14183
14229
|
if (!this.jwt) return;
|
|
14184
14230
|
const controller = new AbortController();
|
|
14185
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
14231
|
+
const timeout = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS3);
|
|
14186
14232
|
try {
|
|
14187
14233
|
const res = await fetch(`${this.serverUrl}/api/live/ingest`, {
|
|
14188
14234
|
method: "POST",
|
|
@@ -14356,7 +14402,7 @@ var PdfReporter = class {
|
|
|
14356
14402
|
this.options = parseOptions(rawOptions);
|
|
14357
14403
|
setLogLevel(this.options.logLevel);
|
|
14358
14404
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14359
|
-
this.version = true ? "0.
|
|
14405
|
+
this.version = true ? "0.21.0" : "0.x";
|
|
14360
14406
|
logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
|
|
14361
14407
|
this.licenseClient = new LicenseClient({
|
|
14362
14408
|
licenseKey: this.options.licenseKey,
|
|
@@ -14434,9 +14480,12 @@ var PdfReporter = class {
|
|
|
14434
14480
|
maybePrintUpdateNotice(licenseInfo, this.version);
|
|
14435
14481
|
return;
|
|
14436
14482
|
}
|
|
14483
|
+
if (collected.stats.total === 0) {
|
|
14484
|
+
logger.warn("No tests were executed \u2014 check your --grep / project filters. The report has nothing to assess.");
|
|
14485
|
+
}
|
|
14437
14486
|
if (this.options.showTrend && collected.stats.total > 0) {
|
|
14438
14487
|
try {
|
|
14439
|
-
this._appendTrend(collected);
|
|
14488
|
+
await this._appendTrend(collected, licenseInfo);
|
|
14440
14489
|
} catch (err) {
|
|
14441
14490
|
logger.warn(`History write failed \u2014 trend data skipped: ${err.message}`);
|
|
14442
14491
|
}
|
|
@@ -14471,7 +14520,7 @@ var PdfReporter = class {
|
|
|
14471
14520
|
}
|
|
14472
14521
|
return this.dataCollector.finalize(result);
|
|
14473
14522
|
}
|
|
14474
|
-
_appendTrend(collected) {
|
|
14523
|
+
async _appendTrend(collected, licenseInfo) {
|
|
14475
14524
|
const historyPath = resolveHistoryPath(this.options, this.cwd);
|
|
14476
14525
|
const hm = new HistoryManager(historyPath);
|
|
14477
14526
|
const suffix = Math.random().toString(36).padEnd(8, "0").slice(2, 6);
|
|
@@ -14488,13 +14537,28 @@ var PdfReporter = class {
|
|
|
14488
14537
|
verdict: deriveVerdict(collected.stats),
|
|
14489
14538
|
flakyTests
|
|
14490
14539
|
};
|
|
14491
|
-
const
|
|
14492
|
-
|
|
14540
|
+
const localEntries = hm.append(entry, this.options.historySize);
|
|
14541
|
+
let trendEntries = localEntries;
|
|
14542
|
+
if (this.options.remoteHistory && licenseInfo.jwt) {
|
|
14543
|
+
const serverUrl = this.options.serverUrl ?? process.env.REPORTFORGE_SERVER_URL ?? DEFAULT_SERVER_URL2;
|
|
14544
|
+
const remote = await appendRemoteHistory({
|
|
14545
|
+
serverUrl,
|
|
14546
|
+
jwt: licenseInfo.jwt,
|
|
14547
|
+
projectId: buildProjectId(this.options.projectName, this.options.outputFile),
|
|
14548
|
+
branchHash: buildBranchHash(collected.environment.branch),
|
|
14549
|
+
entry
|
|
14550
|
+
});
|
|
14551
|
+
if (remote) {
|
|
14552
|
+
trendEntries = remote;
|
|
14553
|
+
logger.debug(`remote history: ${remote.length} entr${remote.length === 1 ? "y" : "ies"} for trend`);
|
|
14554
|
+
}
|
|
14555
|
+
}
|
|
14556
|
+
this._populateHistoryCharts(trendEntries, localEntries, collected);
|
|
14493
14557
|
}
|
|
14494
|
-
_populateHistoryCharts(
|
|
14495
|
-
if (
|
|
14558
|
+
_populateHistoryCharts(trendEntries, localEntries, collected) {
|
|
14559
|
+
if (trendEntries.length >= 2) {
|
|
14496
14560
|
collected.charts.trend = {
|
|
14497
|
-
entries:
|
|
14561
|
+
entries: trendEntries.map((e) => ({
|
|
14498
14562
|
runId: e.runId,
|
|
14499
14563
|
timestamp: e.timestamp,
|
|
14500
14564
|
passRate: e.passRate,
|
|
@@ -14502,11 +14566,11 @@ var PdfReporter = class {
|
|
|
14502
14566
|
duration: e.duration,
|
|
14503
14567
|
verdict: e.verdict
|
|
14504
14568
|
})),
|
|
14505
|
-
delta:
|
|
14569
|
+
delta: trendEntries[0].passRate - trendEntries[1].passRate
|
|
14506
14570
|
};
|
|
14507
14571
|
}
|
|
14508
14572
|
if (this.options.flakinessTopN > 0) {
|
|
14509
|
-
const table = computeTopN(
|
|
14573
|
+
const table = computeTopN(localEntries, this.options.flakinessTopN);
|
|
14510
14574
|
if (table.length > 0) {
|
|
14511
14575
|
collected.charts.flakinessTable = table;
|
|
14512
14576
|
}
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
{{#unless (gt stats.total 0)}}
|
|
2
|
+
{{!-- Zero tests executed (e.g. a --grep that matched nothing): there is nothing
|
|
3
|
+
to assess, so the gate must be NEUTRAL — never "APPROVED TO SHIP" over an
|
|
4
|
+
empty run. --}}
|
|
5
|
+
<div class="release-gate release-gate--neutral">
|
|
6
|
+
<div class="release-gate__icon">○</div>
|
|
7
|
+
<div class="release-gate__body">
|
|
8
|
+
<div class="release-gate__label">Release Recommendation</div>
|
|
9
|
+
<div class="release-gate__verdict release-gate__verdict--neutral">NO TESTS RAN</div>
|
|
10
|
+
<div class="release-gate__detail">
|
|
11
|
+
0 tests were executed — nothing to assess. Check your test filters
|
|
12
|
+
(e.g. --grep), project selection, or test discovery before relying on this report.
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
<div class="release-gate__meta">
|
|
16
|
+
{{#unless (isUnknown environment.branch)}}<span>{{environment.branch}}</span>{{/unless}}
|
|
17
|
+
{{#unless (isUnknown environment.commit)}}<span>{{environment.commit}}</span>{{/unless}}
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
{{else}}
|
|
1
21
|
{{#if (gt stats.failed 0)}}
|
|
2
22
|
<div class="release-gate release-gate--fail">
|
|
3
23
|
<div class="release-gate__icon">✕</div>
|
|
@@ -61,3 +81,4 @@
|
|
|
61
81
|
</div>
|
|
62
82
|
{{/if}}
|
|
63
83
|
{{/if}}
|
|
84
|
+
{{/unless}}
|
|
@@ -289,12 +289,17 @@ code {
|
|
|
289
289
|
.release-gate--fail {
|
|
290
290
|
background: var(--fail-dim); border-color: #E5C9BD;
|
|
291
291
|
}
|
|
292
|
+
/* Zero tests executed — neutral, neither approval nor hold. */
|
|
293
|
+
.release-gate--neutral {
|
|
294
|
+
background: #F5F4F2; border-color: #D8D5D0;
|
|
295
|
+
}
|
|
292
296
|
.release-gate__icon {
|
|
293
297
|
font-size: 22px; font-weight: 700; flex-shrink: 0; width: 32px;
|
|
294
298
|
text-align: center; line-height: 1;
|
|
295
299
|
}
|
|
296
300
|
.release-gate--pass .release-gate__icon { color: var(--pass); }
|
|
297
301
|
.release-gate--fail .release-gate__icon { color: var(--fail); }
|
|
302
|
+
.release-gate--neutral .release-gate__icon { color: var(--text-3); }
|
|
298
303
|
.release-gate__body { flex: 1; }
|
|
299
304
|
.release-gate__label {
|
|
300
305
|
font-family: 'Inter', system-ui, sans-serif; font-size: 8px;
|
|
@@ -307,6 +312,7 @@ code {
|
|
|
307
312
|
}
|
|
308
313
|
.release-gate__verdict--pass { color: var(--pass); }
|
|
309
314
|
.release-gate__verdict--fail { color: var(--fail); }
|
|
315
|
+
.release-gate__verdict--neutral { color: var(--text-2); }
|
|
310
316
|
.release-gate__detail {
|
|
311
317
|
font-family: 'Inter', system-ui, sans-serif; font-size: 10px;
|
|
312
318
|
color: var(--text-2); margin-top: 4px;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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",
|