144 lines
5.2 KiB
TypeScript
144 lines
5.2 KiB
TypeScript
|
|
/**
|
||
|
|
* 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, '<your-server>.cloud-hosting.io'],
|
||
|
|
[/mail\d+\.cloud-hosting\.io/gi, '<your-mail-server>.cloud-hosting.io'],
|
||
|
|
[/whp-demo\.anhh\.co/gi, 'your-site.example.com'],
|
||
|
|
[/whp\d+(-[a-z0-9]+)?\b/gi, '<your-server>'],
|
||
|
|
[/WHP\d+(-[A-Z0-9]+)?\b/g, '<YOUR-SERVER>'],
|
||
|
|
[/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, '<your-server-IP>'],
|
||
|
|
[/\/docker\/users\/[a-z0-9-]+/g, '/docker/users/<your-username>'],
|
||
|
|
[/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<string | null> {
|
||
|
|
// 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); });
|