/** * Verifies eidos recipes render correctly in /test/eidos. * * 1. Toggle (★) — has data-archetype + focus ring on Tab. * 2. Switch — has data-archetype + focus ring on Tab. * 3. Dialog — opens via click, content + overlay visible, persists. * * Saves screenshots to /tmp/eidos-*.png so I can inspect. */ import { chromium } from 'playwright'; import { mkdir } from 'node:fs/promises'; const URL = 'http://localhost:5173/test/eidos'; const OUT = 'g:/tmp/eidos'; async function probePort(): Promise { for (const port of [5173, 5174, 5175]) { try { const r = await fetch(`http://localhost:${port}/`, { signal: AbortSignal.timeout(500) }); if (r.ok || r.status === 404) return `http://localhost:${port}`; } catch {} } return null; } async function main() { const base = await probePort(); if (!base) { console.error('No dev server. Run npm run dev.'); process.exit(2); } await mkdir(OUT, { recursive: true }); const browser = await chromium.launch(); const ctx = await browser.newContext({ viewport: { width: 900, height: 1100 } }); const page = await ctx.newPage(); page.on('console', (msg) => { if (msg.type() === 'error') console.log(`[console error] ${msg.text()}`); }); // ── 1. Toggle ──────────────────────────────────────────────────────────── await page.goto(`${base}/test/eidos/toggle`, { waitUntil: 'networkidle' }); await page.waitForTimeout(300); const toggle = page.locator('[data-toggle]').first(); const toggleAttrs = await toggle.evaluate((el) => ({ dataToggle: el.getAttribute('data-toggle'), dataArchetype: el.getAttribute('data-archetype'), tag: el.tagName.toLowerCase() })); console.log('Toggle attrs:', toggleAttrs); // Click body first so we have a focus origin, then tab into the toggle. await page.locator('body').click({ position: { x: 5, y: 5 } }); await page.waitForTimeout(50); // Tab until we hit the toggle button (skip the Soma layout bar if present). for (let i = 0; i < 10; i++) { await page.keyboard.press('Tab'); const isToggle = await page.evaluate(() => document.activeElement?.hasAttribute('data-toggle') ?? false); if (isToggle) break; } await page.waitForTimeout(100); const toggleFocusOutline = await toggle.evaluate((el) => { const cs = getComputedStyle(el); return { matchesFocusVisible: el.matches(':focus-visible'), outlineStyle: cs.outlineStyle, outlineWidth: cs.outlineWidth, boxShadow: cs.boxShadow }; }); console.log('Toggle ring (Tab focused):', toggleFocusOutline); await page.screenshot({ path: `${OUT}/01-toggle-focused.png`, fullPage: false }); // ── 2. Switch ──────────────────────────────────────────────────────────── await page.goto(`${base}/test/eidos/switch`, { waitUntil: 'networkidle' }); await page.waitForTimeout(300); const sw = page.locator('[data-switch]').first(); const swAttrs = await sw.evaluate((el) => ({ dataSwitch: el.getAttribute('data-switch'), dataArchetype: el.getAttribute('data-archetype'), dataState: el.getAttribute('data-state'), tag: el.tagName.toLowerCase() })); console.log('Switch attrs:', swAttrs); for (let i = 0; i < 5; i++) { await page.keyboard.press('Tab'); const isSwitch = await page.evaluate(() => document.activeElement?.hasAttribute('data-switch') ?? false); if (isSwitch) break; } await page.waitForTimeout(100); const swFocusOutline = await sw.evaluate((el) => { const cs = getComputedStyle(el); return { matchesFocusVisible: el.matches(':focus-visible'), boxShadow: cs.boxShadow, width: cs.width, height: cs.height, background: cs.backgroundColor }; }); console.log('Switch ring (Tab focused):', swFocusOutline); await page.screenshot({ path: `${OUT}/02-switch-focused.png`, fullPage: false }); // ── 3. Dialog ──────────────────────────────────────────────────────────── await page.goto(`${base}/test/eidos/dialog`, { waitUntil: 'networkidle' }); await page.waitForTimeout(300); const dialogTrigger = page.locator('[data-dialog-trigger]').first(); const triggerExists = (await dialogTrigger.count()) > 0; console.log('Dialog trigger exists:', triggerExists); if (triggerExists) { // Inject a sampler that records opacity at every rAF after the click. await page.evaluate(`(() => { window.__samples = []; const trigger = document.querySelector('[data-dialog-trigger]'); const t0 = performance.now(); trigger.click(); const tick = () => { const t = performance.now() - t0; const el = document.querySelector('[data-dialog-content]'); if (el) { const cs = getComputedStyle(el); window.__samples.push({ t: Math.round(t), opacity: parseFloat(cs.opacity), hasStartingStyle: el.hasAttribute('data-starting-style') }); } if (t < 500) requestAnimationFrame(tick); }; requestAnimationFrame(tick); })()`); await page.waitForTimeout(550); const entranceSamples = await page.evaluate('window.__samples') as Array<{ t: number; opacity: number; hasStartingStyle: boolean; }>; const decimated = entranceSamples.filter((_, i) => i % 3 === 0 || i === entranceSamples.length - 1); console.log('Entrance samples (t-ms, opacity, hasStartingStyle):'); for (const s of decimated) { console.log(` t=${s.t}ms opacity=${s.opacity.toFixed(3)} starting=${s.hasStartingStyle}`); } const dialogContent = page.locator('[data-dialog-content]'); await page.screenshot({ path: `${OUT}/03a-dialog-entrance-mid.png`, fullPage: false }); const dialogOverlay = page.locator('[data-dialog-overlay]'); const overlayCount = await dialogOverlay.count(); const contentCount = await dialogContent.count(); console.log(`Dialog opened — overlay count: ${overlayCount}, content count: ${contentCount}`); if (contentCount > 0) { const contentBox = await dialogContent.first().boundingBox(); const overlayBox = await dialogOverlay.first().boundingBox(); const overlayStyles = await dialogOverlay.first().evaluate((el) => { const cs = getComputedStyle(el); return { position: cs.position, inset: cs.inset, top: cs.top, left: cs.left, right: cs.right, bottom: cs.bottom, width: cs.width, height: cs.height, background: cs.backgroundColor, zIndex: cs.zIndex, display: cs.display, visibility: cs.visibility, opacity: cs.opacity, dataArchetype: el.getAttribute('data-archetype'), parentTag: el.parentElement?.tagName.toLowerCase() }; }); const contentStyles = await dialogContent.first().evaluate((el) => { const cs = getComputedStyle(el); return { position: cs.position, top: cs.top, left: cs.left, transform: cs.transform, background: cs.backgroundColor, zIndex: cs.zIndex, display: cs.display, visibility: cs.visibility, opacity: cs.opacity }; }); console.log('Dialog content box:', contentBox); console.log('Dialog overlay box:', overlayBox); console.log('Dialog overlay styles:', overlayStyles); console.log('Dialog content styles:', contentStyles); } await page.screenshot({ path: `${OUT}/03-dialog-open.png`, fullPage: false }); await page.evaluate(`(() => { window.__exitSamples = []; const closeBtn = document.querySelector('[data-dialog-close]'); const t0 = performance.now(); closeBtn.click(); const tick = () => { const t = performance.now() - t0; const el = document.querySelector('[data-dialog-content]'); if (el) { const cs = getComputedStyle(el); window.__exitSamples.push({ t: Math.round(t), opacity: parseFloat(cs.opacity), hasEndingStyle: el.hasAttribute('data-ending-style'), lastAction: el.getAttribute('data-last-action'), borderTopColor: cs.borderTopColor, mounted: true }); } else { window.__exitSamples.push({ t: Math.round(t), opacity: 0, hasEndingStyle: false, lastAction: null, borderTopColor: '', mounted: false }); } if (t < 500) requestAnimationFrame(tick); }; requestAnimationFrame(tick); })()`); await page.waitForTimeout(550); const exitSamples = await page.evaluate('window.__exitSamples') as Array<{ t: number; opacity: number; hasEndingStyle: boolean; lastAction: string | null; borderTopColor: string; mounted: boolean; }>; const exitDecimated = exitSamples.filter((_, i) => i % 3 === 0 || i === exitSamples.length - 1); console.log('Exit samples (t-ms, opacity, hasEndingStyle, lastAction, borderTop, mounted):'); for (const s of exitDecimated) { console.log( ` t=${s.t}ms opacity=${s.opacity.toFixed(3)} ending=${s.hasEndingStyle} lastAction=${s.lastAction} border=${s.borderTopColor} mounted=${s.mounted}` ); } await page.screenshot({ path: `${OUT}/04-dialog-closed.png`, fullPage: false }); } await browser.close(); console.log(`\nScreenshots saved to ${OUT}/`); } main().catch((e) => { console.error(e); process.exit(1); });