import { test, expect } from '@playwright/test' import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = fileURLToPath(new URL('.', import.meta.url)) // Resolve fixture page to file:// URL. function fixtureUrl(name: string): string { return 'file://' + resolve(__dirname, 'pages', `${name}.html`) } // Injected helper: read bounding boxes and build a minimal GeometryWorld. // Returns a world compatible with imhotep-solver evaluateLogic. async function extractWorldFromPage(page: any): Promise { return page.evaluate(() => { const btn = document.querySelector('[data-testid="rendered-button"]') as HTMLElement; const card = document.querySelector('[data-testid="rendered-card"]') as HTMLElement; const btnRect = btn ? btn.getBoundingClientRect() : { left: 0, top: 0, right: 0, bottom: 0 }; const cardRect = card ? card.getBoundingClientRect() : { left: 0, top: 0, right: 0, bottom: 0 }; // Minimal world with just enough data for width/height predicates return { sceneId: 'fixture_scene', snapshotId: 'snap_1', env: { viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, deviceScaleFactor: 1, colorScheme: 'light', pointer: 'fine', hover: false, reducedMotion: false, locale: 'en', writingMode: 'horizontal-tb', }, strings: { values: [] }, subjects: { ids: [1, 2], domNodeId: [10, 20], subjectKind: [1, 1], primaryBoxId: [100, 200], firstFragmentId: [0, 0], fragmentCount: [0, 0], }, dom: { nodeId: [10, 20], parentNodeId: [0, 0], childCount: [0, 0], tagNameStringId: [0, 0], }, boxes: { boxId: [100, 200], subjectId: [1, 2], frameId: [1, 1], borderLeft: [btnRect.left, cardRect.left], borderTop: [btnRect.top, cardRect.top], borderRight: [btnRect.right, cardRect.right], borderBottom: [btnRect.bottom, cardRect.bottom], paddingLeft: [0, 0], paddingTop: [0, 0], paddingRight: [0, 0], paddingBottom: [0, 0], contentLeft: [0, 0], contentTop: [0, 0], contentRight: [0, 0], contentBottom: [0, 0], }, rects: { rectId: [], left: [], top: [], right: [], bottom: [] }, topology: { containingBlockOf: [0, 0], nearestPositionedAncestorOf: [0, 0], scrollContainerOf: [0, 0], stackingContextOf: [0, 0], formattingContextOf: [0, 0], clippingRootOf: [0, 0], paintOrderBucket: [0, 0], paintOrderIndex: [0, 0], }, scroll: { containerId: [], scrollLeft: [], scrollTop: [], scrollWidth: [], scrollHeight: [], clientWidth: [], clientHeight: [] }, clipping: { clipNodeId: [], subjectId: [], clipKind: [], clipLeft: [], clipTop: [], clipRight: [], clipBottom: [], parentClipNodeId: [] }, visibility: { subjectId: [], isRendered: [], isVisible: [], visibleArea: [], clippedArea: [] }, }; }); } // Update props on a React/Vue fixture page via custom event. async function updateProps(page: any, props: unknown): Promise { await page.evaluate((p: unknown) => { window.dispatchEvent(new CustomEvent('imhotep:update-props', { detail: { props: p } })); }, props); // Allow React/Vue re-render to settle await page.waitForTimeout(50); } // Update args on a Storybook-like fixture page via custom event. async function updateArgs(page: any, args: unknown): Promise { await page.evaluate((a: unknown) => { window.dispatchEvent(new CustomEvent('imhotep:update-args', { detail: { args: a } })); }, args); await page.waitForTimeout(50); } // --------------------------------------------------------------------------- // Test 1: React props-generated layout invariants // --------------------------------------------------------------------------- test.describe('E2E: React property-render invariants', () => { test('button is at least 44px wide across all enumerated prop combinations', async ({ page }) => { await page.goto(fixtureUrl('property-render-react')); await page.waitForSelector('[data-testid="rendered-button"]'); const sizes = ['sm', 'md', 'lg'] as const; const disabledStates = [false, true]; for (const size of sizes) { for (const disabled of disabledStates) { await updateProps(page, { button: { size, disabled, label: 'Test' }, card: { title: 'T', content: 'C', compact: false } }); const btn = page.locator('[data-testid="rendered-button"]'); const box = await btn.boundingBox(); expect(box).not.toBeNull(); expect(box!.width).toBeGreaterThanOrEqual(44); expect(box!.height).toBeGreaterThanOrEqual(32); } } }); test('card layout changes with compact prop', async ({ page }) => { await page.goto(fixtureUrl('property-render-react')); await page.waitForSelector('[data-testid="rendered-card"]'); // Normal padding await updateProps(page, { button: { size: 'md', disabled: false, label: 'OK' }, card: { title: 'Title', content: 'Content', compact: false } }); const cardNormal = await page.locator('[data-testid="rendered-card"]').boundingBox(); expect(cardNormal).not.toBeNull(); // Compact padding await updateProps(page, { button: { size: 'md', disabled: false, label: 'OK' }, card: { title: 'Title', content: 'Content', compact: true } }); const cardCompact = await page.locator('[data-testid="rendered-card"]').boundingBox(); expect(cardCompact).not.toBeNull(); // Compact should be shorter (less padding) expect(cardCompact!.height).toBeLessThan(cardNormal!.height); }); test('deterministic FOL evaluates over extracted React scene', async ({ page }) => { await page.goto(fixtureUrl('property-render-react')); await page.waitForSelector('[data-testid="rendered-button"]'); await updateProps(page, { button: { size: 'lg', disabled: false, label: 'Big Button' }, card: { title: 'T', content: 'C', compact: false } }); const world = await extractWorldFromPage(page); // Import solver modules dynamically to avoid top-level dependency issues const { evaluateLogic, registerDefaultPredicates } = await import('imhotep-solver'); registerDefaultPredicates(); const result = evaluateLogic({ formula: { type: 'FormulaNode', kind: 'forall', bindings: [{ type: 'TupleBinding', variables: ['btn'], domain: { type: 'DomainRef', domain: 'elements', selector: '.button' } }], body: { type: 'FormulaNode', kind: 'predicate', predicate: 'atLeast', args: [{ type: 'VariableRef', name: 'btn' }] } }, world, resolver: { resolve(domain: any) { const key = domain.selector ?? domain.domain; if (key === '.button') { return { domainId: 'dom_btn', subjectIds: new Uint32Array([1]), provenance: 'elements(.button)', closed: true, }; } return undefined; } }, options: {} }); expect(result.passed).toBe(true); expect(result.mode).toBe('scene-determinate'); expect(result.formulaResults[0].outcome).toBe('pass'); }); }); // --------------------------------------------------------------------------- // Test 2: Vue props-generated layout invariants // --------------------------------------------------------------------------- test.describe('E2E: Vue property-render invariants', () => { test('button is at least 44px wide across all enumerated prop combinations', async ({ page }) => { await page.goto(fixtureUrl('property-render-vue')); await page.waitForSelector('[data-testid="rendered-button"]'); const sizes = ['sm', 'md', 'lg'] as const; const disabledStates = [false, true]; for (const size of sizes) { for (const disabled of disabledStates) { await updateProps(page, { button: { size, disabled, label: 'Test' }, card: { title: 'T', content: 'C', compact: false } }); const btn = page.locator('[data-testid="rendered-button"]'); const box = await btn.boundingBox(); expect(box).not.toBeNull(); expect(box!.width).toBeGreaterThanOrEqual(44); expect(box!.height).toBeGreaterThanOrEqual(32); } } }); test('card layout changes with compact prop in Vue', async ({ page }) => { await page.goto(fixtureUrl('property-render-vue')); await page.waitForSelector('[data-testid="rendered-card"]'); await updateProps(page, { button: { size: 'md', disabled: false, label: 'OK' }, card: { title: 'Title', content: 'Content', compact: false } }); const cardNormal = await page.locator('[data-testid="rendered-card"]').boundingBox(); await updateProps(page, { button: { size: 'md', disabled: false, label: 'OK' }, card: { title: 'Title', content: 'Content', compact: true } }); const cardCompact = await page.locator('[data-testid="rendered-card"]').boundingBox(); expect(cardNormal).not.toBeNull(); expect(cardCompact).not.toBeNull(); expect(cardCompact!.height).toBeLessThan(cardNormal!.height); }); test('deterministic FOL evaluates over extracted Vue scene', async ({ page }) => { await page.goto(fixtureUrl('property-render-vue')); await page.waitForSelector('[data-testid="rendered-button"]'); await updateProps(page, { button: { size: 'lg', disabled: false, label: 'Big Button' }, card: { title: 'T', content: 'C', compact: false } }); const world = await extractWorldFromPage(page); const { evaluateLogic, registerDefaultPredicates } = await import('imhotep-solver'); registerDefaultPredicates(); const result = evaluateLogic({ formula: { type: 'FormulaNode', kind: 'forall', bindings: [{ type: 'TupleBinding', variables: ['btn'], domain: { type: 'DomainRef', domain: 'elements', selector: '.button' } }], body: { type: 'FormulaNode', kind: 'predicate', predicate: 'atLeast', args: [{ type: 'VariableRef', name: 'btn' }] } }, world, resolver: { resolve(domain: any) { const key = domain.selector ?? domain.domain; if (key === '.button') { return { domainId: 'dom_btn', subjectIds: new Uint32Array([1]), provenance: 'elements(.button)', closed: true, }; } return undefined; } }, options: {} }); expect(result.passed).toBe(true); expect(result.mode).toBe('scene-determinate'); }); }); // --------------------------------------------------------------------------- // Test 3: Storybook-like story args invariants // --------------------------------------------------------------------------- test.describe('E2E: Storybook-like story args invariants', () => { test('story button is at least 44px wide across enumerated arg combinations', async ({ page }) => { await page.goto(fixtureUrl('storybook-like')); await page.waitForSelector('[data-testid="story-button"]'); const sizes = ['sm', 'md', 'lg'] as const; const disabledStates = [false, true]; for (const size of sizes) { for (const disabled of disabledStates) { await updateArgs(page, { size, disabled, label: 'Story' }); const btn = page.locator('[data-testid="story-button"]'); const box = await btn.boundingBox(); expect(box).not.toBeNull(); expect(box!.width).toBeGreaterThanOrEqual(44); } } }); test('controls panel reflects current args', async ({ page }) => { await page.goto(fixtureUrl('storybook-like')); await page.waitForSelector('.controls-panel'); await updateArgs(page, { size: 'lg', disabled: true, label: 'Controlled' }); const panel = page.locator('.controls-panel'); await expect(panel.locator('.control-value').nth(0)).toHaveText('lg'); await expect(panel.locator('.control-value').nth(1)).toHaveText('true'); await expect(panel.locator('.control-value').nth(2)).toHaveText('Controlled'); }); test('deterministic FOL works inside story property run', async ({ page }) => { await page.goto(fixtureUrl('storybook-like')); await page.waitForSelector('[data-testid="story-button"]'); await updateArgs(page, { size: 'md', disabled: false, label: 'FOL Test' }); const world = await page.evaluate(() => { const btn = document.querySelector('[data-testid="story-button"]') as HTMLElement; const btnRect = btn ? btn.getBoundingClientRect() : { left: 0, top: 0, right: 0, bottom: 0 }; return { sceneId: 'story_scene', snapshotId: 'snap_1', env: { viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, deviceScaleFactor: 1, colorScheme: 'light', pointer: 'fine', hover: false, reducedMotion: false, locale: 'en', writingMode: 'horizontal-tb', }, strings: { values: [] }, subjects: { ids: [1], domNodeId: [10], subjectKind: [1], primaryBoxId: [100], firstFragmentId: [0], fragmentCount: [0], }, dom: { nodeId: [10], parentNodeId: [0], childCount: [0], tagNameStringId: [0], }, boxes: { boxId: [100], subjectId: [1], frameId: [1], borderLeft: [btnRect.left], borderTop: [btnRect.top], borderRight: [btnRect.right], borderBottom: [btnRect.bottom], paddingLeft: [0], paddingTop: [0], paddingRight: [0], paddingBottom: [0], contentLeft: [0], contentTop: [0], contentRight: [0], contentBottom: [0], }, visualBoxes: { boxId: [100], subjectId: [1], frameId: [1], borderLeft: [btnRect.left], borderTop: [btnRect.top], borderRight: [btnRect.right], borderBottom: [btnRect.bottom], paddingLeft: [0], paddingTop: [0], paddingRight: [0], paddingBottom: [0], contentLeft: [0], contentTop: [0], contentRight: [0], contentBottom: [0], }, transforms: { transformId: [], subjectId: [], matrixStart: [], matrixLength: [], originX: [], originY: [], }, matrices: { values: [], }, rects: { rectId: [], left: [], top: [], right: [], bottom: [] }, topology: { containingBlockOf: [0], nearestPositionedAncestorOf: [0], scrollContainerOf: [0], stackingContextOf: [0], formattingContextOf: [0], clippingRootOf: [0], paintOrderBucket: [0], paintOrderIndex: [0], }, scroll: { containerId: [], scrollLeft: [], scrollTop: [], scrollWidth: [], scrollHeight: [], clientWidth: [], clientHeight: [] }, clipping: { clipNodeId: [], subjectId: [], clipKind: [], clipLeft: [], clipTop: [], clipRight: [], clipBottom: [], parentClipNodeId: [] }, visibility: { subjectId: [], isRendered: [], isVisible: [], visibleArea: [], clippedArea: [] }, }; }); const { evaluateLogic, registerDefaultPredicates } = await import('imhotep-solver'); registerDefaultPredicates(); const result = evaluateLogic({ formula: { type: 'FormulaNode', kind: 'forall', bindings: [{ type: 'TupleBinding', variables: ['btn'], domain: { type: 'DomainRef', domain: 'elements', selector: '.button' } }], body: { type: 'FormulaNode', kind: 'predicate', predicate: 'atLeast', args: [{ type: 'VariableRef', name: 'btn' }] } }, world, resolver: { resolve(domain: any) { const key = domain.selector ?? domain.domain; if (key === '.button') { return { domainId: 'dom_btn', subjectIds: new Uint32Array([1]), provenance: 'elements(.button)', closed: true, }; } return undefined; } }, options: {} }); expect(result.passed).toBe(true); expect(result.mode).toBe('scene-determinate'); expect(result.proofs.length).toBeGreaterThan(0); }); }); // --------------------------------------------------------------------------- // Test 4: Isolated component document route (Operator-style showroom) // --------------------------------------------------------------------------- test.describe('E2E: Isolated component document route', () => { test('React fixture acts as isolated showroom component', async ({ page }) => { // An isolated component document is just a fixture page that renders // a single component without surrounding app chrome. await page.goto(fixtureUrl('property-render-react')); await page.waitForSelector('[data-testid="rendered-button"]'); // Verify no navigation, no sidebars, no app chrome const body = page.locator('body'); const html = await body.innerHTML(); expect(html).not.toContain('nav'); expect(html).not.toContain('sidebar'); expect(html).not.toContain('header'); // Component should be mountable and testable in isolation await updateProps(page, { button: { size: 'md', disabled: false, label: 'Isolated' }, card: { title: 'Isolated Card', content: 'No app chrome', compact: false } }); const btn = page.locator('[data-testid="rendered-button"]'); await expect(btn).toHaveText('Isolated'); const box = await btn.boundingBox(); expect(box).not.toBeNull(); expect(box!.width).toBeGreaterThanOrEqual(44); }); test('Vue fixture acts as isolated showroom component', async ({ page }) => { await page.goto(fixtureUrl('property-render-vue')); await page.waitForSelector('[data-testid="rendered-button"]'); const body = page.locator('body'); const html = await body.innerHTML(); expect(html).not.toContain('nav'); expect(html).not.toContain('sidebar'); await updateProps(page, { button: { size: 'lg', disabled: true, label: 'Disabled' }, card: { title: 'T', content: 'C', compact: true } }); const btn = page.locator('[data-testid="rendered-button"]'); await expect(btn).toHaveAttribute('disabled', ''); const box = await btn.boundingBox(); expect(box).not.toBeNull(); expect(box!.width).toBeGreaterThanOrEqual(44); }); test('isolated showroom supports deterministic FOL evaluation', async ({ page }) => { await page.goto(fixtureUrl('property-render-react')); await page.waitForSelector('[data-testid="rendered-button"]'); await updateProps(page, { button: { size: 'sm', disabled: false, label: 'Small' }, card: { title: 'T', content: 'C', compact: false } }); const world = await extractWorldFromPage(page); const { evaluateLogic, registerDefaultPredicates } = await import('imhotep-solver'); registerDefaultPredicates(); // Test existential: there exists a button with width >= 44 const result = evaluateLogic({ formula: { type: 'FormulaNode', kind: 'exists', bindings: [{ type: 'TupleBinding', variables: ['btn'], domain: { type: 'DomainRef', domain: 'elements', selector: '.button' } }], body: { type: 'FormulaNode', kind: 'predicate', predicate: 'atLeast', args: [{ type: 'VariableRef', name: 'btn' }] } }, world, resolver: { resolve(domain: any) { const key = domain.selector ?? domain.domain; if (key === '.button') { return { domainId: 'dom_btn', subjectIds: new Uint32Array([1]), provenance: 'elements(.button)', closed: true, }; } return undefined; } }, options: {} }); expect(result.passed).toBe(true); expect(result.mode).toBe('scene-determinate'); expect(result.formulaResults[0].outcome).toBe('pass'); }); });