← All @molecule/* packages · App templates
@molecule/app-e2eCore interface · e2e · App (browser) · v1.2.0 · Apache-2.0
The e2e bond contract: which browser runs an app's end-to-end specs — the live IDE preview inside a molecule sandbox, or real Playwright browsers — plus the rule that picks one per environment.
npm install @molecule/app-e2e@molecule/app-e2e is the e2e core interface on the app (browser) side: the API your app calls, with no vendor inside.
Choose the implementation by bonding one of its 2 providers: @molecule/app-e2e-playwright, @molecule/app-e2e-preview.
// e2e/bonds.ts — scaffolded into every app; wires the provider for THIS environment
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)
// playwright.config.ts — the shared runner settings first, so the app can override any of them
import { defineConfig } from '@playwright/test'
import { e2eRunnerDefaults } from '@molecule/app-e2e'
export default defineConfig({ ...e2eRunnerDefaults(), testDir: './e2e' })
// a script that measures the live page without a test runner
import { requireProvider } from '@molecule/app-e2e'
const page = await requireProvider().connect({ viewport: { width: 390, height: 844 } })
await page.goto('/blog/hello/')
console.log(
await page
.locator('article p')
.first()
.evaluate((el) => getComputedStyle(el).fontSize),
)
await page.close()Providers (2): @molecule/app-e2e-playwright, @molecule/app-e2e-preview
Works with: @molecule/app-bond
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.
The e2e bond contract: which browser runs an app's end-to-end specs.
A provider opens a Playwright-shaped Page. Two bonds exist:
@molecule/app-e2e-playwright launches a real Playwright browser.
Every molecule sandbox image bakes Chromium (headless shell + its system
libraries and fonts), so this is what a sandbox uses — the specs run there
exactly as they do on your own machine and in CI, with no IDE tab involved.@molecule/app-e2e-preview drives the LIVE PREVIEW the molecule.dev
IDE is showing — the page the person is looking at, in their own browser —
over a WebSocket through the dev server. Pick it (MOL_E2E_PROVIDER=preview)
when the point is to drive the tab the person is watching; it needs that
tab to be open and fails fast when it is not.Specs do not import this package directly: they import test and expect
from @molecule/app-e2e-fixtures-default (a drop-in for @playwright/test
whose page fixture comes from the bonded provider) and stay identical in
both places. This core holds only the contract and the accessor, plus
resolveE2EProviderName(), the one rule that decides which bond an
environment gets: MOL_E2E_PROVIDER when set; otherwise, inside a molecule
sandbox (the /etc/mol/app-root marker exists), playwright when a
Playwright browser is installed (hasInstalledBrowser() — it looks under
PLAYWRIGHT_BROWSERS_PATH) and preview when none is; and playwright
everywhere else.
It also holds e2eRunnerDefaults(), the runner settings (workers,
parallelism, retries, failure cap, per-test timeout) every scaffolded
playwright.config.ts spreads first, so one change here reaches every app.
// e2e/bonds.ts — scaffolded into every app; wires the provider for THIS environment
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)
// playwright.config.ts — the shared runner settings first, so the app can override any of them
import { defineConfig } from '@playwright/test'
import { e2eRunnerDefaults } from '@molecule/app-e2e'
export default defineConfig({ ...e2eRunnerDefaults(), testDir: './e2e' })
// a script that measures the live page without a test runner
import { requireProvider } from '@molecule/app-e2e'
const page = await requireProvider().connect({ viewport: { width: 390, height: 844 } })
await page.goto('/blog/hello/')
console.log(
await page
.locator('article p')
.first()
.evaluate((el) => getComputedStyle(el).fontSize),
)
await page.close()
core
npm install @molecule/app-e2e @molecule/app-bond @playwright/test
E2EConnectOptionsOptions a test runner or script passes when opening a page.
interface E2EConnectOptions {
/**
* Base URL for relative `page.goto()` paths (Playwright's `use.baseURL`).
* The preview bond resolves paths against the previewed page's own origin
* instead, so a `http://localhost:<port>` base is simply ignored there.
*/
baseURL?: string
/** Initial viewport (Playwright's `use.viewport`); `null` = leave as is. */
viewport?: E2EViewport | null
/** Default timeout for actions, waits and expect polling, in ms. */
timeout?: number
/** Timeout for navigations, in ms. */
navigationTimeout?: number
}
E2EConsolePayloadA console message forwarded from the page.
interface E2EConsolePayload {
type: string
text: string
url?: string
lineNumber?: number
columnNumber?: number
}
E2EErrorPayloadAn uncaught error forwarded from the page.
interface E2EErrorPayload {
message: string
stack?: string
}
E2EPageOptionsOptions for {@link createEvaluatePage}.
interface E2EPageOptions extends E2EConnectOptions {
/** Bond name used in error messages (`… is not supported by the preview bond`). */
bondName?: string
}
E2EProviderA bond: opens pages.
interface E2EProvider {
/** Bond name, e.g. `preview` or `playwright`. */
readonly name: string
/**
* Open a page. Close it with `page.close()`; for a browser bond that also
* closes the context and browser it opened for this page.
*/
connect(options?: E2EConnectOptions): Promise<Page>
}
E2ETransportThe minimal channel a bond supplies to {@link createEvaluatePage}: run a
function inside the page, navigate, resize, and forward console/error
events. Everything Playwright-shaped is built on top of evaluate.
interface E2ETransport {
/**
* Run `(source)(arg)` inside the page — `source` is a function expression's
* text (sync or async) — and return its JSON-serialisable result.
*/
evaluate(source: string, arg?: unknown, options?: { timeout?: number }): Promise<unknown>
/** Navigate; resolves once the NEW document is connected and ready. */
navigate(
kind: 'goto' | 'reload' | 'back' | 'forward',
url?: string,
options?: { timeout?: number },
): Promise<void>
/** Resize the page; resolves with the size the page actually got. */
viewport(width: number, height: number): Promise<E2EViewport>
/** The page's last known URL. */
url(): string
/** Subscribe to forwarded events; returns an unsubscribe function. */
on(
event: 'console' | 'pageerror' | 'close',
listener: (payload: E2EConsolePayload | E2EErrorPayload | undefined) => void,
): () => void
/** Release the page (and the transport's connection). */
close(): Promise<void>
}
E2EViewportA viewport size in CSS pixels.
interface E2EViewport {
width: number
height: number
}
E2EProviderNameWhich bond the runner should use: the live preview (a molecule sandbox) or real browsers.
type E2EProviderName = 'preview' | 'playwright' | (string & {})
E2ERunnerDefaultsThe runner fields {@link e2eRunnerDefaults} decides.
type E2ERunnerDefaults = Required<
Pick<PlaywrightTestConfig, 'workers' | 'fullyParallel' | 'retries' | 'maxFailures' | 'timeout'>
>
E2EStrictModeErrorA locator matched several elements where an action needs exactly one (Playwright's strict mode).
E2ETimeoutErrorAn action, wait or navigation ran out of time. Named like Playwright's for catch parity.
E2EUnsupportedErrorA Playwright method this bond does not implement. The message names what to use instead.
e2eRunnerDefaults()The runner settings for THIS environment — spread them FIRST into
defineConfig({ ...e2eRunnerDefaults(), … }), so any field the app sets
after the spread wins.
resolveE2EProviderName() === 'preview'):
one worker, not fully parallel — there is one page to drive.'50%' workers, fully parallel.isMoleculeSandbox()): 0 retries, stop after
{@link SANDBOX_MAX_FAILURES} failures, {@link SANDBOX_TEST_TIMEOUT_MS}
per test.process.env.CI), 1 otherwise; no
failure cap; {@link DEFAULT_TEST_TIMEOUT_MS} per test.function e2eRunnerDefaults(): Required<
Pick<PlaywrightTestConfig, 'workers' | 'fullyParallel' | 'retries' | 'maxFailures' | 'timeout'>
>
getProvider()The bonded provider, or null.
function getProvider(): E2EProvider | null
hasInstalledBrowser()Whether a Playwright Chromium (the full browser or the headless shell) is
installed where Playwright will look for it — that is, whether the
playwright bond can launch here. The PLAYWRIGHT_BROWSERS_PATH=0 layout
(browsers inside node_modules) is not probed and reads as "not installed".
function hasInstalledBrowser(): boolean
hasProvider()Whether a provider is bonded.
function hasProvider(): boolean
isMoleculeSandbox()Whether this process runs inside a molecule sandbox (the marker file exists).
function isMoleculeSandbox(): boolean
playwrightBrowsersPath()Where Playwright keeps its browsers: PLAYWRIGHT_BROWSERS_PATH when set
(the molecule sandbox images bake Chromium under it), otherwise Playwright's
own default, ~/.cache/ms-playwright.
function playwrightBrowsersPath(): string
requireProvider()The bonded provider; throws with the fix when none is.
function requireProvider(): E2EProvider
resolveE2EProviderName()Which provider this environment should use: MOL_E2E_PROVIDER when set;
otherwise, inside a molecule sandbox, playwright when a Playwright browser
is installed there (every current sandbox image bakes one) and preview when
none is (an older image — the live IDE preview is then the only renderer);
and playwright everywhere else. Both test and the scaffolded
e2e/bonds.ts read this, so the runner's shape and the bonded provider
always agree.
function resolveE2EProviderName(): E2EProviderName
setProvider(provider)Bond the provider that opens pages (call once, from the project's e2e/bonds.ts).
function setProvider(provider: E2EProvider): void
DEFAULT_TEST_TIMEOUT_MSPer-test timeout everywhere else (your machine, CI), in ms.
const DEFAULT_TEST_TIMEOUT_MS: 60000
SANDBOX_MARKER_PATHThe molecule sandbox writes this marker at boot; its presence means "this is a molecule sandbox".
const SANDBOX_MARKER_PATH: '/etc/mol/app-root'
SANDBOX_MAX_FAILURESInside a sandbox, the run stops after this many failed tests.
const SANDBOX_MAX_FAILURES: 8
SANDBOX_TEST_TIMEOUT_MSPer-test timeout inside a molecule sandbox, in ms.
const SANDBOX_TEST_TIMEOUT_MS: 30000
| Provider | Package |
|---|---|
| E2E via Playwright | @molecule/app-e2e-playwright |
| E2E via Live Preview | @molecule/app-e2e-preview |
Peer dependencies:
@molecule/app-bond ^1.0.1@playwright/test ^1.40.0@molecule/app-bond
@playwright/test
connect() returns Playwright's Page TYPE in every bond, so specs,
helpers and editor completions are the same everywhere. What a bond can
actually do is documented on the bond; the preview bond throws a one-line
error naming the alternative for the few methods that need a real browser
(screenshots, network interception, element handles, iframes).
isMoleculeSandbox() and hasInstalledBrowser() are the two facts the
rule is made of; a playwright.config.ts reads them to turn video, traces
and retries off inside a sandbox, where the run is a feedback loop and
nothing reads the artifacts.
A bond that can only run code INSIDE a page implements {@link E2ETransport}
(evaluate, navigate, viewport, events) and gets the whole
Playwright-shaped page from createEvaluatePage() in
@molecule/app-e2e-fixtures-default.
e2eRunnerDefaults() decides five runner fields; spread it FIRST in
defineConfig({ ...e2eRunnerDefaults(), … }) so an app's own value wins.
Do not re-add workers/fullyParallel/retries/maxFailures per app —
that is how 150 template configs stopped receiving the shared settings.
workers: '50%', fullyParallel: true on a real browser (each worker
has its own browser against the same dev server; on an 8-vCPU sandbox 12
passing tests went 7.6 s on 1 worker → 1.6 s on 4). Over the preview there
is one page, so 1 worker, not fully parallel.retries: 0 in a sandbox — the run is the executor's feedback loop and a
retry only replays the same failure and multiplies the run time. 2 under
CI, 1 on your machine.maxFailures: 8 in a sandbox — a run failing everywhere (usually specs
aimed at the wrong server or base path) stops early and still reports the
failures it hit: 24 failing tests took 73.8 s uncapped → 23.7 s capped, and
one uncapped run with 21 failures took 534 s. No cap elsewhere.timeout: 30_000 per test in a sandbox (video and traces are off there,
so a healthy test is well under it), 60_000 elsewhere. A test that
genuinely runs long says so itself (test.slow() / test.setTimeout());
an app-wide timeout after the spread also overrides the sandbox value.@playwright/test is a peer dependency for its types only; it never
downloads browsers on install.