master-zapier-plan-draft/packages/ui-docs/scripts/walkthrough.mjs
George Lambert cb07f5b321
Some checks are pending
offline / test (push) Waiting to run
Restyle staff and console UIs; add UI-Docs walkthrough and review PDF.
Match CS/sales/accounting/access-staff to the portal indigo system with
dollar amounts, skip links, and empty states. Fleet replica actions move
into overflow menus, roles become chips, Docs become cards, and the
header copy reflects the 0.0.0.0 bind. Simulator uses the same shell
(orange only for faults). Portal API keys are masked; admin customers
edit in a drawer. Catalog uses system-ui. New UI-Docs repo holds
screenshots, usage notes, and UI-REVIEW.pdf.
2026-09-11 18:01:33 -04:00

239 lines
7.8 KiB
JavaScript

#!/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}`);
}