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
@@ -0,0 +1,118 @@
/**
* 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)
},
}
}