@reportforge/playwright-pdf 0.0.1 → 0.1.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
@@ -176,6 +176,18 @@ interface ReporterOptions {
176
176
  * @default true
177
177
  */
178
178
  showTrend?: boolean;
179
+ /**
180
+ * Path to a custom Handlebars (.hbs) template file. When set, takes
181
+ * precedence over `template`. Relative paths resolve from `process.cwd()`.
182
+ * Pass an array to generate one PDF per template in a single run — each
183
+ * output filename gets the template basename injected automatically.
184
+ *
185
+ * @example
186
+ * templatePath: './templates/brand-report.hbs'
187
+ * @example
188
+ * templatePath: ['./templates/exec.hbs', './templates/eng.hbs']
189
+ */
190
+ templatePath?: string | string[];
179
191
  }
180
192
 
181
193
  /**
package/dist/index.d.ts CHANGED
@@ -176,6 +176,18 @@ interface ReporterOptions {
176
176
  * @default true
177
177
  */
178
178
  showTrend?: boolean;
179
+ /**
180
+ * Path to a custom Handlebars (.hbs) template file. When set, takes
181
+ * precedence over `template`. Relative paths resolve from `process.cwd()`.
182
+ * Pass an array to generate one PDF per template in a single run — each
183
+ * output filename gets the template basename injected automatically.
184
+ *
185
+ * @example
186
+ * templatePath: './templates/brand-report.hbs'
187
+ * @example
188
+ * templatePath: ['./templates/exec.hbs', './templates/eng.hbs']
189
+ */
190
+ templatePath?: string | string[];
179
191
  }
180
192
 
181
193
  /**
package/dist/index.js CHANGED
@@ -10373,7 +10373,15 @@ var ReporterOptionsSchema = external_exports.object({
10373
10373
  notify: notifyConfigSchema,
10374
10374
  historyFile: external_exports.string().optional(),
10375
10375
  historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
10376
- showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend)
10376
+ showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
10377
+ templatePath: external_exports.union([
10378
+ 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"),
10379
+ external_exports.array(
10380
+ external_exports.string().regex(/\.hbs$/, "Each templatePath entry must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "Each templatePath basename must not be empty")
10381
+ ).min(1, "templatePath array must not be empty")
10382
+ ]).optional().transform(
10383
+ (v) => v === void 0 ? void 0 : Array.isArray(v) ? v : [v]
10384
+ )
10377
10385
  });
10378
10386
  function parseOptions(raw) {
10379
10387
  const result = ReporterOptionsSchema.safeParse(raw ?? {});
@@ -11461,6 +11469,7 @@ var TemplateEngine = class {
11461
11469
  constructor() {
11462
11470
  this.chartjsInline = "";
11463
11471
  this.fontsBundleCss = "";
11472
+ this.customTemplateCache = /* @__PURE__ */ new Map();
11464
11473
  this.handlebars = import_handlebars.default.create();
11465
11474
  this.templatesDir = import_path3.default.resolve(__dirname, "templates");
11466
11475
  if (!import_fs4.default.existsSync(this.templatesDir)) {
@@ -11490,12 +11499,43 @@ var TemplateEngine = class {
11490
11499
  const ctx = this.buildContext(data, template);
11491
11500
  return compiledLayout(ctx);
11492
11501
  }
11502
+ renderFromPath(data, filePath) {
11503
+ if (!this.customTemplateCache.has(filePath)) {
11504
+ if (!import_fs4.default.existsSync(filePath)) {
11505
+ throw new Error(`[reportforge] Custom template not found: ${filePath}`);
11506
+ }
11507
+ const source = import_fs4.default.readFileSync(filePath, "utf8");
11508
+ this.customTemplateCache.set(filePath, this.handlebars.compile(source));
11509
+ }
11510
+ const compiledFn = this.customTemplateCache.get(filePath);
11511
+ return compiledFn(this.buildCustomContext(data));
11512
+ }
11513
+ buildCustomContext(data) {
11514
+ return {
11515
+ ...data,
11516
+ options: {
11517
+ showCharts: true,
11518
+ showTrend: true,
11519
+ showStackTraces: true,
11520
+ showFullEnvironment: true,
11521
+ showPassRate: true,
11522
+ showRetries: true,
11523
+ showFullFailures: true
11524
+ },
11525
+ tagGroups: this.buildTagGroups(data),
11526
+ slowTests: this.buildSlowTests(data),
11527
+ baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
11528
+ templateCSS: "",
11529
+ chartjsScript: this.buildChartjsScript()
11530
+ };
11531
+ }
11493
11532
  buildContext(data, template) {
11494
11533
  const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
11495
11534
  const templateCSS = this.readStyle(`${template}.css`);
11496
11535
  const optionsMap = {
11497
11536
  minimal: {
11498
11537
  showCharts: false,
11538
+ showTrend: false,
11499
11539
  showStackTraces: true,
11500
11540
  showFullEnvironment: false,
11501
11541
  showPassRate: true,
@@ -11504,6 +11544,7 @@ var TemplateEngine = class {
11504
11544
  },
11505
11545
  detailed: {
11506
11546
  showCharts: true,
11547
+ showTrend: true,
11507
11548
  showStackTraces: true,
11508
11549
  showFullEnvironment: true,
11509
11550
  showPassRate: true,
@@ -11512,6 +11553,7 @@ var TemplateEngine = class {
11512
11553
  },
11513
11554
  executive: {
11514
11555
  showCharts: true,
11556
+ showTrend: true,
11515
11557
  showStackTraces: false,
11516
11558
  showFullEnvironment: false,
11517
11559
  showPassRate: true,
@@ -11519,7 +11561,7 @@ var TemplateEngine = class {
11519
11561
  showFullFailures: false
11520
11562
  }
11521
11563
  };
11522
- const chartjsScript = this.chartjsInline ? `<script>${this.chartjsInline}</script>` : "<!-- Chart.js not bundled; charts disabled --><script>window.__chartsReady=true;</script>";
11564
+ const chartjsScript = this.buildChartjsScript();
11523
11565
  return {
11524
11566
  ...data,
11525
11567
  options: optionsMap[template],
@@ -11530,6 +11572,9 @@ var TemplateEngine = class {
11530
11572
  chartjsScript
11531
11573
  };
11532
11574
  }
11575
+ buildChartjsScript() {
11576
+ return this.chartjsInline ? `<script>${this.chartjsInline}</script>` : "<!-- Chart.js not bundled; charts disabled --><script>window.__chartsReady=true;</script>";
11577
+ }
11533
11578
  buildTagGroups(data) {
11534
11579
  const tagMap = /* @__PURE__ */ new Map();
11535
11580
  for (const project of data.projects) {
@@ -11910,10 +11955,10 @@ var FilenameResolver = class {
11910
11955
  import_fs9.default.mkdirSync(import_path6.default.dirname(absolute), { recursive: true });
11911
11956
  return absolute;
11912
11957
  }
11913
- injectTemplateSuffix(outputPath, templateId) {
11958
+ injectTemplateSuffix(outputPath, suffix) {
11914
11959
  const ext = import_path6.default.extname(outputPath);
11915
11960
  const base = outputPath.slice(0, outputPath.length - ext.length);
11916
- return `${base}-${templateId}${ext}`;
11961
+ return `${base}-${suffix}${ext}`;
11917
11962
  }
11918
11963
  formatDate(d) {
11919
11964
  const yyyy = d.getFullYear();
@@ -12033,9 +12078,15 @@ var PdfGenerator = class {
12033
12078
  }
12034
12079
  }
12035
12080
  async generateAll(data, options, _licenseInfo) {
12036
- const rawTemplates = Array.isArray(options.template) ? options.template : [options.template];
12037
- const templates = [...new Set(rawTemplates)];
12038
- const isMulti = templates.length > 1;
12081
+ const sources = this.resolveTemplateSources(options);
12082
+ const isMulti = sources.length > 1;
12083
+ for (const source of sources) {
12084
+ if (source.kind === "custom" && !import_fs11.default.existsSync(source.absolutePath)) {
12085
+ throw new Error(
12086
+ `[reportforge] Custom template not found: ${source.absolutePath}`
12087
+ );
12088
+ }
12089
+ }
12039
12090
  if (options.logo) {
12040
12091
  const logoBase64 = await this.screenshotEmbedder.embedLogoFile(options.logo);
12041
12092
  data.meta.branding.logoBase64 = logoBase64;
@@ -12052,11 +12103,13 @@ var PdfGenerator = class {
12052
12103
  const outputPaths = [];
12053
12104
  const { page } = await this.browserManager.launch(options.puppeteerExecutablePath);
12054
12105
  try {
12055
- for (const templateId of templates) {
12056
- const outputPath = isMulti ? this.filenameResolver.injectTemplateSuffix(baseOutputPath, templateId) : baseOutputPath;
12106
+ for (const source of sources) {
12107
+ const suffix = source.kind === "builtin" ? source.id : source.label;
12108
+ const outputPath = isMulti ? this.filenameResolver.injectTemplateSuffix(baseOutputPath, suffix) : baseOutputPath;
12109
+ const templateId = source.kind === "builtin" ? source.id : "detailed";
12057
12110
  const templateData = { ...data, meta: { ...data.meta, template: templateId } };
12058
12111
  const templateOptions = { ...options, template: templateId };
12059
- await this.renderPdf(templateData, templateOptions, compression, outputPath, page);
12112
+ await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
12060
12113
  const capBytes = (options.maxFileSizeMb ?? 0) * 1024 * 1024;
12061
12114
  if (capBytes > 0) {
12062
12115
  const sizeBytes = (await import_fs11.default.promises.stat(outputPath)).size;
@@ -12065,7 +12118,7 @@ var PdfGenerator = class {
12065
12118
  logger.warn(
12066
12119
  `PDF size ${fmtMb(sizeBytes)} exceeds cap ${fmtMb(capBytes)} \u2014 re-rendering (jpeg-q=${tighter.quality}, max-w=${tighter.maxWidth}px, inline-failures\u2264${tighter.maxInlineFailures}).`
12067
12120
  );
12068
- await this.renderPdf(templateData, templateOptions, tighter, outputPath, page);
12121
+ await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
12069
12122
  const finalSize = (await import_fs11.default.promises.stat(outputPath)).size;
12070
12123
  if (finalSize > capBytes) {
12071
12124
  logger.warn(
@@ -12088,6 +12141,17 @@ var PdfGenerator = class {
12088
12141
  }
12089
12142
  return outputPaths;
12090
12143
  }
12144
+ resolveTemplateSources(options) {
12145
+ if (options.templatePath && options.templatePath.length > 0) {
12146
+ return [...new Set(options.templatePath)].map((p) => ({
12147
+ kind: "custom",
12148
+ absolutePath: import_path8.default.isAbsolute(p) ? p : import_path8.default.resolve(process.cwd(), p),
12149
+ label: import_path8.default.basename(p, ".hbs")
12150
+ }));
12151
+ }
12152
+ const rawIds = Array.isArray(options.template) ? options.template : [options.template];
12153
+ return [...new Set(rawIds)].map((id) => ({ kind: "builtin", id }));
12154
+ }
12091
12155
  resolveSettings(options, data) {
12092
12156
  return resolveCompression({
12093
12157
  level: options.compressionLevel ?? "auto",
@@ -12122,7 +12186,7 @@ var PdfGenerator = class {
12122
12186
  * Separated from generateAll() so the size-cap retune can reuse it with
12123
12187
  * stricter settings without reshaping the rest of the flow.
12124
12188
  */
12125
- async renderPdf(data, options, compression, outputPath, page) {
12189
+ async renderPdf(data, options, source, compression, outputPath, page) {
12126
12190
  const paginated = await this.failurePaginator.paginate(
12127
12191
  data.failures,
12128
12192
  compression.maxInlineFailures,
@@ -12142,15 +12206,15 @@ var PdfGenerator = class {
12142
12206
  maxWidth: compression.maxWidth
12143
12207
  });
12144
12208
  }
12145
- const templateId = options.template;
12146
- logger.info(`Rendering template: ${templateId}`);
12147
- const html = this.templateEngine.render(renderData, templateId);
12209
+ const templateLabel = source.kind === "builtin" ? source.id : source.label;
12210
+ logger.info(`Rendering template: ${templateLabel}`);
12211
+ const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
12148
12212
  const tempHtmlPath = await this.htmlWriter.write(html);
12149
12213
  const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
12150
12214
  try {
12151
12215
  logger.info(`Navigating to temp HTML (${Math.round(html.length / 1024)} KB)...`);
12152
12216
  await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
12153
- const showCharts = templateId !== "minimal";
12217
+ const showCharts = source.kind === "builtin" ? source.id !== "minimal" : true;
12154
12218
  await this.chartRenderer.waitForCharts(page, showCharts);
12155
12219
  logger.info(`Generating PDF \u2192 ${import_path8.default.relative(process.cwd(), outputPath)}`);
12156
12220
  await page.pdf({
@@ -12386,7 +12450,7 @@ var PdfReporter = class {
12386
12450
  this.dataCollector = new DataCollector();
12387
12451
  this.pdfGenerator = new PdfGenerator();
12388
12452
  this.options = parseOptions(rawOptions);
12389
- const version = true ? "0.0.1" : "0.x";
12453
+ const version = true ? "0.1.0" : "0.x";
12390
12454
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
12391
12455
  this.licenseClient = new LicenseClient({
12392
12456
  licenseKey: this.options.licenseKey,
@@ -12471,7 +12535,7 @@ var PdfReporter = class {
12471
12535
  _buildReportData(collected, licenseInfo) {
12472
12536
  const { projects, stats, failures, environment, charts } = collected;
12473
12537
  const projectName = this.options.projectName ?? this.resolveProjectName();
12474
- const firstTemplate = Array.isArray(this.options.template) ? this.options.template[0] : this.options.template;
12538
+ const firstTemplate = this.options.templatePath?.length ? "detailed" : Array.isArray(this.options.template) ? this.options.template[0] : this.options.template;
12475
12539
  return {
12476
12540
  meta: {
12477
12541
  title: this.options.reportTitle,
@@ -12529,6 +12593,15 @@ var PdfReporter = class {
12529
12593
  logger.warn(`open: true \u2014 could not open ${pdfPath}: ${err.message}`);
12530
12594
  });
12531
12595
  child.unref();
12596
+ if (platform === "win32") {
12597
+ child.on("close", (code) => {
12598
+ if (code !== 0 && code !== null) {
12599
+ logger.warn(
12600
+ `open: true \u2014 shell-open exited with code ${code}. No PDF viewer may be registered for .pdf files on this machine. File is at: ${pdfPath}`
12601
+ );
12602
+ }
12603
+ });
12604
+ }
12532
12605
  } catch (err) {
12533
12606
  logger.warn(`open: true \u2014 unexpected error: ${err.message}`);
12534
12607
  }
package/dist/index.mjs CHANGED
@@ -10374,7 +10374,15 @@ var ReporterOptionsSchema = external_exports.object({
10374
10374
  notify: notifyConfigSchema,
10375
10375
  historyFile: external_exports.string().optional(),
10376
10376
  historySize: external_exports.number().int().min(2).optional().default(DEFAULT_OPTIONS.historySize),
10377
- showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend)
10377
+ showTrend: external_exports.boolean().optional().default(DEFAULT_OPTIONS.showTrend),
10378
+ templatePath: external_exports.union([
10379
+ 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"),
10380
+ external_exports.array(
10381
+ external_exports.string().regex(/\.hbs$/, "Each templatePath entry must end with .hbs").refine((v) => v.slice(0, -4).trim().length > 0, "Each templatePath basename must not be empty")
10382
+ ).min(1, "templatePath array must not be empty")
10383
+ ]).optional().transform(
10384
+ (v) => v === void 0 ? void 0 : Array.isArray(v) ? v : [v]
10385
+ )
10378
10386
  });
10379
10387
  function parseOptions(raw) {
10380
10388
  const result = ReporterOptionsSchema.safeParse(raw ?? {});
@@ -11462,6 +11470,7 @@ var TemplateEngine = class {
11462
11470
  constructor() {
11463
11471
  this.chartjsInline = "";
11464
11472
  this.fontsBundleCss = "";
11473
+ this.customTemplateCache = /* @__PURE__ */ new Map();
11465
11474
  this.handlebars = import_handlebars.default.create();
11466
11475
  this.templatesDir = path4.resolve(__dirname, "templates");
11467
11476
  if (!fs3.existsSync(this.templatesDir)) {
@@ -11491,12 +11500,43 @@ var TemplateEngine = class {
11491
11500
  const ctx = this.buildContext(data, template);
11492
11501
  return compiledLayout(ctx);
11493
11502
  }
11503
+ renderFromPath(data, filePath) {
11504
+ if (!this.customTemplateCache.has(filePath)) {
11505
+ if (!fs3.existsSync(filePath)) {
11506
+ throw new Error(`[reportforge] Custom template not found: ${filePath}`);
11507
+ }
11508
+ const source = fs3.readFileSync(filePath, "utf8");
11509
+ this.customTemplateCache.set(filePath, this.handlebars.compile(source));
11510
+ }
11511
+ const compiledFn = this.customTemplateCache.get(filePath);
11512
+ return compiledFn(this.buildCustomContext(data));
11513
+ }
11514
+ buildCustomContext(data) {
11515
+ return {
11516
+ ...data,
11517
+ options: {
11518
+ showCharts: true,
11519
+ showTrend: true,
11520
+ showStackTraces: true,
11521
+ showFullEnvironment: true,
11522
+ showPassRate: true,
11523
+ showRetries: true,
11524
+ showFullFailures: true
11525
+ },
11526
+ tagGroups: this.buildTagGroups(data),
11527
+ slowTests: this.buildSlowTests(data),
11528
+ baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
11529
+ templateCSS: "",
11530
+ chartjsScript: this.buildChartjsScript()
11531
+ };
11532
+ }
11494
11533
  buildContext(data, template) {
11495
11534
  const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
11496
11535
  const templateCSS = this.readStyle(`${template}.css`);
11497
11536
  const optionsMap = {
11498
11537
  minimal: {
11499
11538
  showCharts: false,
11539
+ showTrend: false,
11500
11540
  showStackTraces: true,
11501
11541
  showFullEnvironment: false,
11502
11542
  showPassRate: true,
@@ -11505,6 +11545,7 @@ var TemplateEngine = class {
11505
11545
  },
11506
11546
  detailed: {
11507
11547
  showCharts: true,
11548
+ showTrend: true,
11508
11549
  showStackTraces: true,
11509
11550
  showFullEnvironment: true,
11510
11551
  showPassRate: true,
@@ -11513,6 +11554,7 @@ var TemplateEngine = class {
11513
11554
  },
11514
11555
  executive: {
11515
11556
  showCharts: true,
11557
+ showTrend: true,
11516
11558
  showStackTraces: false,
11517
11559
  showFullEnvironment: false,
11518
11560
  showPassRate: true,
@@ -11520,7 +11562,7 @@ var TemplateEngine = class {
11520
11562
  showFullFailures: false
11521
11563
  }
11522
11564
  };
11523
- const chartjsScript = this.chartjsInline ? `<script>${this.chartjsInline}</script>` : "<!-- Chart.js not bundled; charts disabled --><script>window.__chartsReady=true;</script>";
11565
+ const chartjsScript = this.buildChartjsScript();
11524
11566
  return {
11525
11567
  ...data,
11526
11568
  options: optionsMap[template],
@@ -11531,6 +11573,9 @@ var TemplateEngine = class {
11531
11573
  chartjsScript
11532
11574
  };
11533
11575
  }
11576
+ buildChartjsScript() {
11577
+ return this.chartjsInline ? `<script>${this.chartjsInline}</script>` : "<!-- Chart.js not bundled; charts disabled --><script>window.__chartsReady=true;</script>";
11578
+ }
11534
11579
  buildTagGroups(data) {
11535
11580
  const tagMap = /* @__PURE__ */ new Map();
11536
11581
  for (const project of data.projects) {
@@ -11911,10 +11956,10 @@ var FilenameResolver = class {
11911
11956
  fs8.mkdirSync(path7.dirname(absolute), { recursive: true });
11912
11957
  return absolute;
11913
11958
  }
11914
- injectTemplateSuffix(outputPath, templateId) {
11959
+ injectTemplateSuffix(outputPath, suffix) {
11915
11960
  const ext = path7.extname(outputPath);
11916
11961
  const base = outputPath.slice(0, outputPath.length - ext.length);
11917
- return `${base}-${templateId}${ext}`;
11962
+ return `${base}-${suffix}${ext}`;
11918
11963
  }
11919
11964
  formatDate(d) {
11920
11965
  const yyyy = d.getFullYear();
@@ -12034,9 +12079,15 @@ var PdfGenerator = class {
12034
12079
  }
12035
12080
  }
12036
12081
  async generateAll(data, options, _licenseInfo) {
12037
- const rawTemplates = Array.isArray(options.template) ? options.template : [options.template];
12038
- const templates = [...new Set(rawTemplates)];
12039
- const isMulti = templates.length > 1;
12082
+ const sources = this.resolveTemplateSources(options);
12083
+ const isMulti = sources.length > 1;
12084
+ for (const source of sources) {
12085
+ if (source.kind === "custom" && !fs10.existsSync(source.absolutePath)) {
12086
+ throw new Error(
12087
+ `[reportforge] Custom template not found: ${source.absolutePath}`
12088
+ );
12089
+ }
12090
+ }
12040
12091
  if (options.logo) {
12041
12092
  const logoBase64 = await this.screenshotEmbedder.embedLogoFile(options.logo);
12042
12093
  data.meta.branding.logoBase64 = logoBase64;
@@ -12053,11 +12104,13 @@ var PdfGenerator = class {
12053
12104
  const outputPaths = [];
12054
12105
  const { page } = await this.browserManager.launch(options.puppeteerExecutablePath);
12055
12106
  try {
12056
- for (const templateId of templates) {
12057
- const outputPath = isMulti ? this.filenameResolver.injectTemplateSuffix(baseOutputPath, templateId) : baseOutputPath;
12107
+ for (const source of sources) {
12108
+ const suffix = source.kind === "builtin" ? source.id : source.label;
12109
+ const outputPath = isMulti ? this.filenameResolver.injectTemplateSuffix(baseOutputPath, suffix) : baseOutputPath;
12110
+ const templateId = source.kind === "builtin" ? source.id : "detailed";
12058
12111
  const templateData = { ...data, meta: { ...data.meta, template: templateId } };
12059
12112
  const templateOptions = { ...options, template: templateId };
12060
- await this.renderPdf(templateData, templateOptions, compression, outputPath, page);
12113
+ await this.renderPdf(templateData, templateOptions, source, compression, outputPath, page);
12061
12114
  const capBytes = (options.maxFileSizeMb ?? 0) * 1024 * 1024;
12062
12115
  if (capBytes > 0) {
12063
12116
  const sizeBytes = (await fs10.promises.stat(outputPath)).size;
@@ -12066,7 +12119,7 @@ var PdfGenerator = class {
12066
12119
  logger.warn(
12067
12120
  `PDF size ${fmtMb(sizeBytes)} exceeds cap ${fmtMb(capBytes)} \u2014 re-rendering (jpeg-q=${tighter.quality}, max-w=${tighter.maxWidth}px, inline-failures\u2264${tighter.maxInlineFailures}).`
12068
12121
  );
12069
- await this.renderPdf(templateData, templateOptions, tighter, outputPath, page);
12122
+ await this.renderPdf(templateData, templateOptions, source, tighter, outputPath, page);
12070
12123
  const finalSize = (await fs10.promises.stat(outputPath)).size;
12071
12124
  if (finalSize > capBytes) {
12072
12125
  logger.warn(
@@ -12089,6 +12142,17 @@ var PdfGenerator = class {
12089
12142
  }
12090
12143
  return outputPaths;
12091
12144
  }
12145
+ resolveTemplateSources(options) {
12146
+ if (options.templatePath && options.templatePath.length > 0) {
12147
+ return [...new Set(options.templatePath)].map((p) => ({
12148
+ kind: "custom",
12149
+ absolutePath: path9.isAbsolute(p) ? p : path9.resolve(process.cwd(), p),
12150
+ label: path9.basename(p, ".hbs")
12151
+ }));
12152
+ }
12153
+ const rawIds = Array.isArray(options.template) ? options.template : [options.template];
12154
+ return [...new Set(rawIds)].map((id) => ({ kind: "builtin", id }));
12155
+ }
12092
12156
  resolveSettings(options, data) {
12093
12157
  return resolveCompression({
12094
12158
  level: options.compressionLevel ?? "auto",
@@ -12123,7 +12187,7 @@ var PdfGenerator = class {
12123
12187
  * Separated from generateAll() so the size-cap retune can reuse it with
12124
12188
  * stricter settings without reshaping the rest of the flow.
12125
12189
  */
12126
- async renderPdf(data, options, compression, outputPath, page) {
12190
+ async renderPdf(data, options, source, compression, outputPath, page) {
12127
12191
  const paginated = await this.failurePaginator.paginate(
12128
12192
  data.failures,
12129
12193
  compression.maxInlineFailures,
@@ -12143,15 +12207,15 @@ var PdfGenerator = class {
12143
12207
  maxWidth: compression.maxWidth
12144
12208
  });
12145
12209
  }
12146
- const templateId = options.template;
12147
- logger.info(`Rendering template: ${templateId}`);
12148
- const html = this.templateEngine.render(renderData, templateId);
12210
+ const templateLabel = source.kind === "builtin" ? source.id : source.label;
12211
+ logger.info(`Rendering template: ${templateLabel}`);
12212
+ const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
12149
12213
  const tempHtmlPath = await this.htmlWriter.write(html);
12150
12214
  const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
12151
12215
  try {
12152
12216
  logger.info(`Navigating to temp HTML (${Math.round(html.length / 1024)} KB)...`);
12153
12217
  await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
12154
- const showCharts = templateId !== "minimal";
12218
+ const showCharts = source.kind === "builtin" ? source.id !== "minimal" : true;
12155
12219
  await this.chartRenderer.waitForCharts(page, showCharts);
12156
12220
  logger.info(`Generating PDF \u2192 ${path9.relative(process.cwd(), outputPath)}`);
12157
12221
  await page.pdf({
@@ -12387,7 +12451,7 @@ var PdfReporter = class {
12387
12451
  this.dataCollector = new DataCollector();
12388
12452
  this.pdfGenerator = new PdfGenerator();
12389
12453
  this.options = parseOptions(rawOptions);
12390
- const version = true ? "0.0.1" : "0.x";
12454
+ const version = true ? "0.1.0" : "0.x";
12391
12455
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
12392
12456
  this.licenseClient = new LicenseClient({
12393
12457
  licenseKey: this.options.licenseKey,
@@ -12472,7 +12536,7 @@ var PdfReporter = class {
12472
12536
  _buildReportData(collected, licenseInfo) {
12473
12537
  const { projects, stats, failures, environment, charts } = collected;
12474
12538
  const projectName = this.options.projectName ?? this.resolveProjectName();
12475
- const firstTemplate = Array.isArray(this.options.template) ? this.options.template[0] : this.options.template;
12539
+ const firstTemplate = this.options.templatePath?.length ? "detailed" : Array.isArray(this.options.template) ? this.options.template[0] : this.options.template;
12476
12540
  return {
12477
12541
  meta: {
12478
12542
  title: this.options.reportTitle,
@@ -12530,6 +12594,15 @@ var PdfReporter = class {
12530
12594
  logger.warn(`open: true \u2014 could not open ${pdfPath}: ${err.message}`);
12531
12595
  });
12532
12596
  child.unref();
12597
+ if (platform === "win32") {
12598
+ child.on("close", (code) => {
12599
+ if (code !== 0 && code !== null) {
12600
+ logger.warn(
12601
+ `open: true \u2014 shell-open exited with code ${code}. No PDF viewer may be registered for .pdf files on this machine. File is at: ${pdfPath}`
12602
+ );
12603
+ }
12604
+ });
12605
+ }
12533
12606
  } catch (err) {
12534
12607
  logger.warn(`open: true \u2014 unexpected error: ${err.message}`);
12535
12608
  }
@@ -10,9 +10,8 @@
10
10
  </div>
11
11
  </div>
12
12
 
13
- {{!-- Trend card: detailed + executive. Minimal skipped (no charts at all).
14
- In multi-template runs generateAll overrides meta.template per render pass. --}}
15
- {{#unless (eq meta.template 'minimal')}}
13
+ {{!-- Trend card: detailed + executive + custom templates. Minimal has showTrend=false. --}}
14
+ {{#if options.showTrend}}
16
15
  {{#if charts.trend}}
17
16
  <div class="trend-card">
18
17
  <div class="trend-card__header">
@@ -29,7 +28,7 @@
29
28
  <div class="trend-run-history" id="run-history-table"></div>
30
29
  </div>
31
30
  {{/if}}
32
- {{/unless}}
31
+ {{/if}}
33
32
 
34
33
  <script>
35
34
  (function() {
@@ -144,7 +143,7 @@
144
143
  }
145
144
  });
146
145
 
147
- {{#unless (eq meta.template 'minimal')}}{{#if charts.trend}}
146
+ {{#if options.showTrend}}{{#if charts.trend}}
148
147
  // entries are newest-first in storage; reverse for oldest-left (chronological) display
149
148
  var trendRaw = {{{safeJsonData charts.trend.entries}}};
150
149
  var trendData = trendRaw.slice().reverse();
@@ -298,7 +297,7 @@
298
297
  histEl.appendChild(row);
299
298
  });
300
299
  }
301
- {{/if}}{{/unless}}
300
+ {{/if}}{{/if}}
302
301
  })();
303
302
  </script>
304
303
  {{else}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportforge/playwright-pdf",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Enterprise-ready PDF reports for Playwright Test — minimal, detailed, and executive templates with CI/CD integrations",
5
5
  "license": "MIT",
6
6
  "author": "ReportForge",