import { test, expect } from '@playwright/test' import { imhotep } from 'imhotep-playwright' import { loadFixtureInPage, waitForFixtureReady } from './harness.js' async function fixtureUrl(category: string): Promise { const { resolveFixturePage } = await import('./harness.js') return 'file://' + resolveFixturePage(category) } test.describe('E2E: States', () => { test('hover state changes geometry via transform', async ({ page }) => { const ui = await imhotep(page) await page.goto(await fixtureUrl('states')) await waitForFixtureReady(page) // Capture default state const defaultNote = await ui.materializeState('[data-testid="hover-btn"]', 'default') expect(defaultNote.status).toBe('native') await page.waitForTimeout(100) const defaultData = await ui.extract('[data-testid="hover-btn"]') const defaultRect = (defaultData as any[])[0].rect // Capture hover state const hoverNote = await ui.materializeState('[data-testid="hover-btn"]', 'hover') expect(hoverNote.status).toBe('native') await page.waitForTimeout(200) const hoverData = await ui.extract('[data-testid="hover-btn"]') const hoverRect = (hoverData as any[])[0].rect // Hover should scale to 1.05, so rect should be slightly larger // Note: getBoundingClientRect includes transforms expect(hoverRect.width).toBeGreaterThanOrEqual(defaultRect.width) expect(hoverRect.height).toBeGreaterThanOrEqual(defaultRect.height) }) test('focus-visible state creates outline geometry', async ({ page }) => { const ui = await imhotep(page) await page.goto(await fixtureUrl('states')) await waitForFixtureReady(page) // Default state await ui.materializeState('[data-testid="focus-input"]', 'default') await page.waitForTimeout(100) const defaultData = await ui.extract('[data-testid="focus-input"]') const defaultRect = (defaultData as any[])[0].rect // Focus-visible state const focusVisibleNote = await ui.materializeState('[data-testid="focus-input"]', 'focusVisible') expect(focusVisibleNote.status).toBe('approximate') expect(focusVisibleNote.note).toContain('approximated') await page.waitForTimeout(200) const focusData = await ui.extract('[data-testid="focus-input"]') const focusRect = (focusData as any[])[0].rect // Element should be focused and visible expect(focusRect.width).toBeGreaterThan(0) expect(focusRect.height).toBeGreaterThan(0) // Verify focus is applied via browser evaluation const isFocused = await page.evaluate(() => { const el = document.querySelector('[data-testid="focus-input"]') return document.activeElement === el }) expect(isFocused).toBe(true) }) test('default vs hover comparison shows measurable difference', async ({ page }) => { const ui = await imhotep(page) await page.goto(await fixtureUrl('states')) await waitForFixtureReady(page) // Get default state of hover card const defaultNote = await ui.materializeState('[data-testid="hover-card-el"]', 'default') expect(defaultNote.status).toBe('native') await page.waitForTimeout(150) const defaultData = await ui.extract('[data-testid="hover-card-el"]') const defaultRect = (defaultData as any[])[0].rect // Get hover state const hoverNote = await ui.materializeState('[data-testid="hover-card-el"]', 'hover') expect(hoverNote.status).toBe('native') await page.waitForTimeout(300) // Verify hover is actually applied in the DOM const isHovered = await page.evaluate(() => { const el = document.querySelector('[data-testid="hover-card-el"]') return el ? el.matches(':hover') : false }) expect(isHovered).toBe(true) const hoverData = await ui.extract('[data-testid="hover-card-el"]') const hoverRect = (hoverData as any[])[0].rect // Hover card translates up by 4px and adds shadow // Bounding rect includes transforms, but sub-pixel rounding may occur expect(hoverRect.y).toBeLessThanOrEqual(defaultRect.y) expect(defaultRect.y - hoverRect.y).toBeGreaterThanOrEqual(0) }) test('focus-visible button shows focus ring', async ({ page }) => { const ui = await imhotep(page) await page.goto(await fixtureUrl('states')) await waitForFixtureReady(page) const focusVisibleNote = await ui.materializeState('[data-testid="focus-btn"]', 'focusVisible') expect(focusVisibleNote.status).toBe('approximate') expect(focusVisibleNote.note).toContain('approximated') await page.waitForTimeout(200) const focusData = await ui.extract('[data-testid="focus-btn"]') const focusRect = (focusData as any[])[0].rect expect(focusRect.width).toBeGreaterThan(0) expect(focusRect.height).toBeGreaterThan(0) // Check that the element has focus-visible styling applied const hasFocusVisible = await page.evaluate(() => { const el = document.querySelector('[data-testid="focus-btn"]') if (!el) return false const styles = window.getComputedStyle(el) return styles.boxShadow !== 'none' || styles.outline !== 'none' }) expect(hasFocusVisible).toBe(true) }) test('active state shows pressed geometry', async ({ page }) => { const ui = await imhotep(page) await page.goto(await fixtureUrl('states')) await waitForFixtureReady(page) // Default state const defaultNote = await ui.materializeState('[data-testid="active-btn"]', 'default') expect(defaultNote.status).toBe('native') await page.waitForTimeout(100) const defaultData = await ui.extract('[data-testid="active-btn"]') const defaultRect = (defaultData as any[])[0].rect // Active state const activeNote = await ui.materializeState('[data-testid="active-btn"]', 'active') expect(activeNote.status).toBe('native') await page.waitForTimeout(100) const activeData = await ui.extract('[data-testid="active-btn"]') const activeRect = (activeData as any[])[0].rect // Active transform scales to 0.95, so rect should be slightly smaller // Note: the bounding rect may or may not include the transform depending on browser // We mainly verify the state materialization works expect(activeRect.width).toBeGreaterThan(0) expect(activeRect.height).toBeGreaterThan(0) }) test('state comparison between two elements', async ({ page }) => { const ui = await imhotep(page) await page.goto(await fixtureUrl('states')) await waitForFixtureReady(page) // Hover box A const hoverNote = await ui.materializeState('[data-testid="state-box-a"]', 'hover') expect(hoverNote.status).toBe('native') await page.waitForTimeout(300) const boxAData = await ui.extract('[data-testid="state-box-a"]') const boxARect = (boxAData as any[])[0].rect // Keep box B in default state (move mouse away first) const defaultNote = await ui.materializeState('[data-testid="state-box-b"]', 'default') expect(defaultNote.status).toBe('native') await page.waitForTimeout(150) const boxBData = await ui.extract('[data-testid="state-box-b"]') const boxBRect = (boxBData as any[])[0].rect // Box A should be scaled up (hover state has transform: scale(1.1)) // Use >= to tolerate sub-pixel rounding; verify hover actually applied expect(boxARect.width).toBeGreaterThanOrEqual(boxBRect.width) expect(boxARect.height).toBeGreaterThanOrEqual(boxBRect.height) }) })