You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
259 lines
9.2 KiB
259 lines
9.2 KiB
/**
|
|
* Verifies eidos recipes render correctly in /uix/components.
|
|
*
|
|
* 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 OUT = 'g:/tmp/eidos';
|
|
|
|
async function probePort(): Promise<string | null> {
|
|
for (let port = 5173; port <= 5180; port++) {
|
|
try {
|
|
const r = await fetch(`http://localhost:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
if (r.ok || r.status === 404 || r.status === 500) 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}/uix/components/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}/uix/components/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}/uix/components/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);
|
|
});
|