Configure Playwright for comprehensive Electron application testing including E2E tests, visual regression, accessibility audits, and cross-platform test matrices
Configures Playwright for comprehensive Electron application testing including E2E, visual regression, and accessibility audits.
npx claudepluginhub a5c-ai/babysitterThis skill is limited to using the following tools:
README.mdConfigure 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.
_electron fixture{
"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"]
}
{
"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"]
}
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
// 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',
},
],
});
// 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-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` });
}
}
// 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+$/);
});
});
// 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');
});
});
// 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();
});
});
// 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/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
waitForLoadState() before interactionselectron-builder-config - Build configurationelectron-mock-factory - Mock Electron APIsvisual-regression-setup - Visual testing setupaccessibility-test-runner - Accessibility auditsdesktop-test-architect - Testing strategyui-automation-specialist - UI automation expertiseActivates when the user asks about AI prompts, needs prompt templates, wants to search for prompts, or mentions prompts.chat. Use for discovering, retrieving, and improving prompts.
Search, retrieve, and install Agent Skills from the prompts.chat registry using MCP tools. Use when the user asks to find skills, browse skill catalogs, install a skill for Claude, or extend Claude's capabilities with reusable AI agent components.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.