162 lines
4.9 KiB
TypeScript
162 lines
4.9 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<ResolvedTarget> {
|
||
|
|
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<void> {
|
||
|
|
// 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<ResolvedTarget> {
|
||
|
|
// 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<void> {
|
||
|
|
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<void> {
|
||
|
|
await page.evaluate((id: string) => {
|
||
|
|
const el = document.getElementById(id)
|
||
|
|
if (el) {
|
||
|
|
el.innerHTML = ''
|
||
|
|
}
|
||
|
|
}, containerId)
|
||
|
|
}
|