187 lines
5.8 KiB
TypeScript
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)
|
|
})
|
|
})
|