feat(screenshots): Playwright capture pipeline (local-only, viewport-only)
This commit is contained in:
50
tools/screenshots/README.md
Normal file
50
tools/screenshots/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Screenshot pipeline
|
||||
|
||||
Captures real WHP screenshots into `src/assets/screenshots/whp/`.
|
||||
|
||||
**Local-only. Never runs in CI.** CI builds the static site; it never opens a browser or hits WHP.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. A demo WHP account (prod recommended for accuracy; staging works internally).
|
||||
2. Network access to that WHP host.
|
||||
3. Node 20+ and Playwright Chromium installed: `npx playwright install chromium`.
|
||||
|
||||
## Configure
|
||||
|
||||
Create `tools/screenshots/.env` (gitignored):
|
||||
|
||||
```
|
||||
WHP_BASE=https://whp01.cloud-hosting.io:8443
|
||||
WHP_USER=demo-kb
|
||||
WHP_PASS=…
|
||||
```
|
||||
|
||||
The script reads these via `process.env`; pass them in your shell or use a `.env` loader like `dotenv-cli`.
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
# Load .env into your shell (one option):
|
||||
set -a; source tools/screenshots/.env; set +a
|
||||
npm run screenshots
|
||||
```
|
||||
|
||||
Outputs one PNG per entry in `shots.config.ts` to `src/assets/screenshots/whp/<id>.png`. Existing files are overwritten.
|
||||
|
||||
## Capture rules
|
||||
|
||||
- **Viewport: 1440×900.** Locked. No `fullPage`. Playwright's viewport screenshots never include browser chrome — no address bar, no tab strip.
|
||||
- **Mask list:** defaults (account ID, server hostname, user IP, billing column) plus per-shot additions.
|
||||
- **No address bar in any image.** Multi-server fleet — we don't want a specific host in any screenshot.
|
||||
- Use `selector` to clip to a region (e.g., just the sidebar) when the full viewport is noisier than useful.
|
||||
|
||||
## Refresh workflow
|
||||
|
||||
UI changed? → `npm run screenshots` locally → review the diffs (`git diff --stat` shows changed PNGs) → eyeball them for accidental leakage → commit → push.
|
||||
|
||||
## Adding a new shot
|
||||
|
||||
1. Add an entry to `shots.config.ts` with a stable `id`.
|
||||
2. `npm run screenshots`.
|
||||
3. Reference the new file in your `.mdx`: ``.
|
||||
83
tools/screenshots/run.ts
Normal file
83
tools/screenshots/run.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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<void> {
|
||||
await page.goto(`${WHP_BASE}/login`);
|
||||
await page.fill('input[name="username"]', WHP_USER);
|
||||
await page.fill('input[name="password"]', WHP_PASS);
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
async function captureShot(page: Page, shot: Shot): Promise<void> {
|
||||
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);
|
||||
|
||||
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<void> {
|
||||
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);
|
||||
});
|
||||
33
tools/screenshots/shots.config.ts
Normal file
33
tools/screenshots/shots.config.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export type Shot = {
|
||||
/** Stable filename id; output is src/assets/screenshots/whp/<id>.png */
|
||||
id: string;
|
||||
/** Path on the WHP host. Leading slash, no scheme/host. */
|
||||
path: string;
|
||||
/** Optional CSS selector to clip to a region instead of the full viewport */
|
||||
selector?: string;
|
||||
/** Optional list of selectors to redact (account ID, IPs, etc.) before snapshot */
|
||||
mask?: string[];
|
||||
/** Defaults: 1440x900 */
|
||||
viewport?: { width: number; height: number };
|
||||
/** Optional selector to wait for before capturing */
|
||||
waitFor?: string;
|
||||
};
|
||||
|
||||
/** Always-applied redactions. Per-shot mask is added on top of this list. */
|
||||
export const DEFAULT_MASK: string[] = [
|
||||
'[data-test="account-id"]',
|
||||
'[data-test="server-hostname"]',
|
||||
'[data-test="user-ip"]',
|
||||
'.billing-column',
|
||||
];
|
||||
|
||||
export const shots: Shot[] = [
|
||||
{ id: 'whp-domains-add', path: '/domains/add' },
|
||||
{ id: 'whp-sites-add', path: '/sites/add' },
|
||||
{ id: 'whp-email-add', path: '/email/add' },
|
||||
{ id: 'whp-backups-settings', path: '/backups/settings' },
|
||||
{ id: 'whp-backups-history', path: '/backups/history' },
|
||||
{ id: 'whp-monitor', path: '/security/monitor' },
|
||||
{ id: 'whp-email-archive', path: '/email/archive' },
|
||||
{ id: 'whp-resources', path: '/overview/resources' },
|
||||
];
|
||||
Reference in New Issue
Block a user