@reportforge/playwright-pdf 0.13.1 → 0.14.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 +28 -0
- package/README.md +2 -1
- package/dist/cli/index.js +62 -22
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +121 -65
- package/dist/index.mjs +121 -65
- package/dist/templates/layouts/detailed.hbs +2 -0
- package/dist/templates/layouts/executive.hbs +4 -2
- package/dist/templates/layouts/minimal.hbs +2 -0
- package/dist/templates/partials/brief-band.hbs +6 -0
- package/dist/templates/partials/chart-summary-grid.hbs +18 -0
- package/dist/templates/partials/chart-trend-card.hbs +16 -0
- package/dist/templates/partials/charts.hbs +7 -30
- package/dist/templates/partials/executive-hero-summary.hbs +66 -0
- package/dist/templates/styles/base.css +22 -0
- package/dist/templates/styles/executive.css +31 -0
- package/dist/templates/styles/minimal.css +9 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -10351,6 +10351,7 @@ var SECTION_KEYS = [
|
|
|
10351
10351
|
"failureAnalysis",
|
|
10352
10352
|
"slowTests",
|
|
10353
10353
|
"defectLog",
|
|
10354
|
+
"briefBand",
|
|
10354
10355
|
// display modifiers
|
|
10355
10356
|
"passRate",
|
|
10356
10357
|
"fullEnvironment",
|
|
@@ -10373,6 +10374,7 @@ var SECTION_DEFAULTS = {
|
|
|
10373
10374
|
failureAnalysis: false,
|
|
10374
10375
|
slowTests: false,
|
|
10375
10376
|
defectLog: false,
|
|
10377
|
+
briefBand: false,
|
|
10376
10378
|
passRate: true,
|
|
10377
10379
|
fullEnvironment: false,
|
|
10378
10380
|
retries: true,
|
|
@@ -10393,6 +10395,7 @@ var SECTION_DEFAULTS = {
|
|
|
10393
10395
|
failureAnalysis: true,
|
|
10394
10396
|
slowTests: true,
|
|
10395
10397
|
defectLog: true,
|
|
10398
|
+
briefBand: false,
|
|
10396
10399
|
passRate: true,
|
|
10397
10400
|
fullEnvironment: true,
|
|
10398
10401
|
retries: true,
|
|
@@ -10401,7 +10404,9 @@ var SECTION_DEFAULTS = {
|
|
|
10401
10404
|
},
|
|
10402
10405
|
executive: {
|
|
10403
10406
|
coverPage: true,
|
|
10404
|
-
|
|
10407
|
+
// Now off: its content (root-cause tally) is subsumed by briefBand below,
|
|
10408
|
+
// which also adds the trend delta. Showing both would say it twice.
|
|
10409
|
+
analysisOneliner: false,
|
|
10405
10410
|
releaseGate: true,
|
|
10406
10411
|
summary: true,
|
|
10407
10412
|
charts: true,
|
|
@@ -10416,6 +10421,7 @@ var SECTION_DEFAULTS = {
|
|
|
10416
10421
|
failureAnalysis: false,
|
|
10417
10422
|
slowTests: true,
|
|
10418
10423
|
defectLog: false,
|
|
10424
|
+
briefBand: true,
|
|
10419
10425
|
passRate: true,
|
|
10420
10426
|
fullEnvironment: false,
|
|
10421
10427
|
retries: false,
|
|
@@ -11062,6 +11068,11 @@ var LicenseClient = class {
|
|
|
11062
11068
|
body: JSON.stringify(body),
|
|
11063
11069
|
signal: controller.signal
|
|
11064
11070
|
});
|
|
11071
|
+
if (res.status === 429 || res.status === 408) {
|
|
11072
|
+
await res.body?.cancel().catch(() => {
|
|
11073
|
+
});
|
|
11074
|
+
throw new Error(`HTTP ${res.status}`);
|
|
11075
|
+
}
|
|
11065
11076
|
if (res.status >= 400 && res.status < 500) {
|
|
11066
11077
|
const text = await res.text().catch(() => "");
|
|
11067
11078
|
throw new Error(`license-denied: ${text || `HTTP ${res.status}`}`);
|
|
@@ -11294,6 +11305,25 @@ function buildOneLiner(clusters) {
|
|
|
11294
11305
|
function dominantCategory(clusters) {
|
|
11295
11306
|
return clusters[0]?.category ?? "unknown";
|
|
11296
11307
|
}
|
|
11308
|
+
function trendClause(passRate, delta) {
|
|
11309
|
+
if (delta === 0) return `Pass rate held steady at ${passRate}%.`;
|
|
11310
|
+
const verb = delta > 0 ? "climbed" : "dropped";
|
|
11311
|
+
const abs = Math.abs(delta);
|
|
11312
|
+
const points = abs === 1 ? "point" : "points";
|
|
11313
|
+
return `Pass rate ${verb} ${abs} ${points} to ${passRate}% since the last run.`;
|
|
11314
|
+
}
|
|
11315
|
+
function causesClause(analysis) {
|
|
11316
|
+
const [head, ...tallyParts] = buildOneLiner(analysis.clusters).split(" \xB7 ");
|
|
11317
|
+
const verb = analysis.clusters.length === 1 ? "remains" : "remain";
|
|
11318
|
+
return `${head} ${verb}: ${tallyParts.join(", ")}.`;
|
|
11319
|
+
}
|
|
11320
|
+
function buildBriefSentence(stats, analysis, trend) {
|
|
11321
|
+
if (trend && trend.delta !== null) {
|
|
11322
|
+
const trendSentence = trendClause(stats.passRate, trend.delta);
|
|
11323
|
+
return analysis && analysis.clusters.length > 0 ? `${trendSentence} ${causesClause(analysis)}` : trendSentence;
|
|
11324
|
+
}
|
|
11325
|
+
return analysis && analysis.clusters.length > 0 ? causesClause(analysis) : void 0;
|
|
11326
|
+
}
|
|
11297
11327
|
|
|
11298
11328
|
// src/analysis/UnclassifiedCollector.ts
|
|
11299
11329
|
init_esm_shims();
|
|
@@ -11620,6 +11650,17 @@ init_esm_shims();
|
|
|
11620
11650
|
// src/collector/stats-utils.ts
|
|
11621
11651
|
init_esm_shims();
|
|
11622
11652
|
var TOP_SUITES_FOR_CHART = 10;
|
|
11653
|
+
function resolveTestStatus(test, result) {
|
|
11654
|
+
if (!result) return "skipped";
|
|
11655
|
+
const expected = test.expectedStatus ?? "passed";
|
|
11656
|
+
const actual = result.status;
|
|
11657
|
+
if (actual === "skipped") return "skipped";
|
|
11658
|
+
if (actual === "timedOut") return "timedOut";
|
|
11659
|
+
if (actual === "passed" && result.retry > 0) return "flaky";
|
|
11660
|
+
if (actual === "passed" && expected === "passed") return "passed";
|
|
11661
|
+
if (actual === "failed" && expected === "failed") return "passed";
|
|
11662
|
+
return "failed";
|
|
11663
|
+
}
|
|
11623
11664
|
function statsFromTests(tests) {
|
|
11624
11665
|
return {
|
|
11625
11666
|
total: tests.length,
|
|
@@ -11695,7 +11736,7 @@ var SuiteWalker = class {
|
|
|
11695
11736
|
}
|
|
11696
11737
|
buildTest(test, resultMap) {
|
|
11697
11738
|
const result = resultMap.get(test.id);
|
|
11698
|
-
const status =
|
|
11739
|
+
const status = resolveTestStatus(test, result);
|
|
11699
11740
|
return {
|
|
11700
11741
|
id: test.id,
|
|
11701
11742
|
title: test.title,
|
|
@@ -11708,17 +11749,6 @@ var SuiteWalker = class {
|
|
|
11708
11749
|
location: test.location ? `${test.location.file}:${test.location.line}` : void 0
|
|
11709
11750
|
};
|
|
11710
11751
|
}
|
|
11711
|
-
resolveStatus(test, result) {
|
|
11712
|
-
if (!result) return "skipped";
|
|
11713
|
-
const expected = test.expectedStatus ?? "passed";
|
|
11714
|
-
const actual = result.status;
|
|
11715
|
-
if (actual === "skipped") return "skipped";
|
|
11716
|
-
if (actual === "timedOut") return "timedOut";
|
|
11717
|
-
if (actual === "passed" && result.retry > 0) return "flaky";
|
|
11718
|
-
if (actual === "passed" && expected === "passed") return "passed";
|
|
11719
|
-
if (actual === "failed" && expected === "failed") return "passed";
|
|
11720
|
-
return "failed";
|
|
11721
|
-
}
|
|
11722
11752
|
};
|
|
11723
11753
|
|
|
11724
11754
|
// src/collector/severity.ts
|
|
@@ -11732,6 +11762,16 @@ var SEVERITY_RANK = {
|
|
|
11732
11762
|
function sortBySeverity(failures) {
|
|
11733
11763
|
return [...failures].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]);
|
|
11734
11764
|
}
|
|
11765
|
+
function inferSeverity(annotations, tags) {
|
|
11766
|
+
const allLabels = [
|
|
11767
|
+
...annotations.map((a) => a.type.toLowerCase()),
|
|
11768
|
+
...tags.map((t) => t.toLowerCase())
|
|
11769
|
+
];
|
|
11770
|
+
if (allLabels.some((l) => l.includes("critical") || l.includes("blocker"))) return "critical";
|
|
11771
|
+
if (allLabels.some((l) => l.includes("high") || l.includes("major"))) return "high";
|
|
11772
|
+
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
11773
|
+
return "medium";
|
|
11774
|
+
}
|
|
11735
11775
|
|
|
11736
11776
|
// src/utils/env.ts
|
|
11737
11777
|
init_esm_shims();
|
|
@@ -11856,6 +11896,10 @@ var DataCollector = class {
|
|
|
11856
11896
|
constructor(captureOpts = {}) {
|
|
11857
11897
|
this.captureOpts = captureOpts;
|
|
11858
11898
|
this.resultMap = /* @__PURE__ */ new Map();
|
|
11899
|
+
// Parallel to resultMap — needed so computeStats() (which only sees
|
|
11900
|
+
// TestResult, not TestCase) can reconcile against expectedStatus the same
|
|
11901
|
+
// way resolveTestStatus does for the suite tree and the failure list.
|
|
11902
|
+
this.expectedStatusMap = /* @__PURE__ */ new Map();
|
|
11859
11903
|
this.failureRecords = [];
|
|
11860
11904
|
this.startTime = Date.now();
|
|
11861
11905
|
}
|
|
@@ -11866,7 +11910,10 @@ var DataCollector = class {
|
|
|
11866
11910
|
}
|
|
11867
11911
|
onTestEnd(test, result) {
|
|
11868
11912
|
this.resultMap.set(test.id, result);
|
|
11869
|
-
|
|
11913
|
+
this.expectedStatusMap.set(test.id, test.expectedStatus);
|
|
11914
|
+
const resolved = resolveTestStatus(test, result);
|
|
11915
|
+
const isRealFailure = resolved === "failed" || resolved === "timedOut";
|
|
11916
|
+
if (isRealFailure) {
|
|
11870
11917
|
const existing = this.failureRecords.findIndex((f) => f.testId === test.id);
|
|
11871
11918
|
const record = this.buildFailureRecord(test, result);
|
|
11872
11919
|
if (existing >= 0) {
|
|
@@ -11906,7 +11953,7 @@ var DataCollector = class {
|
|
|
11906
11953
|
(a) => a.name.toLowerCase().includes("screenshot") && a.contentType.startsWith("image/")
|
|
11907
11954
|
);
|
|
11908
11955
|
const suitePath = test.titlePath().slice(0, -1);
|
|
11909
|
-
const severity =
|
|
11956
|
+
const severity = inferSeverity(test.annotations ?? [], test.tags ?? []);
|
|
11910
11957
|
return {
|
|
11911
11958
|
testId: test.id,
|
|
11912
11959
|
testTitle: test.title,
|
|
@@ -11929,28 +11976,18 @@ var DataCollector = class {
|
|
|
11929
11976
|
execution: executionFromResult(result, this.captureOpts)
|
|
11930
11977
|
};
|
|
11931
11978
|
}
|
|
11932
|
-
inferSeverity(test) {
|
|
11933
|
-
const annotations = test.annotations ?? [];
|
|
11934
|
-
const tags = test.tags ?? [];
|
|
11935
|
-
const allLabels = [
|
|
11936
|
-
...annotations.map((a) => a.type.toLowerCase()),
|
|
11937
|
-
...tags.map((t) => t.toLowerCase())
|
|
11938
|
-
];
|
|
11939
|
-
if (allLabels.some((l) => l.includes("critical") || l.includes("blocker"))) return "critical";
|
|
11940
|
-
if (allLabels.some((l) => l.includes("high") || l.includes("major"))) return "high";
|
|
11941
|
-
if (allLabels.some((l) => l.includes("low") || l.includes("minor"))) return "low";
|
|
11942
|
-
return "medium";
|
|
11943
|
-
}
|
|
11944
11979
|
computeStats(fullResult) {
|
|
11945
11980
|
let total = 0, passed = 0, failed = 0, skipped = 0, flaky = 0, timedOut = 0;
|
|
11946
|
-
for (const [, result] of this.resultMap) {
|
|
11981
|
+
for (const [testId, result] of this.resultMap) {
|
|
11947
11982
|
total++;
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
// push passRate over 100%.
|
|
11983
|
+
const expectedStatus = this.expectedStatusMap.get(testId);
|
|
11984
|
+
const resolved = resolveTestStatus({ expectedStatus }, result);
|
|
11985
|
+
switch (resolved) {
|
|
11952
11986
|
case "passed":
|
|
11953
|
-
|
|
11987
|
+
passed++;
|
|
11988
|
+
break;
|
|
11989
|
+
case "flaky":
|
|
11990
|
+
flaky++;
|
|
11954
11991
|
break;
|
|
11955
11992
|
case "failed":
|
|
11956
11993
|
failed++;
|
|
@@ -11962,9 +11999,6 @@ var DataCollector = class {
|
|
|
11962
11999
|
case "skipped":
|
|
11963
12000
|
skipped++;
|
|
11964
12001
|
break;
|
|
11965
|
-
case "interrupted":
|
|
11966
|
-
failed++;
|
|
11967
|
-
break;
|
|
11968
12002
|
}
|
|
11969
12003
|
}
|
|
11970
12004
|
const duration = fullResult.duration ?? Date.now() - this.startTime;
|
|
@@ -12124,18 +12158,26 @@ var ShardMerger = class {
|
|
|
12124
12158
|
});
|
|
12125
12159
|
}
|
|
12126
12160
|
buildDescribeSuites(jsonSuites, projectName, parentPath) {
|
|
12127
|
-
|
|
12128
|
-
|
|
12129
|
-
|
|
12130
|
-
|
|
12161
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12162
|
+
for (const suite of jsonSuites) {
|
|
12163
|
+
if (!groups.has(suite.title)) groups.set(suite.title, []);
|
|
12164
|
+
groups.get(suite.title).push(suite);
|
|
12165
|
+
}
|
|
12166
|
+
return Array.from(groups.values()).map((group) => {
|
|
12167
|
+
const ref = group[0];
|
|
12168
|
+
const suitePath = [...parentPath, ref.title];
|
|
12169
|
+
const allSpecs = group.flatMap((s) => s.specs);
|
|
12170
|
+
const allChildSuites = group.flatMap((s) => s.suites ?? []);
|
|
12171
|
+
const tests = allSpecs.map((spec) => this.specToTestNode(spec, projectName, suitePath));
|
|
12172
|
+
const childSuites = this.buildDescribeSuites(allChildSuites, projectName, suitePath);
|
|
12131
12173
|
const stats = aggregateStats([
|
|
12132
12174
|
...childSuites.map((s) => s.stats),
|
|
12133
12175
|
statsFromTests(tests)
|
|
12134
12176
|
]);
|
|
12135
12177
|
return {
|
|
12136
|
-
title:
|
|
12178
|
+
title: ref.title,
|
|
12137
12179
|
type: "describe",
|
|
12138
|
-
location:
|
|
12180
|
+
location: ref.file ? `${ref.file}:${ref.line ?? 0}` : void 0,
|
|
12139
12181
|
suites: childSuites,
|
|
12140
12182
|
tests,
|
|
12141
12183
|
stats
|
|
@@ -12241,7 +12283,7 @@ var ShardMerger = class {
|
|
|
12241
12283
|
duration: r.duration
|
|
12242
12284
|
})),
|
|
12243
12285
|
durationMs: lastResult?.duration ?? 0,
|
|
12244
|
-
severity:
|
|
12286
|
+
severity: inferSeverity(test.annotations ?? [], spec.tags ?? []),
|
|
12245
12287
|
execution: lastResult ? buildExecution(
|
|
12246
12288
|
{
|
|
12247
12289
|
// Guard each field — a partial / non-conformant / older shard JSON can
|
|
@@ -12817,25 +12859,18 @@ var PdfEncryptor = class {
|
|
|
12817
12859
|
return;
|
|
12818
12860
|
}
|
|
12819
12861
|
const tmpPath = path9.join(os8.tmpdir(), `rf-encrypted-${Date.now()}.pdf`);
|
|
12820
|
-
const
|
|
12862
|
+
const argFile = path9.join(os8.tmpdir(), `rf-qpdf-args-${crypto3.randomBytes(8).toString("hex")}`);
|
|
12821
12863
|
try {
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
execFileSync("qpdf", [
|
|
12825
|
-
`--password-file=${pwFile}`,
|
|
12826
|
-
"--encrypt",
|
|
12827
|
-
"256",
|
|
12828
|
-
"--",
|
|
12829
|
-
pdfPath,
|
|
12830
|
-
tmpPath
|
|
12831
|
-
], { stdio: "pipe" });
|
|
12864
|
+
const args = ["--encrypt", password, password, "256", "--", pdfPath, tmpPath];
|
|
12865
|
+
fs9.writeFileSync(argFile, args.join("\n"), { mode: 384 });
|
|
12866
|
+
execFileSync("qpdf", [`@${argFile}`], { stdio: "pipe" });
|
|
12832
12867
|
fs9.renameSync(tmpPath, pdfPath);
|
|
12833
12868
|
logger.info(`PDF encrypted with password.`);
|
|
12834
12869
|
} catch (e) {
|
|
12835
|
-
|
|
12870
|
+
throw new Error(`qpdf encryption failed: ${e.message}`);
|
|
12836
12871
|
} finally {
|
|
12837
12872
|
if (fs9.existsSync(tmpPath)) fs9.unlinkSync(tmpPath);
|
|
12838
|
-
fs9.rmSync(
|
|
12873
|
+
fs9.rmSync(argFile, { force: true });
|
|
12839
12874
|
}
|
|
12840
12875
|
}
|
|
12841
12876
|
isQpdfAvailable() {
|
|
@@ -13084,7 +13119,8 @@ var PdfGenerator = class {
|
|
|
13084
13119
|
if (capBytes > 0) {
|
|
13085
13120
|
const sizeBytes = (await fs12.promises.stat(outputPath)).size;
|
|
13086
13121
|
if (sizeBytes > capBytes) {
|
|
13087
|
-
const
|
|
13122
|
+
const inlinedCount = Math.min(templateData.failures.length, compression.maxInlineFailures);
|
|
13123
|
+
const tighter = this.tightenForCap(compression, sizeBytes, capBytes, inlinedCount);
|
|
13088
13124
|
logger.warn(
|
|
13089
13125
|
`PDF size ${fmtMb(sizeBytes)} exceeds cap ${fmtMb(capBytes)} \u2014 re-rendering (jpeg-q=${tighter.quality}, max-w=${tighter.maxWidth}px, inline-failures\u2264${tighter.maxInlineFailures}).`
|
|
13090
13126
|
);
|
|
@@ -13113,11 +13149,30 @@ var PdfGenerator = class {
|
|
|
13113
13149
|
}
|
|
13114
13150
|
resolveTemplateSources(options) {
|
|
13115
13151
|
if (options.templatePath && options.templatePath.length > 0) {
|
|
13116
|
-
|
|
13117
|
-
|
|
13118
|
-
absolutePath
|
|
13119
|
-
|
|
13120
|
-
}
|
|
13152
|
+
const byAbsolutePath = /* @__PURE__ */ new Map();
|
|
13153
|
+
for (const p of options.templatePath) {
|
|
13154
|
+
const absolutePath = path12.isAbsolute(p) ? p : path12.resolve(process.cwd(), p);
|
|
13155
|
+
if (!byAbsolutePath.has(absolutePath)) byAbsolutePath.set(absolutePath, p);
|
|
13156
|
+
}
|
|
13157
|
+
const labelCounts = /* @__PURE__ */ new Map();
|
|
13158
|
+
for (const rawPath of byAbsolutePath.values()) {
|
|
13159
|
+
const label = path12.basename(rawPath, ".hbs");
|
|
13160
|
+
labelCounts.set(label, (labelCounts.get(label) ?? 0) + 1);
|
|
13161
|
+
}
|
|
13162
|
+
const seenPerLabel = /* @__PURE__ */ new Map();
|
|
13163
|
+
return Array.from(byAbsolutePath.entries()).map(([absolutePath, rawPath]) => {
|
|
13164
|
+
const baseLabel = path12.basename(rawPath, ".hbs");
|
|
13165
|
+
let label = baseLabel;
|
|
13166
|
+
if ((labelCounts.get(baseLabel) ?? 0) > 1) {
|
|
13167
|
+
const n = (seenPerLabel.get(baseLabel) ?? 0) + 1;
|
|
13168
|
+
seenPerLabel.set(baseLabel, n);
|
|
13169
|
+
label = `${baseLabel}-${n}`;
|
|
13170
|
+
logger.warn(
|
|
13171
|
+
`Multiple templatePath entries resolve to the basename "${baseLabel}" \u2014 disambiguating output filenames with a numeric suffix ("${label}").`
|
|
13172
|
+
);
|
|
13173
|
+
}
|
|
13174
|
+
return { kind: "custom", absolutePath, label };
|
|
13175
|
+
});
|
|
13121
13176
|
}
|
|
13122
13177
|
const rawIds = Array.isArray(options.template) ? options.template : [options.template];
|
|
13123
13178
|
return [...new Set(rawIds)].map((id) => ({ kind: "builtin", id }));
|
|
@@ -13139,8 +13194,8 @@ var PdfGenerator = class {
|
|
|
13139
13194
|
* Also tightens JPEG quality and width to claw back bytes from each
|
|
13140
13195
|
* remaining image.
|
|
13141
13196
|
*/
|
|
13142
|
-
tightenForCap(prev, actualBytes, capBytes) {
|
|
13143
|
-
const observedPerFailure = actualBytes / Math.max(1,
|
|
13197
|
+
tightenForCap(prev, actualBytes, capBytes, actualInlinedCount) {
|
|
13198
|
+
const observedPerFailure = actualBytes / Math.max(1, actualInlinedCount);
|
|
13144
13199
|
const targetInline = Math.max(20, Math.floor(capBytes * 0.85 / observedPerFailure));
|
|
13145
13200
|
return {
|
|
13146
13201
|
effective: "max",
|
|
@@ -14002,7 +14057,7 @@ var PdfReporter = class {
|
|
|
14002
14057
|
this.liveConsole = false;
|
|
14003
14058
|
this.options = parseOptions(rawOptions);
|
|
14004
14059
|
this.dataCollector = new DataCollector(this.options.capture);
|
|
14005
|
-
const version = true ? "0.
|
|
14060
|
+
const version = true ? "0.14.0" : "0.x";
|
|
14006
14061
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
14007
14062
|
this.licenseClient = new LicenseClient({
|
|
14008
14063
|
licenseKey: this.options.licenseKey,
|
|
@@ -14086,6 +14141,7 @@ var PdfReporter = class {
|
|
|
14086
14141
|
}
|
|
14087
14142
|
const reportData = this._buildReportData(collected, licenseInfo);
|
|
14088
14143
|
runFailureAnalysis(reportData, this.options, this.cwd);
|
|
14144
|
+
reportData.briefSentence = buildBriefSentence(reportData.stats, reportData.analysis, reportData.charts.trend);
|
|
14089
14145
|
let pdfPaths = [];
|
|
14090
14146
|
try {
|
|
14091
14147
|
pdfPaths = await this.pdfGenerator.generateAll(reportData, this.options, licenseInfo);
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
{{#if sections.releaseGate}}{{> release-gate}}{{/if}}
|
|
20
20
|
|
|
21
|
+
{{#if sections.briefBand}}{{> brief-band}}{{/if}}
|
|
22
|
+
|
|
21
23
|
{{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate releaseGateShown=sections.releaseGate) }}{{/if}}
|
|
22
24
|
|
|
23
25
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend) }}{{/if}}
|
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
{{#if sections.releaseGate}}{{> release-gate}}{{/if}}
|
|
20
20
|
|
|
21
|
-
{{#if sections.
|
|
21
|
+
{{#if sections.briefBand}}{{> brief-band}}{{/if}}
|
|
22
22
|
|
|
23
|
-
{{#if sections.
|
|
23
|
+
{{#if sections.summary}}{{> executive-hero-summary options=(obj showPassRate=sections.passRate releaseGateShown=sections.releaseGate) }}{{/if}}
|
|
24
|
+
|
|
25
|
+
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend heroTrend=true) }}{{/if}}
|
|
24
26
|
|
|
25
27
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
26
28
|
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
{{#if sections.releaseGate}}{{> release-gate}}{{/if}}
|
|
20
20
|
|
|
21
|
+
{{#if sections.briefBand}}{{> brief-band}}{{/if}}
|
|
22
|
+
|
|
21
23
|
{{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate releaseGateShown=sections.releaseGate) }}{{/if}}
|
|
22
24
|
|
|
23
25
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend) }}{{/if}}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div class="section charts-grid{{#if options.heroTrend}} charts-grid--secondary{{/if}}">
|
|
2
|
+
<div class="chart-card">
|
|
3
|
+
<h3>Pass Rate</h3>
|
|
4
|
+
{{#if options.heroTrend}}
|
|
5
|
+
<canvas id="passRateChart" width="150" height="150"></canvas>
|
|
6
|
+
{{else}}
|
|
7
|
+
<canvas id="passRateChart" width="186" height="186"></canvas>
|
|
8
|
+
{{/if}}
|
|
9
|
+
</div>
|
|
10
|
+
<div class="chart-card">
|
|
11
|
+
<h3>Suite Results</h3>
|
|
12
|
+
{{#if options.heroTrend}}
|
|
13
|
+
<canvas id="suitesBarChart" width="300" height="150"></canvas>
|
|
14
|
+
{{else}}
|
|
15
|
+
<canvas id="suitesBarChart" width="390" height="186"></canvas>
|
|
16
|
+
{{/if}}
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{{#if options.showTrend}}
|
|
2
|
+
{{#if charts.trend}}
|
|
3
|
+
<div class="trend-card">
|
|
4
|
+
<div class="trend-card__header">
|
|
5
|
+
<span class="trend-card__title">Pass rate trend — last {{charts.trend.entries.length}} runs</span>
|
|
6
|
+
{{#if charts.trend.delta}}
|
|
7
|
+
<span class="delta {{#if (gt charts.trend.delta 0)}}delta-up{{else}}delta-down{{/if}}">
|
|
8
|
+
{{#if (gt charts.trend.delta 0)}}↑{{else}}↓{{/if}} {{absFixed charts.trend.delta}}% vs previous run
|
|
9
|
+
</span>
|
|
10
|
+
{{/if}}
|
|
11
|
+
</div>
|
|
12
|
+
<canvas id="trendChart" width="680" height="160"></canvas>
|
|
13
|
+
<div class="trend-run-history" id="run-history-table"></div>
|
|
14
|
+
</div>
|
|
15
|
+
{{/if}}
|
|
16
|
+
{{/if}}
|
|
@@ -1,33 +1,10 @@
|
|
|
1
1
|
{{#if options.showCharts}}
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<h3>Suite Results</h3>
|
|
9
|
-
<canvas id="suitesBarChart" width="390" height="186"></canvas>
|
|
10
|
-
</div>
|
|
11
|
-
</div>
|
|
12
|
-
|
|
13
|
-
{{!-- Trend card: detailed + executive + custom templates. Minimal has showTrend=false. --}}
|
|
14
|
-
{{#if options.showTrend}}
|
|
15
|
-
{{#if charts.trend}}
|
|
16
|
-
<div class="trend-card">
|
|
17
|
-
<div class="trend-card__header">
|
|
18
|
-
<span class="trend-card__title">Pass rate trend — last {{charts.trend.entries.length}} runs</span>
|
|
19
|
-
{{!-- Delta badge: falsy guard handles null (< 2 entries) and 0 (identical runs). --}}
|
|
20
|
-
{{#if charts.trend.delta}}
|
|
21
|
-
<span class="delta {{#if (gt charts.trend.delta 0)}}delta-up{{else}}delta-down{{/if}}">
|
|
22
|
-
{{#if (gt charts.trend.delta 0)}}↑{{else}}↓{{/if}} {{absFixed charts.trend.delta}}% vs previous run
|
|
23
|
-
</span>
|
|
24
|
-
{{/if}}
|
|
25
|
-
</div>
|
|
26
|
-
<canvas id="trendChart" width="680" height="160"></canvas>
|
|
27
|
-
{{!-- Run history table is populated by JS below; placeholder must exist before script runs. --}}
|
|
28
|
-
<div class="trend-run-history" id="run-history-table"></div>
|
|
29
|
-
</div>
|
|
30
|
-
{{/if}}
|
|
2
|
+
{{#if options.heroTrend}}
|
|
3
|
+
{{> chart-trend-card options=options}}
|
|
4
|
+
{{> chart-summary-grid options=options}}
|
|
5
|
+
{{else}}
|
|
6
|
+
{{> chart-summary-grid options=options}}
|
|
7
|
+
{{> chart-trend-card options=options}}
|
|
31
8
|
{{/if}}
|
|
32
9
|
|
|
33
10
|
{{#if options.showTrend}}
|
|
@@ -112,7 +89,7 @@
|
|
|
112
89
|
labels: ['Passed', 'Failed', 'Timed Out', 'Skipped', 'Flaky'],
|
|
113
90
|
datasets: [{
|
|
114
91
|
data: [passed, failed, timedOut, skipped, flaky],
|
|
115
|
-
backgroundColor: ['#3F7D58', '#C3553C', '#B5803A', '#7A786E', '#
|
|
92
|
+
backgroundColor: ['#3F7D58', '#C3553C', '#B5803A', '#7A786E', '#CA8A04'],
|
|
116
93
|
borderColor: ['#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5', '#FAF9F5'],
|
|
117
94
|
borderWidth: 2,
|
|
118
95
|
hoverOffset: 4
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<div class="section">
|
|
2
|
+
{{#unless options.releaseGateShown}}
|
|
3
|
+
<span class="verdict verdict--{{stats.verdict}}">
|
|
4
|
+
{{verdictLabel stats.verdict}}
|
|
5
|
+
</span>
|
|
6
|
+
{{/unless}}
|
|
7
|
+
|
|
8
|
+
<div class="hero-summary">
|
|
9
|
+
<div class="hero-summary__value">{{stats.passRate}}<span class="hero-summary__pct">%</span></div>
|
|
10
|
+
<div class="hero-summary__label">Pass Rate</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div class="hero-stat-row">
|
|
14
|
+
<div class="hero-stat">
|
|
15
|
+
<span class="hero-stat__value">{{stats.total}}</span>
|
|
16
|
+
<span class="hero-stat__label">Total</span>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="hero-stat{{#unless stats.failed}} hero-stat--zero{{/unless}}">
|
|
19
|
+
<span class="hero-stat__value" style="color:var(--fail);">{{stats.failed}}</span>
|
|
20
|
+
<span class="hero-stat__label">Failed</span>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="hero-stat{{#unless stats.timedOut}} hero-stat--zero{{/unless}}">
|
|
23
|
+
<span class="hero-stat__value" style="color:var(--timeout);">{{stats.timedOut}}</span>
|
|
24
|
+
<span class="hero-stat__label">Timed Out</span>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="hero-stat{{#unless stats.skipped}} hero-stat--zero{{/unless}}">
|
|
27
|
+
<span class="hero-stat__value" style="color:var(--skip);">{{stats.skipped}}</span>
|
|
28
|
+
<span class="hero-stat__label">Skipped</span>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="hero-stat{{#unless stats.flaky}} hero-stat--zero{{/unless}}">
|
|
31
|
+
<span class="hero-stat__value" style="color:var(--flaky);">{{stats.flaky}}</span>
|
|
32
|
+
<span class="hero-stat__label">Flaky</span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="summary-row">
|
|
37
|
+
<div class="summary-row__item">
|
|
38
|
+
<span class="summary-row__label">Duration</span>
|
|
39
|
+
<span class="summary-row__value">{{formatDuration stats.duration}}</span>
|
|
40
|
+
</div>
|
|
41
|
+
{{#unless (or options.showPassRate @root.sections.charts)}}
|
|
42
|
+
<div class="summary-row__item">
|
|
43
|
+
<span class="summary-row__label">Pass Rate</span>
|
|
44
|
+
<span class="summary-row__value" style="color:var(--pass);">{{stats.passRate}}%</span>
|
|
45
|
+
</div>
|
|
46
|
+
{{/unless}}
|
|
47
|
+
<div class="summary-row__item">
|
|
48
|
+
<span class="summary-row__label">Generated</span>
|
|
49
|
+
<span class="summary-row__value">{{formatDate meta.generatedAt}}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div class="summary-row__item">
|
|
52
|
+
<span class="summary-row__label">Project</span>
|
|
53
|
+
<span class="summary-row__value">{{meta.projectName}}</span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{{#if stats.flaky}}
|
|
58
|
+
<div class="flaky-callout">
|
|
59
|
+
<span class="flaky-callout__icon">⚠</span>
|
|
60
|
+
<div class="flaky-callout__body">
|
|
61
|
+
<span class="flaky-callout__title">{{stats.flaky}} Flaky Test{{#if (gt stats.flaky 1)}}s{{/if}} Detected</span>
|
|
62
|
+
<span class="flaky-callout__text">These tests passed on retry. Results may vary across re-runs. Investigate before shipping to production.</span>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
{{/if}}
|
|
66
|
+
</div>
|
|
@@ -635,6 +635,28 @@ code {
|
|
|
635
635
|
|
|
636
636
|
/* These section styles ship to every template so any section can be toggled in styled; ~a few KB of unused CSS in templates where the section is off is an accepted cost. */
|
|
637
637
|
|
|
638
|
+
/* ── Brief band — the executive lede sentence, shared so any template can
|
|
639
|
+
toggle briefBand on. No border/side-stripe (deliberately distinct from the
|
|
640
|
+
card recipe used everywhere else); a tinted surface instead. ── */
|
|
641
|
+
.brief-band {
|
|
642
|
+
display: flex; align-items: flex-start; gap: 14px;
|
|
643
|
+
background: var(--surface-2);
|
|
644
|
+
padding: 18px 22px 20px;
|
|
645
|
+
border-radius: 6px;
|
|
646
|
+
margin-bottom: 22px;
|
|
647
|
+
}
|
|
648
|
+
.brief-band__quote {
|
|
649
|
+
font-family: 'Source Serif Pro', Georgia, serif;
|
|
650
|
+
font-size: 40px; font-weight: 700;
|
|
651
|
+
color: var(--accent); opacity: .5; line-height: .6;
|
|
652
|
+
flex-shrink: 0; margin-top: 8px;
|
|
653
|
+
}
|
|
654
|
+
.brief-band__text {
|
|
655
|
+
font-family: 'Source Serif Pro', Georgia, serif;
|
|
656
|
+
font-style: italic; font-size: 15px;
|
|
657
|
+
color: var(--text-1); line-height: 1.55;
|
|
658
|
+
}
|
|
659
|
+
|
|
638
660
|
/* Charts grid + cards (available to any template via sections.charts) */
|
|
639
661
|
.charts-grid {
|
|
640
662
|
display: grid; grid-template-columns: 210px 1fr;
|
|
@@ -131,6 +131,37 @@
|
|
|
131
131
|
.chart-card h3 {
|
|
132
132
|
letter-spacing: 1.3px;
|
|
133
133
|
}
|
|
134
|
+
.charts-grid--secondary {
|
|
135
|
+
grid-template-columns: 1fr 1fr;
|
|
136
|
+
margin-top: 4px;
|
|
137
|
+
}
|
|
134
138
|
|
|
135
139
|
/* Executive failures now render inline in the suite breakdown (compact, no
|
|
136
140
|
screenshots) — card styles live in base.css. */
|
|
141
|
+
|
|
142
|
+
/* ── Hero summary — executive-exclusive replacement for the 6-card KPI strip ── */
|
|
143
|
+
.hero-summary { text-align: center; padding: 18px 0 2px; }
|
|
144
|
+
.hero-summary__value {
|
|
145
|
+
font-family: 'Source Serif Pro', Georgia, serif;
|
|
146
|
+
font-size: 68px; font-weight: 700; color: var(--pass);
|
|
147
|
+
letter-spacing: -1.5px; line-height: 1;
|
|
148
|
+
}
|
|
149
|
+
.hero-summary__pct { font-size: 32px; }
|
|
150
|
+
.hero-summary__label {
|
|
151
|
+
font-family: 'Inter', system-ui, sans-serif; font-size: 10px; font-weight: 600;
|
|
152
|
+
letter-spacing: 1.4px; text-transform: uppercase; color: var(--text-3); margin-top: 6px;
|
|
153
|
+
}
|
|
154
|
+
.hero-stat-row { display: flex; justify-content: center; gap: 0; margin: 18px 0 24px; }
|
|
155
|
+
.hero-stat {
|
|
156
|
+
display: flex; flex-direction: column; align-items: center;
|
|
157
|
+
padding: 0 22px; border-right: 1px solid var(--border);
|
|
158
|
+
}
|
|
159
|
+
.hero-stat:last-child { border-right: none; }
|
|
160
|
+
.hero-stat--zero { opacity: .45; }
|
|
161
|
+
.hero-stat__value {
|
|
162
|
+
font-family: 'Source Serif Pro', Georgia, serif; font-size: 20px; font-weight: 700; color: var(--text-1);
|
|
163
|
+
}
|
|
164
|
+
.hero-stat__label {
|
|
165
|
+
display: block; font-family: 'Inter', system-ui, sans-serif; font-size: 8px; font-weight: 600;
|
|
166
|
+
letter-spacing: 1px; text-transform: uppercase; color: var(--text-3); margin-top: 4px;
|
|
167
|
+
}
|
|
@@ -6,9 +6,17 @@
|
|
|
6
6
|
padding: 20px 22px;
|
|
7
7
|
background: var(--surface);
|
|
8
8
|
border: 1px solid var(--border-2);
|
|
9
|
-
border-left: 3px solid var(--primary, #CC785C);
|
|
10
9
|
border-radius: 6px;
|
|
11
10
|
margin-bottom: 22px;
|
|
11
|
+
position: relative;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
}
|
|
14
|
+
.report-header::before {
|
|
15
|
+
content: '';
|
|
16
|
+
position: absolute; top: 0; left: 0; right: 0; height: 3px;
|
|
17
|
+
background: var(--accent);
|
|
18
|
+
-webkit-print-color-adjust: exact;
|
|
19
|
+
print-color-adjust: exact;
|
|
12
20
|
}
|
|
13
21
|
.report-header__logo { height: 36px; width: auto; }
|
|
14
22
|
.report-header__title {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reportforge/playwright-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Playwright Test reporter that generates designed PDF reports: minimal, detailed, and executive templates with CI/CD integrations",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ReportForge",
|