playwright-electron-config playwright-electron-config

全面配置Playwright以自动化测试Electron应用,包括端到端、视觉回归、可访问性测试和跨平台支持

测试 0 次安装 0 次浏览 更新于 2/25/2026

playwright-electron-config

配置Playwright以全面测试Electron应用程序,包括端到端测试、视觉回归测试、可访问性审计和跨平台测试矩阵。

功能

  • 为Electron配置Playwright的_electron固定装置
  • 为Electron窗口生成页面对象模型
  • 设置视觉回归测试与快照
  • 配置可访问性测试与axe-core
  • 创建跨平台测试矩阵用于CI
  • 模拟Electron API(对话框、外壳、剪贴板)
  • 测试主进程和渲染进程之间的IPC通信
  • 生成测试覆盖率报告

输入模式

{
  "type": "object",
  "properties": {
    "projectPath": {
      "type": "string",
      "description": "Electron项目根路径"
    },
    "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": "要生成的页面对象"
    },
    "ciIntegration": {
      "type": "object",
      "properties": {
        "provider": { "enum": ["github-actions", "azure-devops", "circleci", "gitlab"] },
        "parallelization": { "type": "boolean", "default": true },
        "sharding": { "type": "number", "description": "分片数量" }
      }
    }
  },
  "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": "生成的CI工作流文件路径"
    }
  },
  "required": ["success", "files"]
}

生成文件结构

tests/
  e2e/
    playwright.config.ts     # 主Playwright配置
    fixtures/
      electron-app.ts        # Electron固定装置
      test-utils.ts          # 测试工具
    page-objects/
      MainWindow.ts          # 页面对象模型
      SettingsDialog.ts
    specs/
      app.spec.ts            # 应用程序测试
      ipc.spec.ts            # IPC测试
      visual.spec.ts         # 视觉回归
      a11y.spec.ts           # 可访问性测试
    mocks/
      electron-api-mocks.ts  # Electron API模拟
      ipc-mocks.ts           # IPC模拟
    snapshots/               # 视觉快照
    reports/                 # 测试报告

代码模板

Electron的Playwright配置

// 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测试固定装置

// 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) => {
    // 启动Electron应用程序
    const electronApp = await electron.launch({
      args: [path.join(__dirname, '../../dist/main/main.js')],
      env: {
        ...process.env,
        NODE_ENV: 'test',
      },
    });

    // 在测试中使用应用程序
    await use(electronApp);

    // 清理
    await electronApp.close();
  },

  mainWindow: async ({ electronApp }, use) => {
    // 等待第一个窗口
    const window = await electronApp.firstWindow();

    // 等待应用程序准备就绪
    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` });
  }
}

IPC测试

// specs/ipc.spec.ts
import { test, expect } from '../fixtures/electron-app';

test.describe('IPC通信', () => {
  test('应向主进程发送消息', async ({ electronApp, mainWindow }) => {
    // 在主进程中评估
    const result = await electronApp.evaluate(async ({ ipcMain }) => {
      return new Promise((resolve) => {
        ipcMain.once('test-channel', (event, data) => {
          resolve(data);
        });
      });
    });

    // 从渲染器发送
    await mainWindow.evaluate(() => {
      window.electronAPI.send('test-channel', { message: 'hello' });
    });

    // 验证
    expect(result).toEqual({ message: 'hello' });
  });

  test('应从主进程接收响应', 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('视觉回归', () => {
  test('主窗口与快照匹配', async ({ mainWindow }) => {
    const page = new MainWindow(mainWindow);

    // 等待动画完成
    await mainWindow.waitForTimeout(500);

    await expect(mainWindow).toHaveScreenshot('main-window.png', {
      maxDiffPixels: 100,
    });
  });

  test('暗色模式与快照匹配', async ({ electronApp, mainWindow }) => {
    // 通过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('设置对话框与快照匹配', 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('可访问性', () => {
  test('主窗口不应有可访问性违规', async ({ mainWindow }) => {
    const accessibilityScanResults = await new AxeBuilder({ page: mainWindow })
      .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
      .analyze();

    expect(accessibilityScanResults.violations).toEqual([]);
  });

  test('键盘导航应工作', async ({ mainWindow }) => {
    // 通过Tab键遍历可聚焦元素
    await mainWindow.keyboard.press('Tab');
    const firstFocused = await mainWindow.evaluate(() =>
      document.activeElement?.getAttribute('data-testid')
    );
    expect(firstFocused).toBeTruthy();

    // 验证焦点可见
    const focusedElement = mainWindow.locator(':focus');
    await expect(focusedElement).toBeVisible();
  });

  test('屏幕阅读器公告应正确', async ({ mainWindow }) => {
    // 检查ARIA标签
    const button = mainWindow.locator('[data-testid="save-button"]');
    await expect(button).toHaveAttribute('aria-label');

    // 检查实时区域
    const liveRegion = mainWindow.locator('[aria-live="polite"]');
    await expect(liveRegion).toBeAttached();
  });
});

Electron API模拟

// 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) => {
      // 在测试中跟踪
      return true;
    };
  });

  return { openedUrls };
}

GitHub Actions CI工作流

# .github/workflows/e2e-tests.yml
name: E2E测试

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: 安装依赖
        run: npm ci

      - name: 构建Electron应用程序
        run: npm run build

      - name: 安装Playwright
        run: npx playwright install --with-deps

      - name: 运行Playwright测试
        run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

      - name: 上传测试工件
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report-${{ matrix.os }}-${{ matrix.shardIndex }}
          path: tests/e2e/reports/

      - name: 上传快照
        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: 合并报告
        run: npx playwright merge-reports merged-reports --reporter html

最佳实践

  1. 使用data-testid属性 - 稳定的选择器用于测试自动化
  2. 等待应用程序就绪状态 - 在交互之前使用waitForLoadState()
  3. 模拟外部依赖项 - 对话框、外壳、网络调用
  4. 在隔离中运行测试 - 按需为每个测试提供新的应用程序实例
  5. 谨慎使用视觉快照 - 为CI变化允许像素公差
  6. 在实际平台上测试 - CI中的跨平台矩阵

社区参考

相关技能

  • electron-builder-config - 构建配置
  • electron-mock-factory - 模拟Electron API
  • visual-regression-setup - 视觉测试设置
  • accessibility-test-runner - 可访问性审计

相关代理

  • desktop-test-architect - 测试策略
  • ui-automation-specialist - UI自动化专长