electron-playwright
Authors Playwright `_electron` tests for packaged Electron desktop apps - launches the app via `electron.launch({ args })`, returns an `ElectronApplication` handle, drives renderer windows as Playwright `Page` objects, and probes the main process via `electronApp.evaluate(({ app, BrowserWindow }) => …)`. Distinct from ordinary browser page automation: this wraps the `_electron` API for launching packaged Electron apps and probing main + renderer processes. Use for end-to-end tests of Electron apps where main-process state, IPC, and renderer DOM must all be asserted from one suite.
Install with skills.sh (any agent)
npx skills add testland/qa --skill electron-playwrightelectron-playwright
Overview
Playwright ships a first-class _electron namespace that launches a packaged Electron app by executablePath and drives both the main process (Node.js, IPC, native modules) and renderer windows (Chromium DOMs) from a single test (pwelectron (opens in new window)). Per Electron's own automated-testing tutorial (electrontest (opens in new window)), Playwright is one of three sanctioned test stacks (alongside WebdriverIO and Selenium) for modern Electron projects.
Differentiation: unlike playwright-testing (which drives a running Chromium / Firefox / WebKit browser via the browser / context / page namespaces), electron-playwright uses the separate _electron namespace to launch a packaged binary by path and probe the main process via electronApp.evaluate() (pwelectronapp (opens in new window)). Renderer-side page patterns (Page Object, accessibility-first locators, trace viewer) carry over.
For legacy Spectron suites, see electron-spectron; for the strategic frame, desktop-test-strategy-reference.
When to use
Step 1 - Install
Per electrontest (opens in new window):
npm install --save-dev @playwright/testPlaywright's _electron module is bundled inside playwright / @playwright/test - no extra package is needed (pwelectron (opens in new window)). Supported Electron versions per pwelectron (opens in new window): "Electron v12.2.0+, v13.4.0+, and v14+".
Step 2 - Author the first test
The canonical example from electrontest (opens in new window):
import { test, expect, _electron as electron } from '@playwright/test';
test('app launches and is not packaged in dev', async () => {
const electronApp = await electron.launch({ args: ['.'] });
// Main-process assertion: probe the Electron `app` module
const isPackaged = await electronApp.evaluate(async ({ app }) => {
return app.isPackaged;
});
expect(isPackaged).toBe(false);
// Renderer assertion: take a screenshot of the first window
const window = await electronApp.firstWindow();
await window.screenshot({ path: 'intro.png' });
await electronApp.close();
});What's going on:
Step 3 - Launching a packaged binary
For tests of the packaged app (the artifact users install):
import path from 'node:path';
import { _electron as electron } from '@playwright/test';
const PACKAGED_BIN = process.platform === 'win32'
? path.resolve('dist/win-unpacked/MyApp.exe')
: process.platform === 'darwin'
? path.resolve('dist/mac/MyApp.app/Contents/MacOS/MyApp')
: path.resolve('dist/linux-unpacked/myapp');
const electronApp = await electron.launch({
executablePath: PACKAGED_BIN,
args: [],
env: { ...process.env, NODE_ENV: 'test' },
recordVideo: { dir: 'test-results/videos' },
});The executablePath, env, cwd, recordVideo, recordHar, and timeout options are documented on the _electron launch reference (pwelectron (opens in new window)).
Step 4 - Probing main + renderer in one test
Multi-surface assertion - main process owns app lifecycle, renderer owns DOM:
test('opening a project loads it into the renderer', async () => {
const electronApp = await electron.launch({ args: ['.'] });
const window = await electronApp.firstWindow();
// Renderer-side action via Playwright Page API
await window.getByRole('button', { name: /open project/i }).click();
await window.getByLabel('Project path').fill('/tmp/demo-project');
await window.getByRole('button', { name: /confirm/i }).click();
// Renderer-side assertion
await expect(window.getByRole('heading', { name: /demo-project/i })).toBeVisible();
// Main-process assertion: recent-projects state mutated
const recents: string[] = await electronApp.evaluate(({ app }) => {
return app.getRecentDocuments();
});
expect(recents).toContain('/tmp/demo-project');
await electronApp.close();
});electronApp.evaluate(pageFunction) returns the function's value and awaits a returned Promise, so async main-process queries work naturally (pwelectronapp (opens in new window)).
Step 5 - Mapping renderer windows to main-process BrowserWindow
When a test needs the underlying BrowserWindow object for a window (to assert size, fullscreen state, devtools open, etc.):
const window = await electronApp.firstWindow();
const bwHandle = await electronApp.browserWindow(window);
const isFullScreen = await bwHandle.evaluate((bw) => bw.isFullScreen());
expect(isFullScreen).toBe(false);electronApp.browserWindow(page) returns the BrowserWindow for a page as a JSHandle; multi-window apps iterate electronApp.windows() (pwelectronapp (opens in new window)). Full API table: references/electron-ci-and-api.md.
Step 6 - Waiting for new windows + console output
The 'window', 'console', and 'close' events fire for each new window, main-process console writes, and process termination respectively (pwelectronapp (opens in new window)); event payloads and the full API table are in references/electron-ci-and-api.md.
// Wait for a secondary window to open after clicking
const [secondary] = await Promise.all([
electronApp.waitForEvent('window'),
window.getByRole('button', { name: /preferences/i }).click(),
]);
await expect(secondary.getByRole('heading', { name: /preferences/i })).toBeVisible();Step 7 - Configuration
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/electron',
fullyParallel: false, // Electron launches are heavy; serialize
workers: 1,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: [
['html'],
['junit', { outputFile: 'reports/electron-junit.xml' }],
],
use: {
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
});Electron-launch tests typically run with workers: 1 because each launch spawns an Electron process with its own GPU/IPC stack; full parallel launches collide on the user-data directory and on GPU-shared-memory regions. (Web-only Playwright defaults to parallel per playwright-testing.)
Step 8 - Running
# All Electron tests
npx playwright test --config=playwright.electron.config.ts
# A specific test file
npx playwright test tests/electron/launch.spec.ts
# Headed (the Electron window stays visible)
npx playwright test --headed
# Trace viewer for a failing run
npx playwright show-trace test-results/<…>/trace.zipThe trace viewer shows DOM snapshots of the renderer windows and the evaluate calls into the main process side-by-side - debug parity with normal Playwright traces (the trace viewer surface is part of the shared Playwright toolchain per playwright-testing).
Step 9 - Parsing results
JUnit XML output (reports/electron-junit.xml from Step 7) feeds junit-xml-analysis for aggregation. The HTML reporter is identical to web-Playwright (pwelectron (opens in new window)).
Step 10 - CI integration
Run across a Windows / macOS / Linux matrix; Linux needs an xvfb-run virtual display because Electron requires a display server, while macOS and Windows GitHub-hosted runners are display-capable out of the box. Full workflow: references/electron-ci-and-api.md.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Driving Electron via plain chromium.launch() from web-only Playwright | Misses main-process surface entirely; can't query app.* or BrowserWindow.* | Use _electron.launch() (pwelectron (opens in new window)) |
Hard-coded executablePath checked into the repo for a single OS | Cross-OS CI matrix breaks | Resolve per process.platform (Step 3) |
Tests that share a single electronApp across many tests without cleanup | One leaked window state contaminates the next test | Per-test electronApp = await electron.launch(...) + await electronApp.close() |
Running Electron tests with default workers > 1 | GPU / user-data directory collisions; flaky launches | workers: 1 (Step 7) |
Forgetting xvfb-run on hosted Linux CI | "Failed to initialize display" launch failure | xvfb-run --auto-servernum wrapper (Step 10) |
Probing main-process modules from window.evaluate() | window.evaluate() runs in the renderer; doesn't see main-process globals | Use electronApp.evaluate() (pwelectronapp (opens in new window)) |
| Mixing Spectron and Playwright assertions in the same suite | Two driver lifecycles compete for the same Electron process | Migrate file-by-file per electron-spectron |
Asserting on Electron internal IDs (__electron_id) for locators | Internal; changes between Electron versions | Use accessibility-first locators (getByRole / getByLabel) per playwright-testing |
Limitations
References
electron-playwright - ElectronApplication API and CI
View source (opens in new window)electron-playwright - ElectronApplication API and CI
Full ElectronApplication API detail and the cross-OS CI workflow, kept out of the SKILL spine. See SKILL.md (opens in new window) for the core launch-and-assert flow.
Sources: Playwright _electron launch reference (opens in new window) and ElectronApplication API (opens in new window).
ElectronApplication API
| Member | Behaviour |
|---|---|
electron.launch({ args, executablePath, env, cwd, recordVideo, recordHar, timeout }) | Launches Electron; executablePath defaults to node_modules/.bin/electron (pwelectron (opens in new window)). |
electronApp.evaluate(fn) | Runs fn in the main process; its first argument is the result of require('electron'), and a returned Promise is awaited (pwelectronapp (opens in new window)). |
electronApp.firstWindow() | Waits for the first window and returns a Playwright Page (pwelectronapp (opens in new window)). |
electronApp.browserWindow(page) | Returns the BrowserWindow JSHandle for a page (pwelectronapp (opens in new window)). |
electronApp.windows() | Returns all opened windows (pwelectronapp (opens in new window)). |
Events (pwelectronapp (opens in new window)):
Supported Electron versions: v12.2.0+, v13.4.0+, and v14+ (pwelectron (opens in new window)).
Cross-OS CI workflow
Linux runners need Xvfb (or another virtual framebuffer) because Electron requires a display server; xvfb-run is the standard wrapper. macOS and Windows GitHub-hosted runners are display-capable out of the box.
# .github/workflows/electron-e2e.yml
jobs:
test:
strategy:
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm ci
- run: npm run build:electron
# On Linux, headless Electron needs a virtual display
- name: Run E2E (Linux with Xvfb)
if: runner.os == 'Linux'
run: xvfb-run --auto-servernum npx playwright test --config=playwright.electron.config.ts
- name: Run E2E (Windows/macOS)
if: runner.os != 'Linux'
run: npx playwright test --config=playwright.electron.config.ts
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-${{ matrix.os }}
path: playwright-report/Related skills
appium-windows-driver
Authors and runs Appium 2.x tests against the Windows driver, the actively-maintained Node.js proxy in front of Microsoft's WinAppDriver: `appium driver install windows`, capabilities (`platformName: windows`, `appium:automationName: windows`, `appium:app`, `appium:appTopLevelWindow`, `appium:appArguments`), Windows gestures (`windows: scroll` / `clickAndDrag` / `keys`), PowerShell prerun/postrun hooks, and CI. Use when the stack already uses Appium for iOS / Android / Mac2 and Windows fits the existing client + capability model; to drive WinAppDriver directly from a Selenium-style client use winappdriver, and for a C#-only FlaUI client use flaui-tests.
at-spi-linux
Authors Linux desktop UI tests via AT-SPI - the DBus-based Assistive Technology Service Provider Interface implemented by `at-spi2-core` (registry daemon + `libatspi` C library + ATK GTK bridge). Covers enabling toolkit accessibility (`gsettings set org.gnome.desktop.interface toolkit-accessibility true`), driving GTK + Qt apps through Python `dogtail` (object-oriented + procedural APIs), inspecting the tree with Accerciser, scripting via `pyatspi`, and CI integration on headless Linux runners with `Xvfb` + `dbus-launch`. Use for Linux-side desktop tests of GTK applications, Qt apps with QAccessible enabled, and Electron apps on Linux.
desktop-test-strategy-reference
Pure-reference catalog of desktop GUI test strategies across Windows, macOS, and Linux. Defines the three accessibility-tree backends (Microsoft UI Automation on Windows, Apple Accessibility / XCTest on macOS, AT-SPI on Linux), the wrapper-tools that drive each backend (WinAppDriver, Appium-Windows, XCUIApplication, AT-SPI clients), the cross-toolkit Electron + Qt paths, and a per-OS decision matrix with accessibility-first locator strategy. Deep operational detail (per-OS asynchronous-wait hierarchies, parallel-test policy, foreground-lock / UAC / TCC / AT-SPI elevation hazards, and the high-DPI / per-monitor test matrix) lives in references/. Use when choosing how to test or automate a desktop GUI application (desktop app testing, GUI automation, automate desktop UI) on Windows, macOS, or Linux - the strategic reference before picking a desktop test stack, ahead of the per-tool implementation skills.
electron-spectron
Legacy reference for Spectron - Electron's original ChromeDriver-based testing framework, officially deprecated 2022-02-01 at v19.0.0. Documents what Spectron was, the architectural reason it became unmaintainable, the migration path to Playwright `_electron`, and the residual support contract for projects still on Spectron. Use only when auditing a legacy suite or planning a migration off Spectron - for new work use Playwright's `_electron` API.
flaui-tests
Authors and runs FlaUI-based Windows UI tests - the .NET-native wrapper around Microsoft UI Automation (UIA2 + UIA3). Covers the `FlaUI.Core` / `FlaUI.UIA2` / `FlaUI.UIA3` NuGet packages, `Application.Launch` / `Application.Attach` lifecycles, `ConditionFactory` + `FindFirstDescendant` locator patterns, `Retry` waits, and xUnit / NUnit / MSTest harness integration. Use when the test stack is C# / .NET-first and the team wants idiomatic in-process UIA calls rather than the HTTP/JSON wire protocol of `winappdriver` or the Appium proxy layer of `appium-windows-driver`.
qt-test-framework
Authors and runs Qt Test - the first-party C++ unit + GUI test framework that ships with Qt 6 (via the `QtTest` module header). Covers the `QTEST_MAIN` / `QTEST_APPLESS_MAIN` / `QTEST_GUILESS_MAIN` entry-point macros, the `QObject` private-slot test pattern, `QVERIFY` / `QCOMPARE` / `QFETCH` assertions, GUI event simulation (`QTest::mouseClick`, `QTest::keyClick`, `QTest::touchEvent`), `QSignalSpy` for signal introspection, `QBENCHMARK` for performance regression, and the `-o file,junitxml` CI output. Use for in-process testing of Qt widgets, QObject signal/slot chains, and Qt Quick / QML application logic; for out-of-process Qt-app driving, use an OS-native accessibility driver instead.
winappdriver
Authors and runs Windows UI tests against WinAppDriver, Microsoft's W3C-WebDriver service for UWP / WPF / WinForms / Win32 apps: installing + launching `WinAppDriver.exe` on `127.0.0.1:4723`, declaring `app` / `platformName` / `appArguments` / `appTopLevelWindow` capabilities, finding elements by `AccessibilityId` / `Name` / `ClassName`, and Windows-runner CI. Use when driving a native Windows app from a Selenium-style client (C#, Java, Python, Ruby, JS); for the actively-maintained Appium 2.x wrapper over the same server use appium-windows-driver, for a C#-only FlaUI client use flaui-tests, and to choose among Windows desktop drivers first use desktop-test-strategy-reference.
xctest-mac-desktop
Authors and runs XCTest UI + unit tests for macOS desktop apps - the Apple-first-party test framework that ships with Xcode. Covers the `XCTestCase` subclass + `test*` method-naming convention, `XCUIApplication` / `XCUIElement` / `XCUIElementQuery` for UI tests, accessibility-identifier-based locators (the stable replacement for label-based queries), `XCTAssert*` macros, `measureBlock:` for performance regressions, and `xcodebuild test` for CI execution. Use when the macOS app is built with Xcode and the test target is in-tree alongside the app - for cross-OS sharing see Appium Mac2 driver as a separate path.