/** * Site Builder screenshot capture — local-only. Logs in to the demo account, * walks through the SB landing + editor, applies the standard redaction step, * and writes PNGs to src/assets/screenshots/whp/. * * Run: WHP_BASE=... WHP_USER=... WHP_PASS=... npx tsx tools/screenshots/capture-site-builder.ts */ import { chromium, type Page } from 'playwright'; import { mkdir } from 'node:fs/promises'; import { resolve, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const OUT_DIR = resolve(__dirname, '../../src/assets/screenshots/whp'); function need(name: string): string { const v = process.env[name]; if (!v) throw new Error(`missing env: ${name}`); return v; } const BASE = need('WHP_BASE'); const USER = need('WHP_USER'); const PASS = need('WHP_PASS'); async function login(page: Page) { await page.goto(`${BASE}/login.php`); await page.fill('input[name="user"]', USER); await page.fill('input[name="password"]', PASS); await page.click('button[type="submit"]'); await page.waitForLoadState('networkidle'); } /** * Neutralise server / domain / user identifiers before snapshot. * Same idea as run.ts redactSensitive, extended for SB's domain pill. */ async function redact(page: Page) { await page.addStyleTag({ content: ` .navbar-text, .brand-full { visibility: hidden !important; } `, }); await page.evaluate(() => { const swaps: [RegExp, string][] = [ [/whp\d+(-[a-z0-9]+)?\.cloud-hosting\.io/gi, '.cloud-hosting.io'], [/mail\d+\.cloud-hosting\.io/gi, '.cloud-hosting.io'], [/whp-demo\.anhh\.co/gi, 'your-site.example.com'], [/whp\d+(-[a-z0-9]+)?\b/gi, ''], [/WHP\d+(-[A-Z0-9]+)?\b/g, ''], [/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, ''], [/\/docker\/users\/[a-z0-9-]+/g, '/docker/users/'], [/demo-user/g, 'your-username'], ]; const walker = document.createTreeWalker(document.body, 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 shot(page: Page, id: string) { await redact(page); const path = resolve(OUT_DIR, `${id}.png`); await page.screenshot({ path, fullPage: false }); console.log(`captured ${id} -> ${path}`); } async function findSiteId(page: Page): Promise { // Open SB landing, click first Build Site, scrape ?site_id=NN from URL await page.goto(`${BASE}/index.php?page=site-builder`); await page.waitForLoadState('networkidle'); const build = page.locator('a, button').filter({ hasText: 'Build Site' }).first(); if (await build.count() === 0) return null; await build.click(); await page.waitForLoadState('networkidle'); const m = page.url().match(/site_id=(\d+)/); return m ? m[1] : null; } async function main() { 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); // 1) SB landing await page.goto(`${BASE}/index.php?page=site-builder`); await page.waitForLoadState('networkidle'); await shot(page, 'whp-site-builder-landing'); // 2) Editor — open from the landing const siteId = await findSiteId(page); if (!siteId) { console.error('no site_id found; ensure the demo account has at least one site'); return; } await page.goto(`${BASE}/site-builder/?site_id=${siteId}`); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2500); await shot(page, 'whp-site-builder-editor'); // 3) Pages tab await page.locator('button:has-text("PAGES"), button:has-text("Pages")').first().click().catch(() => {}); await page.waitForTimeout(1000); await shot(page, 'whp-site-builder-pages'); // 4) Blocks: Sections category await page.locator('button:has-text("BLOCKS"), button:has-text("Blocks")').first().click().catch(() => {}); await page.waitForTimeout(500); await page.locator('text=SECTIONS').first().click().catch(() => {}); await page.waitForTimeout(800); await shot(page, 'whp-site-builder-blocks'); // 5) Templates modal await page.locator('button:has-text("Templates")').first().click().catch(() => {}); await page.waitForTimeout(1500); await shot(page, 'whp-site-builder-templates'); await page.keyboard.press('Escape').catch(() => {}); await page.waitForTimeout(500); // 6) Custom head code modal await page.locator('button:has-text("Code")').first().click().catch(() => {}); await page.waitForTimeout(1500); await shot(page, 'whp-site-builder-code'); await page.keyboard.press('Escape').catch(() => {}); } finally { await browser.close(); } } main().catch(e => { console.error(e); process.exit(1); });