@reportforge/playwright-pdf 0.18.0 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
+ ## [0.19.1] - 2026-07-11
7
+
8
+ ### Fixed
9
+
10
+ - **A blank browser window no longer flashes during report generation on Windows.** Chrome's new headless mode (the only mode left in Chrome 132+) briefly uncloaks a window on some Windows/GPU combinations; the PDF renderer now parks that window far off-screen. No effect on PDF output or other platforms.
11
+
12
+ ---
13
+
14
+ ## [0.19.0] - 2026-07-11
15
+
16
+ ### Added
17
+
18
+ - **New `logLevel` option** (`'silent' | 'error' | 'warn' | 'info' | 'debug'`, default `'info'`) controls how chatty the reporter's `[reportforge]` console lines are. `'debug'` prints the same diagnostics as `RF_DEBUG=1` (useful in CI where env vars are awkward to set); `'silent'` suppresses everything, including error lines. `RF_DEBUG=1` always forces debug output, overriding the option, so "set `RF_DEBUG=1` and send the log" keeps working regardless of config.
19
+ - **`RF_DEBUG=1` now explains previously silent failure paths**: license cache reads/writes, cached-token rejection (including exactly why the reporter re-activated instead of serving from cache), device-id persistence, Chrome discovery, and the qpdf availability probe.
20
+
21
+ ### Changed
22
+
23
+ - A failed write of `~/.reportforge/device.json` now logs a warning, because it means this machine may register a new license seat on every run.
24
+ - The live watch URL is printed once instead of twice; the banner link line now carries the `[reportforge]` prefix so it stays greppable in CI logs.
25
+ - CLI output prefixes are consistent: the `demo` and `export-feedback` commands use the same `[reportforge]` prefix as the reporter.
26
+
27
+ ---
28
+
6
29
  ## [0.18.0] - 2026-07-11
7
30
 
8
31
  ### Changed
package/README.md CHANGED
@@ -189,6 +189,7 @@ All options are the second element of the reporter tuple.
189
189
  | `reportTitle` | `string` | `'Playwright Test Report'` | Custom title for the report cover page and running header. |
190
190
  | `projectName` | `string` | from `package.json` | Project or application name. Inferred from the `package.json` `name` field if absent. |
191
191
  | `open` | `boolean` | `false` | Open the generated PDF automatically after generation. For local use only; ignored in CI. |
192
+ | `logLevel` | `'silent' \| 'error' \| 'warn' \| 'info' \| 'debug'` | `'info'` | Console verbosity for `[reportforge]` log lines. `'silent'` suppresses everything including errors; `'debug'` adds diagnostics. The `RF_DEBUG=1` env var always forces debug, overriding this option. |
192
193
  | `puppeteerExecutablePath` | `string` | auto-detect | Full path to a Chrome or Chromium binary. Falls back to `PUPPETEER_EXECUTABLE_PATH` env, then system Chrome discovery. |
193
194
  | `serverUrl` | `string` | `'https://reportforge.org'` | Base URL for the ReportForge licensing server. Override only for self-hosting or local development. |
194
195
  | `compressionLevel` | `'auto' \| 'none' \| 'balanced' \| 'max'` | `'auto'` | Screenshot JPEG quality preset. `'auto'` picks based on failure volume; `'none'` keeps original PNGs; `'balanced'` uses JPEG q85; `'max'` uses JPEG q70. |
@@ -644,6 +645,18 @@ Set `RF_DEBUG=1` to log the full activation flow and error stack traces:
644
645
  RF_DEBUG=1 npx playwright test 2>&1 | grep '\[reportforge\]'
645
646
  ```
646
647
 
648
+ ### Controlling log output
649
+
650
+ The `logLevel` option sets how chatty the reporter's `[reportforge]` console lines are:
651
+
652
+ ```ts
653
+ ['@reportforge/playwright-pdf', { logLevel: 'warn' }] // warnings and errors only
654
+ ```
655
+
656
+ - `'debug'` prints the same diagnostics as `RF_DEBUG=1` — useful in CI where env vars are awkward to set.
657
+ - `'silent'` suppresses everything, **including error lines**: a failed PDF then surfaces only as a missing file, because the reporter never fails your test run.
658
+ - `RF_DEBUG=1` always forces debug output, overriding `logLevel` — it is the support escape hatch, so "set `RF_DEBUG=1` and send the log" works no matter what the config says.
659
+
647
660
  Common issues:
648
661
 
649
662
  - **`Chrome/Chromium not found`** → install Google Chrome Stable; don't use the Ubuntu `chromium-browser` snap.
package/dist/cli/index.js CHANGED
@@ -42,24 +42,32 @@ var init_cjs_shims = __esm({
42
42
  });
43
43
 
44
44
  // src/utils/logger.ts
45
- var PREFIX, logger;
45
+ function isLevelEnabled(level) {
46
+ return LEVELS[level] >= (process.env.RF_DEBUG ? LEVELS.debug : threshold);
47
+ }
48
+ var PREFIX, LEVELS, threshold, logger;
46
49
  var init_logger = __esm({
47
50
  "src/utils/logger.ts"() {
48
51
  "use strict";
49
52
  init_cjs_shims();
50
53
  PREFIX = "[reportforge]";
54
+ LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
55
+ threshold = LEVELS.info;
51
56
  logger = {
52
57
  info(msg) {
58
+ if (!isLevelEnabled("info")) return;
53
59
  console.log(`${PREFIX} ${msg}`);
54
60
  },
55
61
  warn(msg) {
62
+ if (!isLevelEnabled("warn")) return;
56
63
  console.warn(`${PREFIX} WARN: ${msg}`);
57
64
  },
58
65
  error(msg) {
66
+ if (!isLevelEnabled("error")) return;
59
67
  console.error(`${PREFIX} ERROR: ${msg}`);
60
68
  },
61
69
  debug(msg, data) {
62
- if (!process.env.RF_DEBUG) return;
70
+ if (!isLevelEnabled("debug")) return;
63
71
  const suffix = data !== void 0 ? `
64
72
  ${JSON.stringify(data, null, 2)}` : "";
65
73
  console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
@@ -7228,7 +7236,7 @@ var require_lib2 = __commonJS({
7228
7236
  });
7229
7237
 
7230
7238
  // src/pdf/BrowserManager.ts
7231
- var import_child_process, import_fs3, BrowserManager;
7239
+ var import_child_process, import_fs3, LAUNCH_ARGS, BrowserManager;
7232
7240
  var init_BrowserManager = __esm({
7233
7241
  "src/pdf/BrowserManager.ts"() {
7234
7242
  "use strict";
@@ -7236,29 +7244,33 @@ var init_BrowserManager = __esm({
7236
7244
  import_child_process = require("child_process");
7237
7245
  import_fs3 = __toESM(require("fs"));
7238
7246
  init_logger();
7247
+ LAUNCH_ARGS = [
7248
+ "--no-sandbox",
7249
+ "--disable-setuid-sandbox",
7250
+ "--disable-dev-shm-usage",
7251
+ "--disable-gpu",
7252
+ // Chrome's new headless mode (--headless=new, the only mode left in
7253
+ // Chrome 132+) briefly uncloaks a blank window on some Windows/GPU
7254
+ // combinations. Parking the window far off-screen makes that transient
7255
+ // invisible; harmless everywhere else and does not affect PDF output.
7256
+ "--window-position=-32000,-32000"
7257
+ ];
7239
7258
  BrowserManager = class {
7240
7259
  async launch(executablePath) {
7241
7260
  const puppeteer = require("puppeteer-core");
7242
7261
  const chromePath = this.resolveChromePath(executablePath, puppeteer);
7243
7262
  logger.info(`Launching browser: ${chromePath ?? "puppeteer default"}`);
7244
- const launchArgs = [
7245
- "--no-sandbox",
7246
- "--disable-setuid-sandbox",
7247
- "--disable-dev-shm-usage",
7248
- "--disable-gpu"
7249
- ];
7250
7263
  this.browser = await puppeteer.launch({
7251
7264
  executablePath: chromePath,
7252
7265
  headless: true,
7253
- args: launchArgs
7266
+ args: LAUNCH_ARGS
7254
7267
  });
7255
7268
  const page = await this.browser.newPage();
7256
7269
  return { browser: this.browser, page };
7257
7270
  }
7258
7271
  async close() {
7259
7272
  if (this.browser) {
7260
- await this.browser.close().catch(() => {
7261
- });
7273
+ await this.browser.close().catch((err) => logger.debug("browser close failed (ignored)", { error: err.message }));
7262
7274
  this.browser = void 0;
7263
7275
  }
7264
7276
  }
@@ -7270,7 +7282,8 @@ var init_BrowserManager = __esm({
7270
7282
  const chromeFinder = require_lib2();
7271
7283
  const found = chromeFinder();
7272
7284
  if (found && import_fs3.default.existsSync(found)) return found;
7273
- } catch {
7285
+ } catch (err) {
7286
+ logger.debug("chrome discovery: chrome-finder failed", { error: err.message });
7274
7287
  }
7275
7288
  const linuxPaths = [
7276
7289
  "/usr/bin/google-chrome",
@@ -7288,12 +7301,14 @@ var init_BrowserManager = __esm({
7288
7301
  }).toString().trim().split("\n")[0];
7289
7302
  if (found && import_fs3.default.existsSync(found)) return found;
7290
7303
  } catch {
7304
+ logger.debug("chrome discovery: which lookup found nothing");
7291
7305
  }
7292
7306
  if (puppeteer.executablePath) {
7293
7307
  try {
7294
7308
  const bundled = puppeteer.executablePath();
7295
7309
  if (bundled && import_fs3.default.existsSync(bundled)) return bundled;
7296
- } catch {
7310
+ } catch (err) {
7311
+ logger.debug("chrome discovery: no puppeteer bundled executable", { error: err.message });
7297
7312
  }
7298
7313
  }
7299
7314
  logger.warn(
@@ -7315,6 +7330,7 @@ var init_HtmlWriter = __esm({
7315
7330
  import_os = __toESM(require("os"));
7316
7331
  import_path2 = __toESM(require("path"));
7317
7332
  import_crypto = __toESM(require("crypto"));
7333
+ init_logger();
7318
7334
  HtmlWriter = class {
7319
7335
  async write(html) {
7320
7336
  const tmpDir = import_os.default.tmpdir();
@@ -7325,8 +7341,10 @@ var init_HtmlWriter = __esm({
7325
7341
  }
7326
7342
  async cleanup() {
7327
7343
  if (this.tempPath) {
7328
- await import_fs4.default.promises.unlink(this.tempPath).catch(() => {
7329
- });
7344
+ const tempPath = this.tempPath;
7345
+ await import_fs4.default.promises.unlink(tempPath).catch(
7346
+ (err) => logger.debug("temp HTML cleanup failed (ignored)", { path: tempPath, error: err.message })
7347
+ );
7330
7348
  this.tempPath = void 0;
7331
7349
  }
7332
7350
  }
@@ -7405,7 +7423,8 @@ var init_PdfEncryptor = __esm({
7405
7423
  try {
7406
7424
  (0, import_child_process2.execFileSync)("qpdf", ["--version"], { stdio: "pipe" });
7407
7425
  return true;
7408
- } catch {
7426
+ } catch (err) {
7427
+ logger.debug("qpdf probe failed \u2014 treating as not installed", { error: err.message });
7409
7428
  return false;
7410
7429
  }
7411
7430
  }
@@ -7638,7 +7657,8 @@ var init_PdfGenerator = __esm({
7638
7657
  if (CHARTJS_BUNDLE2) {
7639
7658
  this.templateEngine.setChartjsBundle(CHARTJS_BUNDLE2);
7640
7659
  }
7641
- } catch {
7660
+ } catch (err) {
7661
+ logger.debug("chartjs bundle not available \u2014 charts disabled", { error: err.message });
7642
7662
  }
7643
7663
  }
7644
7664
  /**
@@ -12085,6 +12105,7 @@ var init_schema = __esm({
12085
12105
  reportTitle: external_exports.string().optional().default(DEFAULT_OPTIONS.reportTitle),
12086
12106
  projectName: external_exports.string().optional(),
12087
12107
  open: external_exports.boolean().optional().default(DEFAULT_OPTIONS.open),
12108
+ logLevel: external_exports.enum(["silent", "error", "warn", "info", "debug"]).optional().default("info"),
12088
12109
  puppeteerExecutablePath: external_exports.string().optional(),
12089
12110
  serverUrl: external_exports.string().url().optional(),
12090
12111
  compressionLevel: external_exports.enum(["auto", "none", "balanced", "max"]).optional().default(DEFAULT_OPTIONS.compressionLevel),
@@ -12549,7 +12570,7 @@ function parseArgs() {
12549
12570
  const rawTemplate = templateArg?.slice("--template=".length);
12550
12571
  const rawOutput = outputArg?.slice("--output=".length);
12551
12572
  if (rawTemplate && !VALID_TEMPLATES.has(rawTemplate)) {
12552
- console.warn(`[ReportForge] Unknown template '${rawTemplate}', using 'detailed'. Valid: ${TEMPLATE_IDS.join(", ")}`);
12573
+ console.warn(`[reportforge] Unknown template '${rawTemplate}', using 'detailed'. Valid: ${TEMPLATE_IDS.join(", ")}`);
12553
12574
  }
12554
12575
  const template = rawTemplate && VALID_TEMPLATES.has(rawTemplate) ? rawTemplate : "detailed";
12555
12576
  const outputFile = rawOutput ?? import_path7.default.join("playwright-report", "demo-report.pdf");
@@ -12557,7 +12578,7 @@ function parseArgs() {
12557
12578
  }
12558
12579
  async function run() {
12559
12580
  const { template, outputFile } = parseArgs();
12560
- console.log("[ReportForge] Generating demo report...");
12581
+ console.log("[reportforge] Generating demo report...");
12561
12582
  console.log(` Template : ${template}`);
12562
12583
  console.log(` Output : ${outputFile}`);
12563
12584
  const reportData = buildDemoReportData(template);
@@ -12568,10 +12589,10 @@ async function run() {
12568
12589
  const paths = await generator.generateAll(reportData, options, DEMO_LICENSE_INFO2);
12569
12590
  if (paths.length > 0) {
12570
12591
  console.log(`
12571
- [ReportForge] Demo report written: ${paths[0]}`);
12592
+ [reportforge] Demo report written: ${paths[0]}`);
12572
12593
  console.log(" Open the PDF to preview exactly what ReportForge generates for your Playwright runs.");
12573
12594
  } else {
12574
- console.warn("\n[ReportForge] Warning: no PDF was written \u2014 check that Chrome is available.");
12595
+ console.warn("\n[reportforge] Warning: no PDF was written \u2014 check that Chrome is available.");
12575
12596
  process.exit(1);
12576
12597
  }
12577
12598
  }
@@ -12581,14 +12602,14 @@ async function runDemo() {
12581
12602
  } catch (err) {
12582
12603
  const msg = err instanceof Error ? err.message : String(err);
12583
12604
  if (/chrome|chromium|executable|browser|launcher/i.test(msg)) {
12584
- console.error("\n[ReportForge] Chrome not found.\n");
12605
+ console.error("\n[reportforge] Chrome not found.\n");
12585
12606
  console.error(" Fix: set PUPPETEER_EXECUTABLE_PATH, e.g.:");
12586
12607
  console.error(" PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable \\");
12587
12608
  console.error(" npx @reportforge/playwright-pdf demo\n");
12588
12609
  console.error(` Original error: ${msg}`);
12589
12610
  } else {
12590
12611
  console.error(`
12591
- [ReportForge] Demo generation failed: ${msg}`);
12612
+ [reportforge] Demo generation failed: ${msg}`);
12592
12613
  if (process.env.RF_DEBUG) console.error(err);
12593
12614
  }
12594
12615
  process.exit(1);
@@ -12648,10 +12669,10 @@ ${rows.join("\n")}
12648
12669
  function exportFeedbackCli() {
12649
12670
  const res = exportFeedback();
12650
12671
  if (res.rowCount === 0) {
12651
- process.stdout.write("No locally collected failures found (~/.reportforge/*/unclassified.csv).\n");
12672
+ process.stdout.write("[reportforge] No locally collected failures found (~/.reportforge/*/unclassified.csv).\n");
12652
12673
  } else {
12653
12674
  process.stdout.write(
12654
- `Saved ${res.rowCount} row(s) to ${res.outFile} - tokenized + redacted, LOCAL only.
12675
+ `[reportforge] Saved ${res.rowCount} row(s) to ${res.outFile} - tokenized + redacted, LOCAL only.
12655
12676
  Review it before sharing; there is no upload.
12656
12677
  `
12657
12678
  );
package/dist/index.d.mts CHANGED
@@ -91,6 +91,16 @@ interface ReporterOptions {
91
91
  * @default false
92
92
  */
93
93
  open?: boolean;
94
+ /**
95
+ * Console verbosity for reporter log lines (the `[reportforge]` prefix).
96
+ * `'silent'` suppresses everything, including error lines — a PDF failure
97
+ * then surfaces only as a missing file (the reporter never fails the test
98
+ * run). `'debug'` adds diagnostic detail. The `RF_DEBUG=1` environment
99
+ * variable always forces `'debug'`, overriding this option (support
100
+ * escape hatch).
101
+ * @default 'info'
102
+ */
103
+ logLevel?: 'silent' | 'error' | 'warn' | 'info' | 'debug';
94
104
  /**
95
105
  * Full path to a Chrome/Chromium executable.
96
106
  * Falls back to PUPPETEER_EXECUTABLE_PATH env, then system Chrome discovery.
package/dist/index.d.ts CHANGED
@@ -91,6 +91,16 @@ interface ReporterOptions {
91
91
  * @default false
92
92
  */
93
93
  open?: boolean;
94
+ /**
95
+ * Console verbosity for reporter log lines (the `[reportforge]` prefix).
96
+ * `'silent'` suppresses everything, including error lines — a PDF failure
97
+ * then surfaces only as a missing file (the reporter never fails the test
98
+ * run). `'debug'` adds diagnostic detail. The `RF_DEBUG=1` environment
99
+ * variable always forces `'debug'`, overriding this option (support
100
+ * escape hatch).
101
+ * @default 'info'
102
+ */
103
+ logLevel?: 'silent' | 'error' | 'warn' | 'info' | 'debug';
94
104
  /**
95
105
  * Full path to a Chrome/Chromium executable.
96
106
  * Falls back to PUPPETEER_EXECUTABLE_PATH env, then system Chrome discovery.
package/dist/index.js CHANGED
@@ -10532,6 +10532,7 @@ var ReporterOptionsSchema = external_exports.object({
10532
10532
  reportTitle: external_exports.string().optional().default(DEFAULT_OPTIONS.reportTitle),
10533
10533
  projectName: external_exports.string().optional(),
10534
10534
  open: external_exports.boolean().optional().default(DEFAULT_OPTIONS.open),
10535
+ logLevel: external_exports.enum(["silent", "error", "warn", "info", "debug"]).optional().default("info"),
10535
10536
  puppeteerExecutablePath: external_exports.string().optional(),
10536
10537
  serverUrl: external_exports.string().url().optional(),
10537
10538
  compressionLevel: external_exports.enum(["auto", "none", "balanced", "max"]).optional().default(DEFAULT_OPTIONS.compressionLevel),
@@ -10668,13 +10669,48 @@ var import_fs = __toESM(require("fs"));
10668
10669
  var import_path = __toESM(require("path"));
10669
10670
  var import_os = __toESM(require("os"));
10670
10671
  var import_crypto3 = require("crypto");
10672
+
10673
+ // src/utils/logger.ts
10674
+ init_cjs_shims();
10675
+ var PREFIX = "[reportforge]";
10676
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
10677
+ var threshold = LEVELS.info;
10678
+ function setLogLevel(level) {
10679
+ threshold = LEVELS[level];
10680
+ }
10681
+ function isLevelEnabled(level) {
10682
+ return LEVELS[level] >= (process.env.RF_DEBUG ? LEVELS.debug : threshold);
10683
+ }
10684
+ var logger = {
10685
+ info(msg) {
10686
+ if (!isLevelEnabled("info")) return;
10687
+ console.log(`${PREFIX} ${msg}`);
10688
+ },
10689
+ warn(msg) {
10690
+ if (!isLevelEnabled("warn")) return;
10691
+ console.warn(`${PREFIX} WARN: ${msg}`);
10692
+ },
10693
+ error(msg) {
10694
+ if (!isLevelEnabled("error")) return;
10695
+ console.error(`${PREFIX} ERROR: ${msg}`);
10696
+ },
10697
+ debug(msg, data) {
10698
+ if (!isLevelEnabled("debug")) return;
10699
+ const suffix = data !== void 0 ? `
10700
+ ${JSON.stringify(data, null, 2)}` : "";
10701
+ console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
10702
+ }
10703
+ };
10704
+
10705
+ // src/license/DeviceId.ts
10671
10706
  var DEVICE_FILE = import_path.default.join(import_os.default.homedir(), ".reportforge", "device.json");
10672
10707
  function getStableDeviceId() {
10673
10708
  try {
10674
10709
  const raw = import_fs.default.readFileSync(DEVICE_FILE, "utf8");
10675
10710
  const parsed = JSON.parse(raw);
10676
10711
  if (parsed.id && /^[0-9a-f-]{36}$/i.test(parsed.id)) return parsed.id;
10677
- } catch {
10712
+ } catch (err) {
10713
+ logger.debug("device id: no readable device.json \u2014 creating new id", { error: err.message });
10678
10714
  }
10679
10715
  const id = (0, import_crypto3.randomUUID)();
10680
10716
  try {
@@ -10682,7 +10718,10 @@ function getStableDeviceId() {
10682
10718
  import_fs.default.writeFileSync(DEVICE_FILE, JSON.stringify({ id, createdAt: Date.now() }), {
10683
10719
  mode: 384
10684
10720
  });
10685
- } catch {
10721
+ } catch (err) {
10722
+ logger.warn(
10723
+ `Could not persist device id (~/.reportforge/device.json) \u2014 this machine may register a new seat on every run: ${err.message}`
10724
+ );
10686
10725
  }
10687
10726
  return id;
10688
10727
  }
@@ -10709,10 +10748,13 @@ function getMachineFingerprint() {
10709
10748
  label: `${ci.provider}/${ci.repo} (CI)`
10710
10749
  };
10711
10750
  }
10712
- const hostname = safe(() => import_os2.default.hostname()) ?? "unknown-host";
10751
+ const hostname = safe(() => import_os2.default.hostname());
10752
+ if (hostname === void 0) {
10753
+ logger.debug("fingerprint: os.hostname() failed \u2014 label falls back to unknown-host");
10754
+ }
10713
10755
  return {
10714
10756
  hash: sha256Hex(`dev:${getStableDeviceId()}`),
10715
- label: `${hostname} (dev)`
10757
+ label: `${hostname ?? "unknown-host"} (dev)`
10716
10758
  };
10717
10759
  }
10718
10760
  function detectCiRepo() {
@@ -10754,19 +10796,25 @@ function readCache() {
10754
10796
  if (f.endsWith(".tmp")) {
10755
10797
  try {
10756
10798
  import_fs2.default.unlinkSync(import_path2.default.join(CACHE_DIR, f));
10757
- } catch {
10799
+ } catch (err) {
10800
+ logger.debug("license cache: stale tmp cleanup failed", { file: f, error: err.message });
10758
10801
  }
10759
10802
  }
10760
10803
  }
10761
- } catch {
10804
+ } catch (err) {
10805
+ logger.debug("license cache: cache dir not readable (first run?)", { error: err.message });
10762
10806
  }
10763
10807
  try {
10764
10808
  if (!import_fs2.default.existsSync(CACHE_FILE)) return null;
10765
10809
  const raw = import_fs2.default.readFileSync(CACHE_FILE, "utf8");
10766
10810
  const parsed = JSON.parse(raw);
10767
- if (typeof parsed.jwt !== "string" || typeof parsed.expiresAt !== "number") return null;
10811
+ if (typeof parsed.jwt !== "string" || typeof parsed.expiresAt !== "number") {
10812
+ logger.debug("license cache: malformed entry \u2014 ignoring");
10813
+ return null;
10814
+ }
10768
10815
  return parsed;
10769
- } catch {
10816
+ } catch (err) {
10817
+ logger.debug("license cache: read failed \u2014 treating as no cache", { error: err.message });
10770
10818
  return null;
10771
10819
  }
10772
10820
  }
@@ -10776,13 +10824,15 @@ function writeCache(entry) {
10776
10824
  const tmp = `${CACHE_FILE}.${process.pid}.tmp`;
10777
10825
  import_fs2.default.writeFileSync(tmp, JSON.stringify(entry, null, 2), { mode: 384 });
10778
10826
  import_fs2.default.renameSync(tmp, CACHE_FILE);
10779
- } catch {
10827
+ } catch (err) {
10828
+ logger.debug("license cache: write failed \u2014 will re-activate next run", { error: err.message });
10780
10829
  }
10781
10830
  }
10782
10831
  function clearCache() {
10783
10832
  try {
10784
10833
  import_fs2.default.unlinkSync(CACHE_FILE);
10785
- } catch {
10834
+ } catch (err) {
10835
+ logger.debug("license cache: clear failed", { error: err.message });
10786
10836
  }
10787
10837
  }
10788
10838
 
@@ -10801,27 +10851,6 @@ var import_node_crypto = require("crypto");
10801
10851
  init_cjs_shims();
10802
10852
  var MODEL = { "alpha": 1, "labels": ["assertion", "env-config", "flaky-by-retry", "locator-not-found", "navigation", "network", "timeout"], "logLikelihoods": { "assertion": { "NUM": -4.574710978503383, "assertionerror": -4.980176086611547, "be": -4.980176086611547, "cart": -4.980176086611547, "checkout": -4.980176086611547, "com": -4.980176086611547, "dashboard": -4.980176086611547, "deep": -4.980176086611547, "equal": -4.980176086611547, "equality": -4.980176086611547, "error": -4.980176086611547, "expect": -3.5938817254916566, "expected": -3.1083739097099556, "failed": -4.980176086611547, "false": -4.980176086611547, "got": -4.980176086611547, "id": -4.574710978503383, "john": -4.980176086611547, "locator": -4.574710978503383, "login": -4.980176086611547, "must": -4.980176086611547, "page": -4.980176086611547, "pattern": -4.980176086611547, "received": -3.275427994373122, "string": -4.980176086611547, "to": -4.980176086611547, "tobe": -4.980176086611547, "tobetruthy": -4.980176086611547, "toequal": -4.980176086611547, "tohavecount": -4.980176086611547, "tohavetext": -4.980176086611547, "tohavetitle": -4.980176086611547, "tohavevalue": -4.980176086611547, "truthy": -4.980176086611547, "value": -4.980176086611547 }, "env-config": { "NUM": -4.997212273764115, "api": -4.997212273764115, "at": -4.997212273764115, "back": -4.997212273764115, "base": -4.997212273764115, "baseurl": -4.997212273764115, "browsers": -4.997212273764115, "cannot": -4.997212273764115, "ci": -4.997212273764115, "config": -4.304065093204169, "directory": -4.591747165655951, "dotenv": -4.997212273764115, "enoent": -4.997212273764115, "env": -4.591747165655951, "environment": -4.591747165655951, "error": -4.08092154188996, "existent": -4.997212273764115, "failed": -4.997212273764115, "falling": -4.997212273764115, "file": -4.997212273764115, "find": -4.997212273764115, "from": -4.997212273764115, "in": -4.591747165655951, "invalid": -4.997212273764115, "is": -4.997212273764115, "json": -4.591747165655951, "key": -4.997212273764115, "load": -4.997212273764115, "missing": -4.997212273764115, "module": -4.997212273764115, "no": -4.997212273764115, "non": -4.997212273764115, "not": -4.997212273764115, "open": -4.997212273764115, "or": -4.997212273764115, "path": -4.997212273764115, "playwright": -4.591747165655951, "points": -4.997212273764115, "position": -4.997212273764115, "required": -4.997212273764115, "set": -4.997212273764115, "spec": -4.997212273764115, "such": -4.997212273764115, "syntaxerror": -4.997212273764115, "test": -4.997212273764115, "to": -4.304065093204169, "token": -4.997212273764115, "ts": -4.997212273764115, "tsconfig": -4.997212273764115, "undefined": -4.591747165655951, "unexpected": -4.997212273764115, "url": -4.997212273764115, "values": -4.997212273764115, "variable": -4.591747165655951 }, "flaky-by-retry": { "after": -4.58836306767171, "and": -4.993828175779875, "animation": -4.993828175779875, "attempt": -4.07753744390572, "between": -4.993828175779875, "blip": -4.993828175779875, "cause": -4.993828175779875, "change": -4.993828175779875, "click": -4.993828175779875, "code": -4.993828175779875, "commit": -4.993828175779875, "condition": -4.993828175779875, "dependency": -4.993828175779875, "detached": -4.993828175779875, "deterministic": -4.993828175779875, "element": -4.993828175779875, "failed": -4.300680995219929, "fails": -4.993828175779875, "flaky": -4.07753744390572, "green": -4.993828175779875, "in": -4.993828175779875, "intermittent": -4.993828175779875, "locator": -4.993828175779875, "marked": -4.993828175779875, "network": -4.993828175779875, "no": -4.993828175779875, "non": -4.993828175779875, "on": -3.895215887111765, "order": -4.993828175779875, "passed": -3.895215887111765, "quarantined": -4.993828175779875, "race": -4.993828175779875, "reproducible": -4.993828175779875, "rerun": -4.993828175779875, "retry": -4.07753744390572, "runs": -4.993828175779875, "same": -4.993828175779875, "second": -4.993828175779875, "stale": -4.993828175779875, "succeeded": -4.993828175779875, "test": -4.58836306767171, "the": -4.993828175779875, "then": -4.993828175779875, "timeout": -4.993828175779875, "unstable": -4.993828175779875, "with": -4.993828175779875, "without": -4.993828175779875 }, "locator-not-found": { "an": -4.993828175779875, "attached": -4.993828175779875, "btn": -4.993828175779875, "button": -4.58836306767171, "cart": -4.993828175779875, "checkout": -4.993828175779875, "click": -4.993828175779875, "data": -4.993828175779875, "div": -4.993828175779875, "dom": -4.993828175779875, "element": -4.07753744390572, "elements": -4.300680995219929, "error": -4.58836306767171, "find": -4.993828175779875, "for": -4.58836306767171, "found": -4.58836306767171, "getbyrole": -4.993828175779875, "getbytext": -4.993828175779875, "icon": -4.993828175779875, "id": -4.993828175779875, "is": -4.993828175779875, "locator": -4.07753744390572, "matched": -4.993828175779875, "matches": -4.993828175779875, "missing": -4.993828175779875, "mode": -4.58836306767171, "name": -4.993828175779875, "no": -4.58836306767171, "node": -4.993828175779875, "not": -4.58836306767171, "resolved": -4.58836306767171, "save": -4.993828175779875, "selector": -4.58836306767171, "strict": -4.58836306767171, "submit": -4.993828175779875, "testid": -4.993828175779875, "textcontent": -4.993828175779875, "the": -4.58836306767171, "to": -4.07753744390572, "total": -4.993828175779875, "unable": -4.993828175779875, "violation": -4.58836306767171, "waiting": -4.993828175779875, "with": -4.993828175779875, "xpath": -4.993828175779875 }, "navigation": { "NUM": -4.983606621708336, "aborted": -4.983606621708336, "another": -4.983606621708336, "back": -4.983606621708336, "because": -4.578141513600172, "by": -4.578141513600172, "checkout": -4.983606621708336, "context": -4.983606621708336, "crashed": -4.983606621708336, "dashboard": -4.983606621708336, "destroyed": -4.983606621708336, "detached": -4.983606621708336, "entry": -4.983606621708336, "err": -4.578141513600172, "exceeded": -4.983606621708336, "execution": -4.983606621708336, "failed": -4.983606621708336, "frame": -4.578141513600172, "goback": -4.983606621708336, "goto": -4.290459441148391, "history": -4.983606621708336, "home": -4.983606621708336, "interrupted": -4.578141513600172, "is": -4.983606621708336, "likely": -4.983606621708336, "login": -4.983606621708336, "many": -4.983606621708336, "maybe": -4.983606621708336, "most": -4.983606621708336, "navigate": -4.983606621708336, "navigating": -4.983606621708336, "navigation": -3.7308436532129683, "net": -4.578141513600172, "no": -4.983606621708336, "of": -4.983606621708336, "page": -3.884994333040227, "redirects": -4.983606621708336, "timeout": -4.983606621708336, "to": -3.7308436532129683, "too": -4.983606621708336, "waitforurl": -4.983606621708336, "was": -4.578141513600172 }, "network": { "NUM": -4.57126863431241, "URL": -4.283586561860629, "again": -4.976733742420574, "api": -4.976733742420574, "apirequestcontext": -4.976733742420574, "at": -4.976733742420574, "body": -4.976733742420574, "connect": -4.976733742420574, "connection": -4.57126863431241, "disconnected": -4.976733742420574, "eai": -4.976733742420574, "econnrefused": -4.976733742420574, "err": -3.8781214537524646, "error": -4.976733742420574, "failed": -4.283586561860629, "fetch": -4.976733742420574, "fetching": -4.976733742420574, "from": -4.976733742420574, "get": -4.976733742420574, "getaddrinfo": -4.976733742420574, "goto": -4.976733742420574, "hang": -4.976733742420574, "internet": -4.976733742420574, "loading": -4.976733742420574, "name": -4.976733742420574, "net": -3.8781214537524646, "not": -4.976733742420574, "on": -4.976733742420574, "out": -4.976733742420574, "page": -4.57126863431241, "products": -4.976733742420574, "protocol": -4.976733742420574, "reading": -4.976733742420574, "reason": -4.976733742420574, "refused": -4.976733742420574, "request": -4.57126863431241, "resolved": -4.976733742420574, "resource": -4.976733742420574, "response": -4.976733742420574, "route": -4.976733742420574, "socket": -4.976733742420574, "ssl": -4.976733742420574, "timed": -4.976733742420574, "to": -4.976733742420574, "up": -4.976733742420574, "upstream": -4.976733742420574, "while": -4.976733742420574 }, "timeout": { "NUM": -3.4965075614664802, "after": -5.000584958242754, "be": -4.59511985013459, "cart": -5.000584958242754, "click": -5.000584958242754, "element": -5.000584958242754, "event": -5.000584958242754, "exceeded": -3.6142905971228636, "expect": -5.000584958242754, "fill": -5.000584958242754, "for": -3.7478219897473863, "function": -5.000584958242754, "load": -5.000584958242754, "locator": -4.084294226368599, "navigation": -5.000584958242754, "of": -5.000584958242754, "out": -5.000584958242754, "page": -4.59511985013459, "running": -5.000584958242754, "selector": -5.000584958242754, "submit": -5.000584958242754, "test": -4.59511985013459, "timed": -5.000584958242754, "timeout": -3.6142905971228636, "timeouterror": -4.59511985013459, "to": -4.59511985013459, "tobevisible": -5.000584958242754, "visible": -4.59511985013459, "waitforloadstate": -5.000584958242754, "waitforselector": -5.000584958242754, "waiting": -3.7478219897473863, "while": -5.000584958242754 } }, "playwrightVersion": "1.59.1", "priors": { "assertion": -1.9636097261547143, "env-config": -1.9636097261547143, "flaky-by-retry": -1.9636097261547143, "locator-not-found": -1.9636097261547143, "navigation": -1.9636097261547143, "network": -1.9636097261547143, "timeout": -1.845826690498331 }, "totals": { "assertion": 64, "env-config": 69, "flaky-by-retry": 68, "locator-not-found": 68, "navigation": 65, "network": 63, "timeout": 70 }, "version": 1, "vocabSize": 227 };
10803
10853
 
10804
- // src/utils/logger.ts
10805
- init_cjs_shims();
10806
- var PREFIX = "[reportforge]";
10807
- var logger = {
10808
- info(msg) {
10809
- console.log(`${PREFIX} ${msg}`);
10810
- },
10811
- warn(msg) {
10812
- console.warn(`${PREFIX} WARN: ${msg}`);
10813
- },
10814
- error(msg) {
10815
- console.error(`${PREFIX} ERROR: ${msg}`);
10816
- },
10817
- debug(msg, data) {
10818
- if (!process.env.RF_DEBUG) return;
10819
- const suffix = data !== void 0 ? `
10820
- ${JSON.stringify(data, null, 2)}` : "";
10821
- console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
10822
- }
10823
- };
10824
-
10825
10854
  // src/analysis/ModelStore.ts
10826
10855
  var MODEL_SIG_DOMAIN = "reportforge-model-sig:v1\n";
10827
10856
  var testDeps = null;
@@ -11092,9 +11121,16 @@ var LicenseClient = class {
11092
11121
  function verifyCache(verifier, cached, expectedFp) {
11093
11122
  try {
11094
11123
  const payload = verifier.verify(cached.jwt);
11095
- if (payload.fp !== expectedFp) return null;
11124
+ if (payload.fp !== expectedFp) {
11125
+ logger.debug("license cache rejected: fingerprint mismatch", {
11126
+ cachedFp: payload.fp.slice(0, 12),
11127
+ expectedFp: expectedFp.slice(0, 12)
11128
+ });
11129
+ return null;
11130
+ }
11096
11131
  return payload;
11097
- } catch {
11132
+ } catch (err) {
11133
+ logger.debug("license cache rejected: JWT verify failed", { error: err.message });
11098
11134
  return null;
11099
11135
  }
11100
11136
  }
@@ -11150,7 +11186,8 @@ function maybePrintUpdateNotice(licenseInfo, currentVersion) {
11150
11186
  logger.info(
11151
11187
  `Update available: ${currentVersion} -> ${shown}. Run: npm i -D @reportforge/playwright-pdf@latest`
11152
11188
  );
11153
- } catch {
11189
+ } catch (err) {
11190
+ logger.debug("update notice failed (ignored)", { error: err.message });
11154
11191
  }
11155
11192
  }
11156
11193
 
@@ -12826,29 +12863,33 @@ var ScreenshotEmbedder = class {
12826
12863
  init_cjs_shims();
12827
12864
  var import_child_process2 = require("child_process");
12828
12865
  var import_fs6 = __toESM(require("fs"));
12866
+ var LAUNCH_ARGS = [
12867
+ "--no-sandbox",
12868
+ "--disable-setuid-sandbox",
12869
+ "--disable-dev-shm-usage",
12870
+ "--disable-gpu",
12871
+ // Chrome's new headless mode (--headless=new, the only mode left in
12872
+ // Chrome 132+) briefly uncloaks a blank window on some Windows/GPU
12873
+ // combinations. Parking the window far off-screen makes that transient
12874
+ // invisible; harmless everywhere else and does not affect PDF output.
12875
+ "--window-position=-32000,-32000"
12876
+ ];
12829
12877
  var BrowserManager = class {
12830
12878
  async launch(executablePath) {
12831
12879
  const puppeteer = require("puppeteer-core");
12832
12880
  const chromePath = this.resolveChromePath(executablePath, puppeteer);
12833
12881
  logger.info(`Launching browser: ${chromePath ?? "puppeteer default"}`);
12834
- const launchArgs = [
12835
- "--no-sandbox",
12836
- "--disable-setuid-sandbox",
12837
- "--disable-dev-shm-usage",
12838
- "--disable-gpu"
12839
- ];
12840
12882
  this.browser = await puppeteer.launch({
12841
12883
  executablePath: chromePath,
12842
12884
  headless: true,
12843
- args: launchArgs
12885
+ args: LAUNCH_ARGS
12844
12886
  });
12845
12887
  const page = await this.browser.newPage();
12846
12888
  return { browser: this.browser, page };
12847
12889
  }
12848
12890
  async close() {
12849
12891
  if (this.browser) {
12850
- await this.browser.close().catch(() => {
12851
- });
12892
+ await this.browser.close().catch((err) => logger.debug("browser close failed (ignored)", { error: err.message }));
12852
12893
  this.browser = void 0;
12853
12894
  }
12854
12895
  }
@@ -12860,7 +12901,8 @@ var BrowserManager = class {
12860
12901
  const chromeFinder = require_lib2();
12861
12902
  const found = chromeFinder();
12862
12903
  if (found && import_fs6.default.existsSync(found)) return found;
12863
- } catch {
12904
+ } catch (err) {
12905
+ logger.debug("chrome discovery: chrome-finder failed", { error: err.message });
12864
12906
  }
12865
12907
  const linuxPaths = [
12866
12908
  "/usr/bin/google-chrome",
@@ -12878,12 +12920,14 @@ var BrowserManager = class {
12878
12920
  }).toString().trim().split("\n")[0];
12879
12921
  if (found && import_fs6.default.existsSync(found)) return found;
12880
12922
  } catch {
12923
+ logger.debug("chrome discovery: which lookup found nothing");
12881
12924
  }
12882
12925
  if (puppeteer.executablePath) {
12883
12926
  try {
12884
12927
  const bundled = puppeteer.executablePath();
12885
12928
  if (bundled && import_fs6.default.existsSync(bundled)) return bundled;
12886
- } catch {
12929
+ } catch (err) {
12930
+ logger.debug("chrome discovery: no puppeteer bundled executable", { error: err.message });
12887
12931
  }
12888
12932
  }
12889
12933
  logger.warn(
@@ -12909,8 +12953,10 @@ var HtmlWriter = class {
12909
12953
  }
12910
12954
  async cleanup() {
12911
12955
  if (this.tempPath) {
12912
- await import_fs7.default.promises.unlink(this.tempPath).catch(() => {
12913
- });
12956
+ const tempPath = this.tempPath;
12957
+ await import_fs7.default.promises.unlink(tempPath).catch(
12958
+ (err) => logger.debug("temp HTML cleanup failed (ignored)", { path: tempPath, error: err.message })
12959
+ );
12914
12960
  this.tempPath = void 0;
12915
12961
  }
12916
12962
  }
@@ -12975,7 +13021,8 @@ var PdfEncryptor = class {
12975
13021
  try {
12976
13022
  (0, import_child_process3.execFileSync)("qpdf", ["--version"], { stdio: "pipe" });
12977
13023
  return true;
12978
- } catch {
13024
+ } catch (err) {
13025
+ logger.debug("qpdf probe failed \u2014 treating as not installed", { error: err.message });
12979
13026
  return false;
12980
13027
  }
12981
13028
  }
@@ -13132,7 +13179,8 @@ var PdfGenerator = class {
13132
13179
  if (CHARTJS_BUNDLE2) {
13133
13180
  this.templateEngine.setChartjsBundle(CHARTJS_BUNDLE2);
13134
13181
  }
13135
- } catch {
13182
+ } catch (err) {
13183
+ logger.debug("chartjs bundle not available \u2014 charts disabled", { error: err.message });
13136
13184
  }
13137
13185
  }
13138
13186
  /**
@@ -14048,7 +14096,7 @@ function buildWatchUrl(serverUrl, runId, token) {
14048
14096
  }
14049
14097
  function formatLiveBanner(url) {
14050
14098
  const label = "Live Tracker";
14051
- const link = `Watch live: ${url}`;
14099
+ const link = `[reportforge] Watch live: ${url}`;
14052
14100
  const width = Math.max(label.length, link.length);
14053
14101
  const rule = "\u2500".repeat(width + 2);
14054
14102
  return [rule, ` ${label.padEnd(width)} `, ` ${link.padEnd(width)} `, rule].join("\n");
@@ -14154,8 +14202,9 @@ var PdfReporter = class {
14154
14202
  this.liveSteps = "failed";
14155
14203
  this.liveConsole = false;
14156
14204
  this.options = parseOptions(rawOptions);
14205
+ setLogLevel(this.options.logLevel);
14157
14206
  this.dataCollector = new DataCollector(this.options.capture);
14158
- this.version = true ? "0.18.0" : "0.x";
14207
+ this.version = true ? "0.19.1" : "0.x";
14159
14208
  logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
14160
14209
  this.licenseClient = new LicenseClient({
14161
14210
  licenseKey: this.options.licenseKey,
@@ -14418,8 +14467,9 @@ var PdfReporter = class {
14418
14467
  );
14419
14468
  return;
14420
14469
  }
14421
- logger.info(`Live Tracker: ${watchUrl}`);
14422
- console.log(formatLiveBanner(watchUrl));
14470
+ if (isLevelEnabled("info")) {
14471
+ console.log(formatLiveBanner(watchUrl));
14472
+ }
14423
14473
  const isPrimaryShard = !shard || shard.current === 1;
14424
14474
  const notify = this.options.notify;
14425
14475
  if (isPrimaryShard && notify) {
package/dist/index.mjs CHANGED
@@ -10533,6 +10533,7 @@ var ReporterOptionsSchema = external_exports.object({
10533
10533
  reportTitle: external_exports.string().optional().default(DEFAULT_OPTIONS.reportTitle),
10534
10534
  projectName: external_exports.string().optional(),
10535
10535
  open: external_exports.boolean().optional().default(DEFAULT_OPTIONS.open),
10536
+ logLevel: external_exports.enum(["silent", "error", "warn", "info", "debug"]).optional().default("info"),
10536
10537
  puppeteerExecutablePath: external_exports.string().optional(),
10537
10538
  serverUrl: external_exports.string().url().optional(),
10538
10539
  compressionLevel: external_exports.enum(["auto", "none", "balanced", "max"]).optional().default(DEFAULT_OPTIONS.compressionLevel),
@@ -10669,13 +10670,48 @@ import fs from "fs";
10669
10670
  import path2 from "path";
10670
10671
  import os from "os";
10671
10672
  import { randomUUID } from "crypto";
10673
+
10674
+ // src/utils/logger.ts
10675
+ init_esm_shims();
10676
+ var PREFIX = "[reportforge]";
10677
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
10678
+ var threshold = LEVELS.info;
10679
+ function setLogLevel(level) {
10680
+ threshold = LEVELS[level];
10681
+ }
10682
+ function isLevelEnabled(level) {
10683
+ return LEVELS[level] >= (process.env.RF_DEBUG ? LEVELS.debug : threshold);
10684
+ }
10685
+ var logger = {
10686
+ info(msg) {
10687
+ if (!isLevelEnabled("info")) return;
10688
+ console.log(`${PREFIX} ${msg}`);
10689
+ },
10690
+ warn(msg) {
10691
+ if (!isLevelEnabled("warn")) return;
10692
+ console.warn(`${PREFIX} WARN: ${msg}`);
10693
+ },
10694
+ error(msg) {
10695
+ if (!isLevelEnabled("error")) return;
10696
+ console.error(`${PREFIX} ERROR: ${msg}`);
10697
+ },
10698
+ debug(msg, data) {
10699
+ if (!isLevelEnabled("debug")) return;
10700
+ const suffix = data !== void 0 ? `
10701
+ ${JSON.stringify(data, null, 2)}` : "";
10702
+ console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
10703
+ }
10704
+ };
10705
+
10706
+ // src/license/DeviceId.ts
10672
10707
  var DEVICE_FILE = path2.join(os.homedir(), ".reportforge", "device.json");
10673
10708
  function getStableDeviceId() {
10674
10709
  try {
10675
10710
  const raw = fs.readFileSync(DEVICE_FILE, "utf8");
10676
10711
  const parsed = JSON.parse(raw);
10677
10712
  if (parsed.id && /^[0-9a-f-]{36}$/i.test(parsed.id)) return parsed.id;
10678
- } catch {
10713
+ } catch (err) {
10714
+ logger.debug("device id: no readable device.json \u2014 creating new id", { error: err.message });
10679
10715
  }
10680
10716
  const id = randomUUID();
10681
10717
  try {
@@ -10683,7 +10719,10 @@ function getStableDeviceId() {
10683
10719
  fs.writeFileSync(DEVICE_FILE, JSON.stringify({ id, createdAt: Date.now() }), {
10684
10720
  mode: 384
10685
10721
  });
10686
- } catch {
10722
+ } catch (err) {
10723
+ logger.warn(
10724
+ `Could not persist device id (~/.reportforge/device.json) \u2014 this machine may register a new seat on every run: ${err.message}`
10725
+ );
10687
10726
  }
10688
10727
  return id;
10689
10728
  }
@@ -10710,10 +10749,13 @@ function getMachineFingerprint() {
10710
10749
  label: `${ci.provider}/${ci.repo} (CI)`
10711
10750
  };
10712
10751
  }
10713
- const hostname = safe(() => os2.hostname()) ?? "unknown-host";
10752
+ const hostname = safe(() => os2.hostname());
10753
+ if (hostname === void 0) {
10754
+ logger.debug("fingerprint: os.hostname() failed \u2014 label falls back to unknown-host");
10755
+ }
10714
10756
  return {
10715
10757
  hash: sha256Hex(`dev:${getStableDeviceId()}`),
10716
- label: `${hostname} (dev)`
10758
+ label: `${hostname ?? "unknown-host"} (dev)`
10717
10759
  };
10718
10760
  }
10719
10761
  function detectCiRepo() {
@@ -10755,19 +10797,25 @@ function readCache() {
10755
10797
  if (f.endsWith(".tmp")) {
10756
10798
  try {
10757
10799
  fs2.unlinkSync(path3.join(CACHE_DIR, f));
10758
- } catch {
10800
+ } catch (err) {
10801
+ logger.debug("license cache: stale tmp cleanup failed", { file: f, error: err.message });
10759
10802
  }
10760
10803
  }
10761
10804
  }
10762
- } catch {
10805
+ } catch (err) {
10806
+ logger.debug("license cache: cache dir not readable (first run?)", { error: err.message });
10763
10807
  }
10764
10808
  try {
10765
10809
  if (!fs2.existsSync(CACHE_FILE)) return null;
10766
10810
  const raw = fs2.readFileSync(CACHE_FILE, "utf8");
10767
10811
  const parsed = JSON.parse(raw);
10768
- if (typeof parsed.jwt !== "string" || typeof parsed.expiresAt !== "number") return null;
10812
+ if (typeof parsed.jwt !== "string" || typeof parsed.expiresAt !== "number") {
10813
+ logger.debug("license cache: malformed entry \u2014 ignoring");
10814
+ return null;
10815
+ }
10769
10816
  return parsed;
10770
- } catch {
10817
+ } catch (err) {
10818
+ logger.debug("license cache: read failed \u2014 treating as no cache", { error: err.message });
10771
10819
  return null;
10772
10820
  }
10773
10821
  }
@@ -10777,13 +10825,15 @@ function writeCache(entry) {
10777
10825
  const tmp = `${CACHE_FILE}.${process.pid}.tmp`;
10778
10826
  fs2.writeFileSync(tmp, JSON.stringify(entry, null, 2), { mode: 384 });
10779
10827
  fs2.renameSync(tmp, CACHE_FILE);
10780
- } catch {
10828
+ } catch (err) {
10829
+ logger.debug("license cache: write failed \u2014 will re-activate next run", { error: err.message });
10781
10830
  }
10782
10831
  }
10783
10832
  function clearCache() {
10784
10833
  try {
10785
10834
  fs2.unlinkSync(CACHE_FILE);
10786
- } catch {
10835
+ } catch (err) {
10836
+ logger.debug("license cache: clear failed", { error: err.message });
10787
10837
  }
10788
10838
  }
10789
10839
 
@@ -10802,27 +10852,6 @@ import { createPublicKey as createPublicKey2, verify as verify2 } from "crypto";
10802
10852
  init_esm_shims();
10803
10853
  var MODEL = { "alpha": 1, "labels": ["assertion", "env-config", "flaky-by-retry", "locator-not-found", "navigation", "network", "timeout"], "logLikelihoods": { "assertion": { "NUM": -4.574710978503383, "assertionerror": -4.980176086611547, "be": -4.980176086611547, "cart": -4.980176086611547, "checkout": -4.980176086611547, "com": -4.980176086611547, "dashboard": -4.980176086611547, "deep": -4.980176086611547, "equal": -4.980176086611547, "equality": -4.980176086611547, "error": -4.980176086611547, "expect": -3.5938817254916566, "expected": -3.1083739097099556, "failed": -4.980176086611547, "false": -4.980176086611547, "got": -4.980176086611547, "id": -4.574710978503383, "john": -4.980176086611547, "locator": -4.574710978503383, "login": -4.980176086611547, "must": -4.980176086611547, "page": -4.980176086611547, "pattern": -4.980176086611547, "received": -3.275427994373122, "string": -4.980176086611547, "to": -4.980176086611547, "tobe": -4.980176086611547, "tobetruthy": -4.980176086611547, "toequal": -4.980176086611547, "tohavecount": -4.980176086611547, "tohavetext": -4.980176086611547, "tohavetitle": -4.980176086611547, "tohavevalue": -4.980176086611547, "truthy": -4.980176086611547, "value": -4.980176086611547 }, "env-config": { "NUM": -4.997212273764115, "api": -4.997212273764115, "at": -4.997212273764115, "back": -4.997212273764115, "base": -4.997212273764115, "baseurl": -4.997212273764115, "browsers": -4.997212273764115, "cannot": -4.997212273764115, "ci": -4.997212273764115, "config": -4.304065093204169, "directory": -4.591747165655951, "dotenv": -4.997212273764115, "enoent": -4.997212273764115, "env": -4.591747165655951, "environment": -4.591747165655951, "error": -4.08092154188996, "existent": -4.997212273764115, "failed": -4.997212273764115, "falling": -4.997212273764115, "file": -4.997212273764115, "find": -4.997212273764115, "from": -4.997212273764115, "in": -4.591747165655951, "invalid": -4.997212273764115, "is": -4.997212273764115, "json": -4.591747165655951, "key": -4.997212273764115, "load": -4.997212273764115, "missing": -4.997212273764115, "module": -4.997212273764115, "no": -4.997212273764115, "non": -4.997212273764115, "not": -4.997212273764115, "open": -4.997212273764115, "or": -4.997212273764115, "path": -4.997212273764115, "playwright": -4.591747165655951, "points": -4.997212273764115, "position": -4.997212273764115, "required": -4.997212273764115, "set": -4.997212273764115, "spec": -4.997212273764115, "such": -4.997212273764115, "syntaxerror": -4.997212273764115, "test": -4.997212273764115, "to": -4.304065093204169, "token": -4.997212273764115, "ts": -4.997212273764115, "tsconfig": -4.997212273764115, "undefined": -4.591747165655951, "unexpected": -4.997212273764115, "url": -4.997212273764115, "values": -4.997212273764115, "variable": -4.591747165655951 }, "flaky-by-retry": { "after": -4.58836306767171, "and": -4.993828175779875, "animation": -4.993828175779875, "attempt": -4.07753744390572, "between": -4.993828175779875, "blip": -4.993828175779875, "cause": -4.993828175779875, "change": -4.993828175779875, "click": -4.993828175779875, "code": -4.993828175779875, "commit": -4.993828175779875, "condition": -4.993828175779875, "dependency": -4.993828175779875, "detached": -4.993828175779875, "deterministic": -4.993828175779875, "element": -4.993828175779875, "failed": -4.300680995219929, "fails": -4.993828175779875, "flaky": -4.07753744390572, "green": -4.993828175779875, "in": -4.993828175779875, "intermittent": -4.993828175779875, "locator": -4.993828175779875, "marked": -4.993828175779875, "network": -4.993828175779875, "no": -4.993828175779875, "non": -4.993828175779875, "on": -3.895215887111765, "order": -4.993828175779875, "passed": -3.895215887111765, "quarantined": -4.993828175779875, "race": -4.993828175779875, "reproducible": -4.993828175779875, "rerun": -4.993828175779875, "retry": -4.07753744390572, "runs": -4.993828175779875, "same": -4.993828175779875, "second": -4.993828175779875, "stale": -4.993828175779875, "succeeded": -4.993828175779875, "test": -4.58836306767171, "the": -4.993828175779875, "then": -4.993828175779875, "timeout": -4.993828175779875, "unstable": -4.993828175779875, "with": -4.993828175779875, "without": -4.993828175779875 }, "locator-not-found": { "an": -4.993828175779875, "attached": -4.993828175779875, "btn": -4.993828175779875, "button": -4.58836306767171, "cart": -4.993828175779875, "checkout": -4.993828175779875, "click": -4.993828175779875, "data": -4.993828175779875, "div": -4.993828175779875, "dom": -4.993828175779875, "element": -4.07753744390572, "elements": -4.300680995219929, "error": -4.58836306767171, "find": -4.993828175779875, "for": -4.58836306767171, "found": -4.58836306767171, "getbyrole": -4.993828175779875, "getbytext": -4.993828175779875, "icon": -4.993828175779875, "id": -4.993828175779875, "is": -4.993828175779875, "locator": -4.07753744390572, "matched": -4.993828175779875, "matches": -4.993828175779875, "missing": -4.993828175779875, "mode": -4.58836306767171, "name": -4.993828175779875, "no": -4.58836306767171, "node": -4.993828175779875, "not": -4.58836306767171, "resolved": -4.58836306767171, "save": -4.993828175779875, "selector": -4.58836306767171, "strict": -4.58836306767171, "submit": -4.993828175779875, "testid": -4.993828175779875, "textcontent": -4.993828175779875, "the": -4.58836306767171, "to": -4.07753744390572, "total": -4.993828175779875, "unable": -4.993828175779875, "violation": -4.58836306767171, "waiting": -4.993828175779875, "with": -4.993828175779875, "xpath": -4.993828175779875 }, "navigation": { "NUM": -4.983606621708336, "aborted": -4.983606621708336, "another": -4.983606621708336, "back": -4.983606621708336, "because": -4.578141513600172, "by": -4.578141513600172, "checkout": -4.983606621708336, "context": -4.983606621708336, "crashed": -4.983606621708336, "dashboard": -4.983606621708336, "destroyed": -4.983606621708336, "detached": -4.983606621708336, "entry": -4.983606621708336, "err": -4.578141513600172, "exceeded": -4.983606621708336, "execution": -4.983606621708336, "failed": -4.983606621708336, "frame": -4.578141513600172, "goback": -4.983606621708336, "goto": -4.290459441148391, "history": -4.983606621708336, "home": -4.983606621708336, "interrupted": -4.578141513600172, "is": -4.983606621708336, "likely": -4.983606621708336, "login": -4.983606621708336, "many": -4.983606621708336, "maybe": -4.983606621708336, "most": -4.983606621708336, "navigate": -4.983606621708336, "navigating": -4.983606621708336, "navigation": -3.7308436532129683, "net": -4.578141513600172, "no": -4.983606621708336, "of": -4.983606621708336, "page": -3.884994333040227, "redirects": -4.983606621708336, "timeout": -4.983606621708336, "to": -3.7308436532129683, "too": -4.983606621708336, "waitforurl": -4.983606621708336, "was": -4.578141513600172 }, "network": { "NUM": -4.57126863431241, "URL": -4.283586561860629, "again": -4.976733742420574, "api": -4.976733742420574, "apirequestcontext": -4.976733742420574, "at": -4.976733742420574, "body": -4.976733742420574, "connect": -4.976733742420574, "connection": -4.57126863431241, "disconnected": -4.976733742420574, "eai": -4.976733742420574, "econnrefused": -4.976733742420574, "err": -3.8781214537524646, "error": -4.976733742420574, "failed": -4.283586561860629, "fetch": -4.976733742420574, "fetching": -4.976733742420574, "from": -4.976733742420574, "get": -4.976733742420574, "getaddrinfo": -4.976733742420574, "goto": -4.976733742420574, "hang": -4.976733742420574, "internet": -4.976733742420574, "loading": -4.976733742420574, "name": -4.976733742420574, "net": -3.8781214537524646, "not": -4.976733742420574, "on": -4.976733742420574, "out": -4.976733742420574, "page": -4.57126863431241, "products": -4.976733742420574, "protocol": -4.976733742420574, "reading": -4.976733742420574, "reason": -4.976733742420574, "refused": -4.976733742420574, "request": -4.57126863431241, "resolved": -4.976733742420574, "resource": -4.976733742420574, "response": -4.976733742420574, "route": -4.976733742420574, "socket": -4.976733742420574, "ssl": -4.976733742420574, "timed": -4.976733742420574, "to": -4.976733742420574, "up": -4.976733742420574, "upstream": -4.976733742420574, "while": -4.976733742420574 }, "timeout": { "NUM": -3.4965075614664802, "after": -5.000584958242754, "be": -4.59511985013459, "cart": -5.000584958242754, "click": -5.000584958242754, "element": -5.000584958242754, "event": -5.000584958242754, "exceeded": -3.6142905971228636, "expect": -5.000584958242754, "fill": -5.000584958242754, "for": -3.7478219897473863, "function": -5.000584958242754, "load": -5.000584958242754, "locator": -4.084294226368599, "navigation": -5.000584958242754, "of": -5.000584958242754, "out": -5.000584958242754, "page": -4.59511985013459, "running": -5.000584958242754, "selector": -5.000584958242754, "submit": -5.000584958242754, "test": -4.59511985013459, "timed": -5.000584958242754, "timeout": -3.6142905971228636, "timeouterror": -4.59511985013459, "to": -4.59511985013459, "tobevisible": -5.000584958242754, "visible": -4.59511985013459, "waitforloadstate": -5.000584958242754, "waitforselector": -5.000584958242754, "waiting": -3.7478219897473863, "while": -5.000584958242754 } }, "playwrightVersion": "1.59.1", "priors": { "assertion": -1.9636097261547143, "env-config": -1.9636097261547143, "flaky-by-retry": -1.9636097261547143, "locator-not-found": -1.9636097261547143, "navigation": -1.9636097261547143, "network": -1.9636097261547143, "timeout": -1.845826690498331 }, "totals": { "assertion": 64, "env-config": 69, "flaky-by-retry": 68, "locator-not-found": 68, "navigation": 65, "network": 63, "timeout": 70 }, "version": 1, "vocabSize": 227 };
10804
10854
 
10805
- // src/utils/logger.ts
10806
- init_esm_shims();
10807
- var PREFIX = "[reportforge]";
10808
- var logger = {
10809
- info(msg) {
10810
- console.log(`${PREFIX} ${msg}`);
10811
- },
10812
- warn(msg) {
10813
- console.warn(`${PREFIX} WARN: ${msg}`);
10814
- },
10815
- error(msg) {
10816
- console.error(`${PREFIX} ERROR: ${msg}`);
10817
- },
10818
- debug(msg, data) {
10819
- if (!process.env.RF_DEBUG) return;
10820
- const suffix = data !== void 0 ? `
10821
- ${JSON.stringify(data, null, 2)}` : "";
10822
- console.log(`${PREFIX} DEBUG: ${msg}${suffix}`);
10823
- }
10824
- };
10825
-
10826
10855
  // src/analysis/ModelStore.ts
10827
10856
  var MODEL_SIG_DOMAIN = "reportforge-model-sig:v1\n";
10828
10857
  var testDeps = null;
@@ -11093,9 +11122,16 @@ var LicenseClient = class {
11093
11122
  function verifyCache(verifier, cached, expectedFp) {
11094
11123
  try {
11095
11124
  const payload = verifier.verify(cached.jwt);
11096
- if (payload.fp !== expectedFp) return null;
11125
+ if (payload.fp !== expectedFp) {
11126
+ logger.debug("license cache rejected: fingerprint mismatch", {
11127
+ cachedFp: payload.fp.slice(0, 12),
11128
+ expectedFp: expectedFp.slice(0, 12)
11129
+ });
11130
+ return null;
11131
+ }
11097
11132
  return payload;
11098
- } catch {
11133
+ } catch (err) {
11134
+ logger.debug("license cache rejected: JWT verify failed", { error: err.message });
11099
11135
  return null;
11100
11136
  }
11101
11137
  }
@@ -11151,7 +11187,8 @@ function maybePrintUpdateNotice(licenseInfo, currentVersion) {
11151
11187
  logger.info(
11152
11188
  `Update available: ${currentVersion} -> ${shown}. Run: npm i -D @reportforge/playwright-pdf@latest`
11153
11189
  );
11154
- } catch {
11190
+ } catch (err) {
11191
+ logger.debug("update notice failed (ignored)", { error: err.message });
11155
11192
  }
11156
11193
  }
11157
11194
 
@@ -12827,29 +12864,33 @@ var ScreenshotEmbedder = class {
12827
12864
  init_esm_shims();
12828
12865
  import { execSync as execSync2 } from "child_process";
12829
12866
  import fs7 from "fs";
12867
+ var LAUNCH_ARGS = [
12868
+ "--no-sandbox",
12869
+ "--disable-setuid-sandbox",
12870
+ "--disable-dev-shm-usage",
12871
+ "--disable-gpu",
12872
+ // Chrome's new headless mode (--headless=new, the only mode left in
12873
+ // Chrome 132+) briefly uncloaks a blank window on some Windows/GPU
12874
+ // combinations. Parking the window far off-screen makes that transient
12875
+ // invisible; harmless everywhere else and does not affect PDF output.
12876
+ "--window-position=-32000,-32000"
12877
+ ];
12830
12878
  var BrowserManager = class {
12831
12879
  async launch(executablePath) {
12832
12880
  const puppeteer = __require("puppeteer-core");
12833
12881
  const chromePath = this.resolveChromePath(executablePath, puppeteer);
12834
12882
  logger.info(`Launching browser: ${chromePath ?? "puppeteer default"}`);
12835
- const launchArgs = [
12836
- "--no-sandbox",
12837
- "--disable-setuid-sandbox",
12838
- "--disable-dev-shm-usage",
12839
- "--disable-gpu"
12840
- ];
12841
12883
  this.browser = await puppeteer.launch({
12842
12884
  executablePath: chromePath,
12843
12885
  headless: true,
12844
- args: launchArgs
12886
+ args: LAUNCH_ARGS
12845
12887
  });
12846
12888
  const page = await this.browser.newPage();
12847
12889
  return { browser: this.browser, page };
12848
12890
  }
12849
12891
  async close() {
12850
12892
  if (this.browser) {
12851
- await this.browser.close().catch(() => {
12852
- });
12893
+ await this.browser.close().catch((err) => logger.debug("browser close failed (ignored)", { error: err.message }));
12853
12894
  this.browser = void 0;
12854
12895
  }
12855
12896
  }
@@ -12861,7 +12902,8 @@ var BrowserManager = class {
12861
12902
  const chromeFinder = require_lib2();
12862
12903
  const found = chromeFinder();
12863
12904
  if (found && fs7.existsSync(found)) return found;
12864
- } catch {
12905
+ } catch (err) {
12906
+ logger.debug("chrome discovery: chrome-finder failed", { error: err.message });
12865
12907
  }
12866
12908
  const linuxPaths = [
12867
12909
  "/usr/bin/google-chrome",
@@ -12879,12 +12921,14 @@ var BrowserManager = class {
12879
12921
  }).toString().trim().split("\n")[0];
12880
12922
  if (found && fs7.existsSync(found)) return found;
12881
12923
  } catch {
12924
+ logger.debug("chrome discovery: which lookup found nothing");
12882
12925
  }
12883
12926
  if (puppeteer.executablePath) {
12884
12927
  try {
12885
12928
  const bundled = puppeteer.executablePath();
12886
12929
  if (bundled && fs7.existsSync(bundled)) return bundled;
12887
- } catch {
12930
+ } catch (err) {
12931
+ logger.debug("chrome discovery: no puppeteer bundled executable", { error: err.message });
12888
12932
  }
12889
12933
  }
12890
12934
  logger.warn(
@@ -12910,8 +12954,10 @@ var HtmlWriter = class {
12910
12954
  }
12911
12955
  async cleanup() {
12912
12956
  if (this.tempPath) {
12913
- await fs8.promises.unlink(this.tempPath).catch(() => {
12914
- });
12957
+ const tempPath = this.tempPath;
12958
+ await fs8.promises.unlink(tempPath).catch(
12959
+ (err) => logger.debug("temp HTML cleanup failed (ignored)", { path: tempPath, error: err.message })
12960
+ );
12915
12961
  this.tempPath = void 0;
12916
12962
  }
12917
12963
  }
@@ -12976,7 +13022,8 @@ var PdfEncryptor = class {
12976
13022
  try {
12977
13023
  execFileSync("qpdf", ["--version"], { stdio: "pipe" });
12978
13024
  return true;
12979
- } catch {
13025
+ } catch (err) {
13026
+ logger.debug("qpdf probe failed \u2014 treating as not installed", { error: err.message });
12980
13027
  return false;
12981
13028
  }
12982
13029
  }
@@ -13133,7 +13180,8 @@ var PdfGenerator = class {
13133
13180
  if (CHARTJS_BUNDLE2) {
13134
13181
  this.templateEngine.setChartjsBundle(CHARTJS_BUNDLE2);
13135
13182
  }
13136
- } catch {
13183
+ } catch (err) {
13184
+ logger.debug("chartjs bundle not available \u2014 charts disabled", { error: err.message });
13137
13185
  }
13138
13186
  }
13139
13187
  /**
@@ -14049,7 +14097,7 @@ function buildWatchUrl(serverUrl, runId, token) {
14049
14097
  }
14050
14098
  function formatLiveBanner(url) {
14051
14099
  const label = "Live Tracker";
14052
- const link = `Watch live: ${url}`;
14100
+ const link = `[reportforge] Watch live: ${url}`;
14053
14101
  const width = Math.max(label.length, link.length);
14054
14102
  const rule = "\u2500".repeat(width + 2);
14055
14103
  return [rule, ` ${label.padEnd(width)} `, ` ${link.padEnd(width)} `, rule].join("\n");
@@ -14155,8 +14203,9 @@ var PdfReporter = class {
14155
14203
  this.liveSteps = "failed";
14156
14204
  this.liveConsole = false;
14157
14205
  this.options = parseOptions(rawOptions);
14206
+ setLogLevel(this.options.logLevel);
14158
14207
  this.dataCollector = new DataCollector(this.options.capture);
14159
- this.version = true ? "0.18.0" : "0.x";
14208
+ this.version = true ? "0.19.1" : "0.x";
14160
14209
  logger.info(`@reportforge/playwright-pdf v${this.version} initialised`);
14161
14210
  this.licenseClient = new LicenseClient({
14162
14211
  licenseKey: this.options.licenseKey,
@@ -14419,8 +14468,9 @@ var PdfReporter = class {
14419
14468
  );
14420
14469
  return;
14421
14470
  }
14422
- logger.info(`Live Tracker: ${watchUrl}`);
14423
- console.log(formatLiveBanner(watchUrl));
14471
+ if (isLevelEnabled("info")) {
14472
+ console.log(formatLiveBanner(watchUrl));
14473
+ }
14424
14474
  const isPrimaryShard = !shard || shard.current === 1;
14425
14475
  const notify = this.options.notify;
14426
14476
  if (isPrimaryShard && notify) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reportforge/playwright-pdf",
3
- "version": "0.18.0",
3
+ "version": "0.19.1",
4
4
  "description": "Playwright Test reporter that generates designed PDF reports: minimal, detailed, and executive templates with CI/CD integrations",
5
5
  "license": "Elastic-2.0",
6
6
  "author": "ReportForge",