Agent skill

playwright-electron-config

Configure Playwright for comprehensive Electron application testing including E2E tests, visual regression, accessibility audits, and cross-platform test matrices

Stars 514
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/a5c-ai/babysitter/tree/main/library/specializations/desktop-development/skills/playwright-electron-config

SKILL.md

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 _electron fixture
  • 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

json
{
  "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

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"]
}

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

typescript
// 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

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';

Page Object Model

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` });
  }
}

IPC Testing

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+$/);
  });
});

Visual Regression Testing

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');
  });
});

Accessibility Testing

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();
  });
});

Electron API Mocks

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 };
}

GitHub Actions CI Workflow

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

Best Practices

  1. Use data-testid attributes - Stable selectors for test automation
  2. Wait for app ready state - Use waitForLoadState() before interactions
  3. Mock external dependencies - Dialog, shell, network calls
  4. Run tests in isolation - Fresh app instance per test when needed
  5. Use visual snapshots carefully - Allow pixel tolerance for CI variations
  6. Test on actual platforms - Cross-platform matrix in CI

Community References

Related Skills

  • electron-builder-config - Build configuration
  • electron-mock-factory - Mock Electron APIs
  • visual-regression-setup - Visual testing setup
  • accessibility-test-runner - Accessibility audits

Related Agents

  • desktop-test-architect - Testing strategy
  • ui-automation-specialist - UI automation expertise

Expand your agent's capabilities with these related and highly-rated skills.

a5c-ai/babysitter

gsd-tools

Central utility skill for GSD operations. Provides config parsing, slug generation, timestamps, path operations, and orchestrates calls to other specialized skills. Acts as the unified entry point that the original gsd-tools.cjs provided via its lib/ modules (commands, config, core, init).

514 31
Explore
a5c-ai/babysitter

model-profile-resolution

Resolve model profile (quality/balanced/budget) at orchestration start and map agents to specific models. Enables cost/quality tradeoffs by selecting appropriate AI models for each agent role.

514 31
Explore
a5c-ai/babysitter

verification-suite

Plan structure validation, phase completeness checks, reference integrity verification, and artifact existence confirmation. Provides the structured verification layer ensuring GSD artifacts are well-formed and complete.

514 31
Explore
a5c-ai/babysitter

state-management

STATE.md reading, writing, and field-level updates. Provides cross-session state persistence via .planning/STATE.md with structured fields for current task, completed phases, blockers, decisions, and quick tasks.

514 31
Explore
a5c-ai/babysitter

git-integration

Git commit patterns, formats, and conventions for GSD methodology. Provides atomic commits per task, structured commit messages, planning file commits, branch management, and milestone tag operations.

514 31
Explore
a5c-ai/babysitter

frontmatter-parsing

YAML frontmatter parsing and manipulation for .planning/ documents. Provides read, write, update, query, and validation operations on frontmatter blocks in GSD markdown artifacts.

514 31
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results