/** * SceneTarget discriminated union and type guards. * * The scene target is the stage. It must be possible to mount any component, * any story, any page, and treat it as a deterministic scene. The adapter * boundary is the moat that keeps renderer concerns out of the core. */ import type { Environment } from './types.js' // --------------------------------------------------------------------------- // SceneTarget Union // --------------------------------------------------------------------------- export type SceneTarget = | { kind: 'page'; url: string } | { kind: 'playwright-page'; pageRef: string; url?: string } | { kind: 'storybook-story'; storyId: string; storybookUrl: string } | { kind: 'react-component'; rendererId: string; componentId: string } | { kind: 'vue-component'; rendererId: string; componentId: string } | { kind: 'custom-renderer'; rendererId: string; targetId: string } | { kind: 'fixture'; fixtureId: string } // --------------------------------------------------------------------------- // RenderCase Contract // --------------------------------------------------------------------------- export interface RenderCase { caseId: string input: unknown env?: Partial metadata?: Record } // --------------------------------------------------------------------------- // Type Guards // --------------------------------------------------------------------------- export function isSceneTarget(value: unknown): value is SceneTarget { if (typeof value !== 'object' || value === null) return false const obj = value as Record if (typeof obj.kind !== 'string') return false switch (obj.kind) { case 'page': return typeof obj.url === 'string' case 'playwright-page': return typeof obj.pageRef === 'string' && (obj.url === undefined || typeof obj.url === 'string') case 'storybook-story': return typeof obj.storyId === 'string' && typeof obj.storybookUrl === 'string' case 'react-component': return typeof obj.rendererId === 'string' && typeof obj.componentId === 'string' case 'vue-component': return typeof obj.rendererId === 'string' && typeof obj.componentId === 'string' case 'custom-renderer': return typeof obj.rendererId === 'string' && typeof obj.targetId === 'string' case 'fixture': return typeof obj.fixtureId === 'string' default: return false } } export function isPageTarget(target: SceneTarget): target is Extract { return target.kind === 'page' } export function isPlaywrightPageTarget( target: SceneTarget ): target is Extract { return target.kind === 'playwright-page' } export function isStorybookStoryTarget( target: SceneTarget ): target is Extract { return target.kind === 'storybook-story' } export function isReactComponentTarget( target: SceneTarget ): target is Extract { return target.kind === 'react-component' } export function isVueComponentTarget( target: SceneTarget ): target is Extract { return target.kind === 'vue-component' } export function isCustomRendererTarget( target: SceneTarget ): target is Extract { return target.kind === 'custom-renderer' } export function isFixtureTarget(target: SceneTarget): target is Extract { return target.kind === 'fixture' } export function isComponentTarget( target: SceneTarget ): target is | Extract | Extract | Extract { return target.kind === 'react-component' || target.kind === 'vue-component' || target.kind === 'custom-renderer' } export function isRendererTarget( target: SceneTarget ): target is | Extract | Extract | Extract | Extract { return ( target.kind === 'react-component' || target.kind === 'vue-component' || target.kind === 'storybook-story' || target.kind === 'custom-renderer' ) } // --------------------------------------------------------------------------- // Matchers // --------------------------------------------------------------------------- export function matchSceneTarget( target: SceneTarget, cases: { page: (url: string) => R 'playwright-page': (pageRef: string, url?: string) => R 'storybook-story': (storyId: string, storybookUrl: string) => R 'react-component': (rendererId: string, componentId: string) => R 'vue-component': (rendererId: string, componentId: string) => R 'custom-renderer': (rendererId: string, targetId: string) => R fixture: (fixtureId: string) => R } ): R { switch (target.kind) { case 'page': return cases.page(target.url) case 'playwright-page': return cases['playwright-page'](target.pageRef, target.url) case 'storybook-story': return cases['storybook-story'](target.storyId, target.storybookUrl) case 'react-component': return cases['react-component'](target.rendererId, target.componentId) case 'vue-component': return cases['vue-component'](target.rendererId, target.componentId) case 'custom-renderer': return cases['custom-renderer'](target.rendererId, target.targetId) case 'fixture': return cases.fixture(target.fixtureId) } } // --------------------------------------------------------------------------- // Factory Functions // --------------------------------------------------------------------------- export function pageTarget(url: string): SceneTarget { return { kind: 'page', url } } export function playwrightPageTarget(pageRef: string, url?: string): SceneTarget { return { kind: 'playwright-page', pageRef, url } } export function storybookStoryTarget(storyId: string, storybookUrl: string): SceneTarget { return { kind: 'storybook-story', storyId, storybookUrl } } export function reactComponentTarget(rendererId: string, componentId: string): SceneTarget { return { kind: 'react-component', rendererId, componentId } } export function vueComponentTarget(rendererId: string, componentId: string): SceneTarget { return { kind: 'vue-component', rendererId, componentId } } export function customRendererTarget(rendererId: string, targetId: string): SceneTarget { return { kind: 'custom-renderer', rendererId, targetId } } export function fixtureTarget(fixtureId: string): SceneTarget { return { kind: 'fixture', fixtureId } } // --------------------------------------------------------------------------- // RenderCase Factory // --------------------------------------------------------------------------- export function createRenderCase( caseId: string, input: unknown, env?: Partial, metadata?: Record ): RenderCase { return { caseId, input, env, metadata } }