v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)

This commit is contained in:
John Dvorak
2025-08-15 10:00:00 -07:00
commit 92deb689cd
321 changed files with 79170 additions and 0 deletions
+161
View File
@@ -0,0 +1,161 @@
/**
* 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)
}