.agents/skills/playwright-best-practices/testing-patterns/electron.md
npm install -D @playwright/test electron
// playwright.config.ts
import {defineConfig} from '@playwright/test'
export default defineConfig({
testDir: './tests',
timeout: 30000,
use: {
trace: 'on-first-retry',
},
})
// fixtures/electron.ts
import {test as base, _electron as electron, ElectronApplication, Page} from '@playwright/test'
type ElectronFixtures = {
electronApp: ElectronApplication
window: Page
}
export const test = base.extend<ElectronFixtures>({
electronApp: async ({}, use) => {
// Launch Electron app
const electronApp = await electron.launch({
args: ['.', '--no-sandbox'],
env: {
...process.env,
NODE_ENV: 'test',
},
})
await use(electronApp)
// Cleanup
await electronApp.close()
},
window: async ({electronApp}, use) => {
// Wait for first window
const window = await electronApp.firstWindow()
// Wait for app to be ready
await window.waitForLoadState('domcontentloaded')
await use(window)
},
})
export {expect} from '@playwright/test'
// Advanced launch configuration
const electronApp = await electron.launch({
args: ['main.js', '--custom-flag'],
cwd: '/path/to/app',
env: {
...process.env,
ELECTRON_ENABLE_LOGGING: '1',
NODE_ENV: 'test',
},
timeout: 30000,
// For packaged apps
executablePath: '/path/to/MyApp.app/Contents/MacOS/MyApp',
})
test('launch in dev mode', async () => {
const electronApp = await electron.launch({
args: ['.'], // Points to package.json main
})
const window = await electronApp.firstWindow()
await expect(window.locator('h1')).toContainText('My App')
await electronApp.close()
})
test('launch packaged app', async () => {
const appPath =
process.platform === 'darwin'
? '/Applications/MyApp.app/Contents/MacOS/MyApp'
: process.platform === 'win32'
? 'C:\\Program Files\\MyApp\\MyApp.exe'
: '/usr/bin/myapp'
const electronApp = await electron.launch({
executablePath: appPath,
})
const window = await electronApp.firstWindow()
await expect(window).toHaveTitle(/MyApp/)
await electronApp.close()
})
test('handle multiple windows', async ({electronApp}) => {
const mainWindow = await electronApp.firstWindow()
// Trigger new window
await mainWindow.getByRole('button', {name: 'Open Settings'}).click()
// Wait for new window
const settingsWindow = await electronApp.waitForEvent('window')
// Both windows are now accessible
await expect(settingsWindow.locator('h1')).toHaveText('Settings')
await expect(mainWindow.locator('h1')).toHaveText('Main')
// Get all windows
const windows = electronApp.windows()
expect(windows.length).toBe(2)
})
test('access main process', async ({electronApp}) => {
// Evaluate in main process context
const appPath = await electronApp.evaluate(async ({app}) => {
return app.getAppPath()
})
expect(appPath).toContain('my-electron-app')
})
test('electron API access', async ({electronApp}) => {
// Get app version
const version = await electronApp.evaluate(async ({app}) => {
return app.getVersion()
})
expect(version).toMatch(/^\d+\.\d+\.\d+$/)
// Get platform info
const platform = await electronApp.evaluate(async ({app}) => {
return process.platform
})
expect(['darwin', 'win32', 'linux']).toContain(platform)
// Check if app is ready
const isReady = await electronApp.evaluate(async ({app}) => {
return app.isReady()
})
expect(isReady).toBe(true)
})
test('check window properties', async ({electronApp, window}) => {
// Get BrowserWindow from main process
const windowBounds = await electronApp.evaluate(async ({BrowserWindow}) => {
const win = BrowserWindow.getAllWindows()[0]
return win.getBounds()
})
expect(windowBounds.width).toBeGreaterThan(0)
expect(windowBounds.height).toBeGreaterThan(0)
// Check window state
const isMaximized = await electronApp.evaluate(async ({BrowserWindow}) => {
const win = BrowserWindow.getAllWindows()[0]
return win.isMaximized()
})
// Check window title
const title = await electronApp.evaluate(async ({BrowserWindow}) => {
const win = BrowserWindow.getAllWindows()[0]
return win.getTitle()
})
expect(title).toBeTruthy()
})
test('renderer interactions', async ({window}) => {
// Standard Playwright page interactions
await window.getByRole('button', {name: 'Click Me'}).click()
await expect(window.getByText('Clicked!')).toBeVisible()
// Fill forms
await window.getByLabel('Username').fill('testuser')
await window.getByLabel('Password').fill('password123')
await window.getByRole('button', {name: 'Login'}).click()
// Verify navigation
await expect(window).toHaveURL(/dashboard/)
})
test('node integration', async ({window}) => {
// If nodeIntegration is enabled
const nodeVersion = await window.evaluate(() => {
return (window as any).process?.version
})
// Check if Node APIs are available
const hasFs = await window.evaluate(() => {
return typeof (window as any).require === 'function'
})
})
test('context isolation', async ({window}) => {
// Test preload script exposed APIs
const apiAvailable = await window.evaluate(() => {
return typeof (window as any).electronAPI !== 'undefined'
})
expect(apiAvailable).toBe(true)
// Call exposed API
const result = await window.evaluate(async () => {
return await (window as any).electronAPI.getAppVersion()
})
expect(result).toMatch(/^\d+\.\d+\.\d+$/)
})
test('IPC invoke', async ({window}) => {
// Test preload-exposed IPC call
const result = await window.evaluate(async () => {
return await (window as any).electronAPI.getData('user-settings')
})
expect(result).toHaveProperty('theme')
})
test('main to renderer IPC', async ({electronApp, window}) => {
// Set up listener in renderer
await window.evaluate(() => {
;(window as any).receivedMessage = null
;(window as any).electronAPI.onMessage((msg: string) => {
;(window as any).receivedMessage = msg
})
})
// Send from main process
await electronApp.evaluate(async ({BrowserWindow}) => {
const win = BrowserWindow.getAllWindows()[0]
win.webContents.send('message', 'Hello from main!')
})
// Verify receipt
await window.waitForFunction(() => (window as any).receivedMessage !== null)
const message = await window.evaluate(() => (window as any).receivedMessage)
expect(message).toBe('Hello from main!')
})
// In test setup or fixture
test('mock IPC handler', async ({electronApp, window}) => {
// Override IPC handler in main process
await electronApp.evaluate(async ({ipcMain}) => {
// Remove existing handler
ipcMain.removeHandler('fetch-data')
// Add mock handler
ipcMain.handle('fetch-data', async () => {
return {mocked: true, data: 'test-data'}
})
})
// Test with mocked handler
const result = await window.evaluate(async () => {
return await (window as any).electronAPI.fetchData()
})
expect(result.mocked).toBe(true)
})
test('file dialog', async ({electronApp, window}) => {
// Mock dialog response
await electronApp.evaluate(async ({dialog}) => {
dialog.showOpenDialog = async () => ({
canceled: false,
filePaths: ['/mock/path/file.txt'],
})
})
// Trigger file open
await window.getByRole('button', {name: 'Open File'}).click()
// Verify file was "opened"
await expect(window.getByText('file.txt')).toBeVisible()
})
test('save dialog', async ({electronApp, window}) => {
await electronApp.evaluate(async ({dialog}) => {
dialog.showSaveDialog = async () => ({
canceled: false,
filePath: '/mock/path/saved-file.txt',
})
})
await window.getByRole('button', {name: 'Save'}).click()
await expect(window.getByText('Saved successfully')).toBeVisible()
})
test('application menu', async ({electronApp}) => {
// Get menu structure
const menuLabels = await electronApp.evaluate(async ({Menu}) => {
const menu = Menu.getApplicationMenu()
return menu?.items.map((item) => item.label) || []
})
expect(menuLabels).toContain('File')
expect(menuLabels).toContain('Edit')
// Trigger menu action
await electronApp.evaluate(async ({Menu}) => {
const menu = Menu.getApplicationMenu()
const fileMenu = menu?.items.find((item) => item.label === 'File')
const newItem = fileMenu?.submenu?.items.find((item) => item.label === 'New')
newItem?.click()
})
})
test('notifications', async ({electronApp, window}) => {
// Mock Notification
let notificationShown = false
await electronApp.evaluate(async ({Notification}) => {
const OriginalNotification = Notification
;(global as any).Notification = class extends OriginalNotification {
constructor(options: any) {
super(options)
;(global as any).lastNotification = options
}
}
})
// Trigger notification
await window.getByRole('button', {name: 'Notify'}).click()
// Verify notification was created
const notification = await electronApp.evaluate(async () => {
return (global as any).lastNotification
})
expect(notification.title).toBe('New Message')
})
test('clipboard operations', async ({electronApp, window}) => {
// Write to clipboard
await electronApp.evaluate(async ({clipboard}) => {
clipboard.writeText('Test clipboard content')
})
// Paste in app
await window.getByRole('textbox').focus()
await window.keyboard.press('ControlOrMeta+v')
// Read clipboard
const clipboardContent = await electronApp.evaluate(async ({clipboard}) => {
return clipboard.readText()
})
expect(clipboardContent).toBe('Test clipboard content')
})
// fixtures/packaged-electron.ts
import {test as base, _electron as electron} from '@playwright/test'
import path from 'path'
import {execSync} from 'child_process'
export const test = base.extend({
electronApp: async ({}, use) => {
// Build the app first (or use pre-built)
const distPath = path.join(__dirname, '../dist')
let executablePath: string
if (process.platform === 'darwin') {
executablePath = path.join(distPath, 'mac', 'MyApp.app', 'Contents', 'MacOS', 'MyApp')
} else if (process.platform === 'win32') {
executablePath = path.join(distPath, 'win-unpacked', 'MyApp.exe')
} else {
executablePath = path.join(distPath, 'linux-unpacked', 'myapp')
}
const electronApp = await electron.launch({executablePath})
await use(electronApp)
await electronApp.close()
},
})
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Not closing ElectronApplication | Resource leaks | Always call electronApp.close() in cleanup |
| Hardcoded executable paths | Breaks cross-platform | Use platform detection |
| Testing packaged app without building | Outdated code | Build before testing or test dev mode |
| Ignoring IPC in tests | Missing coverage | Test IPC communication explicitly |
| Not mocking native dialogs | Tests hang waiting for input | Mock dialog responses |