#!/usr/bin/env node /** * Exports the evidence report to PDF, headlessly. * * Uses the Chromium that Playwright already installed, so there is no new * dependency and no need to open a browser by hand. The browser's own * File > Print also works and produces the same layout; this exists so the * export can be scripted, and so CI can attach a PDF to a build. * * node scripts/report-pdf.mjs everything * node scripts/report-pdf.mjs --summary summary plus the narrated scenarios only * node scripts/report-pdf.mjs --out x.pdf write somewhere else */ import { existsSync, mkdirSync, statSync } from 'node:fs'; import { dirname, isAbsolute, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { chromium } from '@playwright/test'; const E2E = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const REPORT_DIR = join(E2E, 'evidence-report'); const SOURCE = join(REPORT_DIR, 'index.html'); const args = process.argv.slice(2); const summaryOnly = args.includes('--summary'); const outFlag = args.indexOf('--out'); const requested = outFlag !== -1 ? args[outFlag + 1] : undefined; const OUT = requested ? isAbsolute(requested) ? requested : resolve(process.cwd(), requested) : join(REPORT_DIR, summaryOnly ? 'TravelHub-test-report-summary.pdf' : 'TravelHub-test-report.pdf'); if (!existsSync(SOURCE)) { console.error( `No report found at ${SOURCE}\n` + 'Run the suite first: npm run test:e2e', ); process.exit(1); } const browser = await chromium.launch(); try { const page = await browser.newPage(); await page.goto(pathToFileURL(SOURCE).href, { waitUntil: 'load' }); const counts = await page.evaluate(async (opts) => { if (opts.summaryOnly) { // Drop scenarios that carry only a screenshot, then any area left empty. for (const el of document.querySelectorAll('.scenario')) { if (el.dataset.facts === '0') { el.remove(); } } for (const area of document.querySelectorAll('.area')) { if (!area.querySelector('.scenario')) { area.remove(); } } } // A collapsed
would print as a one-line summary. for (const details of document.querySelectorAll('details')) { details.open = true; } // Lazy images never load off-screen, and the PDF renderer does not scroll. // Only the screenshots: the lightbox holds an empty that never loads. const images = [...document.querySelectorAll('img.shot')]; for (const img of images) { img.loading = 'eager'; } await Promise.all( images.map((img) => img.complete ? null : new Promise((done) => { img.addEventListener('load', done, { once: true }); img.addEventListener('error', done, { once: true }); }), ), ); return { scenarios: document.querySelectorAll('.scenario').length, images: images.length, unloaded: images.filter((i) => i.naturalWidth === 0).length, }; }, { summaryOnly }); if (counts.unloaded > 0) { console.warn(` warning: ${counts.unloaded} screenshot(s) failed to load`); } await page.emulateMedia({ media: 'print' }); const foot = '
' + 'TravelHub — test execution report' + 'Page of ' + '
'; mkdirSync(dirname(OUT), { recursive: true }); await page.pdf({ path: OUT, format: 'A4', printBackground: true, displayHeaderFooter: true, headerTemplate: '
', // suppress Chromium's default header footerTemplate: foot, margin: { top: '14mm', bottom: '16mm', left: '12mm', right: '12mm' }, }); const mb = (statSync(OUT).size / 1024 / 1024).toFixed(1); console.log( `PDF written: ${OUT}\n` + ` ${counts.scenarios} scenarios · ${counts.images} screenshots · ${mb} MB` + (summaryOnly ? ' (summary: narrated scenarios only)' : ''), ); } finally { await browser.close(); }