@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/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
- suiteBreakdown: false,
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: this.buildSlowTests(data),
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: this.buildSlowTests(data),
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
- return tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
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.6.2" : "0.x";
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
- suiteBreakdown: false,
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: this.buildSlowTests(data),
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: this.buildSlowTests(data),
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
- return tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
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.6.2" : "0.x";
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}} &nbsp;·&nbsp; 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:&nbsp;
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:88px;">Status</th>
35
+ <th style="width:112px;">Status</th>
29
36
  <th style="width:72px;">Duration</th>
30
- {{#if ../options.showRetries}}<th style="width:52px;">Retries</th>{{/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>{{statusBadge status}}</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 ../../options.showRetries}}<td style="color:var(--text-3);text-align:center;">{{retryCount}}</td>{{/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>