@molecule/app-e2e
Core 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-e2eHow it works
@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
Reference
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-playwrightlaunches 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-previewdrives 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.
Quick Start
// 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()
Type
core
Installation
npm install @molecule/app-e2e @molecule/app-bond @playwright/test
API
Interfaces
E2EConnectOptions
Options 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
}
E2EConsolePayload
A console message forwarded from the page.
interface E2EConsolePayload {
type: string
text: string
url?: string
lineNumber?: number
columnNumber?: number
}
E2EErrorPayload
An uncaught error forwarded from the page.
interface E2EErrorPayload {
message: string
stack?: string
}
E2EPageOptions
Options for createEvaluatePage.
interface E2EPageOptions extends E2EConnectOptions {
/** Bond name used in error messages (`… is not supported by the preview bond`). */
bondName?: string
}
E2EProvider
A 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>
}
E2ETransport
The minimal channel a bond supplies to 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>
}
E2EViewport
A viewport size in CSS pixels.
interface E2EViewport {
width: number
height: number
}
Types
E2EProviderName
Which bond the runner should use: the live preview (a molecule sandbox) or real browsers.
type E2EProviderName = 'preview' | 'playwright' | (string & {})
E2ERunnerDefaults
The runner fields e2eRunnerDefaults decides.
type E2ERunnerDefaults = Required<
Pick<PlaywrightTestConfig, 'workers' | 'fullyParallel' | 'retries' | 'maxFailures' | 'timeout'>
>
Classes
E2EStrictModeError
A locator matched several elements where an action needs exactly one (Playwright's strict mode).
E2ETimeoutError
An action, wait or navigation ran out of time. Named like Playwright's for catch parity.
E2EUnsupportedError
A Playwright method this bond does not implement. The message names what to use instead.
Functions
e2eRunnerDefaults()
The runner settings for THIS environment — spread them FIRST into
defineConfig({ ...e2eRunnerDefaults(), … }), so any field the app sets
after the spread wins.
- Over the preview provider (
resolveE2EProviderName() === 'preview'): one worker, not fully parallel — there is one page to drive. - On a real browser:
'50%'workers, fully parallel. - Inside a molecule sandbox (
isMoleculeSandbox()): 0 retries, stop afterSANDBOX_MAX_FAILURESfailures,SANDBOX_TEST_TIMEOUT_MSper test. - Elsewhere: 2 retries under CI (
process.env.CI), 1 otherwise; no failure cap;DEFAULT_TEST_TIMEOUT_MSper 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
Constants
DEFAULT_TEST_TIMEOUT_MS
Per-test timeout everywhere else (your machine, CI), in ms.
const DEFAULT_TEST_TIMEOUT_MS: 60000
SANDBOX_MARKER_PATH
The 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_FAILURES
Inside a sandbox, the run stops after this many failed tests.
const SANDBOX_MAX_FAILURES: 8
SANDBOX_TEST_TIMEOUT_MS
Per-test timeout inside a molecule sandbox, in ms.
const SANDBOX_TEST_TIMEOUT_MS: 30000
Available Providers
| Provider | Package |
|---|---|
| E2E via Playwright | @molecule/app-e2e-playwright |
| E2E via Live Preview | @molecule/app-e2e-preview |
Injection Notes
Requirements
Peer dependencies:
@molecule/app-bond^1.0.1@playwright/test^1.40.0
Runtime Dependencies
-
@molecule/app-bond -
@playwright/test -
connect()returns Playwright'sPageTYPE 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()andhasInstalledBrowser()are the two facts the rule is made of; aplaywright.config.tsreads 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
E2ETransport(evaluate,navigate,viewport, events) and gets the whole Playwright-shaped page fromcreateEvaluatePage()in@molecule/app-e2e-fixtures-default. -
e2eRunnerDefaults()decides five runner fields; spread it FIRST indefineConfig({ ...e2eRunnerDefaults(), … })so an app's own value wins. Do not re-addworkers/fullyParallel/retries/maxFailuresper app — that is how 150 template configs stopped receiving the shared settings.workers: '50%',fullyParallel: trueon 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: 0in 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: 8in 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_000per test in a sandbox (video and traces are off there, so a healthy test is well under it),60_000elsewhere. A test that genuinely runs long says so itself (test.slow()/test.setTimeout()); an app-widetimeoutafter the spread also overrides the sandbox value.
-
@playwright/testis a peer dependency for its types only; it never downloads browsers on install.