69 lines
2.3 KiB
TypeScript
69 lines
2.3 KiB
TypeScript
/**
|
|
* Storybook renderer adapter for Imhotep Playwright.
|
|
*
|
|
* Navigates to a Storybook story URL or uses Storybook's internal APIs
|
|
* to render a specific story. Accepts story args as input.
|
|
*/
|
|
|
|
import { Page } from 'playwright'
|
|
import type { SceneTarget } from 'imhotep-core/scene-target'
|
|
import { RendererAdapter } from './renderers.js'
|
|
|
|
export interface StorybookAdapterOptions {
|
|
/** Base URL of the Storybook instance. */
|
|
storybookUrl: string
|
|
}
|
|
|
|
export function createStorybookAdapter(options: StorybookAdapterOptions): RendererAdapter {
|
|
const { storybookUrl } = options
|
|
|
|
return {
|
|
id: 'storybook',
|
|
|
|
async mount(page: Page, target: SceneTarget, input: unknown): Promise<void> {
|
|
if (target.kind !== 'storybook-story') {
|
|
throw new Error(`Storybook adapter received non-storybook target: ${target.kind}`)
|
|
}
|
|
|
|
// Build the Storybook iframe URL.
|
|
// Standard Storybook v6+ URL format: ?path=/story/{storyId}
|
|
// With args: ?path=/story/{storyId}&args={json}
|
|
const storyId = target.storyId
|
|
const args = input as Record<string, unknown> | undefined
|
|
|
|
const url = new URL(storybookUrl)
|
|
// Only add Storybook path param for non-file URLs.
|
|
// File URLs are used for fixture-based testing where the HTML
|
|
// page itself is the "story" and does not parse ?path params.
|
|
if (url.protocol !== 'file:') {
|
|
url.searchParams.set('path', `/story/${storyId}`)
|
|
}
|
|
|
|
if (args && Object.keys(args).length > 0) {
|
|
url.searchParams.set('args', JSON.stringify(args))
|
|
}
|
|
|
|
await page.goto(url.toString())
|
|
|
|
// Wait for the story to be rendered.
|
|
// Storybook renders into #root (v6) or #storybook-root (v7+).
|
|
try {
|
|
await page.waitForSelector('#root, #storybook-root', { state: 'visible', timeout: 10000 })
|
|
} catch {
|
|
// If neither selector is found, the story may still be loading.
|
|
// Fall through and let downstream extraction handle any issues.
|
|
}
|
|
},
|
|
|
|
async unmount(page: Page): Promise<void> {
|
|
// Storybook unmount is a no-op; navigation away handles cleanup.
|
|
await page.evaluate(() => {
|
|
const root = document.getElementById('root') || document.getElementById('storybook-root')
|
|
if (root) {
|
|
root.innerHTML = ''
|
|
}
|
|
})
|
|
},
|
|
}
|
|
}
|