veza/apps/web/scripts/capture-visual-baseline.mjs

132 lines
4.3 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* Capture visual baseline or current screenshots for pixel-accurate regression.
* Usage:
* node scripts/capture-visual-baseline.mjs [--baseline]
* --baseline Write to visual-tests/baselines/ (default: visual-tests/current/)
*
* Requires: dev server running at baseURL (default http://localhost:5173).
* Set PLAYWRIGHT_BASE_URL or VITE_FRONTEND_URL to override.
*
* Viewport: 1280×720, dark theme, reduced motion for stability.
*/
import { chromium } from 'playwright';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(__dirname, '..');
const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || process.env.VITE_FRONTEND_URL || 'http://localhost:5173';
const OUT_DIR = process.argv.includes('--baseline')
? path.join(ROOT, 'visual-tests', 'baselines')
: path.join(ROOT, 'visual-tests', 'current');
const VIEWPORT = { width: 1280, height: 720 };
const ANIMATION_SETTLE_MS = 800;
function ensureDarkTheme(page) {
return page.evaluate(() => {
document.documentElement.classList.add('dark');
document.documentElement.setAttribute('data-theme', 'dark');
});
}
async function waitSettle(page, ms = ANIMATION_SETTLE_MS) {
await page.waitForTimeout(ms);
}
async function captureFull(page, name) {
await ensureDarkTheme(page);
await waitSettle(page);
const file = path.join(OUT_DIR, `${name}-desktop.png`);
fs.mkdirSync(OUT_DIR, { recursive: true });
await page.screenshot({ path: file, fullPage: true });
console.log(' captured:', file);
}
async function captureLocator(page, locator, name) {
await ensureDarkTheme(page);
await waitSettle(page);
const file = path.join(OUT_DIR, `${name}-desktop.png`);
fs.mkdirSync(OUT_DIR, { recursive: true });
await locator.screenshot({ path: file });
console.log(' captured:', file);
}
const CRITICAL_SCREENS = [
{ name: 'login', url: '/login', auth: false, full: true },
{ name: 'register', url: '/register', auth: false, full: true },
{ name: 'dashboard', url: '/dashboard', auth: true, full: true },
{ name: 'sidebar', url: '/dashboard', auth: true, locator: 'aside.fixed' },
{ name: 'playlists', url: '/playlists', auth: true, full: true },
{ name: 'library', url: '/library', auth: true, full: true },
{ name: '404', url: '/non-existent-route-404', auth: false, full: true },
];
async function main() {
console.log('Capture target:', OUT_DIR);
console.log('Base URL:', BASE_URL);
const browser = await chromium.launch({ headless: true });
const storageStatePath = path.join(ROOT, 'e2e', '.auth', 'user.json');
const hasAuth = fs.existsSync(storageStatePath);
const contextOptions = {
viewport: VIEWPORT,
deviceScaleFactor: 1,
locale: 'en-US',
reducedMotion: 'reduce',
};
if (hasAuth) {
try {
contextOptions.storageState = storageStatePath;
} catch (e) {
console.warn('Could not set auth state:', e.message);
}
} else {
console.warn('No e2e/.auth/user.json — authenticated screens may redirect to login.');
}
const context = await browser.newContext(contextOptions);
const page = await context.newPage();
for (const screen of CRITICAL_SCREENS) {
if (screen.auth && !hasAuth) {
console.log('Skip (auth required):', screen.name);
continue;
}
if (!screen.auth) {
await context.clearCookies();
}
const fullUrl = BASE_URL.replace(/\/$/, '') + screen.url;
console.log('Open:', screen.name, fullUrl);
await page.goto(fullUrl, { waitUntil: 'networkidle', timeout: 20000 }).catch(() => {});
if (screen.locator) {
const el = page.locator(screen.locator).first();
const visible = await el.waitFor({ state: 'visible', timeout: 8000 }).then(() => true).catch(() => false);
if (visible) {
await captureLocator(page, el, screen.name);
} else {
console.log(' skipped: locator not visible');
}
} else {
await page.waitForSelector('body', { timeout: 5000 }).catch(() => {});
await captureFull(page, screen.name);
}
}
await browser.close();
console.log('Done.');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});