import { test, expect } from '@playwright/test' import { imhotepComponent, react, getDefaultRendererRegistry } from 'imhotep-playwright' import type { Page } from 'playwright' import { generatedDomain } from 'imhotep-core/property-contracts' import fc from 'fast-check' import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = fileURLToPath(new URL('.', import.meta.url)) function fixtureUrl(name: string): string { return 'file://' + resolve(__dirname, 'pages', `${name}.html`) } /** Custom React 18 adapter that properly unmounts roots between property runs. */ function createReact18Adapter() { return { id: 'react', async mount(page: Page, _target: unknown, input: unknown) { await page.evaluate((props) => { const container = document.getElementById('__imhotep-mount') if (!container) throw new Error('Mount container #__imhotep-mount not found') // Unmount any previous React root to avoid duplicate React roots. const prevRoot = (window as any).__imhotepReactRoot if (prevRoot) { prevRoot.unmount() } container.innerHTML = '' const Component = (window as any).__imhotepComponents?.Button if (!Component) throw new Error('Button component not found on window.__imhotepComponents') const createElement = (window as any).__imhotepCreateElement const reactDom = (window as any).__imhotepReactDom const element = createElement(Component, props) const root = reactDom.createRoot(container) ;(window as any).__imhotepReactRoot = root root.render(element) }, input) }, async unmount(page: Page) { await page.evaluate(() => { const root = (window as any).__imhotepReactRoot if (root) { root.unmount() ;(window as any).__imhotepReactRoot = null } const container = document.getElementById('__imhotep-mount') if (container) container.innerHTML = '' }) } } } test.describe('E2E: React component sampled props', () => { test('forAllProps mounts component with generated props and asserts layout', async ({ page }) => { // Navigate to the component fixture page first await page.goto(fixtureUrl('property-component-react')) await page.waitForSelector('#__imhotep-mount', { state: 'attached' }) // Register the React renderer adapter const registry = getDefaultRendererRegistry() registry.register('react', createReact18Adapter()) const handle = imhotepComponent('Button', { renderer: react({ reactDom: {}, createElement: () => null }), fc: fc as any, }) const domain = generatedDomain( fc.record({ size: fc.constantFrom('sm', 'md', 'lg'), label: fc.string({ minLength: 1, maxLength: 15 }), disabled: fc.boolean(), }), { seed: 42, numRuns: 15 } ) let runCount = 0 const result = await handle.forAllProps( page, domain, async (scene, _ctx) => { runCount++ const data = await scene.extract('[data-testid="component-button"]') expect(Array.isArray(data)).toBe(true) expect((data as any[]).length).toBe(1) const box = (data as any[])[0].rect as { x: number; y: number; width: number; height: number } // Assert button is inside viewport expect(box.x).toBeGreaterThanOrEqual(0) expect(box.y).toBeGreaterThanOrEqual(0) expect(box.x + box.width).toBeLessThanOrEqual(1280) expect(box.y + box.height).toBeLessThanOrEqual(720) // Assert button has minimum width expect(box.width).toBeGreaterThanOrEqual(44) expect(box.height).toBeGreaterThanOrEqual(32) } ) expect(result.passed).toBe(true) expect(result.mode).toBe('sampled') expect(result.seed).toBe(42) expect(runCount).toBe(15) }) test('component receives actual props not just metadata', async ({ page }) => { await page.goto(fixtureUrl('property-component-react')) await page.waitForSelector('#__imhotep-mount', { state: 'attached' }) const registry = getDefaultRendererRegistry() registry.register('react', createReact18Adapter()) const handle = imhotepComponent('Button', { renderer: react({ reactDom: {}, createElement: () => null }), fc: fc as any, }) const domain = generatedDomain( fc.record({ size: fc.constantFrom('sm', 'md', 'lg'), label: fc.string({ minLength: 1, maxLength: 10 }), }), { seed: 77, numRuns: 10 } ) const observedProps: Array<{ size: string; label: string }> = [] const result = await handle.forAllProps( page, domain, async (scene, ctx) => { const input = ctx.input as { size: string; label: string } observedProps.push(input) // Verify the DOM reflects the actual props const btn = page.locator('[data-testid="component-button"]') const text = await btn.textContent() expect(text).toBe(input.label) // Verify size class is applied const classAttr = await btn.getAttribute('class') expect(classAttr).toContain('btn-' + input.size) const data = await scene.extract('[data-testid="component-button"]') expect((data as any[]).length).toBe(1) } ) expect(result.passed).toBe(true) expect(observedProps.length).toBeGreaterThan(0) // Verify we saw different sizes (probabilistic but very likely with 10 runs) const sizes = new Set(observedProps.map(p => p.size)) expect(sizes.size).toBeGreaterThanOrEqual(1) }) })