← All @molecule/* packages · App templates

@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-e2e

npm · Source on GitHub

How 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

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.ts JSDoc, 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.

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 {@link 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 {@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>
}

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 {@link 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 after {@link SANDBOX_MAX_FAILURES} failures, {@link SANDBOX_TEST_TIMEOUT_MS} per test.
  • Elsewhere: 2 retries under CI (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

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

ProviderPackage
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'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.