119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
|
|
/**
|
||
|
|
* Vue renderer adapter for Imhotep Playwright.
|
||
|
|
*
|
||
|
|
* Mounts a Vue component into a container div on the page.
|
||
|
|
* Uses Vue's createApp API (Vue 3) or constructor (Vue 2) depending
|
||
|
|
* on what is available in the injected Vue runtime.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { Page } from 'playwright'
|
||
|
|
import type { SceneTarget } from 'imhotep-core/scene-target'
|
||
|
|
import { RendererAdapter } from './renderers.js'
|
||
|
|
|
||
|
|
export interface VueAdapterOptions {
|
||
|
|
/** Id for this renderer instance (defaults to 'vue'). */
|
||
|
|
id?: string
|
||
|
|
/**
|
||
|
|
* Vue createApp entry point; used for capability detection at adapter
|
||
|
|
* construction time (e.g. determining Vue 3 vs Vue 2). The actual runtime
|
||
|
|
* must be exposed on the page as `window.__imhotepVueCreateApp`.
|
||
|
|
*/
|
||
|
|
createApp: (component: unknown, props?: Record<string, unknown>) => {
|
||
|
|
mount(selector: string): void
|
||
|
|
unmount(): void
|
||
|
|
}
|
||
|
|
/** Container selector or id to mount into. */
|
||
|
|
containerSelector?: string
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createVueAdapter(options: VueAdapterOptions): RendererAdapter {
|
||
|
|
const { createApp, containerSelector = '#__imhotep-mount' } = options
|
||
|
|
|
||
|
|
return {
|
||
|
|
id: options.id ?? 'vue',
|
||
|
|
|
||
|
|
async mount(page: Page, target: SceneTarget, input: unknown): Promise<void> {
|
||
|
|
if (target.kind !== 'vue-component') {
|
||
|
|
throw new Error(`Vue adapter received non-Vue target: ${target.kind}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const component = (input as Record<string, unknown>)?.component ?? target.componentId
|
||
|
|
const props = (input as Record<string, unknown>)?.props ?? input
|
||
|
|
|
||
|
|
await page.evaluate(
|
||
|
|
(args: {
|
||
|
|
componentRef: string
|
||
|
|
propsJson: string
|
||
|
|
containerSelector: string
|
||
|
|
}) => {
|
||
|
|
const container = document.querySelector(args.containerSelector)
|
||
|
|
if (!container) {
|
||
|
|
throw new Error(`Mount container not found: ${args.containerSelector}`)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear previous content.
|
||
|
|
container.innerHTML = ''
|
||
|
|
|
||
|
|
// Resolve Vue createApp BEFORE looking for the component.
|
||
|
|
// This gives a clearer error when Vue is missing vs component missing.
|
||
|
|
const createApp = (window as unknown as Record<string, unknown>).__imhotepVueCreateApp as
|
||
|
|
| ((component: unknown, props?: Record<string, unknown>) => {
|
||
|
|
mount(selector: string): void
|
||
|
|
unmount(): void
|
||
|
|
})
|
||
|
|
| undefined
|
||
|
|
|
||
|
|
if (!createApp) {
|
||
|
|
throw new Error(
|
||
|
|
'Vue createApp not found on window.__imhotepVueCreateApp. ' +
|
||
|
|
'Expose Vue.createApp as window.__imhotepVueCreateApp before mounting. ' +
|
||
|
|
'Example: window.__imhotepVueCreateApp = Vue.createApp'
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Resolve the component from the global scope.
|
||
|
|
const componentMap = (window as unknown as Record<string, unknown>).__imhotepComponents as
|
||
|
|
| Record<string, unknown>
|
||
|
|
| undefined
|
||
|
|
const Component = componentMap?.[args.componentRef] ?? (window as unknown as Record<string, unknown>)[args.componentRef]
|
||
|
|
|
||
|
|
if (!Component) {
|
||
|
|
throw new Error(
|
||
|
|
`Component "${args.componentRef}" not found on window. ` +
|
||
|
|
`Expose it as window.__imhotepComponents = { "${args.componentRef}": MyComponent } ` +
|
||
|
|
`or window["${args.componentRef}"] = MyComponent`
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
const parsedProps = JSON.parse(args.propsJson)
|
||
|
|
|
||
|
|
const app = createApp(Component, parsedProps)
|
||
|
|
app.mount(args.containerSelector)
|
||
|
|
},
|
||
|
|
{
|
||
|
|
componentRef: component as string,
|
||
|
|
propsJson: JSON.stringify(props),
|
||
|
|
containerSelector,
|
||
|
|
}
|
||
|
|
)
|
||
|
|
},
|
||
|
|
|
||
|
|
async unmount(page: Page): Promise<void> {
|
||
|
|
await page.evaluate((selector: string) => {
|
||
|
|
// Attempt Vue 3 unmount if the element has a __vue_app__ property.
|
||
|
|
const container = document.querySelector(selector)
|
||
|
|
if (container) {
|
||
|
|
const app = (container as unknown as Record<string, unknown>).__vue_app__ as
|
||
|
|
| { unmount(): void }
|
||
|
|
| undefined
|
||
|
|
if (app?.unmount) {
|
||
|
|
app.unmount()
|
||
|
|
} else {
|
||
|
|
container.innerHTML = ''
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, containerSelector)
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|