/** * 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 { 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 | 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 { // 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 = '' } }) }, } }