#!/usr/bin/env node /** Headless Chrome walkthrough: screenshots + console errors for every UI. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import puppeteer from 'puppeteer-core'; const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const SHOTS = path.join(ROOT, 'screenshots'); const LOG = path.join(ROOT, 'walkthrough-console.json'); const CHROME = process.env.CHROME_PATH || '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; fs.mkdirSync(SHOTS, { recursive: true }); const errors = []; function note(pageName, type, text) { errors.push({ page: pageName, type, text: String(text).slice(0, 800) }); } const browser = await puppeteer.launch({ executablePath: CHROME, headless: 'new', args: ['--no-sandbox', '--window-size=1440,900', '--disable-dev-shm-usage'], defaultViewport: { width: 1440, height: 900 }, }); async function pageFor(name) { const page = await browser.newPage(); page.on('pageerror', (err) => note(name, 'pageerror', err.message)); page.on('console', (msg) => { if (msg.type() === 'error') note(name, 'console', msg.text()); }); page.on('response', (res) => { if (res.status() >= 500) note(name, 'http', `${res.status()} ${res.url()}`); }); return page; } async function shot(page, file) { await page.screenshot({ path: path.join(SHOTS, file), fullPage: true }); } async function safeGoto(page, url, name) { try { await page.goto(url, { waitUntil: 'networkidle2', timeout: 20000 }); } catch (err) { note(name, 'goto', `${url} ${err.message}`); try { await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 12000 }); } catch (err2) { note(name, 'goto2', `${url} ${err2.message}`); } } } try { // Operator console { const p = await pageFor('fleet'); await safeGoto(p, 'http://127.0.0.1:3850/', 'fleet'); await new Promise((r) => setTimeout(r, 1500)); await shot(p, '01-console-fleet.png'); const more = await p.$('.more-btn'); if (more) { await more.click(); await new Promise((r) => setTimeout(r, 300)); await shot(p, '01b-console-fleet-overflow.png'); await p.keyboard.press('Escape'); } await p.click('nav button[data-tab="trace"]'); await new Promise((r) => setTimeout(r, 400)); await shot(p, '02-console-trace.png'); const run = await p.$('#run'); if (run) { await run.click(); await new Promise((r) => setTimeout(r, 2500)); await shot(p, '02b-console-trace-run.png'); } await p.click('nav button[data-tab="docs"]'); await new Promise((r) => setTimeout(r, 400)); await shot(p, '03-console-docs.png'); await p.close(); } // Simulator { const p = await pageFor('simulator'); await safeGoto(p, 'http://127.0.0.1:3847/', 'simulator'); await new Promise((r) => setTimeout(r, 800)); await shot(p, '04-simulator.png'); const run = await p.$('#run'); if (run) { await run.click(); await new Promise((r) => setTimeout(r, 2500)); await shot(p, '04b-simulator-run.png'); } await p.close(); } // Staff UIs for (const [name, url, file, extra] of [ ['cs', 'http://127.0.0.1:3011/', '05-cs.png', true], ['sales', 'http://127.0.0.1:3012/', '06-sales.png', false], ['accounting', 'http://127.0.0.1:3013/', '07-accounting.png', false], ['staff', 'http://127.0.0.1:3025/', '08-access-staff.png', true], ]) { const p = await pageFor(name); await safeGoto(p, url, name); await shot(p, file.replace('.png', '-empty.png')); const btn = await p.$('button'); if (btn) { await btn.click(); await new Promise((r) => setTimeout(r, 800)); } await shot(p, file); if (extra) { const credit = await p.$('button[onclick="credit()"]'); if (credit) { await credit.click(); await new Promise((r) => setTimeout(r, 800)); await shot(p, file.replace('.png', '-credit.png')); } } await p.close(); } // Admin { const p = await pageFor('admin'); await safeGoto(p, 'http://127.0.0.1:3000/admin/', 'admin'); await shot(p, '09-admin-login.png'); await p.type('#login-username', 'admin'); await p.type('#login-password', 'admin-dev-key'); await p.click('#login-form button[type="submit"]'); await new Promise((r) => setTimeout(r, 1200)); await shot(p, '10-admin-rate-card.png'); const tabs = await p.$$('aside nav button'); for (const [i, label] of [ [1, '11-admin-tiers.png'], [2, '12-admin-customers.png'], [3, '13-admin-statement.png'], [4, '14-admin-invoices.png'], [5, '15-admin-reports.png'], ]) { if (tabs[i]) { await tabs[i].click(); await new Promise((r) => setTimeout(r, 600)); await shot(p, label); } } const edit = await p.$('button[onclick^="openCustomerDrawer"]'); if (edit) { await p.click('aside nav button[data-tab="customers"]'); await new Promise((r) => setTimeout(r, 400)); const e2 = await p.$('button[onclick^="openCustomerDrawer"]'); if (e2) { await e2.click(); await new Promise((r) => setTimeout(r, 400)); await shot(p, '12b-admin-customer-drawer.png'); await p.evaluate(() => window.closeCustomerDrawer?.()); } } await p.close(); } // Portal { const p = await pageFor('portal'); await safeGoto(p, 'http://127.0.0.1:3000/portal/', 'portal'); await shot(p, '16-portal-login.png'); const toggle = await p.$('#auth-toggle'); if (toggle) { await toggle.click(); await new Promise((r) => setTimeout(r, 200)); } const email = `walk+${Date.now()}@example.com`; const nameEl = await p.$('#auth-name'); if (nameEl) await nameEl.type('Walkthrough User'); await p.type('#auth-email', email); await p.type('#auth-password', 'Walkthrough1!'); await p.click('#auth-submit'); await new Promise((r) => setTimeout(r, 1500)); await shot(p, '17-portal-dashboard.png'); const invoices = await p.$('nav button[data-tab="invoices"]'); if (invoices) { await invoices.click(); await new Promise((r) => setTimeout(r, 400)); await shot(p, '18-portal-invoices-empty.png'); } const stmt = await p.$('nav button[data-tab="statement"]'); if (stmt) { await stmt.click(); await new Promise((r) => setTimeout(r, 400)); await shot(p, '19-portal-statement.png'); } const docs = await p.$('nav button[data-tab="docs"]'); if (docs) { await docs.click(); await new Promise((r) => setTimeout(r, 400)); await shot(p, '20-portal-api-pricing.png'); } await p.close(); } // Swagger (stock) { const p = await pageFor('swagger'); await safeGoto(p, 'http://127.0.0.1:3000/docs', 'swagger'); await new Promise((r) => setTimeout(r, 800)); await shot(p, '21-swagger-stock.png'); await p.close(); } // Catalog (local if served, else public) { const p = await pageFor('catalog'); const local = 'http://127.0.0.1:8765/'; try { const r = await fetch(local, { signal: AbortSignal.timeout(1500) }); await safeGoto(p, r.ok ? local : 'https://zapier.georgelambert.org/', 'catalog'); } catch { await safeGoto(p, 'https://zapier.georgelambert.org/', 'catalog'); } await new Promise((r) => setTimeout(r, 800)); await shot(p, '22-catalog.png'); await p.close(); } } finally { await browser.close(); } const real = errors.filter((e) => !/favicon|Failed to load resource.*404/.test(e.text)); fs.writeFileSync(LOG, JSON.stringify({ capturedAt: new Date().toISOString(), errors: real, all: errors }, null, 2)); console.log(JSON.stringify({ shots: fs.readdirSync(SHOTS).length, errors: real.length }, null, 2)); if (real.length) { for (const e of real) console.log(`ERR ${e.page} ${e.type}: ${e.text}`); }