Babysitter playwright-electron-config
Configure Playwright for comprehensive Electron application testing including E2E tests, visual regression, accessibility audits, and cross-platform test matrices
install
source · Clone the upstream repo
git clone https://github.com/a5c-ai/babysitter
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/a5c-ai/babysitter "$T" && mkdir -p ~/.claude/skills && cp -r "$T/library/specializations/desktop-development/skills/playwright-electron-config" ~/.claude/skills/a5c-ai-babysitter-playwright-electron-config && rm -rf "$T"
manifest:
library/specializations/desktop-development/skills/playwright-electron-config/SKILL.mdsource content
playwright-electron-config
Configure Playwright for comprehensive Electron application testing. This skill sets up the complete testing infrastructure including E2E tests, visual regression testing, accessibility audits, and cross-platform test matrices with CI/CD integration.
Capabilities
- Configure Playwright for Electron with
fixture_electron - Generate page object models for Electron windows
- Set up visual regression testing with snapshots
- Configure accessibility testing with axe-core
- Create cross-platform test matrices for CI
- Mock Electron APIs (dialog, shell, clipboard)
- Test IPC communication between main and renderer
- Generate test coverage reports
Input Schema
{ "type": "object", "properties": { "projectPath": { "type": "string", "description": "Path to the Electron project root" }, "testDir": { "type": "string", "default": "tests/e2e" }, "features": { "type": "array", "items": { "enum": [ "visualRegression", "accessibility", "coverage", "performance", "ipcTesting", "multiWindow", "systemDialogMocks" ] }, "default": ["visualRegression", "accessibility", "ipcTesting"] }, "platforms": { "type": "array", "items": { "enum": ["windows", "macos", "linux"] }, "default": ["windows", "macos", "linux"] }, "pageObjects": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "selectors": { "type": "object" } } }, "description": "Page objects to generate" }, "ciIntegration": { "type": "object", "properties": { "provider": { "enum": ["github-actions", "azure-devops", "circleci", "gitlab"] }, "parallelization": { "type": "boolean", "default": true }, "sharding": { "type": "number", "description": "Number of shards" } } } }, "required": ["projectPath"] }
Output Schema
{ "type": "object", "properties": { "success": { "type": "boolean" }, "files": { "type": "array", "items": { "type": "object", "properties": { "path": { "type": "string" }, "type": { "enum": ["config", "fixture", "pageObject", "test", "helper", "ci"] } } } }, "commands": { "type": "object", "properties": { "runTests": { "type": "string" }, "updateSnapshots": { "type": "string" }, "showReport": { "type": "string" } } }, "ciWorkflow": { "type": "string", "description": "Path to generated CI workflow file" } }, "required": ["success", "files"] }
Generated File Structure
tests/ e2e/ playwright.config.ts # Main Playwright config fixtures/ electron-app.ts # Electron fixture test-utils.ts # Test utilities page-objects/ MainWindow.ts # Page object models SettingsDialog.ts specs/ app.spec.ts # Application tests ipc.spec.ts # IPC tests visual.spec.ts # Visual regression a11y.spec.ts # Accessibility tests mocks/ electron-api-mocks.ts # Electron API mocks ipc-mocks.ts # IPC mocks snapshots/ # Visual snapshots reports/ # Test reports
Code Templates
Playwright Configuration for Electron
// playwright.config.ts import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests/e2e/specs', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: [ ['html', { outputFolder: 'tests/e2e/reports' }], ['json', { outputFile: 'tests/e2e/reports/results.json' }], ], use: { trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, projects: [ { name: 'electron', testMatch: '**/*.spec.ts', }, ], });
Electron Test Fixture
// fixtures/electron-app.ts import { test as base, ElectronApplication, Page } from '@playwright/test'; import { _electron as electron } from 'playwright'; import path from 'path'; export type TestFixtures = { electronApp: ElectronApplication; mainWindow: Page; }; export const test = base.extend<TestFixtures>({ electronApp: async ({}, use) => { // Launch Electron app const electronApp = await electron.launch({ args: [path.join(__dirname, '../../dist/main/main.js')], env: { ...process.env, NODE_ENV: 'test', }, }); // Use the app in tests await use(electronApp); // Cleanup await electronApp.close(); }, mainWindow: 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';
Page Object Model
// page-objects/MainWindow.ts import { Page, Locator } from '@playwright/test'; export class MainWindow { readonly page: Page; readonly titleBar: Locator; readonly sidebar: Locator; readonly mainContent: Locator; readonly statusBar: Locator; constructor(page: Page) { this.page = page; this.titleBar = page.locator('[data-testid="title-bar"]'); this.sidebar = page.locator('[data-testid="sidebar"]'); this.mainContent = page.locator('[data-testid="main-content"]'); this.statusBar = page.locator('[data-testid="status-bar"]'); } async getTitle(): Promise<string> { return this.page.title(); } async openSettings(): Promise<void> { await this.page.click('[data-testid="settings-button"]'); await this.page.waitForSelector('[data-testid="settings-dialog"]'); } async navigateTo(section: string): Promise<void> { await this.sidebar.locator(`[data-section="${section}"]`).click(); await this.page.waitForLoadState('networkidle'); } async screenshot(name: string): Promise<Buffer> { return this.page.screenshot({ path: `tests/e2e/snapshots/${name}.png` }); } }
IPC Testing
// specs/ipc.spec.ts import { test, expect } from '../fixtures/electron-app'; test.describe('IPC Communication', () => { test('should send message to main process', async ({ electronApp, mainWindow }) => { // Evaluate in main process const result = await electronApp.evaluate(async ({ ipcMain }) => { return new Promise((resolve) => { ipcMain.once('test-channel', (event, data) => { resolve(data); }); }); }); // Send from renderer await mainWindow.evaluate(() => { window.electronAPI.send('test-channel', { message: 'hello' }); }); // Verify expect(result).toEqual({ message: 'hello' }); }); test('should receive response from main process', async ({ mainWindow }) => { const response = await mainWindow.evaluate(async () => { return window.electronAPI.invoke('get-app-version'); }); expect(response).toMatch(/^\d+\.\d+\.\d+$/); }); });
Visual Regression Testing
// specs/visual.spec.ts import { test, expect } from '../fixtures/electron-app'; import { MainWindow } from '../page-objects/MainWindow'; test.describe('Visual Regression', () => { test('main window matches snapshot', async ({ mainWindow }) => { const page = new MainWindow(mainWindow); // Wait for animations to complete await mainWindow.waitForTimeout(500); await expect(mainWindow).toHaveScreenshot('main-window.png', { maxDiffPixels: 100, }); }); test('dark mode matches snapshot', async ({ electronApp, mainWindow }) => { // Toggle dark mode via IPC await mainWindow.evaluate(() => { window.electronAPI.invoke('set-theme', 'dark'); }); await mainWindow.waitForTimeout(300); await expect(mainWindow).toHaveScreenshot('main-window-dark.png', { maxDiffPixels: 100, }); }); test('settings dialog matches snapshot', async ({ mainWindow }) => { const page = new MainWindow(mainWindow); await page.openSettings(); const dialog = mainWindow.locator('[data-testid="settings-dialog"]'); await expect(dialog).toHaveScreenshot('settings-dialog.png'); }); });
Accessibility Testing
// specs/a11y.spec.ts import { test, expect } from '../fixtures/electron-app'; import AxeBuilder from '@axe-core/playwright'; test.describe('Accessibility', () => { test('main window should have no accessibility violations', async ({ mainWindow }) => { const accessibilityScanResults = await new AxeBuilder({ page: mainWindow }) .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) .analyze(); expect(accessibilityScanResults.violations).toEqual([]); }); test('keyboard navigation should work', async ({ mainWindow }) => { // Tab through focusable elements await mainWindow.keyboard.press('Tab'); const firstFocused = await mainWindow.evaluate(() => document.activeElement?.getAttribute('data-testid') ); expect(firstFocused).toBeTruthy(); // Verify focus is visible const focusedElement = mainWindow.locator(':focus'); await expect(focusedElement).toBeVisible(); }); test('screen reader announcements should be correct', async ({ mainWindow }) => { // Check ARIA labels const button = mainWindow.locator('[data-testid="save-button"]'); await expect(button).toHaveAttribute('aria-label'); // Check live regions const liveRegion = mainWindow.locator('[aria-live="polite"]'); await expect(liveRegion).toBeAttached(); }); });
Electron API Mocks
// mocks/electron-api-mocks.ts import { ElectronApplication } from '@playwright/test'; export async function mockDialog( electronApp: ElectronApplication, response: { filePaths?: string[]; canceled?: boolean } ) { await electronApp.evaluate( async ({ dialog }, response) => { dialog.showOpenDialog = async () => response; dialog.showSaveDialog = async () => ({ filePath: response.filePaths?.[0], canceled: response.canceled ?? false, }); }, response ); } export async function mockClipboard( electronApp: ElectronApplication, content: string ) { await electronApp.evaluate( async ({ clipboard }, content) => { clipboard.readText = () => content; clipboard.writeText = () => {}; }, content ); } export async function mockShell(electronApp: ElectronApplication) { const openedUrls: string[] = []; await electronApp.evaluate(async ({ shell }) => { shell.openExternal = async (url) => { // Track in test return true; }; }); return { openedUrls }; }
GitHub Actions CI Workflow
# .github/workflows/e2e-tests.yml name: E2E Tests on: push: branches: [main] pull_request: branches: [main] jobs: test: strategy: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] shardIndex: [1, 2, 3, 4] shardTotal: [4] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - name: Install dependencies run: npm ci - name: Build Electron app run: npm run build - name: Install Playwright run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload test artifacts uses: actions/upload-artifact@v4 if: always() with: name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }} path: tests/e2e/reports/ - name: Upload snapshots uses: actions/upload-artifact@v4 if: failure() with: name: snapshots-${{ matrix.os }}-${{ matrix.shardIndex }} path: tests/e2e/snapshots/ merge-reports: needs: test runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: pattern: playwright-report-* merge-multiple: true path: merged-reports - name: Merge reports run: npx playwright merge-reports merged-reports --reporter html
Best Practices
- Use data-testid attributes - Stable selectors for test automation
- Wait for app ready state - Use
before interactionswaitForLoadState() - Mock external dependencies - Dialog, shell, network calls
- Run tests in isolation - Fresh app instance per test when needed
- Use visual snapshots carefully - Allow pixel tolerance for CI variations
- Test on actual platforms - Cross-platform matrix in CI
Community References
Related Skills
- Build configurationelectron-builder-config
- Mock Electron APIselectron-mock-factory
- Visual testing setupvisual-regression-setup
- Accessibility auditsaccessibility-test-runner
Related Agents
- Testing strategydesktop-test-architect
- UI automation expertiseui-automation-specialist