Skip to content

Commit 7520437

Browse files
DavertMikclaude
andcommitted
feat(helpers): add Lightpanda helper, share server lifecycle in CDPBrowser
Lightpanda is a thin CDPBrowser subclass that self-launches `lightpanda serve` with telemetry disabled. The spawn/free-port/wait/kill lifecycle moves from Obscura into CDPBrowser so both helpers share it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ff47c22 commit 7520437

19 files changed

Lines changed: 3217 additions & 272 deletions

‎.github/workflows/lightpanda.yml‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Lightpanda Helper Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- 4.x
7+
pull_request:
8+
branches:
9+
- '**'
10+
11+
concurrency:
12+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
13+
cancel-in-progress: true
14+
15+
permissions:
16+
contents: read
17+
18+
env:
19+
CI: true
20+
FORCE_COLOR: 1
21+
LIGHTPANDA_VERSION: 1.0.0
22+
LIGHTPANDA_SHA256: aa5a4b8ed53d1e38b3c73f5b2647d0a84a82e6744557f45f9a9c85858aa031c3
23+
LIGHTPANDA_DISABLE_TELEMETRY: true
24+
25+
jobs:
26+
build:
27+
runs-on: ubuntu-22.04
28+
timeout-minutes: 15
29+
30+
strategy:
31+
matrix:
32+
node-version: [22.x]
33+
34+
steps:
35+
- uses: actions/checkout@v6
36+
with:
37+
persist-credentials: false
38+
- name: Use Node.js ${{ matrix.node-version }}
39+
uses: actions/setup-node@v6
40+
with:
41+
node-version: ${{ matrix.node-version }}
42+
- uses: shivammathur/setup-php@v2
43+
with:
44+
php-version: 8.0
45+
- name: npm install
46+
run: npm i --force
47+
- name: Cache Lightpanda binary
48+
uses: actions/cache@v4
49+
with:
50+
path: ~/lightpanda-bin/lightpanda
51+
key: lightpanda-${{ env.LIGHTPANDA_VERSION }}-x86_64-linux
52+
- name: Download Lightpanda
53+
run: |
54+
mkdir -p ~/lightpanda-bin
55+
if [ ! -f ~/lightpanda-bin/lightpanda ]; then
56+
curl -sfL --retry 3 -o ~/lightpanda-bin/lightpanda https://github.com/lightpanda-io/browser/releases/download/${LIGHTPANDA_VERSION}/lightpanda-x86_64-linux
57+
fi
58+
echo "${LIGHTPANDA_SHA256} $HOME/lightpanda-bin/lightpanda" | sha256sum -c -
59+
chmod +x ~/lightpanda-bin/lightpanda
60+
~/lightpanda-bin/lightpanda version
61+
- name: start test server
62+
run: |
63+
php -S 127.0.0.1:8000 -t test/data/app &
64+
sleep 1
65+
curl -sf http://127.0.0.1:8000/info > /dev/null
66+
- name: run lightpanda helper tests
67+
run: |
68+
export LIGHTPANDA_PATH="$HOME/lightpanda-bin/lightpanda"
69+
npm run test:unit:webbapi:lightpanda
70+
- name: run lightpanda acceptance tests
71+
run: |
72+
export LIGHTPANDA_PATH="$HOME/lightpanda-bin/lightpanda"
73+
./bin/codecept.js run -c test/acceptance/codecept.Lightpanda.js --grep @Lightpanda --debug

‎Bunoshfile.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ const inheritedHelperDocs = {
399399
exclude: [/Title/, /Popup/, /Cookie/, /Url/, /^press/, /^refreshPage/, /^resizeWindow/, /Script$/, /cursor/, /Css/, /Tab$/, /^wait/],
400400
},
401401
Obscura: { parent: 'CDPBrowser' },
402+
Lightpanda: { parent: 'CDPBrowser' },
402403
Kitesurf: { parent: 'CDPBrowser', excludeConfig: ['endpoint', 'headers'] },
403404
}
404405

@@ -425,11 +426,12 @@ async function docsHelperMarkdown(name, { parent, exclude = [], excludeConfig =
425426
const documentation = await import('documentation')
426427
const buildOptions = { shallow: true, sortOrder: ['alpha'] }
427428
const doc = await documentation.build([`docs/build/${name}.js`], buildOptions)
429+
doc.sort((a, b) => (b.kind === 'class') - (a.kind === 'class'))
428430
let members = doc[0].members.instance
429431

430432
if (parent) {
431433
const parentDoc = await documentation.build([`docs/build/${parent}.js`], buildOptions)
432-
for (const method of parentDoc[0].members.instance) {
434+
for (const method of parentDoc.find(c => c.kind === 'class').members.instance) {
433435
if (exclude.some(f => method.name.match(f))) continue
434436
if (members.some(m => m.name === method.name)) continue
435437
members.push(method)

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
| 🌐 Web | Puppeteer | [![Puppeteer Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml) |
1212
| 🌐 Web | WebDriver | [![WebDriver Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml) |
1313
| 🌐 Web | Obscura | [![Obscura Helper Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/obscura.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/obscura.yml) |
14+
| 🌐 Web | Lightpanda | [![Lightpanda Helper Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/lightpanda.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/lightpanda.yml) |
1415
| 📱 Mobile | Appium | [![Appium Tests - Android](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appium_Android.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/appium_Android.yml) |
1516

1617
# CodeceptJS [![Made in Ukraine](https://img.shields.io/badge/made_in-ukraine-ffd700.svg?labelColor=0057b7)](https://stand-with-ukraine.pp.ua)
@@ -44,6 +45,7 @@ CodeceptJS uses **Helper** modules to provide actions to `I` object. Currently,
4445
- [**Puppeteer**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Puppeteer.md) - uses Google Chrome's Puppeteer for fast headless testing.
4546
- [**WebDriver**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/WebDriver.md) - uses [webdriverio](http://webdriver.io/) to run tests via WebDriver or Devtools protocol.
4647
- [**Obscura**](https://codecept.io/helpers/Obscura) - drives the lightweight Obscura browser through Chrome DevTools Protocol. See [Alternative Browser Engines](https://codecept.io/alternative-browsers).
48+
- [**Lightpanda**](https://codecept.io/helpers/Lightpanda) - drives the Lightpanda headless browser through Chrome DevTools Protocol. See [Alternative Browser Engines](https://codecept.io/alternative-browsers).
4749
- [**Appium**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Appium.md) - for **mobile testing** with Appium
4850
- [**Detox**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Detox.md) - This is a wrapper on top of Detox library, aimed to unify testing experience for CodeceptJS framework. Detox provides a grey box testing for mobile applications, playing especially well for React Native apps.
4951

‎docs/alternative-browsers.md‎

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ title: Alternative Browser Engines
66
# Alternative Browser Engines
77

88
::: warning Experimental
9-
The `Obscura` and `Kitesurf` helpers are experimental in CodeceptJS 4.2. Pin browser versions in CI and retain Playwright or WebDriver coverage for compatibility-critical tests.
9+
The `Obscura`, `Lightpanda` and `Kitesurf` helpers are experimental in CodeceptJS 4.2. Pin browser versions in CI and retain Playwright or WebDriver coverage for compatibility-critical tests.
1010
:::
1111

1212
Playwright and Puppeteer drive full Chromium — the most accurate way to test what users see.
@@ -17,11 +17,15 @@ through dedicated helpers:
1717
V8 engine. From v0.2.0, the default release build also renders — real layout, computed styles,
1818
and screenshots — with `-no-render` builds still available for pure-speed, nothing-painted
1919
scraping mode. Release archives are available for Linux, macOS, and Windows.
20+
- **[Lightpanda](https://lightpanda.io)** — an open-source browser written from scratch in Zig for
21+
automation, with a real V8 engine and broad Web API coverage. It never paints: no screenshots,
22+
but it does compute enough style and layout for visibility checks. Binaries are available for
23+
Linux and macOS.
2024
- **[Kitesurf](https://blog.cloudflare.com/kitesurf/)** — Cloudflare's browser that runs in V8
2125
isolates on Cloudflare Workers, with a real layout and rendering pipeline. Cloud-only,
2226
free in beta, planned to be open-sourced.
2327

24-
Both speak Chrome DevTools Protocol. CodeceptJS drives them with raw CDP — one round-trip per
28+
All of them speak Chrome DevTools Protocol. CodeceptJS drives them with raw CDP — one round-trip per
2529
action, no stale element handles — which is why suites on these browsers run fast and never hang
2630
on navigation races.
2731

@@ -56,6 +60,10 @@ redirect happens, cookie is set — do not need a GPU raster pipeline. Obscura e
5660
app's real JavaScript in real V8; it only skips painting. For API-adjacent flows
5761
(login → dashboard data appears), that is exactly the right amount of browser.
5862

63+
Lightpanda takes the same trade further: it has no rendering pipeline at all, which is what makes
64+
it start in milliseconds and use a fraction of Chromium's memory. Pick it when the suite never
65+
needs a screenshot; pick Obscura when it does.
66+
5967
**Constrained environments.** ARM CI runners, thin containers, air-gapped machines:
6068
a static binary with no system dependencies goes where Chromium will not.
6169

@@ -64,7 +72,9 @@ as an Obscura capability rather than a browser-compatibility guarantee from Code
6472

6573
## When to stay with Playwright
6674

67-
- Anything visual: visual regression, PDF (neither helper exposes PDF output). Obscura's v0.2.0+
75+
- Anything visual: visual regression, PDF (none of these helpers exposes PDF output). Lightpanda
76+
has no screenshots at all, no scrolling, and only computes the CSS properties visibility checks
77+
need. Obscura's v0.2.0+
6878
rendering/CSS engine is new and independently implemented — expect edge cases and gaps versus a
6979
real browser, especially around inherited properties and less common computed-style values.
7080
- Visibility semantics on `-no-render` Obscura builds (and v0.1.x): every element reports as
@@ -88,6 +98,12 @@ CodeceptJS 4.2 is tested in CI with Obscura 0.2.2. Obscura 0.2.x is recommended;
8898
},
8999
}
90100

101+
helpers: {
102+
Lightpanda: {
103+
url: 'http://localhost:3000',
104+
},
105+
}
106+
91107
helpers: {
92108
Kitesurf: {
93109
url: 'https://staging.myapp.com',
@@ -131,18 +147,29 @@ process — there is nothing to start by hand in the common case:
131147
that "start Obscura by hand and just run the tests" keeps working without any config, while
132148
self-launch is still the default for everyone else.
133149

150+
### Lightpanda's connection modes
151+
152+
Lightpanda works the same way, with `lightpanda` in place of `obscura`: the binary is resolved
153+
from `binaryPath`, then `LIGHTPANDA_PATH`, then `lightpanda` on `PATH`, and `lightpanda serve` is
154+
started on a free port and stopped when the run ends. Setting `endpoint` attaches to an instance
155+
you manage yourself, for example the `lightpanda/browser` Docker image. CodeceptJS is tested in CI
156+
with Lightpanda 1.0.0; see [Installation](/installation#lightpanda-experimental).
157+
158+
The helper always starts Lightpanda with `LIGHTPANDA_DISABLE_TELEMETRY=true`. When you start the
159+
server yourself, set that variable yourself.
160+
134161
## Capability matrix
135162

136-
| | Playwright | Obscura | Kitesurf |
137-
|---|---|---|---|
138-
| Real JS execution (V8) | yes | yes | yes |
139-
| Layout / getBoundingClientRect | yes | yes (v0.2.0+ default builds); synthetic on `-no-render`/v0.1.x | yes |
140-
| Screenshots | yes | yes (v0.2.0+ default builds); no on `-no-render`/v0.1.x | yes |
141-
| Visibility assertions | yes | yes (v0.2.0+ default builds); no, DOM-presence only, on `-no-render`/v0.1.x | yes |
142-
| Screencast / video (`screencast` plugin) | yes — WebM via `page.screencast`, with caption burn-in | yes — APNG via CDP `Page.startScreencast`, assembled in-process (v0.2.0+ default builds; verified PNG frames on the live server); no caption burn-in | untested |
143-
| Startup model | local browser process | standalone local binary | remote cloud session |
144-
| Parallel scale | machine-bound | machine-bound (light) | near-unlimited (cloud) |
145-
| Where it runs | local/grid | local | Cloudflare only |
146-
| License / cost | open source | Apache-2.0 | proprietary, free beta |
147-
148-
See helper reference pages: [Obscura](/helpers/Obscura), [Kitesurf](/helpers/Kitesurf).
163+
| | Playwright | Obscura | Lightpanda | Kitesurf |
164+
|---|---|---|---|---|
165+
| Real JS execution (V8) | yes | yes | yes | yes |
166+
| Layout / getBoundingClientRect | yes | yes (v0.2.0+ default builds); synthetic on `-no-render`/v0.1.x | computed boxes, nothing painted | yes |
167+
| Screenshots | yes | yes (v0.2.0+ default builds); no on `-no-render`/v0.1.x | no | yes |
168+
| Visibility assertions | yes | yes (v0.2.0+ default builds); no, DOM-presence only, on `-no-render`/v0.1.x | yes | yes |
169+
| Screencast / video (`screencast` plugin) | yes — WebM via `page.screencast`, with caption burn-in | yes — APNG via CDP `Page.startScreencast`, assembled in-process (v0.2.0+ default builds; verified PNG frames on the live server); no caption burn-in | no | untested |
170+
| Startup model | local browser process | standalone local binary | standalone local binary | remote cloud session |
171+
| Parallel scale | machine-bound | machine-bound (light) | machine-bound (light) | near-unlimited (cloud) |
172+
| Where it runs | local/grid | local | local (Linux, macOS) | Cloudflare only |
173+
| License / cost | open source | Apache-2.0 | AGPL-3.0 | proprietary, free beta |
174+
175+
See helper reference pages: [Obscura](/helpers/Obscura), [Lightpanda](/helpers/Lightpanda), [Kitesurf](/helpers/Kitesurf).

‎docs/helpers/Kitesurf.md‎

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ sidebar: auto
55
title: Kitesurf
66
---
77

8+
89
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
910

1011
## Kitesurf
@@ -1742,7 +1743,9 @@ Returns **[Promise][4]<void>**&#x20;
17421743

17431744
### _connect
17441745

1745-
Resolves the CDP endpoint and opens the underlying `CDPConnection`, storing it on `this.cdp`.
1746+
In SELF-MANAGED mode, resolves and spawns the browser server (or courtesy-attaches to an
1747+
already-running one on :9222) exactly once via `_resolveSelfManaged`. Then resolves the CDP
1748+
endpoint and opens the underlying `CDPConnection`, storing it on `this.cdp`.
17461749

17471750
### _ensureClient
17481751

@@ -1790,6 +1793,14 @@ Evaluates a JavaScript expression in the page attached to the current session vi
17901793

17911794
Returns **[Promise][4]<any>** the evaluated value, or `undefined` if the expression has no result.
17921795

1796+
### _findFreePort
1797+
1798+
Picks a free TCP port on 127.0.0.1 by briefly listening on port 0 and reading back the
1799+
OS-assigned port. Used when `options.port` isn't explicitly set, so multiple `run-workers`
1800+
workers never collide on the same port.
1801+
1802+
Returns **[Promise][4]<[number][7]>** a free port.
1803+
17931804
### _grabCurrentPath
17941805

17951806
Resolves the current page URL to a `pathname`, ignoring the origin, query string, and hash.
@@ -1824,6 +1835,26 @@ retrying once, mirroring the `'__NO_CLIENT__'` handling right next to it.
18241835

18251836
* `needsXPath` **[boolean][11]**
18261837

1838+
### _manageServer
1839+
1840+
Opts a subclass into managing its own browser server process. Called from a subclass
1841+
constructor with the raw user config and a server descriptor:
1842+
1843+
* `name` — binary name looked up on `PATH`, also used in messages
1844+
* `envVar` — environment variable that may point at the binary
1845+
* `args(port)` — arguments the binary is spawned with to serve CDP on `port`
1846+
* `env` — extra environment variables for the spawned process
1847+
* `releases` — URL shown when no binary can be resolved
1848+
1849+
An explicit `endpoint` in the user config always wins: the helper stays in ATTACH mode and
1850+
never spawns or kills anything. Otherwise the helper becomes SELF-MANAGED and the default
1851+
`endpoint` is cleared until `_resolveSelfManaged` sets the real one.
1852+
1853+
#### Parameters
1854+
1855+
* `config` **[object][5]** the config object the helper was constructed with.
1856+
* `server` **[object][5]** the server descriptor described above.
1857+
18271858
### _needsVisibleTextFallback
18281859

18291860
Determines whether `see`/`dontSee`/`waitForText` should read whole-page text through the
@@ -1931,6 +1962,25 @@ The `xpath`/`innerText` probes determine *whether* their respective fallback is
19311962
do not install anything — injection stays deferred to `_ensureClient`'s reactive install and
19321963
`_textSource`'s own read, matching `amOnPage` no longer eagerly installing the client.
19331964

1965+
### _probeUp
1966+
1967+
Probes a `/json/version`-style URL with a short timeout, used for the courtesy-attach check.
1968+
1969+
#### Parameters
1970+
1971+
* `url` **[string][3]**&#x20;
1972+
1973+
Returns **[Promise][4]<[boolean][11]>** true if the URL answered.
1974+
1975+
### _resolveBinary
1976+
1977+
Resolves the binary to spawn, in priority order: `options.binaryPath`, then the server
1978+
descriptor's environment variable, then its name on `PATH`. The `PATH` lookup walks the
1979+
directories itself instead of shelling out to `which`, which does not exist on Windows: on
1980+
Windows every `PATHEXT` suffix is tried, so a `.exe` on `PATH` is found too.
1981+
1982+
Returns **([string][3] | null)** an absolute or relative path to the binary, or null if none resolved.
1983+
19341984
### _resolveEndpoint
19351985

19361986
Acquires a Kitesurf browser session from the Cloudflare Browser Run API and resolves it to
@@ -1939,6 +1989,18 @@ which resolves a fixed local endpoint instead of provisioning a cloud session pe
19391989

19401990
Returns **[Promise][4]<[string][3]>** a `wss://` debugger URL ready to be passed to `CDPConnection`.
19411991

1992+
### _resolveSelfManaged
1993+
1994+
Resolves how to reach a SELF-MANAGED browser, trying, in order: spawn a binary
1995+
(`binaryPath` config, then the descriptor's `envVar`, then its `name` on `PATH`) on
1996+
`options.port` or a free port, courtesy-attach to `http://127.0.0.1:9222` if something already
1997+
answers there (never killed by this helper), or throw a loud, actionable error. Sets
1998+
`this.options.endpoint` as a side effect.
1999+
2000+
A spawn failure (e.g. a bad binary) is delivered asynchronously by Node as an `error` event;
2001+
it is recorded on `this.serverError` and surfaced as a rejection from `_waitForServer` instead
2002+
of crashing the process as an uncaught exception.
2003+
19422004
### _run
19432005

19442006
Delegates a find-and-act call to `window.__codecept.run(candidates, action, payload)`. This is
@@ -2155,6 +2217,14 @@ navigation, competing for the same CDP connection with real work.
21552217

21562218
Returns **[Promise][4]<void>**&#x20;
21572219

2220+
### _waitForServer
2221+
2222+
Polls `http://127.0.0.1:<port>/json/version` until the spawned server responds,
2223+
`this.serverError` is set by the process' `error` event, or `options.serverStartTimeout`
2224+
elapses. The process typically comes up within tens of milliseconds — a 20ms retry interval
2225+
keeps the wasted tail after the server is actually ready small, since this cost is paid once
2226+
per run and counts directly toward real-world startup latency.
2227+
21582228
### _withinBegin
21592229

21602230
Starts a `within` block, scoping every subsequent `_run` call (and therefore every element

0 commit comments

Comments
 (0)