Lightpanda is an open-source headless browser
written from scratch, without Chromium. It starts in milliseconds, holds a
fraction of Chrome’s memory, and speaks the Chrome DevTools Protocol.
lightpanda() from @e2e-dev/lightpanda is a
browser provider for it: each worker slot gets
its own lightpanda serve, started on a free port when the run starts and
stopped when it ends. Tests and CI stay the same; the config changes only
in the target’s browser.
Lightpanda has no layout engine. It builds the DOM, runs JavaScript, and exposes the accessibility tree the engine reads, which is what locators, assertions, and agent steps use. Anything that depends on where a node is painted behaves differently; see What does not work before moving a suite over.
Setup
Install the package:
And the Lightpanda nightly binary, which the provider starts:
The provider runs lightpanda from PATH, ~/.lightpanda/lightpanda, or
~/.local/bin/lightpanda, the places
agent-browser looks too, or
the binary LIGHTPANDA_PATH names in the environment e2e run starts in,
or binary in the options. None found fails the run before any test,
naming the install page.
Each worker gets its own server, so workers sets how many run at once,
and the run’s log names each one:
Attach to a running server
A Lightpanda already running, such as the Docker image in CI, is a
connect target rather than a provider: the
engine attaches to its CDP endpoint and leaves it running.
Every worker then shares that server. One lightpanda serve takes 16
connections by default; raise its --cdp-max-connections above workers
when you run more.
Options
The provider leases one server per worker slot for the run; it has no
scope: 'attempt', since Lightpanda does not expose the default context
identity a per-attempt lease reattaches by. A server that does not answer
on its port within ten seconds fails the lease, and so does a binary that
exits first, with its stderr in the message.
Fetch more of the page
lightpanda serve requests no sub-resources by default: no images, no
external stylesheets, no iframes. loadResources opts into each, as
--load-resources on the server:
Inline styles, <style> blocks, and the hidden attribute already decide
visibility; stylesheet makes an external file’s rules count too, so a
node a linked stylesheet hides is hidden to a query. image fetches image
headers so load and error reflect the real status.
What works
Against the e2e testbed’s local suite on a Lightpanda nightly from mid-October 2026, these pass unchanged:
app.open,app.back, andexpect(browser).toHaveURLandtoHaveTitle.- Every
screenquery (getByRole,getByLabel,getByText,getByTestId, filters,nth) and the actions on them:tap,fill,check,select,press, and keyboard input. - Polling assertions on text, value, count, and state.
browser.routemocks and request waits.browser.setCookiesfrom a test, after a page is open.app.screenshotand the screenshot of a failure. Lightpanda paints a plain render of the DOM, not the page as Chrome would show it.- Agent steps:
agent.act,agent.assert,agent.extract, andagent.waitForread the screen tree, which Lightpanda provides in full. The testbed’s agent suite passes its tree-driven flows (todos, wizard, sign-in, hover, context menu, long press, upload); what fails is drawn on a canvas, dragged, or below the fold. A judgment on pixels sees the plain render, so keep the agent on the tree.
A test that asserts on what the user sees, not where it is drawn, moves over as is.
What does not work
Each of these fails the test with the engine’s usual error, not a hang. The reasons are Lightpanda’s, and its Playwright compatibility issue tracks most of them.
A Lightpanda release that closes one of these makes the row above go away with no change on the e2e side; the engine sends the same CDP commands it sends Chrome.
Where it fits
Lightpanda suits a suite that is heavy on the agent’s reading of the screen
and light on geometry: smoke flows, forms, navigation, API-backed pages, and
agent.act steps whose checks are text and state. It runs that suite in a
fraction of Chrome’s time and memory, on a box with no Chrome installed.
Keep a Chromium target for the rest, and select by target when both are in one config: