Files
Imhotep/packages/imhotep-fixtures/src/e2e-storybook.test.ts
T

187 lines
5.8 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { imhotepStory } from 'imhotep-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`)
}
test.describe('E2E: Storybook story property run', () => {
test('imhotepStory forAllProps navigates to story URL', async ({ page }) => {
const handle = imhotepStory('button--primary', {
storybookUrl: fixtureUrl('storybook-like'),
fc: fc as any,
})
const domain = generatedDomain(
fc.record({
size: fc.constantFrom('sm', 'md', 'lg'),
disabled: fc.boolean(),
label: fc.constant('Story Button'),
}),
{ seed: 42, numRuns: 5 }
)
let runCount = 0
const result = await handle.forAllProps(
page,
domain,
async (scene, _ctx) => {
runCount++
// Verify the page loaded by extracting an element
const data = await scene.extract('[data-testid="story-button"]')
expect(Array.isArray(data)).toBe(true)
expect((data as any[]).length).toBe(1)
}
)
expect(result.passed).toBe(true)
expect(runCount).toBe(5)
})
test('imhotepStory forAllProps applies generated args', async ({ page }) => {
const handle = imhotepStory('button--primary', {
storybookUrl: fixtureUrl('storybook-like'),
fc: fc as any,
})
const domain = generatedDomain(
fc.record({
size: fc.constantFrom('sm', 'md', 'lg'),
disabled: fc.boolean(),
label: fc.constant('Story Button'),
}),
{ seed: 42, numRuns: 10 }
)
const sizes: string[] = []
const result = await handle.forAllProps(
page,
domain,
async (_scene, ctx) => {
const input = ctx.input as { size: string; disabled: boolean; label: string }
sizes.push(input.size)
}
)
expect(result.passed).toBe(true)
// With 10 runs and 3 size values, we should see variety
expect(sizes.length).toBe(10)
expect(new Set(sizes).size).toBeGreaterThanOrEqual(1)
})
test('button width invariant across all generated arg combinations', async ({ page }) => {
const handle = imhotepStory('button--primary', {
storybookUrl: fixtureUrl('storybook-like'),
fc: fc as any,
})
const domain = generatedDomain(
fc.record({
size: fc.constantFrom('sm', 'md', 'lg'),
disabled: fc.boolean(),
label: fc.string({ minLength: 1, maxLength: 15 }),
}),
{ seed: 42, numRuns: 20 }
)
let runCount = 0
const result = await handle.forAllProps(
page,
domain,
async (scene, _ctx) => {
runCount++
const data = await scene.extract('[data-testid="story-button"]')
expect(Array.isArray(data)).toBe(true)
expect((data as any[]).length).toBe(1)
const box = (data as any[])[0].rect as { width: number; height: number }
// Minimum width invariant: all buttons must be at least 60px wide
// (sm is 60px min-width per the fixture CSS)
expect(box.width).toBeGreaterThanOrEqual(60)
// Minimum height invariant: all buttons must be at least 32px tall
expect(box.height).toBeGreaterThanOrEqual(32)
}
)
expect(result.passed).toBe(true)
expect(result.mode).toBe('sampled')
expect(runCount).toBe(20)
})
test('size prop affects actual rendered dimensions', async ({ page }) => {
const handle = imhotepStory('button--primary', {
storybookUrl: fixtureUrl('storybook-like'),
fc: fc as any,
})
const domain = generatedDomain(
fc.record({
size: fc.constantFrom('sm', 'md', 'lg'),
disabled: fc.boolean(),
label: fc.constant('Btn'),
}),
{ seed: 123, numRuns: 15 }
)
const observed: Array<{ size: string; width: number; height: number }> = []
const result = await handle.forAllProps(
page,
domain,
async (scene, ctx) => {
const input = ctx.input as { size: string }
const data = await scene.extract('[data-testid="story-button"]')
const box = (data as any[])[0].rect as { width: number; height: number }
observed.push({ size: input.size, width: box.width, height: box.height })
}
)
expect(result.passed).toBe(true)
expect(observed.length).toBe(15)
// Verify that different sizes were observed
const sizes = new Set(observed.map(o => o.size))
expect(sizes.size).toBeGreaterThanOrEqual(1)
// Verify that lg buttons are wider than sm buttons on average
const avgWidth = (size: string) => {
const widths = observed.filter(o => o.size === size).map(o => o.width)
return widths.reduce((a, b) => a + b, 0) / (widths.length || 1)
}
expect(avgWidth('lg')).toBeGreaterThanOrEqual(avgWidth('sm'))
})
test('reproducible by seed across story runs', async ({ page }) => {
const handle = imhotepStory('button--primary', {
storybookUrl: fixtureUrl('storybook-like'),
fc: fc as any,
})
const domain = generatedDomain(
fc.record({
size: fc.constantFrom('sm', 'md', 'lg'),
disabled: fc.boolean(),
label: fc.string({ minLength: 1, maxLength: 5 }),
}),
{ seed: 777, numRuns: 10 }
)
const sizes1: string[] = []
await handle.forAllProps(page, domain, async (_scene, ctx) => {
sizes1.push((ctx.input as { size: string }).size)
})
const sizes2: string[] = []
await handle.forAllProps(page, domain, async (_scene, ctx) => {
sizes2.push((ctx.input as { size: string }).size)
})
expect(sizes1).toEqual(sizes2)
})
})