v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user