Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Comprehensive Playwright testing guide covering E2E, component, API, visual, accessibility, and security tests.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
testing-patterns/electron.md
1# Electron Testing23## Table of Contents451. [Setup & Configuration](#setup--configuration)62. [Launching Electron Apps](#launching-electron-apps)73. [Main Process Testing](#main-process-testing)84. [Renderer Process Testing](#renderer-process-testing)95. [IPC Communication](#ipc-communication)106. [Native Features](#native-features)117. [Packaging & Distribution](#packaging--distribution)1213## Setup & Configuration1415### Installation1617```bash18npm install -D @playwright/test electron19```2021### Basic Configuration2223```typescript24// playwright.config.ts25import { defineConfig } from "@playwright/test";2627export default defineConfig({28testDir: "./tests",29timeout: 30000,30use: {31trace: "on-first-retry",32},33});34```3536### Electron Test Fixture3738```typescript39// fixtures/electron.ts40import {41test as base,42_electron as electron,43ElectronApplication,44Page,45} from "@playwright/test";4647type ElectronFixtures = {48electronApp: ElectronApplication;49window: Page;50};5152export const test = base.extend<ElectronFixtures>({53electronApp: async ({}, use) => {54// Launch Electron app55const electronApp = await electron.launch({56args: [".", "--no-sandbox"],57env: {58...process.env,59NODE_ENV: "test",60},61});6263await use(electronApp);6465// Cleanup66await electronApp.close();67},6869window: async ({ electronApp }, use) => {70// Wait for first window71const window = await electronApp.firstWindow();7273// Wait for app to be ready74await window.waitForLoadState("domcontentloaded");7576await use(window);77},78});7980export { expect } from "@playwright/test";81```8283### Launch Options8485```typescript86// Advanced launch configuration87const electronApp = await electron.launch({88args: ["main.js", "--custom-flag"],89cwd: "/path/to/app",90env: {91...process.env,92ELECTRON_ENABLE_LOGGING: "1",93NODE_ENV: "test",94},95timeout: 30000,96// For packaged apps97executablePath: "/path/to/MyApp.app/Contents/MacOS/MyApp",98});99```100101## Launching Electron Apps102103### Development Mode104105```typescript106test("launch in dev mode", async () => {107const electronApp = await electron.launch({108args: ["."], // Points to package.json main109});110111const window = await electronApp.firstWindow();112await expect(window.locator("h1")).toContainText("My App");113114await electronApp.close();115});116```117118### Packaged Application119120```typescript121test("launch packaged app", async () => {122const appPath =123process.platform === "darwin"124? "/Applications/MyApp.app/Contents/MacOS/MyApp"125: process.platform === "win32"126? "C:\\Program Files\\MyApp\\MyApp.exe"127: "/usr/bin/myapp";128129const electronApp = await electron.launch({130executablePath: appPath,131});132133const window = await electronApp.firstWindow();134await expect(window).toHaveTitle(/MyApp/);135136await electronApp.close();137});138```139140### Multiple Windows141142```typescript143test("handle multiple windows", async ({ electronApp }) => {144const mainWindow = await electronApp.firstWindow();145146// Trigger new window147await mainWindow.getByRole("button", { name: "Open Settings" }).click();148149// Wait for new window150const settingsWindow = await electronApp.waitForEvent("window");151152// Both windows are now accessible153await expect(settingsWindow.locator("h1")).toHaveText("Settings");154await expect(mainWindow.locator("h1")).toHaveText("Main");155156// Get all windows157const windows = electronApp.windows();158expect(windows.length).toBe(2);159});160```161162## Main Process Testing163164### Evaluate in Main Process165166```typescript167test("access main process", async ({ electronApp }) => {168// Evaluate in main process context169const appPath = await electronApp.evaluate(async ({ app }) => {170return app.getAppPath();171});172173expect(appPath).toContain("my-electron-app");174});175```176177### Access Electron APIs178179```typescript180test("electron API access", async ({ electronApp }) => {181// Get app version182const version = await electronApp.evaluate(async ({ app }) => {183return app.getVersion();184});185expect(version).toMatch(/^\d+\.\d+\.\d+$/);186187// Get platform info188const platform = await electronApp.evaluate(async ({ app }) => {189return process.platform;190});191expect(["darwin", "win32", "linux"]).toContain(platform);192193// Check if app is ready194const isReady = await electronApp.evaluate(async ({ app }) => {195return app.isReady();196});197expect(isReady).toBe(true);198});199```200201### BrowserWindow Properties202203```typescript204test("check window properties", async ({ electronApp, window }) => {205// Get BrowserWindow from main process206const windowBounds = await electronApp.evaluate(async ({ BrowserWindow }) => {207const win = BrowserWindow.getAllWindows()[0];208return win.getBounds();209});210211expect(windowBounds.width).toBeGreaterThan(0);212expect(windowBounds.height).toBeGreaterThan(0);213214// Check window state215const isMaximized = await electronApp.evaluate(async ({ BrowserWindow }) => {216const win = BrowserWindow.getAllWindows()[0];217return win.isMaximized();218});219220// Check window title221const title = await electronApp.evaluate(async ({ BrowserWindow }) => {222const win = BrowserWindow.getAllWindows()[0];223return win.getTitle();224});225expect(title).toBeTruthy();226});227```228229## Renderer Process Testing230231### Standard Page Testing232233```typescript234test("renderer interactions", async ({ window }) => {235// Standard Playwright page interactions236await window.getByRole("button", { name: "Click Me" }).click();237await expect(window.getByText("Clicked!")).toBeVisible();238239// Fill forms240await window.getByLabel("Username").fill("testuser");241await window.getByLabel("Password").fill("password123");242await window.getByRole("button", { name: "Login" }).click();243244// Verify navigation245await expect(window).toHaveURL(/dashboard/);246});247```248249### Access Node.js in Renderer250251```typescript252test("node integration", async ({ window }) => {253// If nodeIntegration is enabled254const nodeVersion = await window.evaluate(() => {255return (window as any).process?.version;256});257258// Check if Node APIs are available259const hasFs = await window.evaluate(() => {260return typeof (window as any).require === "function";261});262});263```264265### Context Isolation Testing266267```typescript268test("context isolation", async ({ window }) => {269// Test preload script exposed APIs270const apiAvailable = await window.evaluate(() => {271return typeof (window as any).electronAPI !== "undefined";272});273expect(apiAvailable).toBe(true);274275// Call exposed API276const result = await window.evaluate(async () => {277return await (window as any).electronAPI.getAppVersion();278});279expect(result).toMatch(/^\d+\.\d+\.\d+$/);280});281```282283## IPC Communication284285### Testing IPC from Renderer286287```typescript288test("IPC invoke", async ({ window }) => {289// Test preload-exposed IPC call290const result = await window.evaluate(async () => {291return await (window as any).electronAPI.getData("user-settings");292});293294expect(result).toHaveProperty("theme");295});296```297298### Testing IPC from Main Process299300```typescript301test("main to renderer IPC", async ({ electronApp, window }) => {302// Set up listener in renderer303await window.evaluate(() => {304(window as any).receivedMessage = null;305(window as any).electronAPI.onMessage((msg: string) => {306(window as any).receivedMessage = msg;307});308});309310// Send from main process311await electronApp.evaluate(async ({ BrowserWindow }) => {312const win = BrowserWindow.getAllWindows()[0];313win.webContents.send("message", "Hello from main!");314});315316// Verify receipt317await window.waitForFunction(() => (window as any).receivedMessage !== null);318const message = await window.evaluate(() => (window as any).receivedMessage);319expect(message).toBe("Hello from main!");320});321```322323### Mock IPC Handlers324325```typescript326// In test setup or fixture327test("mock IPC handler", async ({ electronApp, window }) => {328// Override IPC handler in main process329await electronApp.evaluate(async ({ ipcMain }) => {330// Remove existing handler331ipcMain.removeHandler("fetch-data");332333// Add mock handler334ipcMain.handle("fetch-data", async () => {335return { mocked: true, data: "test-data" };336});337});338339// Test with mocked handler340const result = await window.evaluate(async () => {341return await (window as any).electronAPI.fetchData();342});343344expect(result.mocked).toBe(true);345});346```347348## Native Features349350### File System Dialogs351352```typescript353test("file dialog", async ({ electronApp, window }) => {354// Mock dialog response355await electronApp.evaluate(async ({ dialog }) => {356dialog.showOpenDialog = async () => ({357canceled: false,358filePaths: ["/mock/path/file.txt"],359});360});361362// Trigger file open363await window.getByRole("button", { name: "Open File" }).click();364365// Verify file was "opened"366await expect(window.getByText("file.txt")).toBeVisible();367});368369test("save dialog", async ({ electronApp, window }) => {370await electronApp.evaluate(async ({ dialog }) => {371dialog.showSaveDialog = async () => ({372canceled: false,373filePath: "/mock/path/saved-file.txt",374});375});376377await window.getByRole("button", { name: "Save" }).click();378await expect(window.getByText("Saved successfully")).toBeVisible();379});380```381382### Menu Testing383384```typescript385test("application menu", async ({ electronApp }) => {386// Get menu structure387const menuLabels = await electronApp.evaluate(async ({ Menu }) => {388const menu = Menu.getApplicationMenu();389return menu?.items.map((item) => item.label) || [];390});391392expect(menuLabels).toContain("File");393expect(menuLabels).toContain("Edit");394395// Trigger menu action396await electronApp.evaluate(async ({ Menu }) => {397const menu = Menu.getApplicationMenu();398const fileMenu = menu?.items.find((item) => item.label === "File");399const newItem = fileMenu?.submenu?.items.find(400(item) => item.label === "New",401);402newItem?.click();403});404});405```406407### Native Notifications408409```typescript410test("notifications", async ({ electronApp, window }) => {411// Mock Notification412let notificationShown = false;413await electronApp.evaluate(async ({ Notification }) => {414const OriginalNotification = Notification;415(global as any).Notification = class extends OriginalNotification {416constructor(options: any) {417super(options);418(global as any).lastNotification = options;419}420};421});422423// Trigger notification424await window.getByRole("button", { name: "Notify" }).click();425426// Verify notification was created427const notification = await electronApp.evaluate(async () => {428return (global as any).lastNotification;429});430431expect(notification.title).toBe("New Message");432});433```434435### Clipboard436437```typescript438test("clipboard operations", async ({ electronApp, window }) => {439// Write to clipboard440await electronApp.evaluate(async ({ clipboard }) => {441clipboard.writeText("Test clipboard content");442});443444// Paste in app445await window.getByRole("textbox").focus();446await window.keyboard.press("ControlOrMeta+v");447448// Read clipboard449const clipboardContent = await electronApp.evaluate(async ({ clipboard }) => {450return clipboard.readText();451});452453expect(clipboardContent).toBe("Test clipboard content");454});455```456457## Packaging & Distribution458459### Testing Packaged Apps460461```typescript462// fixtures/packaged-electron.ts463import { test as base, _electron as electron } from "@playwright/test";464import path from "path";465import { execSync } from "child_process";466467export const test = base.extend({468electronApp: async ({}, use) => {469// Build the app first (or use pre-built)470const distPath = path.join(__dirname, "../dist");471472let executablePath: string;473if (process.platform === "darwin") {474executablePath = path.join(475distPath,476"mac",477"MyApp.app",478"Contents",479"MacOS",480"MyApp",481);482} else if (process.platform === "win32") {483executablePath = path.join(distPath, "win-unpacked", "MyApp.exe");484} else {485executablePath = path.join(distPath, "linux-unpacked", "myapp");486}487488const electronApp = await electron.launch({ executablePath });489await use(electronApp);490await electronApp.close();491},492});493```494495## Anti-Patterns to Avoid496497| Anti-Pattern | Problem | Solution |498| ------------------------------------- | ---------------------------- | -------------------------------------------- |499| Not closing ElectronApplication | Resource leaks | Always call `electronApp.close()` in cleanup |500| Hardcoded executable paths | Breaks cross-platform | Use platform detection |501| Testing packaged app without building | Outdated code | Build before testing or test dev mode |502| Ignoring IPC in tests | Missing coverage | Test IPC communication explicitly |503| Not mocking native dialogs | Tests hang waiting for input | Mock dialog responses |504505## Related References506507- **Fixtures**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for custom fixture patterns508- **Component Testing**: See [component-testing.md](component-testing.md) for renderer testing patterns509- **Debugging**: See [debugging.md](../debugging/debugging.md) for troubleshooting510