Files
TS_Proto/e2e/scripts/report-pdf.mjs
T
nilesh.pandClaude Opus 4.8 4d3eb18a6c Initial commit: TravelHub multi-tenant flight booking platform
Angular + Go + PostgreSQL sample with strict per-tenant isolation.

- Backend: Go API (basic auth, tenant-scoped repository layer, pgx pool)
- Frontend: Angular 22 SPA (search, checkout, bookings, dashboard)
- Database: schema, seed data and demo bookings migrations
- Tests: 311 across Go (integration), Angular (unit) and Playwright (e2e),
  plus a generated evidence report with PDF export
- Security: whitebox VAPT report under security/

Secrets (.env, tokens, DB credentials) are git-ignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-13 11:38:01 +05:30

127 lines
4.2 KiB
JavaScript

#!/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 <details> 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 <img> 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 =
'<div style="width:100%;font-size:8pt;color:#9ca3af;padding:0 12mm;' +
'font-family:Segoe UI,Helvetica,Arial,sans-serif;display:flex;justify-content:space-between">' +
'<span>TravelHub — test execution report</span>' +
'<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>' +
'</div>';
mkdirSync(dirname(OUT), { recursive: true });
await page.pdf({
path: OUT,
format: 'A4',
printBackground: true,
displayHeaderFooter: true,
headerTemplate: '<div></div>', // 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();
}