import type { BrowserCommands, BrowserPage, CDPSession } from 'vitest/browser' import type { SnapshotTestMeta } from 'vitest-plugin-vis/client-api' import { isVitestBrowser } from './is_vitest_browser.ts' import { toMatchImageSnapshot } from './page/to_match_image_snapshot.ts' type BrowserModule = Awaited type VitestModule = Awaited /** * How the proxy gets hold of the vitest modules. * * They are loaded dynamically because this module is also reached from a plain * Storybook preview, where `vitest` and `vitest/browser` are not available. * Passing `undefined` stands for "not a vitest browser run": nothing is loaded * and the proxies stay empty. */ export type VitestProxyLoaders = { loadBrowser: () => Promise loadVitest: () => Promise } /** * Build the `page` / `commands` / `cdp` / `getCurrentTest` proxies over `loaders`. * * Exported for tests: it is the only way to observe the window between module * load and the dynamic imports settling. */ export function createVitestProxy(loaders: VitestProxyLoaders | undefined) { let browserContext: BrowserModule | undefined let vitest: VitestModule | undefined const browserReady = loaders ? loaders.loadBrowser().then((m) => { m.page.extend({ toMatchImageSnapshot }) browserContext = m }) : Promise.resolve() const vitestReady = loaders ? loaders.loadVitest().then((m) => { vitest = m }) : Promise.resolve() const ready = Promise.all([browserReady, vitestReady]).then(() => undefined) const page = new Proxy({} as any, { get(_target, prop) { const r = (browserContext?.page as any)?.[prop] if (prop === 'toMatchImageSnapshot' && r === undefined) { return () => {} } return r }, }) const commands = new Proxy({} as any, { get(_target, prop) { // Outside a vitest browser run there is nothing to wait for: keep // yielding `undefined` so the addon guards stay in charge (#829). if (!loaders) return undefined if (browserContext) return (browserContext.commands as any)[prop] // The import has not settled yet (#835). Commands are async RPC calls, // so hand back a function that waits for the module instead of // `undefined`, which fails as ` is not a function`. return (...args: unknown[]) => browserReady.then(() => (browserContext!.commands as any)[prop](...args)) }, }) /** * The Chrome DevTools Protocol session of the current test, like `cdp()` from * `vitest/browser`, but safe to import from code a plain Storybook preview * also loads. * * Session calls made before the import settles wait for it (#835). CDP is * available only with the Playwright provider on Chromium. */ const cdp = (): CDPSession => { if (!loaders) throw new Error('cdp() is only available in a Vitest browser run.') return new Proxy({} as any, { get(_target, prop) { return (...args: unknown[]) => browserReady.then(() => (browserContext!.cdp() as any)[prop](...args)) }, }) } const getCurrentTest = () => vitest?.TestRunner.getCurrentTest() as | (ReturnType & SnapshotTestMeta) | undefined /** * Resolves once the vitest modules behind `page`, `commands`, and * `getCurrentTest` are loaded. * * `page` and `getCurrentTest` are read synchronously, so hooks that depend on * them must await this first (#835). */ const whenReady = () => ready return { page, commands, cdp, getCurrentTest, whenReady } } const proxy = createVitestProxy( isVitestBrowser() ? { loadBrowser: () => import('vitest/browser'), loadVitest: () => import('vitest') } : undefined, ) export const page = proxy.page export const commands = proxy.commands export const cdp = proxy.cdp export const getCurrentTest = proxy.getCurrentTest export const whenVitestProxyReady = proxy.whenReady