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