@reportforge/playwright-pdf 0.6.3 → 0.7.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 +12 -10
- package/dist/cli/index.js +93 -34
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +71 -13
- package/dist/index.mjs +71 -13
- package/dist/templates/layouts/detailed.hbs +2 -8
- package/dist/templates/layouts/executive.hbs +2 -8
- package/dist/templates/layouts/minimal.hbs +2 -8
- package/dist/templates/partials/charts.hbs +13 -7
- package/dist/templates/partials/executive-summary.hbs +18 -4
- package/dist/templates/partials/failure-card.hbs +27 -0
- package/dist/templates/partials/suite-breakdown.hbs +31 -5
- package/dist/templates/styles/base.css +98 -26
- package/dist/templates/styles/detailed.css +1 -44
- package/dist/templates/styles/executive.css +2 -32
- package/dist/templates/styles/minimal.css +1 -46
- package/package.json +1 -1
- package/dist/templates/partials/failure-analysis.hbs +0 -42
- package/dist/templates/partials/failure-deep-dive.hbs +0 -62
- package/dist/templates/partials/slow-tests.hbs +0 -27
package/dist/index.mjs
CHANGED
|
@@ -10330,7 +10330,8 @@ var DEFAULT_OPTIONS = {
|
|
|
10330
10330
|
maxFileSizeMb: 8,
|
|
10331
10331
|
historySize: 10,
|
|
10332
10332
|
showTrend: true,
|
|
10333
|
-
flakinessTopN: 5
|
|
10333
|
+
flakinessTopN: 5,
|
|
10334
|
+
slowTestThreshold: 10
|
|
10334
10335
|
};
|
|
10335
10336
|
|
|
10336
10337
|
// src/templates/sections.ts
|
|
@@ -10407,7 +10408,10 @@ var SECTION_DEFAULTS = {
|
|
|
10407
10408
|
trend: true,
|
|
10408
10409
|
requirementsMatrix: false,
|
|
10409
10410
|
ciEnvironment: true,
|
|
10410
|
-
|
|
10411
|
+
// Suite breakdown now hosts the inline failure detail + slow badge, so the
|
|
10412
|
+
// executive layout renders it too (compact: fullFailures stays false → no
|
|
10413
|
+
// screenshots; failureAnalysis stays off → no per-test chip).
|
|
10414
|
+
suiteBreakdown: true,
|
|
10411
10415
|
failureDeepDive: true,
|
|
10412
10416
|
failureAnalysis: false,
|
|
10413
10417
|
slowTests: true,
|
|
@@ -10434,6 +10438,7 @@ function resolveSections(template, userSections) {
|
|
|
10434
10438
|
}
|
|
10435
10439
|
const result = { ...base, ...flat, ...perTemplate };
|
|
10436
10440
|
if (!result.charts) result.trend = false;
|
|
10441
|
+
if (!result.failureDeepDive) result.failureAnalysis = false;
|
|
10437
10442
|
return result;
|
|
10438
10443
|
}
|
|
10439
10444
|
function allSectionsOn() {
|
|
@@ -10510,6 +10515,7 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10510
10515
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
10511
10516
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
10512
10517
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10518
|
+
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10513
10519
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
10514
10520
|
templatePath: external_exports.union([
|
|
10515
10521
|
external_exports.string().regex(/\.hbs$/, "templatePath must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "templatePath basename must not be empty"),
|
|
@@ -11173,6 +11179,14 @@ function classifyOne(f, model) {
|
|
|
11173
11179
|
function classifyAll(failures, model) {
|
|
11174
11180
|
return failures.map((f) => classifyOne(f, model));
|
|
11175
11181
|
}
|
|
11182
|
+
function perTestClassifications(classified) {
|
|
11183
|
+
return classified.map((c) => ({
|
|
11184
|
+
testId: c.failure.testId,
|
|
11185
|
+
category: c.category,
|
|
11186
|
+
message: representativeMessage(c.failure.error.message),
|
|
11187
|
+
strength: strengthOf(c.margin)
|
|
11188
|
+
}));
|
|
11189
|
+
}
|
|
11176
11190
|
function clusterClassified(classified) {
|
|
11177
11191
|
const groups = /* @__PURE__ */ new Map();
|
|
11178
11192
|
for (const c of classified) {
|
|
@@ -11348,6 +11362,7 @@ function runFailureAnalysis(reportData, options, cwd) {
|
|
|
11348
11362
|
const analysed = reportData.failures.slice(0, cap);
|
|
11349
11363
|
const model = loadModel(fa.autoUpdateModel);
|
|
11350
11364
|
const classified = classifyAll(analysed, model);
|
|
11365
|
+
reportData.classifications = perTestClassifications(classified);
|
|
11351
11366
|
if (templatesConsumeAnalysis(options)) {
|
|
11352
11367
|
reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
|
|
11353
11368
|
}
|
|
@@ -12034,6 +12049,7 @@ function jsonForInlineScript(value) {
|
|
|
12034
12049
|
function isUnknownValue(v) {
|
|
12035
12050
|
return !v || v === "unknown";
|
|
12036
12051
|
}
|
|
12052
|
+
var SLOW_MEDIAN_FACTOR = 2;
|
|
12037
12053
|
function deriveOptions(s) {
|
|
12038
12054
|
return {
|
|
12039
12055
|
showCharts: s.charts,
|
|
@@ -12070,17 +12086,17 @@ var TemplateEngine = class {
|
|
|
12070
12086
|
setChartjsBundle(inline) {
|
|
12071
12087
|
this.chartjsInline = inline;
|
|
12072
12088
|
}
|
|
12073
|
-
render(data, template, userSections) {
|
|
12089
|
+
render(data, template, userSections, slowTestThreshold) {
|
|
12074
12090
|
const layoutPath = path7.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
12075
12091
|
if (!fs5.existsSync(layoutPath)) {
|
|
12076
12092
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
12077
12093
|
}
|
|
12078
12094
|
const layoutSource = fs5.readFileSync(layoutPath, "utf8");
|
|
12079
12095
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
12080
|
-
const ctx = this.buildContext(data, template, userSections);
|
|
12096
|
+
const ctx = this.buildContext(data, template, userSections, slowTestThreshold);
|
|
12081
12097
|
return compiledLayout(ctx);
|
|
12082
12098
|
}
|
|
12083
|
-
renderFromPath(data, filePath) {
|
|
12099
|
+
renderFromPath(data, filePath, slowTestThreshold) {
|
|
12084
12100
|
if (!this.customTemplateCache.has(filePath)) {
|
|
12085
12101
|
if (!fs5.existsSync(filePath)) {
|
|
12086
12102
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
@@ -12089,32 +12105,41 @@ var TemplateEngine = class {
|
|
|
12089
12105
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
12090
12106
|
}
|
|
12091
12107
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
12092
|
-
return compiledFn(this.buildCustomContext(data));
|
|
12108
|
+
return compiledFn(this.buildCustomContext(data, slowTestThreshold));
|
|
12093
12109
|
}
|
|
12094
|
-
buildCustomContext(data) {
|
|
12110
|
+
buildCustomContext(data, slowTestThreshold) {
|
|
12095
12111
|
const sections = allSectionsOn();
|
|
12112
|
+
const slow = this.buildSlowTests(data);
|
|
12096
12113
|
return {
|
|
12097
12114
|
...data,
|
|
12115
|
+
projects: this.enrichProjects(data, slow, sections, slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold),
|
|
12098
12116
|
options: deriveOptions(sections),
|
|
12099
12117
|
sections,
|
|
12100
12118
|
tagGroups: this.buildTagGroups(data),
|
|
12101
|
-
slowTests:
|
|
12119
|
+
slowTests: slow.ranked,
|
|
12102
12120
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12103
12121
|
templateCSS: "",
|
|
12104
12122
|
chartjsScript: this.buildChartjsScript()
|
|
12105
12123
|
};
|
|
12106
12124
|
}
|
|
12107
|
-
buildContext(data, template, userSections) {
|
|
12125
|
+
buildContext(data, template, userSections, slowTestThreshold) {
|
|
12108
12126
|
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
12109
12127
|
const templateCSS = this.readStyle(`${template}.css`);
|
|
12110
12128
|
const sections = resolveSections(template, userSections);
|
|
12111
12129
|
const options = deriveOptions(sections);
|
|
12130
|
+
const slow = this.buildSlowTests(data);
|
|
12112
12131
|
return {
|
|
12113
12132
|
...data,
|
|
12133
|
+
projects: this.enrichProjects(
|
|
12134
|
+
data,
|
|
12135
|
+
slow,
|
|
12136
|
+
sections,
|
|
12137
|
+
slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
12138
|
+
),
|
|
12114
12139
|
options,
|
|
12115
12140
|
sections,
|
|
12116
12141
|
tagGroups: this.buildTagGroups(data),
|
|
12117
|
-
slowTests:
|
|
12142
|
+
slowTests: slow.ranked,
|
|
12118
12143
|
baseCSS,
|
|
12119
12144
|
templateCSS,
|
|
12120
12145
|
chartjsScript: this.buildChartjsScript()
|
|
@@ -12151,6 +12176,7 @@ var TemplateEngine = class {
|
|
|
12151
12176
|
for (const test of suite.tests) {
|
|
12152
12177
|
if (test.duration > 0) {
|
|
12153
12178
|
tests.push({
|
|
12179
|
+
id: test.id,
|
|
12154
12180
|
title: test.title,
|
|
12155
12181
|
suiteTitle: suite.title,
|
|
12156
12182
|
durationMs: test.duration,
|
|
@@ -12162,7 +12188,39 @@ var TemplateEngine = class {
|
|
|
12162
12188
|
}
|
|
12163
12189
|
}
|
|
12164
12190
|
}
|
|
12165
|
-
|
|
12191
|
+
const sortedDurations = tests.map((t) => t.durationMs).sort((a, b) => a - b);
|
|
12192
|
+
const medianMs = sortedDurations.length ? sortedDurations[Math.floor((sortedDurations.length - 1) / 2)] : 0;
|
|
12193
|
+
const ranked = tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
12194
|
+
return { ranked, timedTotal: tests.length, medianMs };
|
|
12195
|
+
}
|
|
12196
|
+
/**
|
|
12197
|
+
* Project the suite tree into its render form: attach each test's failure detail
|
|
12198
|
+
* (by testId), root-cause chip, and slow rank. A pure projection — the source
|
|
12199
|
+
* ReportData is never mutated. Gating lives here (not the template): a field is
|
|
12200
|
+
* attached only when its section is on, so the template just checks presence.
|
|
12201
|
+
* `failuresById` is built from the (possibly overflow-trimmed) `failures` array,
|
|
12202
|
+
* so a trimmed failure yields no inline card; the overflow note points to the sidecar.
|
|
12203
|
+
*/
|
|
12204
|
+
enrichProjects(data, slow, sections, slowTestThreshold) {
|
|
12205
|
+
const failuresById = sections.failureDeepDive ? new Map(data.failures.map((f) => [f.testId, f])) : /* @__PURE__ */ new Map();
|
|
12206
|
+
const chipById = sections.failureAnalysis ? new Map((data.classifications ?? []).map((c) => [c.testId, c])) : /* @__PURE__ */ new Map();
|
|
12207
|
+
const slowRankById = /* @__PURE__ */ new Map();
|
|
12208
|
+
if (sections.slowTests && slow.timedTotal > slowTestThreshold) {
|
|
12209
|
+
const floorMs = slow.medianMs * SLOW_MEDIAN_FACTOR;
|
|
12210
|
+
slow.ranked.filter((t) => t.durationMs >= floorMs).forEach((t, i) => slowRankById.set(t.id, i + 1));
|
|
12211
|
+
}
|
|
12212
|
+
const enrichTest = (t) => ({
|
|
12213
|
+
...t,
|
|
12214
|
+
failure: failuresById.get(t.id),
|
|
12215
|
+
chip: chipById.get(t.id),
|
|
12216
|
+
slowRank: slowRankById.get(t.id)
|
|
12217
|
+
});
|
|
12218
|
+
const enrichSuite = (s) => ({
|
|
12219
|
+
...s,
|
|
12220
|
+
suites: s.suites.map(enrichSuite),
|
|
12221
|
+
tests: s.tests.map(enrichTest)
|
|
12222
|
+
});
|
|
12223
|
+
return data.projects.map((p) => ({ ...p, suites: p.suites.map(enrichSuite) }));
|
|
12166
12224
|
}
|
|
12167
12225
|
readStyle(filename) {
|
|
12168
12226
|
const cached = this.styleCache.get(filename);
|
|
@@ -12841,7 +12899,7 @@ var PdfGenerator = class {
|
|
|
12841
12899
|
}
|
|
12842
12900
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
12843
12901
|
logger.info(`Rendering template: ${templateLabel}`);
|
|
12844
|
-
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
|
|
12902
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections, options.slowTestThreshold) : this.templateEngine.renderFromPath(renderData, source.absolutePath, options.slowTestThreshold);
|
|
12845
12903
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
12846
12904
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
12847
12905
|
try {
|
|
@@ -13238,7 +13296,7 @@ var PdfReporter = class {
|
|
|
13238
13296
|
this.dataCollector = new DataCollector();
|
|
13239
13297
|
this.pdfGenerator = new PdfGenerator();
|
|
13240
13298
|
this.options = parseOptions(rawOptions);
|
|
13241
|
-
const version = true ? "0.
|
|
13299
|
+
const version = true ? "0.7.1" : "0.x";
|
|
13242
13300
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13243
13301
|
this.licenseClient = new LicenseClient({
|
|
13244
13302
|
licenseKey: this.options.licenseKey,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
{{#if sections.releaseGate}}{{> release-gate}}{{/if}}
|
|
20
20
|
|
|
21
|
-
{{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate) }}{{/if}}
|
|
21
|
+
{{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate releaseGateShown=sections.releaseGate) }}{{/if}}
|
|
22
22
|
|
|
23
23
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend) }}{{/if}}
|
|
24
24
|
|
|
@@ -26,13 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
{{#if sections.ciEnvironment}}{{> ci-environment options=(obj showFullEnvironment=sections.fullEnvironment) }}{{/if}}
|
|
28
28
|
|
|
29
|
-
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries) }}{{/if}}
|
|
30
|
-
|
|
31
|
-
{{#if sections.failureDeepDive}}{{> failure-deep-dive options=(obj showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
32
|
-
|
|
33
|
-
{{#if sections.failureAnalysis}}{{> failure-analysis}}{{/if}}
|
|
34
|
-
|
|
35
|
-
{{#if sections.slowTests}}{{> slow-tests limit=10 showProject=true}}{{/if}}
|
|
29
|
+
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
36
30
|
|
|
37
31
|
{{#if sections.defectLog}}{{> defect-log }}{{/if}}
|
|
38
32
|
|
|
@@ -18,23 +18,17 @@
|
|
|
18
18
|
|
|
19
19
|
{{#if sections.releaseGate}}{{> release-gate}}{{/if}}
|
|
20
20
|
|
|
21
|
-
{{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate) }}{{/if}}
|
|
21
|
+
{{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate releaseGateShown=sections.releaseGate) }}{{/if}}
|
|
22
22
|
|
|
23
23
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend) }}{{/if}}
|
|
24
24
|
|
|
25
25
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
26
26
|
|
|
27
|
-
{{#if sections.slowTests}}{{> slow-tests limit=5 showProject=false}}{{/if}}
|
|
28
|
-
|
|
29
|
-
{{#if sections.failureDeepDive}}{{> failure-deep-dive options=(obj showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
30
|
-
|
|
31
|
-
{{#if sections.failureAnalysis}}{{> failure-analysis}}{{/if}}
|
|
32
|
-
|
|
33
27
|
{{#if sections.defectLog}}{{> defect-log }}{{/if}}
|
|
34
28
|
|
|
35
29
|
{{#if sections.ciEnvironment}}{{> ci-environment options=(obj showFullEnvironment=sections.fullEnvironment) }}{{/if}}
|
|
36
30
|
|
|
37
|
-
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries) }}{{/if}}
|
|
31
|
+
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
38
32
|
|
|
39
33
|
<div class="report-footer">
|
|
40
34
|
<span>{{meta.projectName}} · Playwright Test Report</span>
|
|
@@ -18,19 +18,13 @@
|
|
|
18
18
|
|
|
19
19
|
{{#if sections.releaseGate}}{{> release-gate}}{{/if}}
|
|
20
20
|
|
|
21
|
-
{{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate) }}{{/if}}
|
|
21
|
+
{{#if sections.summary}}{{> executive-summary options=(obj showPassRate=sections.passRate releaseGateShown=sections.releaseGate) }}{{/if}}
|
|
22
22
|
|
|
23
23
|
{{#if sections.charts}}{{> charts options=(obj showCharts=true showTrend=sections.trend) }}{{/if}}
|
|
24
24
|
|
|
25
25
|
{{#if sections.requirementsMatrix}}{{> requirements-matrix }}{{/if}}
|
|
26
26
|
|
|
27
|
-
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries) }}{{/if}}
|
|
28
|
-
|
|
29
|
-
{{#if sections.failureDeepDive}}{{> failure-deep-dive options=(obj showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
30
|
-
|
|
31
|
-
{{#if sections.failureAnalysis}}{{> failure-analysis}}{{/if}}
|
|
32
|
-
|
|
33
|
-
{{#if sections.slowTests}}{{> slow-tests limit=10 showProject=true}}{{/if}}
|
|
27
|
+
{{#if sections.suiteBreakdown}}{{> suite-breakdown options=(obj showRetries=sections.retries showFullFailures=sections.fullFailures showStackTraces=sections.stackTraces) }}{{/if}}
|
|
34
28
|
|
|
35
29
|
{{#if sections.defectLog}}{{> defect-log }}{{/if}}
|
|
36
30
|
|
|
@@ -166,14 +166,14 @@
|
|
|
166
166
|
y: {
|
|
167
167
|
stacked: true,
|
|
168
168
|
// Widen the label gutter so the longest label isn't clipped at the canvas edge.
|
|
169
|
-
afterFit: function(scale) { if (!scale.$rfPadded) { scale.$rfPadded = true; scale.width +=
|
|
169
|
+
afterFit: function(scale) { if (!scale.$rfPadded) { scale.$rfPadded = true; scale.width += 30; } },
|
|
170
170
|
ticks: {
|
|
171
171
|
font: { size: 9, family: 'Inter, sans-serif' }, color: '#1F1F1C', autoSkip: false,
|
|
172
|
-
// Truncate long suite / describe labels
|
|
173
|
-
// instead of clipping at the canvas edge.
|
|
172
|
+
// Truncate only very long suite / describe labels; the wider gutter above
|
|
173
|
+
// lets most labels show in full instead of clipping at the canvas edge.
|
|
174
174
|
callback: function(value) {
|
|
175
175
|
var l = this.getLabelForValue(value);
|
|
176
|
-
return l.length >
|
|
176
|
+
return l.length > 24 ? l.slice(0, 23) + '…' : l;
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
179
|
grid: { display: false },
|
|
@@ -215,11 +215,17 @@
|
|
|
215
215
|
|
|
216
216
|
// Smart y-axis: pad above/below the actual data range so variation is readable
|
|
217
217
|
// rather than a flat line at the top of a 0–100 axis when all runs pass at 96%.
|
|
218
|
+
// Pick the step FIRST, then snap the bounds to multiples of that step, so every
|
|
219
|
+
// tick (min, min+step, … max) is a round value — never 45% / 70% from a min that
|
|
220
|
+
// isn't itself a multiple of the step. Clamps to 0/100 keep the bounds round too.
|
|
218
221
|
var minRate = Math.min.apply(null, trendRates);
|
|
219
222
|
var maxRate = Math.max.apply(null, trendRates);
|
|
220
223
|
var yPad = Math.max(8, Math.round((maxRate - minRate) * 0.4));
|
|
221
|
-
var
|
|
222
|
-
var
|
|
224
|
+
var rawMin = Math.max(0, minRate - yPad);
|
|
225
|
+
var rawMax = Math.min(100, maxRate + yPad);
|
|
226
|
+
var yStep = (rawMax - rawMin) <= 20 ? 5 : (rawMax - rawMin) <= 50 ? 10 : 25;
|
|
227
|
+
var yMin = Math.max(0, Math.floor(rawMin / yStep) * yStep);
|
|
228
|
+
var yMax = Math.min(100, Math.ceil(rawMax / yStep) * yStep);
|
|
223
229
|
|
|
224
230
|
// Data-label plugin: renders pass-rate % above each point.
|
|
225
231
|
// Canvas ID guard prevents firing on the doughnut and bar charts.
|
|
@@ -279,7 +285,7 @@
|
|
|
279
285
|
callback: function(v) { return v + '%'; },
|
|
280
286
|
font: { size: 8.5, family: 'Inter, sans-serif' },
|
|
281
287
|
color: '#8C8B82',
|
|
282
|
-
|
|
288
|
+
stepSize: yStep,
|
|
283
289
|
},
|
|
284
290
|
grid: { color: 'rgba(31,31,28,0.07)' },
|
|
285
291
|
border: { color: 'transparent' },
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
<div class="section">
|
|
2
|
+
{{!-- The release gate (when the layout renders it) already states the verdict
|
|
3
|
+
prominently, so the chip would just repeat it — suppress it then. The layout
|
|
4
|
+
passes options.releaseGateShown; it is absent for custom templates, so the
|
|
5
|
+
chip renders by default there (a custom template may not include a gate). --}}
|
|
6
|
+
{{#unless options.releaseGateShown}}
|
|
2
7
|
<span class="verdict verdict--{{stats.verdict}}">
|
|
3
8
|
{{verdictLabel stats.verdict}}
|
|
4
9
|
</span>
|
|
10
|
+
{{/unless}}
|
|
5
11
|
|
|
6
12
|
<div class="kpi-strip">
|
|
7
13
|
<div class="kpi-card kpi-card--total">
|
|
@@ -15,19 +21,21 @@
|
|
|
15
21
|
<div class="kpi-card__rate">{{stats.passRate}}%</div>
|
|
16
22
|
{{/if}}
|
|
17
23
|
</div>
|
|
18
|
-
|
|
24
|
+
{{!-- Zero-value status cards are dimmed: a coloured "0" for an absent status
|
|
25
|
+
draws the eye to nothing and dilutes the counts that matter. --}}
|
|
26
|
+
<div class="kpi-card kpi-card--fail{{#unless stats.failed}} kpi-card--zero{{/unless}}">
|
|
19
27
|
<div class="kpi-card__value">{{stats.failed}}</div>
|
|
20
28
|
<div class="kpi-card__label">Failed</div>
|
|
21
29
|
</div>
|
|
22
|
-
<div class="kpi-card kpi-card--timedout">
|
|
30
|
+
<div class="kpi-card kpi-card--timedout{{#unless stats.timedOut}} kpi-card--zero{{/unless}}">
|
|
23
31
|
<div class="kpi-card__value">{{stats.timedOut}}</div>
|
|
24
32
|
<div class="kpi-card__label">Timed Out</div>
|
|
25
33
|
</div>
|
|
26
|
-
<div class="kpi-card kpi-card--skip">
|
|
34
|
+
<div class="kpi-card kpi-card--skip{{#unless stats.skipped}} kpi-card--zero{{/unless}}">
|
|
27
35
|
<div class="kpi-card__value">{{stats.skipped}}</div>
|
|
28
36
|
<div class="kpi-card__label">Skipped</div>
|
|
29
37
|
</div>
|
|
30
|
-
<div class="kpi-card kpi-card--flaky">
|
|
38
|
+
<div class="kpi-card kpi-card--flaky{{#unless stats.flaky}} kpi-card--zero{{/unless}}">
|
|
31
39
|
<div class="kpi-card__value">{{stats.flaky}}</div>
|
|
32
40
|
<div class="kpi-card__label">Flaky</div>
|
|
33
41
|
</div>
|
|
@@ -38,10 +46,16 @@
|
|
|
38
46
|
<span class="summary-row__label">Duration</span>
|
|
39
47
|
<span class="summary-row__value">{{formatDuration stats.duration}}</span>
|
|
40
48
|
</div>
|
|
49
|
+
{{!-- Pass rate normally shows on the Passed KPI card and the doughnut centre, so
|
|
50
|
+
it is omitted here to avoid a third restatement on one page. Fallback: if
|
|
51
|
+
BOTH of those are off (passRate modifier off AND charts off, e.g. a stripped
|
|
52
|
+
minimal), show it here so the headline metric never disappears entirely. --}}
|
|
53
|
+
{{#unless (or options.showPassRate @root.sections.charts)}}
|
|
41
54
|
<div class="summary-row__item">
|
|
42
55
|
<span class="summary-row__label">Pass Rate</span>
|
|
43
56
|
<span class="summary-row__value" style="color:var(--pass);">{{stats.passRate}}%</span>
|
|
44
57
|
</div>
|
|
58
|
+
{{/unless}}
|
|
45
59
|
<div class="summary-row__item">
|
|
46
60
|
<span class="summary-row__label">Generated</span>
|
|
47
61
|
<span class="summary-row__value">{{formatDate meta.generatedAt}}</span>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{!-- Inline failure detail, rendered inside the single .failure-detail block in
|
|
2
|
+
the suite breakdown (which provides the rail, indent, and page-break guard).
|
|
3
|
+
The test row already shows the title/location, so this is error + screenshot
|
|
4
|
+
+ retry history only. Caller passes the context:
|
|
5
|
+
failure — the FailureRecord for this test
|
|
6
|
+
showFull — include the screenshot (compact templates pass false)
|
|
7
|
+
showStack — render the stack trace instead of the bare message --}}
|
|
8
|
+
{{#if (and failure.error.stack showStack)}}
|
|
9
|
+
<pre>{{failure.error.stack}}</pre>
|
|
10
|
+
{{else}}
|
|
11
|
+
<div class="failure-item__message">{{failure.error.message}}</div>
|
|
12
|
+
{{/if}}
|
|
13
|
+
|
|
14
|
+
{{#if (and showFull failure.screenshotBase64)}}
|
|
15
|
+
<img class="screenshot" src="{{failure.screenshotBase64}}" alt="Screenshot at failure">
|
|
16
|
+
{{/if}}
|
|
17
|
+
|
|
18
|
+
{{#if failure.retryHistory.length}}
|
|
19
|
+
<div class="retry-history">
|
|
20
|
+
Retry history:
|
|
21
|
+
{{#each failure.retryHistory}}
|
|
22
|
+
<span class="retry-attempt retry-attempt--{{status}}">
|
|
23
|
+
#{{addOne attempt}} {{status}} · {{formatDuration duration}}
|
|
24
|
+
</span>
|
|
25
|
+
{{/each}}
|
|
26
|
+
</div>
|
|
27
|
+
{{/if}}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<div class="section">
|
|
2
2
|
<h2>Suite Breakdown</h2>
|
|
3
|
+
{{#if (and overflowFailureCount sections.failureDeepDive)}}
|
|
4
|
+
<div class="overflow-banner">
|
|
5
|
+
Inline failure detail covers the first {{failures.length}} failures.
|
|
6
|
+
{{overflowFailureCount}} more {{#if (gt overflowFailureCount 1)}}are{{else}}is{{/if}}
|
|
7
|
+
available in <code>{{overflowSidecarName}}</code> alongside this report.
|
|
8
|
+
</div>
|
|
9
|
+
{{/if}}
|
|
3
10
|
{{#each projects}}
|
|
4
11
|
<h3 style="color:var(--primary,#CC785C);margin-bottom:8px;">{{name}}</h3>
|
|
5
12
|
{{#each suites}}
|
|
@@ -25,14 +32,14 @@
|
|
|
25
32
|
<thead>
|
|
26
33
|
<tr>
|
|
27
34
|
<th>Test</th>
|
|
28
|
-
<th style="width:
|
|
35
|
+
<th style="width:112px;">Status</th>
|
|
29
36
|
<th style="width:72px;">Duration</th>
|
|
30
|
-
{{#if
|
|
37
|
+
{{#if @root.options.showRetries}}<th style="width:52px;">Retries</th>{{/if}}
|
|
31
38
|
</tr>
|
|
32
39
|
</thead>
|
|
33
40
|
<tbody>
|
|
34
41
|
{{#each tests}}
|
|
35
|
-
<tr class="no-break">
|
|
42
|
+
<tr class="no-break{{#if failure}} test-row--has-detail{{/if}}">
|
|
36
43
|
<td>
|
|
37
44
|
<div style="font-weight:500;">{{title}}</div>
|
|
38
45
|
{{#if location}}<div class="test-path">{{location}}</div>{{/if}}
|
|
@@ -42,10 +49,29 @@
|
|
|
42
49
|
</div>
|
|
43
50
|
{{/if}}
|
|
44
51
|
</td>
|
|
45
|
-
<td
|
|
52
|
+
<td style="white-space:nowrap;">
|
|
53
|
+
{{statusBadge status}}{{#if slowRank}}<span class="badge badge--slow" title="Among the slowest tests">SLOW</span>{{/if}}
|
|
54
|
+
</td>
|
|
46
55
|
<td style="color:var(--text-2);font-family:'JetBrains Mono',monospace;font-size:9.5px;">{{formatDuration duration}}</td>
|
|
47
|
-
{{#if
|
|
56
|
+
{{#if @root.options.showRetries}}<td style="color:var(--text-3);text-align:center;">{{retryCount}}</td>{{/if}}
|
|
57
|
+
</tr>
|
|
58
|
+
{{#if failure}}
|
|
59
|
+
<tr class="failure-row no-break">
|
|
60
|
+
<td colspan="{{#if @root.options.showRetries}}4{{else}}3{{/if}}">
|
|
61
|
+
<div class="failure-detail failure-detail--{{failure.severity}}">
|
|
62
|
+
{{> failure-card failure=failure showFull=@root.options.showFullFailures showStack=@root.options.showStackTraces}}
|
|
63
|
+
{{#if chip}}
|
|
64
|
+
<div class="root-cause-chip root-cause-chip--{{chip.category}}">
|
|
65
|
+
<span class="root-cause-chip__tag">Root cause</span>
|
|
66
|
+
<span class="root-cause-chip__label">{{categoryLabel chip.category}}</span>
|
|
67
|
+
<span class="root-cause-chip__msg">{{chip.message}}</span>
|
|
68
|
+
<span class="root-cause-chip__strength">{{strengthLabel chip.strength}} match</span>
|
|
69
|
+
</div>
|
|
70
|
+
{{/if}}
|
|
71
|
+
</div>
|
|
72
|
+
</td>
|
|
48
73
|
</tr>
|
|
74
|
+
{{/if}}
|
|
49
75
|
{{/each}}
|
|
50
76
|
</tbody>
|
|
51
77
|
</table>
|
|
@@ -153,6 +153,12 @@ tr:last-child td { border-bottom: none; }
|
|
|
153
153
|
|
|
154
154
|
/* ── Page breaks ────────────────────────────────────────────────────────── */
|
|
155
155
|
.no-break { page-break-inside: avoid; }
|
|
156
|
+
/* Keep a failed test row glued to its inline failure detail: the detail block is
|
|
157
|
+
page-break-inside:avoid, so when it can't fit the remaining space it moves to the
|
|
158
|
+
next page — these pull the test row along so the title never orphans above it. */
|
|
159
|
+
.test-row--has-detail { page-break-after: avoid; break-after: avoid; }
|
|
160
|
+
.test-row--has-detail > td { border-bottom: none; }
|
|
161
|
+
.failure-row { page-break-before: avoid; break-before: avoid; }
|
|
156
162
|
|
|
157
163
|
/* ── Section wrapper ────────────────────────────────────────────────────── */
|
|
158
164
|
/* Large sections (suite breakdown, failures, tables) MAY flow across pages —
|
|
@@ -292,12 +298,90 @@ code {
|
|
|
292
298
|
.release-gate__sep { color: var(--border-3); }
|
|
293
299
|
.release-gate__flaky-warn { color: var(--flaky); font-weight: 600; }
|
|
294
300
|
|
|
295
|
-
/* ──
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
.
|
|
299
|
-
.failure-
|
|
300
|
-
.failure-
|
|
301
|
+
/* ── Inline failure detail — ONE block hanging off the failed test row ────────
|
|
302
|
+
A single severity-coloured left rail + hairline dividers tie the error,
|
|
303
|
+
retry history, and root cause together as the failed test's detail (not
|
|
304
|
+
three separate boxes). Lives here so every template's breakdown can use it. */
|
|
305
|
+
.failure-row > td { padding: 0; border: none; background: transparent; }
|
|
306
|
+
.failure-detail {
|
|
307
|
+
border-left: 3px solid var(--fail);
|
|
308
|
+
margin: 2px 10px 10px 12px; /* slight indent under the test row + gap before next row */
|
|
309
|
+
padding: 11px 0 11px 16px;
|
|
310
|
+
page-break-inside: avoid;
|
|
311
|
+
}
|
|
312
|
+
.failure-detail--critical { border-left-color: var(--fail); }
|
|
313
|
+
.failure-detail--high { border-left-color: #A35E1E; }
|
|
314
|
+
.failure-detail--medium { border-left-color: var(--timeout); }
|
|
315
|
+
.failure-detail--low { border-left-color: var(--text-3); }
|
|
316
|
+
|
|
317
|
+
/* error — flat on the rail, no inner box (override the base pre panel) */
|
|
318
|
+
.failure-detail pre {
|
|
319
|
+
margin: 0; padding: 0;
|
|
320
|
+
background: transparent; border: none; border-radius: 0; max-height: none;
|
|
321
|
+
}
|
|
322
|
+
.failure-detail .failure-item__message {
|
|
323
|
+
background: transparent; border: none; border-radius: 0; padding: 0; margin: 0;
|
|
324
|
+
font-family: 'JetBrains Mono', monospace;
|
|
325
|
+
font-size: 9.5px; color: #8A2E1E; word-break: break-word; line-height: 1.6;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* retry history — divider above, same block */
|
|
329
|
+
.retry-history {
|
|
330
|
+
font-size: 9px; color: var(--text-3); margin-top: 11px;
|
|
331
|
+
padding-top: 10px; border-top: 1px solid var(--border);
|
|
332
|
+
font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
|
|
333
|
+
letter-spacing: 0.5px; text-transform: uppercase;
|
|
334
|
+
}
|
|
335
|
+
.retry-attempt {
|
|
336
|
+
display: inline-block; padding: 2px 7px;
|
|
337
|
+
border-radius: 3px; margin-right: 4px;
|
|
338
|
+
font-size: 8.5px; font-family: 'Inter', system-ui, sans-serif; font-weight: 600;
|
|
339
|
+
-webkit-print-color-adjust: exact;
|
|
340
|
+
print-color-adjust: exact;
|
|
341
|
+
}
|
|
342
|
+
.retry-attempt--failed { background: var(--fail-dim); color: var(--fail); border: 1px solid #E5C9BD; }
|
|
343
|
+
.retry-attempt--passed { background: var(--pass-dim); color: var(--pass); border: 1px solid #C5DBC9; }
|
|
344
|
+
|
|
345
|
+
/* Slow-test badge — a compact tag inline beside the status badge (min-width:0
|
|
346
|
+
overrides the .badge 50px floor so it doesn't wrap onto its own line). */
|
|
347
|
+
.badge--slow {
|
|
348
|
+
background: var(--timeout-dim); color: var(--timeout);
|
|
349
|
+
border: 1px solid #E5D2A8;
|
|
350
|
+
min-width: 0; margin-left: 5px;
|
|
351
|
+
padding: 1px 5px; font-size: 7.5px; letter-spacing: 0.4px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/* Root cause — footer of the same failure block: a divider above, no box and no
|
|
355
|
+
second rail (the block already carries one). */
|
|
356
|
+
.root-cause-chip {
|
|
357
|
+
display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px;
|
|
358
|
+
margin-top: 11px; padding-top: 10px;
|
|
359
|
+
border-top: 1px solid var(--border);
|
|
360
|
+
font-family: 'Inter', system-ui, sans-serif; font-size: 9.5px;
|
|
361
|
+
}
|
|
362
|
+
.root-cause-chip__tag {
|
|
363
|
+
font-size: 8px; font-weight: 700; letter-spacing: 0.8px; text-transform: uppercase;
|
|
364
|
+
color: var(--text-3);
|
|
365
|
+
}
|
|
366
|
+
.root-cause-chip__label { font-weight: 700; color: var(--text-1); }
|
|
367
|
+
.root-cause-chip__msg {
|
|
368
|
+
font-family: 'JetBrains Mono', monospace; color: var(--text-2);
|
|
369
|
+
word-break: break-word;
|
|
370
|
+
}
|
|
371
|
+
.root-cause-chip__strength {
|
|
372
|
+
margin-left: auto; font-size: 8.5px; font-weight: 600;
|
|
373
|
+
letter-spacing: 0.5px; text-transform: uppercase; color: var(--text-3);
|
|
374
|
+
}
|
|
375
|
+
/* Subtle category cue — tint only the category name (no box, no second rail), so
|
|
376
|
+
the chip stays "quiet" but failures are still scannable by root-cause type. */
|
|
377
|
+
.root-cause-chip--assertion .root-cause-chip__label,
|
|
378
|
+
.root-cause-chip--locator-not-found .root-cause-chip__label { color: oklch(52% 0.19 25); }
|
|
379
|
+
.root-cause-chip--timeout .root-cause-chip__label { color: oklch(50% 0.13 65); }
|
|
380
|
+
.root-cause-chip--network .root-cause-chip__label,
|
|
381
|
+
.root-cause-chip--navigation .root-cause-chip__label { color: oklch(50% 0.15 260); }
|
|
382
|
+
.root-cause-chip--flaky-by-retry .root-cause-chip__label { color: oklch(50% 0.15 290); }
|
|
383
|
+
.root-cause-chip--env-config .root-cause-chip__label { color: oklch(48% 0.14 145); }
|
|
384
|
+
.root-cause-chip--unknown .root-cause-chip__label { color: var(--text-2); }
|
|
301
385
|
|
|
302
386
|
/* ── History trend card — shared by detailed + executive (charts.hbs) ─────── */
|
|
303
387
|
.trend-card {
|
|
@@ -586,6 +670,11 @@ code {
|
|
|
586
670
|
print-color-adjust: exact;
|
|
587
671
|
}
|
|
588
672
|
|
|
673
|
+
/* Dimmed zero-value KPI card — a coloured "0" for an absent status (no timeouts,
|
|
674
|
+
no flakes) draws the eye to nothing; muting it lets the real counts lead. Shared
|
|
675
|
+
modifier so every template's kpi-strip gets it. */
|
|
676
|
+
.kpi-card--zero { opacity: 0.42; }
|
|
677
|
+
|
|
589
678
|
/* ── Suite breakdown — shared so any template (incl. executive) can toggle it on ─ */
|
|
590
679
|
/* Values match the detailed template; minimal.css keeps its own tuned overrides
|
|
591
680
|
for .suite-group (margin) and .suite-stats (gap), which win via templateCSS order. */
|
|
@@ -656,23 +745,6 @@ code {
|
|
|
656
745
|
font-size: 9.5px; color: var(--text-1);
|
|
657
746
|
}
|
|
658
747
|
|
|
659
|
-
/*
|
|
660
|
-
|
|
661
|
-
.
|
|
662
|
-
.failure-analysis .oneliner { font-weight: 600; }
|
|
663
|
-
.analysis-truncated { font-size: 0.85em; opacity: 0.8; }
|
|
664
|
-
|
|
665
|
-
.cluster { border-left: 3px solid; padding-left: 12px; margin-bottom: 16px; }
|
|
666
|
-
.cluster-header { display: flex; gap: 12px; align-items: baseline; }
|
|
667
|
-
.cluster-strength--strong { color: oklch(55% 0.15 145); }
|
|
668
|
-
.cluster-strength--moderate { color: oklch(60% 0.18 65); }
|
|
669
|
-
.cluster-strength--weak { color: oklch(55% 0.20 25); }
|
|
670
|
-
.cluster--timeout { border-color: oklch(60% 0.18 65); }
|
|
671
|
-
.cluster--assertion { border-color: oklch(55% 0.20 25); }
|
|
672
|
-
.cluster--locator-not-found { border-color: oklch(55% 0.20 25); }
|
|
673
|
-
.cluster--network { border-color: oklch(55% 0.15 260); }
|
|
674
|
-
.cluster--navigation { border-color: oklch(55% 0.15 260); }
|
|
675
|
-
.cluster--flaky-by-retry { border-color: oklch(55% 0.15 290); }
|
|
676
|
-
.cluster--env-config { border-color: oklch(55% 0.15 145); }
|
|
677
|
-
.cluster--unknown { border-color: oklch(60% 0.00 0); }
|
|
678
|
-
.confidence-warning { font-size: 0.8em; opacity: 0.75; }
|
|
748
|
+
/* The aggregate failure-analysis cluster section was retired — root causes now
|
|
749
|
+
render as inline per-test chips in the suite breakdown (.root-cause-chip above).
|
|
750
|
+
The .analysis-oneliner one-liner lives further up and is still used by executive. */
|