← All @molecule/* packages · App templates
@molecule/app-e2e-previewProvider bond · e2e · App (browser) · v1.0.8 · Apache-2.0
Drives the live IDE preview page as a Playwright-shaped page over a same-origin WebSocket through the dev server — no browser binary in the sandbox
npm install @molecule/app-e2e-previewnpm · Source on GitHub · Implements @molecule/app-e2e
@molecule/app-e2e-preview is a provider bond on the app (browser) side: it implements the e2e core interface (@molecule/app-e2e) with a concrete vendor or library behind it.
Your code calls the core; you wire this provider once at startup. Swapping vendors later is one line in that wiring, not a rewrite.
// e2e/bonds.ts (scaffolded)
import { resolveE2EProviderName, setProvider } from '@molecule/app-e2e'
import { provider as playwright } from '@molecule/app-e2e-playwright'
import { provider as preview } from '@molecule/app-e2e-preview'
setProvider(resolveE2EProviderName() === 'preview' ? preview : playwright)
// vite-preview-bridge-plugin.ts (scaffolded) — the plugin rides the existing molecule plugin
import { molE2EPreviewPlugin } from '@molecule/app-e2e-preview/vite'
// any script — measure the live page without a test runner
import { connectPreview } from '@molecule/app-e2e-preview'
const page = await connectPreview()
await page.setViewportSize({ width: 390, height: 844 })
console.log(
await page
.locator('article p')
.first()
.evaluate((el) => getComputedStyle(el).fontSize),
)
await page.close()Works with: @molecule/app-bond, @molecule/app-e2e, @molecule/app-e2e-fixtures-default
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
E2E bond that drives the LIVE PREVIEW — the page the molecule.dev IDE is
already showing, in the person's own browser — as a Playwright-shaped page.
The browser that renders the preview is the browser that runs the test, so
the spec exercises exactly the tab the person is watching. (A sandbox also
ships its own Chromium; @molecule/app-e2e-playwright is the bond that runs
the same specs there with no tab involved, and the one a sandbox picks by
default — choose this bond with MOL_E2E_PROVIDER=preview.)
Three small parts:
vite dev and vite preview it serves a tiny page client, injects it
into each document, and attaches a WebSocket hub to the dev server.
vite build output is untouched.evaluate requests,
navigates on request, asks the framing IDE to resize on
setViewportSize, and forwards console output, errors and dialogs.role=driver
(loopback only, with the hub's token) and turns one connected page into
@molecule/app-e2e's Playwright-shaped Page.// e2e/bonds.ts (scaffolded)
import { resolveE2EProviderName, setProvider } from '@molecule/app-e2e'
import { provider as playwright } from '@molecule/app-e2e-playwright'
import { provider as preview } from '@molecule/app-e2e-preview'
setProvider(resolveE2EProviderName() === 'preview' ? preview : playwright)
// vite-preview-bridge-plugin.ts (scaffolded) — the plugin rides the existing molecule plugin
import { molE2EPreviewPlugin } from '@molecule/app-e2e-preview/vite'
// any script — measure the live page without a test runner
import { connectPreview } from '@molecule/app-e2e-preview'
const page = await connectPreview()
await page.setViewportSize({ width: 390, height: 844 })
console.log(
await page
.locator('article p')
.first()
.evaluate((el) => getComputedStyle(el).fontSize),
)
await page.close()
provider
npm install @molecule/app-e2e-preview @molecule/app-bond @molecule/app-e2e @molecule/app-e2e-fixtures-default @playwright/test vite ws
npm install -D @types/ws
E2EHubA running hub.
interface E2EHub {
/** The driver token (also written to the token file once the server listens). */
readonly token: string
/** Pages currently connected. */
pages(): PagePeer[]
/** Detach from the server and close every socket. */
close(): void
}
HubEventHub → driver event envelope.
interface HubEvent {
event: string
pageId?: string
[key: string]: unknown
}
MolE2EPreviewPluginOptionsOptions for {@link molE2EPreviewPlugin}.
interface MolE2EPreviewPluginOptions {
/** Turn the plugin off (also `MOL_E2E_PREVIEW_PLUGIN=0`). Default on. */
enabled?: boolean
}
PagePeerA page currently connected to the hub.
interface PagePeer {
id: string
href: string
title: string
hidden: boolean
framed: boolean
/** The app's base path as the page reports it ('/blog/'; '/' when none). */
base: string
connectedAt: number
lastSeen: number
}
PreviewConnectOptionsOptions for provider.connect() beyond the core's.
interface PreviewConnectOptions extends E2EConnectOptions {
/** Full hub URL (`ws://127.0.0.1:5173/__mol/e2e`); overrides port discovery. */
url?: string
/** Dev-server port to try first (the sandbox serves the preview on 5173). */
port?: number
/** Driver token; defaults to `MOL_E2E_TOKEN` or the hub's token file. */
token?: string
/** Drive one specific page (an id from `listPages()`); defaults to the most recently seen visible page. */
pageId?: string
/** How long to wait for a preview page to be connected before failing, ms. */
connectTimeout?: number
}
UpgradeCapableServerThe slice of http.Server / http2.Http2SecureServer the hub uses — Vite
hands over either, and both emit upgrade with the same arguments.
interface UpgradeCapableServer {
on(event: 'upgrade', listener: UpgradeListener): unknown
off(event: 'upgrade', listener: UpgradeListener): unknown
once(event: 'listening' | 'close', listener: () => void): unknown
address(): AddressInfo | string | null
readonly listening: boolean
}
attachE2EHub(httpServer, options?)Attach the hub to an HTTP server (Vite's server.httpServer). Safe next to
Vite's own HMR upgrade listener: each ignores the other's path.
function attachE2EHub(
httpServer: UpgradeCapableServer,
options?: { token?: string; path?: string },
): E2EHub
connectPreview(options?)Open the live preview as a Playwright-shaped page from any script (node scripts/check.mjs).
function connectPreview(options?: PreviewConnectOptions): Promise<Page>
injectE2EClientTag(html, base?)Put the client and runtime tags at the top of <head> unless they are already there.
function injectE2EClientTag(html: string, base?: string): string
listPreviewPages(options?)The preview pages currently connected to the hub.
function listPreviewPages(options?: PreviewConnectOptions): Promise<PagePeer[]>
molE2EPreviewPlugin(options?)The Vite plugin every scaffolded app carries (through its scaffold-owned preview plugin file). Dev and preview servers get the hub and the client; builds are untouched.
function molE2EPreviewPlugin(options?: MolE2EPreviewPluginOptions): Plugin<any>
tokenFilePath(port)The hub writes its driver token here so a runner on the same machine can find it.
function tokenFilePath(port: number): string
E2E_CLIENT_PATHPath the page client script is served from (dev and vite preview).
const E2E_CLIENT_PATH: '/__mol/e2e-client.js'
E2E_PREVIEW_CLIENT_SCRIPTThe client script's source, ready to serve as text/javascript.
const E2E_PREVIEW_CLIENT_SCRIPT: string
E2E_RUNTIME_PATHPath the in-page runtime (@molecule/app-e2e-fixtures-default's locator
engine) is served from, so it is installed with the document instead of
being sent through the hub on the first call after every navigation.
const E2E_RUNTIME_PATH: '/__mol/e2e-runtime.js'
E2E_WS_PATHWebSocket path the hub listens on (same origin as the previewed page).
const E2E_WS_PATH: '/__mol/e2e'
providerThe bond: setProvider(provider) in your e2e/bonds.ts.
const provider: E2EProvider
Implements @molecule/app-e2e interface.
Setup function to register this provider with the core interface:
import { setProvider } from '@molecule/app-e2e'
import { provider } from '@molecule/app-e2e-preview'
export function setupE2ePreview(): void {
setProvider(provider)
}
Peer dependencies:
@molecule/app-bond ^1.0.1@molecule/app-e2e ^1.0.0@molecule/app-e2e-fixtures-default ^1.0.3@playwright/test ^1.40.0vite ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0@molecule/app-bond
@molecule/app-e2e
@molecule/app-e2e-fixtures-default
@playwright/test
vite
ws
Every page the dev server sends is a page the driver can see. The plugin puts its client into every HTML response — Vite's own index and the HTML an app renders from its own dev middleware alike (a static-site generator's post routes, an SSR handler). A page the preview shows is a page the hub lists; if it is not, the app is bypassing the dev server.
Someone must be looking, and the bond fails fast when nobody is. The
renderer is a browser tab showing the preview. connect() waits 5 s
(connectTimeout) for a page to be attached and then fails naming the
cause and the alternative: open the preview in the IDE or in any tab, or
run with MOL_E2E_PROVIDER=playwright, which needs no tab. The runner
opens a connection per test (in a fresh worker process after each
failure), so after one full wait finds no page the driver leaves a marker
beside the hub's token file and the next connects fail at once (for a
minute, or until the hub lists a page) — a spec file with no tab reports
it once, in seconds, instead of hanging once per test. Any connected viewer will do — a desktop
tab keeps working while a phone sleeps. The IDE holds a screen wake lock
during builds and re-delivers commands when a hidden tab wakes.
A background tab is as fast as a foreground one. Browsers throttle a hidden page's timers to once a second (once a minute after a while), so nothing in the page waits on a timer: every call is answered at once and the driver, on its own clock, re-asks while an element is still on its way. The plugin also serves the locator runtime as a script with the document, so the first call after a navigation costs one round trip, not a 60 KB install. Budget one to two seconds per navigation through the sandbox proxy and about a fifth of a second per action; a whole spec file normally finishes in well under a minute.
Discovery. The driver tries MOL_E2E_PREVIEW_URL, then ports
MOL_E2E_PREVIEW_PORT, 5173 (the sandbox preview port), VITE_PORT,
3000, reading each hub's token from MOL_E2E_TOKEN or the file the hub
writes in the OS temp dir. Only loopback drivers with the token are
accepted, so a preview URL never becomes a way to run code in someone
else's tab.
URLs. page.goto('/path') navigates the previewed page relative to
ITS origin; an absolute http://localhost:<port>/path is rewritten to the
same path there, so specs written for a local base URL run unchanged.
Base path. An app served under a base (Vite base: '/blog/') has
every URL under it; the page knows its base (the plugin tags the client
with it) and a goto outside it fails at once naming the base, instead
of landing on the dev server's "did you mean /blog/…" page — which has
no app and no client, so nothing there can be driven. The page also
announces the base to the IDE that frames it (molecule:base), so the
IDE's own navigation stays inside the app.
Viewport. page.setViewportSize posts molecule:viewport to the
framing IDE, which resizes the frame; in a plain tab the size cannot
change and the call throws (the configured project viewport is applied
with a warning instead of a failure).
What is not here is listed in @molecule/app-e2e's docs — screenshots,
network interception, element handles, iframes — each method throws with
the alternative; @molecule/app-e2e-playwright runs the same spec with
real browsers when you need them.
Imported (non-Vite) apps get the hub only if their dev server can host it; today the plugin covers every Vite-served app, which is every molecule scaffold.