veza/apps/web/scripts/capture-visual-baseline.mjs
senke 4305d66aa2 ui(storybook): add allowedHosts config and update DESIGN_TOKENS docs
- .storybook/main.ts: add viteFinal with allowedHosts for veza.fr/com
  domains
- DESIGN_TOKENS.md: document modal max-height tokens and typography
  exceptions (avatar xs text-[10px])
- capture-visual-baseline.mjs: fix locator selectors for sidebar and
  player captures

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 22:49:24 +01:00

131 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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