Migrating a suite
This page moves an existing Playwright suite to Pyro: install the package, rewrite the imports, point the runner at your config, update scripts and CI, then fix the first failures. Most suites need no changes beyond the imports. Plan time for re-baselining screenshots and for the behaviour differences.
1. Install
bun add --dev @povio/lightning-panda
# or
npm install --save-dev @povio/lightning-pandaThe package bundles the browser for macOS 14+ (Apple silicon and Intel) and Linux x64 / arm64 with glibc 2.39+ (for example Ubuntu 24.04). There is no browser download step and no playwright install.
lightningpanda test runs your tests with bun test, so Bun must be on PATH even if the rest of your project uses Node. Check with bun --version. See Installation for platform details.
2. Rewrite the imports
Every import of @playwright/test, playwright, playwright/test and playwright-core becomes @povio/lightning-panda/playwright. Type-only imports change the same way.
import { test, expect } from '@playwright/test'; // before
import { test, expect } from '@povio/lightning-panda/playwright'; // after
import type { Page, Locator } from '@playwright/test'; // before
import type { Page, Locator } from '@povio/lightning-panda/playwright'; // after
import { chromium, devices } from 'playwright'; // before
import { chromium, devices } from '@povio/lightning-panda/playwright'; // afterThis command rewrites them in place. It matches the module name only when it is the whole quoted string, so it works for import, import type, export ... from, import() and require(). It works with both BSD (macOS) and GNU tools because it uses Perl:
grep -rlE "['\"](@playwright/test|playwright/test|playwright-core|playwright)['\"]" \
e2e playwright.config.ts \
| xargs perl -pi -e 's{(["\x27])(?:\@playwright/test|playwright/test|playwright-core|playwright)\1}{$1\@povio/lightning-panda/playwright$1}g'Replace e2e with the directories that hold your tests, fixtures and page objects. Run git diff afterwards and review it.
It leaves subpath imports such as @playwright/test/reporter alone. Custom reporters do not run under lightningpanda test, so remove those files or keep them out of the test directory.
Shared helpers count too
A helper module that still imports @playwright/test loads Playwright into the Bun test process. Search the whole repository, not only the test directory: grep -rn "@playwright/test\|from 'playwright'" --include='*.ts' .
3. The config file
lightningpanda test reads the first of these that exists:
- The file named by
-c/--config, or by$LP_CONFIG($PLAYWRIGHT_CONFIGalso works). lightningpanda.config.ts,.mts,.mjsor.jsin the working directory.playwright.config.ts,.mts,.mjsor.jsin the working directory.
So your existing playwright.config.ts works after its import is rewritten:
import { defineConfig, devices } from '@povio/lightning-panda/playwright';
export default defineConfig({
testDir: './e2e',
timeout: 30_000,
retries: process.env.CI ? 2 : 0,
use: { baseURL: 'http://localhost:3000' },
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
webServer: { command: 'bun run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI },
});A lightningpanda.config.ts beside it takes precedence. That is how both runners can share one repository during a migration (see below).
Things to check in the config:
- Projects for other browsers. All projects run on Pyro, and
lightningpanda testruns each file once, under the first project whosetestDir/testMatchinclude it. A config withchromium,firefoxandwebkitprojects therefore runs each test once, with thechromiumproject'suse. Select one with--project. devices. Only five descriptors exist:Desktop Chrome,Desktop Chrome HiDPI,Desktop Edge,Pixel 7andiPhone 15. Any other name isundefined, and spreading it adds nothing. Of a descriptor,viewportanduserAgenttake effect;deviceScaleFactor,isMobileandhasTouchdo not.reporteris ignored. Pass--reporter list|line|doton the command line.use.trace,use.video,use.screenshotare accepted and do nothing.
Config and the test runner lists every field that is honoured.
4. Run it
bunx lightningpanda test # all projects' files, half the CPUs as workers
bunx lightningpanda test e2e/checkout # files whose path matches the regex
bunx lightningpanda test -g "closes" -j 4You can also run a file with plain bun test, in one process, with $LP_CONFIG naming the config:
LP_CONFIG=playwright.config.ts bun test ./e2e/checkout.spec.tsThe difference: under plain bun test, Bun reports a test that calls test.skip() at run time as passed, because Bun cannot mark a running test as skipped. Workers, fullyParallel and the runner's own report also need lightningpanda test. Use plain bun test for quick runs of one file.
5. package.json scripts
{
"scripts": {
"e2e": "lightningpanda test",
"e2e:update": "lightningpanda test -u",
"e2e:one": "LP_CONFIG=playwright.config.ts bun test"
}
}Drop scripts that use playwright show-report, playwright codegen, --ui, --trace or --shard. They have no equivalent (see Overview).
6. Remove Playwright
Once nothing imports it:
bun remove @playwright/test playwright playwright-core
# or
npm uninstall @playwright/test playwright playwright-coreAlso remove npx playwright install from setup scripts, Dockerfiles and CI, and any @playwright/test entry in tsconfig.json types.
7. CI
The browser comes with the npm package, so CI needs Node or Bun for the install, and Bun for the run. On GitHub Actions:
jobs:
e2e:
runs-on: ubuntu-24.04 # glibc 2.39
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bunx lightningpanda test --reporter dotCompared with a Playwright job:
- Remove the
npx playwright install --with-depsstep and any browser cache. - Remove
--shard=i/nmatrices;lightningpanda testhas no sharding. Use more workers (-j) on one machine instead. - Remove steps that upload
playwright-report/or traces. On failure,test-results/(theoutputDir) still holds-actual,-expectedand-diffimages of screenshot mismatches. - The runner exits with status 1 when a test fails.
For more CI patterns see CI recipes.
8. Re-baseline screenshots
Pyro renders pages with its own CPU renderer. Screenshots differ from Chromium's at the pixel level, so every existing toHaveScreenshot baseline will fail. Write new ones once, review them, and commit them:
bunx lightningpanda test -u allCommit the .json file written beside each .png: it holds the render's display-list hash, which lets later runs pass without rasterizing. See Screenshots and snapshots.
Running both side by side
You do not have to switch the whole suite at once. Two ways work.
Split the files between two configs. lightningpanda test prefers lightningpanda.config.ts; playwright test only reads playwright.config.ts. Give each runner its own files:
// lightningpanda.config.ts: the migrated files
import { defineConfig } from '@povio/lightning-panda/playwright';
export default defineConfig({
testDir: './e2e',
testMatch: ['checkout.spec.ts', 'positions/*.spec.ts'],
use: { baseURL: 'http://localhost:3000' },
});// playwright.config.ts: still on Playwright, skips the migrated files
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
testIgnore: ['checkout.spec.ts', 'positions/*.spec.ts'],
use: { baseURL: 'http://localhost:3000' },
});Rewrite the imports of a file when you move it to the first list. Keep shared fixtures in two copies, or migrate them first.
Switch one fixtures module. If all tests import test and expect from a local module, that module can pick the implementation. The package's own parity suite works this way (packages/lightning-panda/test/playwright/fixtures.ts):
// e2e/fixtures.ts (an ES module project)
import * as lp from '@povio/lightning-panda/playwright';
const stock = process.env.PW_STOCK ? await import('@playwright/test') : undefined;
export const test: typeof lp.test = stock ? (stock.test as any) : lp.test;
export const expect: typeof lp.expect = stock ? (stock.expect as any) : lp.expect;PW_STOCK=1 npx playwright test runs the suite on Playwright and Chromium; bunx lightningpanda test runs it on Pyro. Comparing the two runs shows which failures come from the migration.
Typical first failures
| Symptom | Cause | Fix |
|---|---|---|
page.click is not a function (also page.fill, page.textContent(selector), page.isVisible(selector), ...) | Playwright's selector-first page methods are not implemented. | Use locators: page.locator(selector).click(). |
Every toHaveScreenshot fails | Baselines came from Chromium. | Re-baseline with -u all (step 8). |
unknown field 'animations' (or mask, caret, scale, omitBackground) | toHaveScreenshot accepts only threshold, maxDiffPixels, maxDiffPixelRatio, clip, fullPage and timeout. | Remove the option. |
| An assertion on a loading spinner or "Saving..." text times out | Actions settle before they return; the transient state is already gone. | Assert the final state, or wait for the request. See Behaviour differences. |
A confirm() dialog acts as if Cancel was pressed | A dialog with no page.on('dialog') listener is dismissed. | Register a listener before the action, as Playwright requires. |
toHaveRole('button') fails on a <button> | toHaveRole reads the role attribute only, not implicit roles. | Use getByRole('button') to find it, or drop the assertion. |
| An element inside an open shadow root is not found | CSS and getByRole locators do not enter shadow roots (measured on 0.0.33); getByText does. | Locate by text, or test through the host element's API. |
| Each test runs once although there are three browser projects | All projects run on Pyro, each file once. | Expected. Use --project to pick one. |
await page.goto(url) returns null, or its response has no headers | Pooled pages turn goto into a route change, which returns null; a normal goto response carries status and URL only. | Check status with waitForResponse, or read headers from page.request. |
afterEach that takes a screenshot when testInfo.status !== testInfo.expectedStatus never fires | testInfo.status stays 'passed' during hooks. | See Behaviour differences. |
getByTestId(/regex/) or click({ clickCount: 3 }) throws "not supported yet" | Not implemented. | Use a string test id; use dblclick() or evaluate for triple clicks. |
lightningpanda test: unknown option --shard | Flag not supported. | Remove it (see step 7). |
| App code reports an unsupported browser, or a web API is undefined | Pyro is not Chromium: its UA and API surface differ. | See Web platform support. |
Checklist
- [ ]
@povio/lightning-pandainstalled;bun --versionworks locally and in CI. - [ ] No imports of
@playwright/test,playwright,playwright/testorplaywright-coreleft (grep -rn). - [ ] Config file found (
bunx lightningpanda test --listlists your tests). - [ ] Browser-specific projects reviewed;
devicesnames exist. - [ ] Unsupported screenshot options removed; baselines rewritten with
-u alland committed with their.jsonfiles. - [ ]
page.click(selector)-style calls rewritten as locators. - [ ] Dialog listeners registered where the app uses
confirm,alertorprompt. - [ ] Assertions on transient states rewritten.
- [ ]
package.jsonscripts and CI updated;playwright install, report uploads and sharding removed. - [ ] Playwright packages removed.
- [ ] Optional: slow app boots moved to booted pages.