/** * Target resolution logic for Imhotep Playwright. * * Given a SceneTarget, produce a Playwright page with the target mounted. * This module bridges the SceneTarget contract to concrete Playwright * operations while keeping renderer concerns behind the adapter boundary. */ import { Page } from 'playwright' import { pathToFileURL } from 'url' import type { SceneTarget } from 'imhotep-core/scene-target' import { isPageTarget, isFixtureTarget, isRendererTarget, isStorybookStoryTarget, isComponentTarget, } from 'imhotep-core/scene-target' import { RendererRegistry, getDefaultRendererRegistry, RendererAdapter } from './renderers.js' // --------------------------------------------------------------------------- // Target Resolution Result // --------------------------------------------------------------------------- export interface ResolvedTarget { /** The Playwright page that now hosts the mounted target. */ page: Page /** Whether the target was mounted via a renderer adapter. */ mounted: boolean /** The renderer adapter used, if any. */ adapter?: RendererAdapter } // --------------------------------------------------------------------------- // Target Resolution // --------------------------------------------------------------------------- export interface TargetResolverOptions { /** Renderer registry to use for component and story targets. */ registry?: RendererRegistry } /** * Resolve a SceneTarget to a Playwright page with the target mounted. * * For 'page' targets: navigate to the URL. * For 'fixture' targets: load fixture HTML. * For renderer targets: use the registered adapter to mount into the page. */ export async function resolveTarget( page: Page, target: SceneTarget, options?: TargetResolverOptions & { input?: unknown } ): Promise { const registry = options?.registry ?? getDefaultRendererRegistry() if (isPageTarget(target)) { await page.goto(target.url) return { page, mounted: false } } if (isFixtureTarget(target)) { await loadFixture(page, target.fixtureId) return { page, mounted: false } } if (isRendererTarget(target)) { return await mountRendererTarget(page, target, registry, options?.input) } // Fallback: treat as page navigation if url is present (playwright-page) if (target.kind === 'playwright-page' && target.url) { await page.goto(target.url) return { page, mounted: false } } throw new Error(`Cannot resolve SceneTarget of kind "${(target as SceneTarget).kind}"`) } // --------------------------------------------------------------------------- // Fixture Loading // --------------------------------------------------------------------------- async function loadFixture(page: Page, fixtureId: string): Promise { // Fixtures are loaded as file URLs or served content. // If the fixtureId looks like a path, construct a file URL. // Otherwise assume it is already a resolvable URL. let url: string if (fixtureId.startsWith('http')) { url = fixtureId } else if (fixtureId.startsWith('file://')) { url = fixtureId } else { url = pathToFileURL(fixtureId).href } await page.goto(url) } // --------------------------------------------------------------------------- // Renderer Target Mounting // --------------------------------------------------------------------------- async function mountRendererTarget( page: Page, target: SceneTarget, registry: RendererRegistry, input?: unknown ): Promise { // Extract rendererId from the target. let rendererId: string if (isStorybookStoryTarget(target)) { // Storybook uses a special rendererId derived from its kind. rendererId = 'storybook' } else if (isComponentTarget(target)) { rendererId = target.rendererId } else { throw new Error(`Unexpected renderer target kind: ${(target as SceneTarget).kind}`) } const adapter = registry.resolve(rendererId) if (!adapter) { throw new Error(`No renderer adapter registered for rendererId "${rendererId}"`) } // Ensure a mount container exists on the page. await ensureMountContainer(page) // Mount via the adapter, passing the actual render input. await adapter.mount(page, target, input ?? {}) return { page, mounted: true, adapter } } /** * Ensure the page has a container div for renderer mounting. */ export async function ensureMountContainer(page: Page, containerId = '__imhotep-mount'): Promise { await page.evaluate((id: string) => { if (!document.getElementById(id)) { const div = document.createElement('div') div.id = id document.body.appendChild(div) } }, containerId) } /** * Clear the mount container on the page. */ export async function clearMountContainer(page: Page, containerId = '__imhotep-mount'): Promise { await page.evaluate((id: string) => { const el = document.getElementById(id) if (el) { el.innerHTML = '' } }, containerId) }