import { chromium, type Page, type Locator } from 'playwright'; import { mkdir } from 'node:fs/promises'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { DEFAULT_MASK, shots, type Shot } from './shots.config.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp'); function envOrDie(name: string): string { const v = process.env[name]; if (!v) { throw new Error( `Missing env var: ${name}. Put it in tools/screenshots/.env (gitignored).`, ); } return v; } const WHP_BASE = envOrDie('WHP_BASE'); // e.g., https://whp01.cloud-hosting.io:8443 const WHP_USER = envOrDie('WHP_USER'); const WHP_PASS = envOrDie('WHP_PASS'); async function login(page: Page): Promise { await page.goto(`${WHP_BASE}/login.php`); await page.fill('input[name="user"]', WHP_USER); await page.fill('input[name="password"]', WHP_PASS); await page.click('button[type="submit"]'); await page.waitForLoadState('networkidle'); } /** * Neutralise server-identifying and account-specific text/labels before the * screenshot — we run a multi-server fleet, so docs shouldn't bake in a single * host's name or IP. * * Runs in the page context. Self-contained: no external bindings, no eval. */ async function redactSensitive(page: Page): Promise { await page.addStyleTag({ content: ` .navbar-text { visibility: hidden !important; } .brand-full { visibility: hidden !important; } `, }); // String swaps for inline text that mask-by-selector can't cover. await page.evaluate(() => { const root = document.body; const swaps: [RegExp, string][] = [ // server / mail / nameserver hostnames (with possible -s3 etc. suffix) [/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '.cloud-hosting.io'], [/mail\d+\.cloud-hosting\.io/gi, '.cloud-hosting.io'], [/ns[12]\.whp\d+\.cloud-hosting\.io/gi, 'ns.your-server.cloud-hosting.io'], // backup target / bucket names that bake in server number [/whp\d+(-[a-z0-9]+)?\b/gi, ''], [/WHP\d+(-[A-Z0-9]+)?\b/g, ''], // IP addresses [/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, ''], // home dir + demo user [/\/docker\/users\/[a-z0-9-]+/g, '/docker/users/'], [/demo-user/g, 'your-username'], ]; const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT); const nodes: Text[] = []; let n: Node | null = walker.nextNode(); while (n) { nodes.push(n as Text); n = walker.nextNode(); } for (const node of nodes) { let v = node.nodeValue ?? ''; for (const [re, replacement] of swaps) v = v.replace(re, replacement); if (v !== node.nodeValue) node.nodeValue = v; } }); } async function captureShot(page: Page, shot: Shot): Promise { const viewport = shot.viewport ?? { width: 1440, height: 900 }; await page.setViewportSize(viewport); await page.goto(`${WHP_BASE}${shot.path}`); await page.waitForLoadState('networkidle'); if (shot.waitFor) await page.waitForSelector(shot.waitFor); await redactSensitive(page); const maskSelectors = [...DEFAULT_MASK, ...(shot.mask ?? [])]; const maskLocators: Locator[] = maskSelectors.map((sel) => page.locator(sel)); const outPath = resolve(OUT_DIR, `${shot.id}.png`); if (shot.selector) { await page.locator(shot.selector).screenshot({ path: outPath, mask: maskLocators, }); } else { // Viewport-only — Playwright never includes browser chrome await page.screenshot({ path: outPath, fullPage: false, mask: maskLocators, }); } console.log(`captured ${shot.id} -> ${outPath}`); } async function main(): Promise { await mkdir(OUT_DIR, { recursive: true }); const browser = await chromium.launch({ headless: true }); const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, }); const page = await ctx.newPage(); try { await login(page); for (const shot of shots) { await captureShot(page, shot); } } finally { await browser.close(); } } main().catch((err) => { console.error(err); process.exit(1); });