@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 +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +91 -18
- package/dist/index.mjs +91 -18
- package/dist/templates/partials/charts.hbs +5 -6
- package/package.json +1 -1
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.
|
|
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,
|
|
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}-${
|
|
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
|
|
12037
|
-
const
|
|
12038
|
-
const
|
|
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
|
|
12056
|
-
const
|
|
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
|
|
12146
|
-
logger.info(`Rendering template: ${
|
|
12147
|
-
const html = this.templateEngine.render(renderData,
|
|
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 =
|
|
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
|
|
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.
|
|
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,
|
|
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}-${
|
|
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
|
|
12038
|
-
const
|
|
12039
|
-
const
|
|
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
|
|
12057
|
-
const
|
|
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
|
|
12147
|
-
logger.info(`Rendering template: ${
|
|
12148
|
-
const html = this.templateEngine.render(renderData,
|
|
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 =
|
|
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
|
|
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
|
|
14
|
-
|
|
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
|
-
{{/
|
|
31
|
+
{{/if}}
|
|
33
32
|
|
|
34
33
|
<script>
|
|
35
34
|
(function() {
|
|
@@ -144,7 +143,7 @@
|
|
|
144
143
|
}
|
|
145
144
|
});
|
|
146
145
|
|
|
147
|
-
{{#
|
|
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}}{{/
|
|
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
|
|
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",
|