diff --git a/.github/workflows/lightpanda.yml b/.github/workflows/lightpanda.yml new file mode 100644 index 000000000..ad777a4eb --- /dev/null +++ b/.github/workflows/lightpanda.yml @@ -0,0 +1,63 @@ +name: Lightpanda Helper Tests + +on: + push: + branches: + - 4.x + pull_request: + branches: + - '**' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CI: true + FORCE_COLOR: 1 + LIGHTPANDA_DISABLE_TELEMETRY: true + +jobs: + build: + runs-on: ubuntu-22.04 + timeout-minutes: 15 + + strategy: + matrix: + node-version: [22.x] + + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.0 + - name: npm install + run: npm i --force + - name: Download Lightpanda + run: | + mkdir -p ~/lightpanda-bin + curl -sfL --retry 3 -o ~/lightpanda-bin/lightpanda https://github.com/lightpanda-io/browser/releases/latest/download/lightpanda-x86_64-linux + chmod +x ~/lightpanda-bin/lightpanda + ~/lightpanda-bin/lightpanda version + - name: start test server + run: | + php -S 127.0.0.1:8000 -t test/data/app & + sleep 1 + curl -sf http://127.0.0.1:8000/info > /dev/null + - name: run lightpanda helper tests + run: | + export LIGHTPANDA_PATH="$HOME/lightpanda-bin/lightpanda" + npm run test:unit:webbapi:lightpanda + - name: run lightpanda acceptance tests + run: | + export LIGHTPANDA_PATH="$HOME/lightpanda-bin/lightpanda" + ./bin/codecept.js run -c test/acceptance/codecept.Lightpanda.js --grep @Lightpanda --debug diff --git a/Bunoshfile.js b/Bunoshfile.js index 12a8d5682..f3cf66ff2 100644 --- a/Bunoshfile.js +++ b/Bunoshfile.js @@ -399,6 +399,7 @@ const inheritedHelperDocs = { exclude: [/Title/, /Popup/, /Cookie/, /Url/, /^press/, /^refreshPage/, /^resizeWindow/, /Script$/, /cursor/, /Css/, /Tab$/, /^wait/], }, Obscura: { parent: 'CDPBrowser' }, + Lightpanda: { parent: 'CDPBrowser' }, Kitesurf: { parent: 'CDPBrowser', excludeConfig: ['endpoint', 'headers'] }, } @@ -425,11 +426,12 @@ async function docsHelperMarkdown(name, { parent, exclude = [], excludeConfig = const documentation = await import('documentation') const buildOptions = { shallow: true, sortOrder: ['alpha'] } const doc = await documentation.build([`docs/build/${name}.js`], buildOptions) + doc.sort((a, b) => (b.kind === 'class') - (a.kind === 'class')) let members = doc[0].members.instance if (parent) { const parentDoc = await documentation.build([`docs/build/${parent}.js`], buildOptions) - for (const method of parentDoc[0].members.instance) { + for (const method of parentDoc.find(c => c.kind === 'class').members.instance) { if (exclude.some(f => method.name.match(f))) continue if (members.some(m => m.name === method.name)) continue members.push(method) diff --git a/README.md b/README.md index 22dde6ccc..663e6ae2b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ | 🌐 Web | Puppeteer | [![Puppeteer Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/puppeteer.yml) | | 🌐 Web | WebDriver | [![WebDriver Tests](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml/badge.svg)](https://github.com/codeceptjs/CodeceptJS/actions/workflows/webdriver.yml) | | 🌐 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) | +| 🌐 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) | | 📱 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) | # 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, - [**Puppeteer**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Puppeteer.md) - uses Google Chrome's Puppeteer for fast headless testing. - [**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. - [**Obscura**](https://codecept.io/helpers/Obscura) - drives the lightweight Obscura browser through Chrome DevTools Protocol. See [Alternative Browser Engines](https://codecept.io/alternative-browsers). +- [**Lightpanda**](https://codecept.io/helpers/Lightpanda) - drives the Lightpanda headless browser through Chrome DevTools Protocol. See [Alternative Browser Engines](https://codecept.io/alternative-browsers). - [**Appium**](https://github.com/codeceptjs/CodeceptJS/blob/master/docs/helpers/Appium.md) - for **mobile testing** with Appium - [**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. diff --git a/docs/alternative-browsers.md b/docs/alternative-browsers.md index f3ea7dd8e..e9f76bfe3 100644 --- a/docs/alternative-browsers.md +++ b/docs/alternative-browsers.md @@ -6,143 +6,180 @@ title: Alternative Browser Engines # Alternative Browser Engines ::: warning Experimental -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. +The `Obscura`, `Lightpanda` and `Kitesurf` helpers are experimental. Keep a Playwright or WebDriver job for compatibility-critical tests. ::: -Playwright and Puppeteer drive full Chromium — the most accurate way to test what users see. -But a new class of lightweight, agent-era browsers has appeared, and CodeceptJS can drive them -through dedicated helpers: - -- **[Obscura](https://github.com/h4ckf0r0day/obscura)** — an open-source Rust browser with a real - V8 engine. From v0.2.0, the default release build also renders — real layout, computed styles, - and screenshots — with `-no-render` builds still available for pure-speed, nothing-painted - scraping mode. Release archives are available for Linux, macOS, and Windows. -- **[Kitesurf](https://blog.cloudflare.com/kitesurf/)** — Cloudflare's browser that runs in V8 - isolates on Cloudflare Workers, with a real layout and rendering pipeline. Cloud-only, - free in beta, planned to be open-sourced. - -Both speak Chrome DevTools Protocol. CodeceptJS drives them with raw CDP — one round-trip per -action, no stale element handles — which is why suites on these browsers run fast and never hang -on navigation races. - -## When are they better than Playwright? - -**Smoke suites where startup and execution time matter.** Obscura is distributed as a standalone -binary and is designed for lightweight browser automation. Benchmark it against your own pages and -CI environment before choosing it for a PR gate. - -**Massive parallel scale.** Kitesurf sessions are Cloudflare Workers — they spawn in about a -second, cost nothing while idle, and there is no practical ceiling on how many you run at once. -Combined with `run-workers`, every worker acquires its own cloud browser: - - // codecept.conf.js — each worker independently loads the config, - // so each one gets its own Kitesurf session automatically - export const config = { - helpers: { - Kitesurf: { - url: 'https://staging.myapp.com', - }, - }, - } - - npx codeceptjs run-workers 16 - -Sixteen cloud browsers, zero local resources, feedback in the time of your slowest test. -Scale the number up as far as your suite can split — the browsers are no longer -the bottleneck, and your CI runner only coordinates. - -**Testing the DOM, not the pixels.** Most functional assertions — text appears, form submits, -redirect happens, cookie is set — do not need a GPU raster pipeline. Obscura executes your -app's real JavaScript in real V8; it only skips painting. For API-adjacent flows -(login → dashboard data appears), that is exactly the right amount of browser. - -**Constrained environments.** ARM CI runners, thin containers, air-gapped machines: -a static binary with no system dependencies goes where Chromium will not. - -**Optional stealth builds.** Obscura publishes separate `-stealth` archives. Treat their behaviour -as an Obscura capability rather than a browser-compatibility guarantee from CodeceptJS. - -## When to stay with Playwright - -- Anything visual: visual regression, PDF (neither helper exposes PDF output). Obscura's v0.2.0+ - rendering/CSS engine is new and independently implemented — expect edge cases and gaps versus a - real browser, especially around inherited properties and less common computed-style values. -- Visibility semantics on `-no-render` Obscura builds (and v0.1.x): every element reports as - visible — `seeElement`/`dontSeeElement` throw and point you to `seeElementInDOM`. On v0.2.0+ - default builds, `CDPBrowser` detects the real layout engine per binary and visibility works - normally. -- Complex input: drag-and-drop, hover chains, file uploads, iframes, multi-tab, service workers. -- Cross-browser coverage (Firefox, WebKit). -- Testing local apps with Kitesurf: the cloud browser must reach your app; use a tunnel - (`cloudflared tunnel --url http://localhost:3000`) or a deployed environment. - -## Configuration - -CodeceptJS 4.2 is tested in CI with Obscura 0.2.2. Obscura 0.2.x is recommended; 0.1.x and -`-no-render` builds operate without layout, visibility assertions, or screenshots. See -[Installation](/installation#obscura-experimental) for platform-specific archive names. - - helpers: { - Obscura: { - url: 'http://localhost:3000', - }, - } - - helpers: { - Kitesurf: { - url: 'https://staging.myapp.com', - accountId: process.env.CF_ACCOUNT_ID, - apiToken: process.env.CF_API_TOKEN, - }, - } - -### Obscura's three connection modes - -Obscura manages its own `obscura serve` process, the same way Playwright manages its own browser -process — there is nothing to start by hand in the common case: - -- **Self-launch (default)** — leave `endpoint` unset. The helper resolves a binary - (`binaryPath` in the config, then `OBSCURA_PATH`, then `obscura` on `PATH`), spawns - `obscura serve` on a free port, and kills it when the run ends. Not setting `port` is - intentional: a free port is picked automatically, which is what makes `run-workers` - collision-free — every worker gets its own instance without any config. - - helpers: { - Obscura: { - url: 'http://localhost:3000', - binaryPath: '/usr/local/bin/obscura', // optional override, like Playwright's executablePath - }, - } - -- **Attach** — set `endpoint` explicitly to connect to an Obscura instance you manage yourself - (already running locally, in a container, or on a remote host). The helper only connects; it - never spawns or kills anything. - - helpers: { - Obscura: { - url: 'http://localhost:3000', - endpoint: 'http://127.0.0.1:9222', - }, - } - -- **Courtesy-attach** — only relevant when `endpoint` is unset and no binary can be resolved - either. If something is already answering on the conventional `http://127.0.0.1:9222`, the - helper attaches to it (and, again, never kills it) instead of failing outright. This exists so - that "start Obscura by hand and just run the tests" keeps working without any config, while - self-launch is still the default for everyone else. - -## Capability matrix - -| | Playwright | Obscura | Kitesurf | -|---|---|---|---| -| Real JS execution (V8) | yes | yes | yes | -| Layout / getBoundingClientRect | yes | yes (v0.2.0+ default builds); synthetic on `-no-render`/v0.1.x | yes | -| Screenshots | yes | yes (v0.2.0+ default builds); no on `-no-render`/v0.1.x | yes | -| Visibility assertions | yes | yes (v0.2.0+ default builds); no, DOM-presence only, on `-no-render`/v0.1.x | yes | -| 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 | -| Startup model | local browser process | standalone local binary | remote cloud session | -| Parallel scale | machine-bound | machine-bound (light) | near-unlimited (cloud) | -| Where it runs | local/grid | local | Cloudflare only | -| License / cost | open source | Apache-2.0 | proprietary, free beta | - -See helper reference pages: [Obscura](/helpers/Obscura), [Kitesurf](/helpers/Kitesurf). +Playwright drives full Chromium, Firefox and WebKit: the most accurate way to test what users see, at the cost of a heavy browser per worker. Alternative engines trade part of that accuracy for speed, a small footprint, or cloud scale. They are a good fit for functional tests (text appears, form submits, redirect happens, cookie is set) and a poor one for anything visual. + +Your tests do not change. `I.amOnPage`, `I.click`, `I.fillField`, `I.see`, `I.seeElement` and the rest of the `I.*` web API work the same; you only swap the helper in the config. All engines below speak Chrome DevTools Protocol, so there is no `npx playwright install` step. + +Compared to Playwright, none of them supports iframes (`switchTo`), multiple tabs, popups, `dragAndDrop`, `moveCursorTo`, or Playwright-only APIs such as `usePlaywrightTo` and `mockRoute`. Tag scenarios that need those and skip them on the alternative engine: + +```sh +npx codeceptjs run --grep @playwright-only --invert +``` + +To see which `I.*` actions the configured engine supports: + +```sh +npx codeceptjs list +``` + +## Obscura + +[Obscura](https://github.com/h4ckf0r0day/obscura) is a lightweight open-source browser with a real V8 engine and its own rendering engine, distributed as a single binary for Linux, macOS and Windows. Unlike Lightpanda it renders: layout, visibility checks, screenshots and the `screencast` plugin work. + +Limitations: + +- `attachFile` does not upload files. +- Clicks inside nested shadow DOM do not work. +- `focus` and `blur` have no effect. +- Clipboard actions do not work. +- A `` is then addressed by that attribute through the CDP `DOM` domain, which +`CDPBrowser` otherwise never uses, to call `DOM.setFileInputFiles`; any other element (a +drag-and-drop dropzone) instead gets a synthetic `dragenter`/`dragover`/`drop` sequence with a +`DataTransfer` built from the file's contents, entirely in-page. The marker is removed again in +a `finally`. + +```js +I.attachFile('Avatar', 'data/avatar.jpg'); +I.attachFile('#file', 'data/avatar.jpg'); +I.attachFile('#dropzone', 'data/avatar.jpg'); +``` + +#### Parameters + +* `field` **([string][3] | [object][5])** located by label|name|CSS|XPath|strict locator. +* `pathToFile` **[string][3]** path to file, relative to `codecept_dir`. +* `context` **([string][3]? | [object][5])** (optional, `null` by default) element to search in CSS|XPath|Strict locator. + +Returns **[Promise][4]** + +### blur + +Removes focus from a given element. + +```js +I.blur('#name'); +``` + +#### Parameters + +* `locator` **([string][3] | [object][5])** element located by CSS|XPath|strict locator. + +Returns **[Promise][4]** + +### checkOption + +Selects a checkbox or radio button. +Element is located by label or name or CSS or XPath. + +```js +I.checkOption('#agree'); +I.checkOption('I Agree to Terms and Conditions'); +I.checkOption('agree', '//form'); +``` + +#### Parameters + +* `field` **([string][3] | [object][5])** checkbox located by label | name | CSS | XPath | strict locator. +* `context` **([string][3]? | [object][5])** (optional, `null` by default) element located by CSS | XPath | strict locator. + +Returns **[Promise][4]** + +### clearClipboard + +Clears the system clipboard. + +```js +I.clearClipboard(); +I.seeClipboardEquals(''); +``` + +Returns **[Promise][4]** + +### clearCookie + +Clears a cookie by name, +if none provided clears all cookies. + +```js +I.clearCookie(); +I.clearCookie('test'); +``` + +#### Parameters + +* `name` **([string][3] | null)** (optional, `null` by default) cookie name + +Returns **[Promise][4]** + +### clearField + +Clears a `