@reportforge/playwright-pdf 0.6.2 → 0.7.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 +15 -0
- package/README.md +15 -13
- package/dist/{demo/cli.js → cli/index.js} +5726 -5267
- package/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +67 -13
- package/dist/index.mjs +67 -13
- package/dist/templates/layouts/detailed.hbs +1 -7
- package/dist/templates/layouts/executive.hbs +1 -7
- package/dist/templates/layouts/minimal.hbs +1 -7
- package/dist/templates/partials/failure-card.hbs +27 -0
- package/dist/templates/partials/suite-breakdown.hbs +30 -4
- package/dist/templates/styles/base.css +87 -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 +2 -3
- package/dist/cli/export-feedback.js +0 -87
- 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.d.mts
CHANGED
|
@@ -234,6 +234,14 @@ interface ReporterOptions {
|
|
|
234
234
|
* @default 5
|
|
235
235
|
*/
|
|
236
236
|
flakinessTopN?: number;
|
|
237
|
+
/**
|
|
238
|
+
* Minimum number of timed tests before `SLOW` badges appear in the suite
|
|
239
|
+
* breakdown. The badge marks the 10 slowest tests; below this many timed
|
|
240
|
+
* tests every test would trivially be the "slowest", so badges stay hidden.
|
|
241
|
+
* Set to 0 to always badge the slowest. Must be a non-negative integer.
|
|
242
|
+
* @default 10
|
|
243
|
+
*/
|
|
244
|
+
slowTestThreshold?: number;
|
|
237
245
|
/**
|
|
238
246
|
* Path to a custom Handlebars (.hbs) template file. When set, takes
|
|
239
247
|
* precedence over `template`. Relative paths resolve from `process.cwd()`.
|
package/dist/index.d.ts
CHANGED
|
@@ -234,6 +234,14 @@ interface ReporterOptions {
|
|
|
234
234
|
* @default 5
|
|
235
235
|
*/
|
|
236
236
|
flakinessTopN?: number;
|
|
237
|
+
/**
|
|
238
|
+
* Minimum number of timed tests before `SLOW` badges appear in the suite
|
|
239
|
+
* breakdown. The badge marks the 10 slowest tests; below this many timed
|
|
240
|
+
* tests every test would trivially be the "slowest", so badges stay hidden.
|
|
241
|
+
* Set to 0 to always badge the slowest. Must be a non-negative integer.
|
|
242
|
+
* @default 10
|
|
243
|
+
*/
|
|
244
|
+
slowTestThreshold?: number;
|
|
237
245
|
/**
|
|
238
246
|
* Path to a custom Handlebars (.hbs) template file. When set, takes
|
|
239
247
|
* precedence over `template`. Relative paths resolve from `process.cwd()`.
|
package/dist/index.js
CHANGED
|
@@ -10329,7 +10329,8 @@ var DEFAULT_OPTIONS = {
|
|
|
10329
10329
|
maxFileSizeMb: 8,
|
|
10330
10330
|
historySize: 10,
|
|
10331
10331
|
showTrend: true,
|
|
10332
|
-
flakinessTopN: 5
|
|
10332
|
+
flakinessTopN: 5,
|
|
10333
|
+
slowTestThreshold: 10
|
|
10333
10334
|
};
|
|
10334
10335
|
|
|
10335
10336
|
// src/templates/sections.ts
|
|
@@ -10406,7 +10407,10 @@ var SECTION_DEFAULTS = {
|
|
|
10406
10407
|
trend: true,
|
|
10407
10408
|
requirementsMatrix: false,
|
|
10408
10409
|
ciEnvironment: true,
|
|
10409
|
-
|
|
10410
|
+
// Suite breakdown now hosts the inline failure detail + slow badge, so the
|
|
10411
|
+
// executive layout renders it too (compact: fullFailures stays false → no
|
|
10412
|
+
// screenshots; failureAnalysis stays off → no per-test chip).
|
|
10413
|
+
suiteBreakdown: true,
|
|
10410
10414
|
failureDeepDive: true,
|
|
10411
10415
|
failureAnalysis: false,
|
|
10412
10416
|
slowTests: true,
|
|
@@ -10433,6 +10437,7 @@ function resolveSections(template, userSections) {
|
|
|
10433
10437
|
}
|
|
10434
10438
|
const result = { ...base, ...flat, ...perTemplate };
|
|
10435
10439
|
if (!result.charts) result.trend = false;
|
|
10440
|
+
if (!result.failureDeepDive) result.failureAnalysis = false;
|
|
10436
10441
|
return result;
|
|
10437
10442
|
}
|
|
10438
10443
|
function allSectionsOn() {
|
|
@@ -10509,6 +10514,7 @@ var ReporterOptionsSchema = external_exports.object({
|
|
|
10509
10514
|
historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
|
|
10510
10515
|
showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
|
|
10511
10516
|
flakinessTopN: external_exports.number().int().min(0, "flakinessTopN must be 0 or greater").optional().default(DEFAULT_OPTIONS.flakinessTopN),
|
|
10517
|
+
slowTestThreshold: external_exports.number().int().min(0, "slowTestThreshold must be 0 or greater").optional().default(DEFAULT_OPTIONS.slowTestThreshold),
|
|
10512
10518
|
failureAnalysis: failureAnalysisConfigSchema,
|
|
10513
10519
|
templatePath: external_exports.union([
|
|
10514
10520
|
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"),
|
|
@@ -11172,6 +11178,14 @@ function classifyOne(f, model) {
|
|
|
11172
11178
|
function classifyAll(failures, model) {
|
|
11173
11179
|
return failures.map((f) => classifyOne(f, model));
|
|
11174
11180
|
}
|
|
11181
|
+
function perTestClassifications(classified) {
|
|
11182
|
+
return classified.map((c) => ({
|
|
11183
|
+
testId: c.failure.testId,
|
|
11184
|
+
category: c.category,
|
|
11185
|
+
message: representativeMessage(c.failure.error.message),
|
|
11186
|
+
strength: strengthOf(c.margin)
|
|
11187
|
+
}));
|
|
11188
|
+
}
|
|
11175
11189
|
function clusterClassified(classified) {
|
|
11176
11190
|
const groups = /* @__PURE__ */ new Map();
|
|
11177
11191
|
for (const c of classified) {
|
|
@@ -11347,6 +11361,7 @@ function runFailureAnalysis(reportData, options, cwd) {
|
|
|
11347
11361
|
const analysed = reportData.failures.slice(0, cap);
|
|
11348
11362
|
const model = loadModel(fa.autoUpdateModel);
|
|
11349
11363
|
const classified = classifyAll(analysed, model);
|
|
11364
|
+
reportData.classifications = perTestClassifications(classified);
|
|
11350
11365
|
if (templatesConsumeAnalysis(options)) {
|
|
11351
11366
|
reportData.analysis = analyseClassified(classified, reportData.failures.length, fa);
|
|
11352
11367
|
}
|
|
@@ -12069,17 +12084,17 @@ var TemplateEngine = class {
|
|
|
12069
12084
|
setChartjsBundle(inline) {
|
|
12070
12085
|
this.chartjsInline = inline;
|
|
12071
12086
|
}
|
|
12072
|
-
render(data, template, userSections) {
|
|
12087
|
+
render(data, template, userSections, slowTestThreshold) {
|
|
12073
12088
|
const layoutPath = import_path3.default.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
12074
12089
|
if (!import_fs4.default.existsSync(layoutPath)) {
|
|
12075
12090
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
12076
12091
|
}
|
|
12077
12092
|
const layoutSource = import_fs4.default.readFileSync(layoutPath, "utf8");
|
|
12078
12093
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
12079
|
-
const ctx = this.buildContext(data, template, userSections);
|
|
12094
|
+
const ctx = this.buildContext(data, template, userSections, slowTestThreshold);
|
|
12080
12095
|
return compiledLayout(ctx);
|
|
12081
12096
|
}
|
|
12082
|
-
renderFromPath(data, filePath) {
|
|
12097
|
+
renderFromPath(data, filePath, slowTestThreshold) {
|
|
12083
12098
|
if (!this.customTemplateCache.has(filePath)) {
|
|
12084
12099
|
if (!import_fs4.default.existsSync(filePath)) {
|
|
12085
12100
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
@@ -12088,32 +12103,41 @@ var TemplateEngine = class {
|
|
|
12088
12103
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
12089
12104
|
}
|
|
12090
12105
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
12091
|
-
return compiledFn(this.buildCustomContext(data));
|
|
12106
|
+
return compiledFn(this.buildCustomContext(data, slowTestThreshold));
|
|
12092
12107
|
}
|
|
12093
|
-
buildCustomContext(data) {
|
|
12108
|
+
buildCustomContext(data, slowTestThreshold) {
|
|
12094
12109
|
const sections = allSectionsOn();
|
|
12110
|
+
const slow = this.buildSlowTests(data);
|
|
12095
12111
|
return {
|
|
12096
12112
|
...data,
|
|
12113
|
+
projects: this.enrichProjects(data, slow, sections, slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold),
|
|
12097
12114
|
options: deriveOptions(sections),
|
|
12098
12115
|
sections,
|
|
12099
12116
|
tagGroups: this.buildTagGroups(data),
|
|
12100
|
-
slowTests:
|
|
12117
|
+
slowTests: slow.ranked,
|
|
12101
12118
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12102
12119
|
templateCSS: "",
|
|
12103
12120
|
chartjsScript: this.buildChartjsScript()
|
|
12104
12121
|
};
|
|
12105
12122
|
}
|
|
12106
|
-
buildContext(data, template, userSections) {
|
|
12123
|
+
buildContext(data, template, userSections, slowTestThreshold) {
|
|
12107
12124
|
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
12108
12125
|
const templateCSS = this.readStyle(`${template}.css`);
|
|
12109
12126
|
const sections = resolveSections(template, userSections);
|
|
12110
12127
|
const options = deriveOptions(sections);
|
|
12128
|
+
const slow = this.buildSlowTests(data);
|
|
12111
12129
|
return {
|
|
12112
12130
|
...data,
|
|
12131
|
+
projects: this.enrichProjects(
|
|
12132
|
+
data,
|
|
12133
|
+
slow,
|
|
12134
|
+
sections,
|
|
12135
|
+
slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
12136
|
+
),
|
|
12113
12137
|
options,
|
|
12114
12138
|
sections,
|
|
12115
12139
|
tagGroups: this.buildTagGroups(data),
|
|
12116
|
-
slowTests:
|
|
12140
|
+
slowTests: slow.ranked,
|
|
12117
12141
|
baseCSS,
|
|
12118
12142
|
templateCSS,
|
|
12119
12143
|
chartjsScript: this.buildChartjsScript()
|
|
@@ -12150,6 +12174,7 @@ var TemplateEngine = class {
|
|
|
12150
12174
|
for (const test of suite.tests) {
|
|
12151
12175
|
if (test.duration > 0) {
|
|
12152
12176
|
tests.push({
|
|
12177
|
+
id: test.id,
|
|
12153
12178
|
title: test.title,
|
|
12154
12179
|
suiteTitle: suite.title,
|
|
12155
12180
|
durationMs: test.duration,
|
|
@@ -12161,7 +12186,36 @@ var TemplateEngine = class {
|
|
|
12161
12186
|
}
|
|
12162
12187
|
}
|
|
12163
12188
|
}
|
|
12164
|
-
|
|
12189
|
+
const ranked = tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
12190
|
+
return { ranked, timedTotal: tests.length };
|
|
12191
|
+
}
|
|
12192
|
+
/**
|
|
12193
|
+
* Project the suite tree into its render form: attach each test's failure detail
|
|
12194
|
+
* (by testId), root-cause chip, and slow rank. A pure projection — the source
|
|
12195
|
+
* ReportData is never mutated. Gating lives here (not the template): a field is
|
|
12196
|
+
* attached only when its section is on, so the template just checks presence.
|
|
12197
|
+
* `failuresById` is built from the (possibly overflow-trimmed) `failures` array,
|
|
12198
|
+
* so a trimmed failure yields no inline card; the overflow note points to the sidecar.
|
|
12199
|
+
*/
|
|
12200
|
+
enrichProjects(data, slow, sections, slowTestThreshold) {
|
|
12201
|
+
const failuresById = sections.failureDeepDive ? new Map(data.failures.map((f) => [f.testId, f])) : /* @__PURE__ */ new Map();
|
|
12202
|
+
const chipById = sections.failureAnalysis ? new Map((data.classifications ?? []).map((c) => [c.testId, c])) : /* @__PURE__ */ new Map();
|
|
12203
|
+
const slowRankById = /* @__PURE__ */ new Map();
|
|
12204
|
+
if (sections.slowTests && slow.timedTotal > slowTestThreshold) {
|
|
12205
|
+
slow.ranked.forEach((t, i) => slowRankById.set(t.id, i + 1));
|
|
12206
|
+
}
|
|
12207
|
+
const enrichTest = (t) => ({
|
|
12208
|
+
...t,
|
|
12209
|
+
failure: failuresById.get(t.id),
|
|
12210
|
+
chip: chipById.get(t.id),
|
|
12211
|
+
slowRank: slowRankById.get(t.id)
|
|
12212
|
+
});
|
|
12213
|
+
const enrichSuite = (s) => ({
|
|
12214
|
+
...s,
|
|
12215
|
+
suites: s.suites.map(enrichSuite),
|
|
12216
|
+
tests: s.tests.map(enrichTest)
|
|
12217
|
+
});
|
|
12218
|
+
return data.projects.map((p) => ({ ...p, suites: p.suites.map(enrichSuite) }));
|
|
12165
12219
|
}
|
|
12166
12220
|
readStyle(filename) {
|
|
12167
12221
|
const cached = this.styleCache.get(filename);
|
|
@@ -12840,7 +12894,7 @@ var PdfGenerator = class {
|
|
|
12840
12894
|
}
|
|
12841
12895
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
12842
12896
|
logger.info(`Rendering template: ${templateLabel}`);
|
|
12843
|
-
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
|
|
12897
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections, options.slowTestThreshold) : this.templateEngine.renderFromPath(renderData, source.absolutePath, options.slowTestThreshold);
|
|
12844
12898
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
12845
12899
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
12846
12900
|
try {
|
|
@@ -13237,7 +13291,7 @@ var PdfReporter = class {
|
|
|
13237
13291
|
this.dataCollector = new DataCollector();
|
|
13238
13292
|
this.pdfGenerator = new PdfGenerator();
|
|
13239
13293
|
this.options = parseOptions(rawOptions);
|
|
13240
|
-
const version = true ? "0.
|
|
13294
|
+
const version = true ? "0.7.0" : "0.x";
|
|
13241
13295
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13242
13296
|
this.licenseClient = new LicenseClient({
|
|
13243
13297
|
licenseKey: this.options.licenseKey,
|
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
|
}
|
|
@@ -12070,17 +12085,17 @@ var TemplateEngine = class {
|
|
|
12070
12085
|
setChartjsBundle(inline) {
|
|
12071
12086
|
this.chartjsInline = inline;
|
|
12072
12087
|
}
|
|
12073
|
-
render(data, template, userSections) {
|
|
12088
|
+
render(data, template, userSections, slowTestThreshold) {
|
|
12074
12089
|
const layoutPath = path7.join(this.templatesDir, "layouts", `${template}.hbs`);
|
|
12075
12090
|
if (!fs5.existsSync(layoutPath)) {
|
|
12076
12091
|
throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
|
|
12077
12092
|
}
|
|
12078
12093
|
const layoutSource = fs5.readFileSync(layoutPath, "utf8");
|
|
12079
12094
|
const compiledLayout = this.handlebars.compile(layoutSource);
|
|
12080
|
-
const ctx = this.buildContext(data, template, userSections);
|
|
12095
|
+
const ctx = this.buildContext(data, template, userSections, slowTestThreshold);
|
|
12081
12096
|
return compiledLayout(ctx);
|
|
12082
12097
|
}
|
|
12083
|
-
renderFromPath(data, filePath) {
|
|
12098
|
+
renderFromPath(data, filePath, slowTestThreshold) {
|
|
12084
12099
|
if (!this.customTemplateCache.has(filePath)) {
|
|
12085
12100
|
if (!fs5.existsSync(filePath)) {
|
|
12086
12101
|
throw new Error(`[reportforge] Custom template not found: ${filePath}`);
|
|
@@ -12089,32 +12104,41 @@ var TemplateEngine = class {
|
|
|
12089
12104
|
this.customTemplateCache.set(filePath, this.handlebars.compile(source));
|
|
12090
12105
|
}
|
|
12091
12106
|
const compiledFn = this.customTemplateCache.get(filePath);
|
|
12092
|
-
return compiledFn(this.buildCustomContext(data));
|
|
12107
|
+
return compiledFn(this.buildCustomContext(data, slowTestThreshold));
|
|
12093
12108
|
}
|
|
12094
|
-
buildCustomContext(data) {
|
|
12109
|
+
buildCustomContext(data, slowTestThreshold) {
|
|
12095
12110
|
const sections = allSectionsOn();
|
|
12111
|
+
const slow = this.buildSlowTests(data);
|
|
12096
12112
|
return {
|
|
12097
12113
|
...data,
|
|
12114
|
+
projects: this.enrichProjects(data, slow, sections, slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold),
|
|
12098
12115
|
options: deriveOptions(sections),
|
|
12099
12116
|
sections,
|
|
12100
12117
|
tagGroups: this.buildTagGroups(data),
|
|
12101
|
-
slowTests:
|
|
12118
|
+
slowTests: slow.ranked,
|
|
12102
12119
|
baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
|
|
12103
12120
|
templateCSS: "",
|
|
12104
12121
|
chartjsScript: this.buildChartjsScript()
|
|
12105
12122
|
};
|
|
12106
12123
|
}
|
|
12107
|
-
buildContext(data, template, userSections) {
|
|
12124
|
+
buildContext(data, template, userSections, slowTestThreshold) {
|
|
12108
12125
|
const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
|
|
12109
12126
|
const templateCSS = this.readStyle(`${template}.css`);
|
|
12110
12127
|
const sections = resolveSections(template, userSections);
|
|
12111
12128
|
const options = deriveOptions(sections);
|
|
12129
|
+
const slow = this.buildSlowTests(data);
|
|
12112
12130
|
return {
|
|
12113
12131
|
...data,
|
|
12132
|
+
projects: this.enrichProjects(
|
|
12133
|
+
data,
|
|
12134
|
+
slow,
|
|
12135
|
+
sections,
|
|
12136
|
+
slowTestThreshold ?? DEFAULT_OPTIONS.slowTestThreshold
|
|
12137
|
+
),
|
|
12114
12138
|
options,
|
|
12115
12139
|
sections,
|
|
12116
12140
|
tagGroups: this.buildTagGroups(data),
|
|
12117
|
-
slowTests:
|
|
12141
|
+
slowTests: slow.ranked,
|
|
12118
12142
|
baseCSS,
|
|
12119
12143
|
templateCSS,
|
|
12120
12144
|
chartjsScript: this.buildChartjsScript()
|
|
@@ -12151,6 +12175,7 @@ var TemplateEngine = class {
|
|
|
12151
12175
|
for (const test of suite.tests) {
|
|
12152
12176
|
if (test.duration > 0) {
|
|
12153
12177
|
tests.push({
|
|
12178
|
+
id: test.id,
|
|
12154
12179
|
title: test.title,
|
|
12155
12180
|
suiteTitle: suite.title,
|
|
12156
12181
|
durationMs: test.duration,
|
|
@@ -12162,7 +12187,36 @@ var TemplateEngine = class {
|
|
|
12162
12187
|
}
|
|
12163
12188
|
}
|
|
12164
12189
|
}
|
|
12165
|
-
|
|
12190
|
+
const ranked = tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
|
|
12191
|
+
return { ranked, timedTotal: tests.length };
|
|
12192
|
+
}
|
|
12193
|
+
/**
|
|
12194
|
+
* Project the suite tree into its render form: attach each test's failure detail
|
|
12195
|
+
* (by testId), root-cause chip, and slow rank. A pure projection — the source
|
|
12196
|
+
* ReportData is never mutated. Gating lives here (not the template): a field is
|
|
12197
|
+
* attached only when its section is on, so the template just checks presence.
|
|
12198
|
+
* `failuresById` is built from the (possibly overflow-trimmed) `failures` array,
|
|
12199
|
+
* so a trimmed failure yields no inline card; the overflow note points to the sidecar.
|
|
12200
|
+
*/
|
|
12201
|
+
enrichProjects(data, slow, sections, slowTestThreshold) {
|
|
12202
|
+
const failuresById = sections.failureDeepDive ? new Map(data.failures.map((f) => [f.testId, f])) : /* @__PURE__ */ new Map();
|
|
12203
|
+
const chipById = sections.failureAnalysis ? new Map((data.classifications ?? []).map((c) => [c.testId, c])) : /* @__PURE__ */ new Map();
|
|
12204
|
+
const slowRankById = /* @__PURE__ */ new Map();
|
|
12205
|
+
if (sections.slowTests && slow.timedTotal > slowTestThreshold) {
|
|
12206
|
+
slow.ranked.forEach((t, i) => slowRankById.set(t.id, i + 1));
|
|
12207
|
+
}
|
|
12208
|
+
const enrichTest = (t) => ({
|
|
12209
|
+
...t,
|
|
12210
|
+
failure: failuresById.get(t.id),
|
|
12211
|
+
chip: chipById.get(t.id),
|
|
12212
|
+
slowRank: slowRankById.get(t.id)
|
|
12213
|
+
});
|
|
12214
|
+
const enrichSuite = (s) => ({
|
|
12215
|
+
...s,
|
|
12216
|
+
suites: s.suites.map(enrichSuite),
|
|
12217
|
+
tests: s.tests.map(enrichTest)
|
|
12218
|
+
});
|
|
12219
|
+
return data.projects.map((p) => ({ ...p, suites: p.suites.map(enrichSuite) }));
|
|
12166
12220
|
}
|
|
12167
12221
|
readStyle(filename) {
|
|
12168
12222
|
const cached = this.styleCache.get(filename);
|
|
@@ -12841,7 +12895,7 @@ var PdfGenerator = class {
|
|
|
12841
12895
|
}
|
|
12842
12896
|
const templateLabel = source.kind === "builtin" ? source.id : source.label;
|
|
12843
12897
|
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);
|
|
12898
|
+
const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections, options.slowTestThreshold) : this.templateEngine.renderFromPath(renderData, source.absolutePath, options.slowTestThreshold);
|
|
12845
12899
|
const tempHtmlPath = await this.htmlWriter.write(html);
|
|
12846
12900
|
const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
|
|
12847
12901
|
try {
|
|
@@ -13238,7 +13292,7 @@ var PdfReporter = class {
|
|
|
13238
13292
|
this.dataCollector = new DataCollector();
|
|
13239
13293
|
this.pdfGenerator = new PdfGenerator();
|
|
13240
13294
|
this.options = parseOptions(rawOptions);
|
|
13241
|
-
const version = true ? "0.
|
|
13295
|
+
const version = true ? "0.7.0" : "0.x";
|
|
13242
13296
|
logger.info(`@reportforge/playwright-pdf v${version} initialised`);
|
|
13243
13297
|
this.licenseClient = new LicenseClient({
|
|
13244
13298
|
licenseKey: this.options.licenseKey,
|
|
@@ -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
|
|
|
@@ -24,17 +24,11 @@
|
|
|
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>
|
|
@@ -24,13 +24,7 @@
|
|
|
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
|
|
|
@@ -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,9 +32,9 @@
|
|
|
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>
|
|
@@ -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>
|