@reportforge/playwright-pdf 0.5.0 → 0.6.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.js CHANGED
@@ -10332,6 +10332,113 @@ var DEFAULT_OPTIONS = {
10332
10332
  flakinessTopN: 5
10333
10333
  };
10334
10334
 
10335
+ // src/templates/sections.ts
10336
+ init_cjs_shims();
10337
+ var SECTION_KEYS = [
10338
+ // block toggles
10339
+ "coverPage",
10340
+ "analysisOneliner",
10341
+ "releaseGate",
10342
+ "summary",
10343
+ "charts",
10344
+ "trend",
10345
+ "requirementsMatrix",
10346
+ "ciEnvironment",
10347
+ "suiteBreakdown",
10348
+ "failureDeepDive",
10349
+ "failureAnalysis",
10350
+ "slowTests",
10351
+ "defectLog",
10352
+ // display modifiers
10353
+ "passRate",
10354
+ "fullEnvironment",
10355
+ "retries",
10356
+ "fullFailures",
10357
+ "stackTraces"
10358
+ ];
10359
+ var SECTION_DEFAULTS = {
10360
+ minimal: {
10361
+ coverPage: false,
10362
+ analysisOneliner: false,
10363
+ releaseGate: true,
10364
+ summary: true,
10365
+ charts: false,
10366
+ trend: false,
10367
+ requirementsMatrix: false,
10368
+ ciEnvironment: true,
10369
+ suiteBreakdown: true,
10370
+ failureDeepDive: true,
10371
+ failureAnalysis: false,
10372
+ slowTests: false,
10373
+ defectLog: false,
10374
+ passRate: true,
10375
+ fullEnvironment: false,
10376
+ retries: true,
10377
+ fullFailures: true,
10378
+ stackTraces: true
10379
+ },
10380
+ detailed: {
10381
+ coverPage: false,
10382
+ analysisOneliner: false,
10383
+ releaseGate: true,
10384
+ summary: true,
10385
+ charts: true,
10386
+ trend: true,
10387
+ requirementsMatrix: true,
10388
+ ciEnvironment: true,
10389
+ suiteBreakdown: true,
10390
+ failureDeepDive: true,
10391
+ failureAnalysis: true,
10392
+ slowTests: true,
10393
+ defectLog: true,
10394
+ passRate: true,
10395
+ fullEnvironment: true,
10396
+ retries: true,
10397
+ fullFailures: true,
10398
+ stackTraces: true
10399
+ },
10400
+ executive: {
10401
+ coverPage: true,
10402
+ analysisOneliner: true,
10403
+ releaseGate: true,
10404
+ summary: true,
10405
+ charts: true,
10406
+ trend: true,
10407
+ requirementsMatrix: false,
10408
+ ciEnvironment: true,
10409
+ suiteBreakdown: false,
10410
+ failureDeepDive: true,
10411
+ failureAnalysis: false,
10412
+ slowTests: true,
10413
+ defectLog: false,
10414
+ passRate: true,
10415
+ fullEnvironment: false,
10416
+ retries: false,
10417
+ fullFailures: false,
10418
+ stackTraces: false
10419
+ }
10420
+ };
10421
+ var SECTION_KEY_SET = new Set(SECTION_KEYS);
10422
+ function resolveSections(template, userSections) {
10423
+ const base = SECTION_DEFAULTS[template];
10424
+ if (!userSections) return { ...base };
10425
+ const flat = {};
10426
+ for (const [k, v] of Object.entries(userSections)) {
10427
+ if (v !== void 0 && SECTION_KEY_SET.has(k)) flat[k] = v;
10428
+ }
10429
+ const perTemplateRaw = userSections[template] ?? {};
10430
+ const perTemplate = {};
10431
+ for (const [k, v] of Object.entries(perTemplateRaw)) {
10432
+ if (v !== void 0) perTemplate[k] = v;
10433
+ }
10434
+ const result = { ...base, ...flat, ...perTemplate };
10435
+ if (!result.charts) result.trend = false;
10436
+ return result;
10437
+ }
10438
+ function allSectionsOn() {
10439
+ return Object.fromEntries(SECTION_KEYS.map((k) => [k, true]));
10440
+ }
10441
+
10335
10442
  // src/config/schema.ts
10336
10443
  var hexColor = external_exports.string().regex(
10337
10444
  /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/,
@@ -10364,6 +10471,15 @@ var failureAnalysisConfigSchema = external_exports.object({
10364
10471
  collectUnclassified: external_exports.boolean().default(true),
10365
10472
  autoUpdateModel: external_exports.boolean().default(true)
10366
10473
  }).default({});
10474
+ var sectionFlagsShape = Object.fromEntries(
10475
+ SECTION_KEYS.map((k) => [k, external_exports.boolean().optional()])
10476
+ );
10477
+ var sectionFlagsSchema = external_exports.object(sectionFlagsShape).strict();
10478
+ var sectionsSchema = sectionFlagsSchema.extend({
10479
+ minimal: sectionFlagsSchema.optional(),
10480
+ detailed: sectionFlagsSchema.optional(),
10481
+ executive: sectionFlagsSchema.optional()
10482
+ }).strict();
10367
10483
  var ReporterOptionsSchema = external_exports.object({
10368
10484
  outputFile: external_exports.string().optional().default(DEFAULT_OPTIONS.outputFile),
10369
10485
  template: external_exports.union([
@@ -10401,7 +10517,8 @@ var ReporterOptionsSchema = external_exports.object({
10401
10517
  ).min(1, "templatePath array must not be empty")
10402
10518
  ]).optional().transform(
10403
10519
  (v) => v === void 0 ? void 0 : Array.isArray(v) ? v : [v]
10404
- )
10520
+ ),
10521
+ sections: sectionsSchema.optional()
10405
10522
  });
10406
10523
  function parseOptions(raw) {
10407
10524
  const result = ReporterOptionsSchema.safeParse(raw ?? {});
@@ -10993,11 +11110,33 @@ var STRENGTH = {
10993
11110
  strongMinMargin: 0.25
10994
11111
  };
10995
11112
  var STRENGTH_LEVELS = ["weak", "moderate", "strong"];
10996
- var FLAKY_MARGIN = 999;
11113
+ var OVERRIDE_MARGIN = 999;
10997
11114
 
10998
11115
  // src/analysis/FailureClusterer.ts
10999
11116
  var MAX_TESTS_PER_CLUSTER = 20;
11000
11117
  var MAX_MESSAGE_CHARS = 200;
11118
+ function errorHeader(message) {
11119
+ const beforeCallLog = message.split(/\n\s*Call log:/i)[0];
11120
+ return beforeCallLog.split("\n").filter((line) => !/^\s*[-+]?\s*(?:Expected|Received)\b/i.test(line)).join("\n");
11121
+ }
11122
+ function isAssertionTimeout(message) {
11123
+ return /Expect "[^"]+" with timeout/.test(message) && /resolved to [1-9]\d* element/.test(message);
11124
+ }
11125
+ function isNetwork(message) {
11126
+ return /net::ERR_(CONNECTION|NAME_NOT_RESOLVED|INTERNET_DISCONNECTED|SSL|TIMED_OUT|ADDRESS|NETWORK_CHANGED|EMPTY_RESPONSE)|\bECONN(?:REFUSED|RESET)\b|\bENOTFOUND\b|\bEAI_AGAIN\b|getaddrinfo|socket hang up|fetch failed/.test(errorHeader(message));
11127
+ }
11128
+ function isNavigation(message) {
11129
+ return /net::ERR_(?:ABORTED|TOO_MANY_REDIRECTS)|navigation interrupted|interrupted by (?:a|another) navigation|because of a navigation|no history entry|page crashed|Execution context was destroyed|frame was detached/.test(errorHeader(message));
11130
+ }
11131
+ function isLocatorNotFound(message) {
11132
+ return /strict mode violation|resolved to 0 element|element not found|not attached to the DOM|No node found for selector|no element matches|Unable to find an element/i.test(errorHeader(message));
11133
+ }
11134
+ var MESSAGE_RULES = [
11135
+ [isAssertionTimeout, "assertion"],
11136
+ [isNavigation, "navigation"],
11137
+ [isNetwork, "network"],
11138
+ [isLocatorNotFound, "locator-not-found"]
11139
+ ];
11001
11140
  function strengthOf(margin) {
11002
11141
  if (margin >= STRENGTH.strongMinMargin) return "strong";
11003
11142
  if (margin >= STRENGTH.moderateMinMargin) return "moderate";
@@ -11020,11 +11159,15 @@ function representativeMessage(message) {
11020
11159
  return firstLine.length > MAX_MESSAGE_CHARS ? `${firstLine.slice(0, MAX_MESSAGE_CHARS)}\u2026` : firstLine;
11021
11160
  }
11022
11161
  function classifyOne(f, model) {
11162
+ const frame = topFrame(f.error.stack);
11023
11163
  if (f.retryHistory.some((r) => r.status === "passed")) {
11024
- return { category: "flaky-by-retry", margin: FLAKY_MARGIN, failure: f, frame: topFrame(f.error.stack) };
11164
+ return { category: "flaky-by-retry", margin: OVERRIDE_MARGIN, failure: f, frame };
11165
+ }
11166
+ for (const [match, category] of MESSAGE_RULES) {
11167
+ if (match(f.error.message)) return { category, margin: OVERRIDE_MARGIN, failure: f, frame };
11025
11168
  }
11026
11169
  const { label, margin } = classify(model, classifyInput(f));
11027
- return { category: label, margin, failure: f, frame: topFrame(f.error.stack) };
11170
+ return { category: label, margin, failure: f, frame };
11028
11171
  }
11029
11172
  function classifyAll(failures, model) {
11030
11173
  return failures.map((f) => classifyOne(f, model));
@@ -11884,11 +12027,26 @@ function formatDuration(ms) {
11884
12027
  }
11885
12028
 
11886
12029
  // src/templates/engine.ts
12030
+ function jsonForInlineScript(value) {
12031
+ return JSON.stringify(value).replace(/<\//g, "<\\/");
12032
+ }
12033
+ function deriveOptions(s) {
12034
+ return {
12035
+ showCharts: s.charts,
12036
+ showTrend: s.trend,
12037
+ showStackTraces: s.stackTraces,
12038
+ showFullEnvironment: s.fullEnvironment,
12039
+ showPassRate: s.passRate,
12040
+ showRetries: s.retries,
12041
+ showFullFailures: s.fullFailures
12042
+ };
12043
+ }
11887
12044
  var TemplateEngine = class {
11888
12045
  constructor() {
11889
12046
  this.chartjsInline = "";
11890
12047
  this.fontsBundleCss = "";
11891
12048
  this.customTemplateCache = /* @__PURE__ */ new Map();
12049
+ this.styleCache = /* @__PURE__ */ new Map();
11892
12050
  this.handlebars = import_handlebars.default.create();
11893
12051
  this.templatesDir = import_path3.default.resolve(__dirname, "templates");
11894
12052
  if (!import_fs4.default.existsSync(this.templatesDir)) {
@@ -11908,14 +12066,14 @@ var TemplateEngine = class {
11908
12066
  setChartjsBundle(inline) {
11909
12067
  this.chartjsInline = inline;
11910
12068
  }
11911
- render(data, template) {
12069
+ render(data, template, userSections) {
11912
12070
  const layoutPath = import_path3.default.join(this.templatesDir, "layouts", `${template}.hbs`);
11913
12071
  if (!import_fs4.default.existsSync(layoutPath)) {
11914
12072
  throw new Error(`[reportforge] Template layout not found: ${layoutPath}`);
11915
12073
  }
11916
12074
  const layoutSource = import_fs4.default.readFileSync(layoutPath, "utf8");
11917
12075
  const compiledLayout = this.handlebars.compile(layoutSource);
11918
- const ctx = this.buildContext(data, template);
12076
+ const ctx = this.buildContext(data, template, userSections);
11919
12077
  return compiledLayout(ctx);
11920
12078
  }
11921
12079
  renderFromPath(data, filePath) {
@@ -11930,17 +12088,11 @@ var TemplateEngine = class {
11930
12088
  return compiledFn(this.buildCustomContext(data));
11931
12089
  }
11932
12090
  buildCustomContext(data) {
12091
+ const sections = allSectionsOn();
11933
12092
  return {
11934
12093
  ...data,
11935
- options: {
11936
- showCharts: true,
11937
- showTrend: true,
11938
- showStackTraces: true,
11939
- showFullEnvironment: true,
11940
- showPassRate: true,
11941
- showRetries: true,
11942
- showFullFailures: true
11943
- },
12094
+ options: deriveOptions(sections),
12095
+ sections,
11944
12096
  tagGroups: this.buildTagGroups(data),
11945
12097
  slowTests: this.buildSlowTests(data),
11946
12098
  baseCSS: this.fontsBundleCss + "\n" + this.readStyle("base.css"),
@@ -11948,47 +12100,20 @@ var TemplateEngine = class {
11948
12100
  chartjsScript: this.buildChartjsScript()
11949
12101
  };
11950
12102
  }
11951
- buildContext(data, template) {
12103
+ buildContext(data, template, userSections) {
11952
12104
  const baseCSS = this.fontsBundleCss + "\n" + this.readStyle("base.css");
11953
12105
  const templateCSS = this.readStyle(`${template}.css`);
11954
- const optionsMap = {
11955
- minimal: {
11956
- showCharts: false,
11957
- showTrend: false,
11958
- showStackTraces: true,
11959
- showFullEnvironment: false,
11960
- showPassRate: true,
11961
- showRetries: true,
11962
- showFullFailures: true
11963
- },
11964
- detailed: {
11965
- showCharts: true,
11966
- showTrend: true,
11967
- showStackTraces: true,
11968
- showFullEnvironment: true,
11969
- showPassRate: true,
11970
- showRetries: true,
11971
- showFullFailures: true
11972
- },
11973
- executive: {
11974
- showCharts: true,
11975
- showTrend: true,
11976
- showStackTraces: false,
11977
- showFullEnvironment: false,
11978
- showPassRate: true,
11979
- showRetries: false,
11980
- showFullFailures: false
11981
- }
11982
- };
11983
- const chartjsScript = this.buildChartjsScript();
12106
+ const sections = resolveSections(template, userSections);
12107
+ const options = deriveOptions(sections);
11984
12108
  return {
11985
12109
  ...data,
11986
- options: optionsMap[template],
12110
+ options,
12111
+ sections,
11987
12112
  tagGroups: this.buildTagGroups(data),
11988
12113
  slowTests: this.buildSlowTests(data),
11989
12114
  baseCSS,
11990
12115
  templateCSS,
11991
- chartjsScript
12116
+ chartjsScript: this.buildChartjsScript()
11992
12117
  };
11993
12118
  }
11994
12119
  buildChartjsScript() {
@@ -12036,9 +12161,12 @@ var TemplateEngine = class {
12036
12161
  return tests.sort((a, b) => b.durationMs - a.durationMs).slice(0, 10);
12037
12162
  }
12038
12163
  readStyle(filename) {
12164
+ const cached = this.styleCache.get(filename);
12165
+ if (cached !== void 0) return cached;
12039
12166
  const stylePath = import_path3.default.join(this.templatesDir, "styles", filename);
12040
- if (!import_fs4.default.existsSync(stylePath)) return "";
12041
- return import_fs4.default.readFileSync(stylePath, "utf8");
12167
+ const css = import_fs4.default.existsSync(stylePath) ? import_fs4.default.readFileSync(stylePath, "utf8") : "";
12168
+ this.styleCache.set(filename, css);
12169
+ return css;
12042
12170
  }
12043
12171
  registerPartials() {
12044
12172
  const partialsDir = import_path3.default.join(this.templatesDir, "partials");
@@ -12099,7 +12227,7 @@ var TemplateEngine = class {
12099
12227
  if (!Array.isArray(arr)) return "";
12100
12228
  const values = arr.map((item) => {
12101
12229
  const v = item[key];
12102
- return typeof v === "string" ? JSON.stringify(v) : String(v ?? 0);
12230
+ return typeof v === "string" ? jsonForInlineScript(v) : String(v ?? 0);
12103
12231
  });
12104
12232
  return new import_handlebars.default.SafeString(values.join(","));
12105
12233
  }
@@ -12110,7 +12238,7 @@ var TemplateEngine = class {
12110
12238
  this.handlebars.registerHelper("absFixed", (n) => Math.abs(n).toFixed(0));
12111
12239
  this.handlebars.registerHelper(
12112
12240
  "safeJsonData",
12113
- (value) => new import_handlebars.default.SafeString(JSON.stringify(value).replace(/<\//g, "<\\/"))
12241
+ (value) => new import_handlebars.default.SafeString(jsonForInlineScript(value))
12114
12242
  );
12115
12243
  this.handlebars.registerHelper(
12116
12244
  "reverse",
@@ -12593,6 +12721,12 @@ var PdfGenerator = class {
12593
12721
  `Compression: level=${compression.effective} jpeg-q=${compression.quality} max-w=${compression.maxWidth}px inline-failures\u2264${compression.maxInlineFailures}`
12594
12722
  );
12595
12723
  const outputPaths = [];
12724
+ if (options.sections !== void 0) {
12725
+ const hasCustom = sources.some((s) => s.kind !== "builtin");
12726
+ if (hasCustom) {
12727
+ logger.warn('[reportforge] "sections" applies to built-in templates only; it is ignored for custom templatePath templates.');
12728
+ }
12729
+ }
12596
12730
  const { page } = await this.browserManager.launch(options.puppeteerExecutablePath);
12597
12731
  try {
12598
12732
  for (const source of sources) {
@@ -12700,13 +12834,13 @@ var PdfGenerator = class {
12700
12834
  }
12701
12835
  const templateLabel = source.kind === "builtin" ? source.id : source.label;
12702
12836
  logger.info(`Rendering template: ${templateLabel}`);
12703
- const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
12837
+ const html = source.kind === "builtin" ? this.templateEngine.render(renderData, source.id, options.sections) : this.templateEngine.renderFromPath(renderData, source.absolutePath);
12704
12838
  const tempHtmlPath = await this.htmlWriter.write(html);
12705
12839
  const fileUrl = this.htmlWriter.toFileUrl(tempHtmlPath);
12706
12840
  try {
12707
12841
  logger.info(`Navigating to temp HTML (${Math.round(html.length / 1024)} KB)...`);
12708
12842
  await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 6e4 });
12709
- const showCharts = source.kind === "builtin" ? source.id !== "minimal" : true;
12843
+ const showCharts = source.kind === "builtin" ? resolveSections(source.id, options.sections).charts : true;
12710
12844
  await this.chartRenderer.waitForCharts(page, showCharts);
12711
12845
  logger.info(`Generating PDF \u2192 ${import_path8.default.relative(process.cwd(), outputPath)}`);
12712
12846
  await page.pdf({
@@ -13097,7 +13231,7 @@ var PdfReporter = class {
13097
13231
  this.dataCollector = new DataCollector();
13098
13232
  this.pdfGenerator = new PdfGenerator();
13099
13233
  this.options = parseOptions(rawOptions);
13100
- const version = true ? "0.5.0" : "0.x";
13234
+ const version = true ? "0.6.0" : "0.x";
13101
13235
  logger.info(`@reportforge/playwright-pdf v${version} initialised`);
13102
13236
  this.licenseClient = new LicenseClient({
13103
13237
  licenseKey: this.options.licenseKey,