Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Configure Playwright for comprehensive Electron application testing including E2E tests, visual regression, accessibility audits, and cross-platform test matrices
.claude/skills/a5c-ai-playwright-electron-config/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 59% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 132% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 427% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 203% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 224% | 0% |
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.
_electron fixturejson{ "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"] }
json{ "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 reportstypescript// 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', }, ], });
typescript// 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';
typescript// 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` }); } }
typescript// 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+$/); }); });
typescript// 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'); }); });
typescript// 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(); }); });
typescript// 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 }; }
yaml# .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 expertise| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 18,998 | 32,245 | +70% | 1 | 1 | 0% | 4,880 | 4,656 | -5% | 0 | 0 | — |
case-02 | fail→pass | 22,888 | 12,687 | -45% | 1 | 1 | 0% | 4,459 | 7,095 | +59% | 0 | 0 | — |
case-03 | fail→fail | 9,582 | 6,835 | -29% | 1 | 1 | 0% | 2,080 | 5,118 | +146% | 0 | 0 | — |
case-04 | pass→pass | 9,530 | 6,397 | -33% | 1 | 1 | 0% | 1,869 | 5,063 | +171% | 0 | 0 | — |
case-05 | pass→pass | 15,124 | 10,726 | -29% | 1 | 1 | 0% | 2,394 | 5,507 | +130% | 0 | 0 | — |
case-06 | pass→pass | 13,324 | 8,680 | -35% | 1 | 1 | 0% | 2,162 | 5,562 | +157% | 0 | 0 | — |
case-07 | pass→pass | 11,793 | 10,026 | -15% | 1 | 1 | 0% | 2,489 | 5,769 | +132% | 0 | 0 | — |
case-08 | fail→pass | 12,987 | 11,294 | -13% | 1 | 1 | 0% | 2,539 | 5,888 | +132% | 0 | 0 | — |
case-09 | pass→pass | 12,904 | 10,358 | -20% | 1 | 1 | 0% | 2,379 | 5,386 | +126% | 0 | 0 | — |
case-10 | fail→pass | 4,891 | 4,841 | -1% | 1 | 1 | 0% | 896 | 4,725 | +427% | 0 | 0 | — |
case-11 | pass→pass | 12,324 | 7,656 | -38% | 1 | 1 | 0% | 2,498 | 5,538 | +122% | 0 | 0 | — |
case-12 | fail→pass | 9,030 | 3,193 | -65% | 1 | 1 | 0% | 1,481 | 4,481 | +203% | 0 | 0 | — |
case-13 | fail→pass | 7,990 | 2,382 | -70% | 1 | 1 | 0% | 1,312 | 4,254 | +224% | 0 | 0 | — |
case-14 | pass→fail | 12,960 | 7,553 | -42% | 1 | 1 | 0% | 2,375 | 5,247 | +121% | 0 | 0 | — |
case-15 | pass→pass | 14,303 | 7,860 | -45% | 1 | 1 | 0% | 2,377 | 5,469 | +130% | 0 | 0 | — |
case-16 | pass→pass | 12,008 | 7,516 | -37% | 1 | 1 | 0% | 2,170 | 5,122 | +136% | 0 | 0 | — |
case-17 | pass→pass | 8,688 | 3,128 | -64% | 1 | 1 | 0% | 1,599 | 4,282 | +168% | 0 | 0 | — |
case-18 | fail→pass | 2,856 | 1,507 | -47% | 1 | 1 | 0% | 529 | 3,974 | +651% | 0 | 0 | — |
case-19 | fail→fail | 14,364 | 13,155 | -8% | 1 | 1 | 0% | 3,069 | 6,435 | +110% | 0 | 0 | — |
case-20 | fail→fail | 14,581 | 12,829 | -12% | 1 | 1 | 0% | 3,125 | 6,641 | +113% | 0 | 0 | — |
case-21 | fail→fail | 13,813 | 19,085 | +38% | 1 | 1 | 0% | 1,608 | 6,015 | +274% | 0 | 0 | — |
case-22 | fail→pass | 14,614 | 1,886 | -87% | 1 | 1 | 0% | 2,062 | 4,077 | +98% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +27 percentage points is the difference between those two pass rates over the 21 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.