agentsclimarketplace

Electron e2e cdp testing

Skill 14790897/my-skills/skills/electron-e2e-cdp-testing

Install
npx -y skills add 14790897/my-skills --skill electron-e2e-cdp-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Electron 34+ 桌面应用端到端测试技能。当需要为 Electron 应用编写或调试 E2E 测试、遇到 Playwright _electron.launch 不可用、CI 中 Chromium SIGTRAP 崩溃、ELECTRON_RUN_AS_NODE 导致 Electron 以 Node 模式运行、或需要 CDP 远程调试连接方案时使用此技能。涵盖 CDP 连接、进程管理、跨平台 kill、CI sandbox 处理、concurrency 策略。适用于 Electron 34 及以上版本。

SKILL.md

9.6 KB, as published. Nobody here has run it

Electron E2E CDP 测试

概述

Electron 34+ 移除了 --remote-debugging-port CLI 标志支持,导致 Playwright 的 _electron.launch() 不可用。本技能提供基于 CDP(Chrome DevTools Protocol)的完整测试方案,包括本地运行、CI 集成和常见坑点排查。

何时使用

  • 为 Electron 34+ 桌面应用编写 E2E 测试
  • _electron.launch() 报错或 Electron 启动后立即退出
  • CI 中 Electron 测试出现 SIGTRAP / sandbox 崩溃
  • 需要在 CI 中运行真实 Electron 的完整 E2E 测试

核心方案:CDP 连接替代 _electron.launch()

原理

旧方案(Electron 33 及以下)新方案(Electron 34+)
Playwright 传 --remote-debugging-port CLI 标志app.commandLine.appendSwitch() + chromium.connectOverCDP()
_electron.launch() 一键启动手动 spawn Electron binary + CDP 连接

实施步骤

1. 主进程添加 CDP 端口开关

src/main/index.tsmain() 函数最开头(app.whenReady() 之前)添加:

export function main(): void {
  // 必须在 app.whenReady() 之前调用 —— Chromium 初始化时读取此 switch
  if (process.env.E2E_TEST) {
    app.commandLine.appendSwitch("remote-debugging-port", "8315");
  }
  app.whenReady().then(() => {
    // ...
  });
}

关键: 用环境变量控制,生产构建不受影响。

2. 测试文件中 spawn Electron + CDP 连接

import { test, expect, chromium } from "@playwright/test";
import { spawn, execSync, type ChildProcess } from "node:child_process";
import { resolve } from "node:path";

const CDP_PORT = 8315;
const CDP_URL = `http://localhost:${CDP_PORT}`;
const BOOT_TIMEOUT = 90_000;

const ELECTRON_BIN = require("electron") as string;

// 等待 CDP 端点可用
async function waitForCDP(timeout = 60_000): Promise<void> {
  const start = Date.now();
  while (Date.now() - start < timeout) {
    try {
      const res = await fetch(`${CDP_URL}/json/version`, {
        signal: AbortSignal.timeout(2000),
      });
      if (res.ok) return;
    } catch {
      /* 未就绪 */
    }
    await new Promise((r) => setTimeout(r, 500));
  }
  throw new Error(
    `CDP endpoint at ${CDP_URL} not available within ${timeout}ms`,
  );
}

// 跨平台杀进程树
function killProcessTree(proc: ChildProcess): void {
  if (!proc || proc.killed) return;
  const pid = proc.pid;
  if (!pid) return;
  try {
    if (process.platform === "win32") {
      execSync(`taskkill /pid ${pid} /T /F`, { stdio: "ignore" });
    } else {
      proc.kill("SIGTERM");
      setTimeout(() => {
        try {
          proc.kill("SIGKILL");
        } catch {}
      }, 3000);
    }
  } catch {
    try {
      proc.kill("SIGKILL");
    } catch {}
  }
}

test.beforeAll(async () => {
  // ⚠️ 关键 1:删除 ELECTRON_RUN_AS_NODE(见坑点 1)
  const env = { ...process.env, E2E_TEST: "1" };
  delete env.ELECTRON_RUN_AS_NODE;

  // ⚠️ 关键 2:CI 环境必须传 --no-sandbox 等参数(见坑点 2)
  const electronArgs = [
    ".",
    "--no-sandbox",
    "--disable-gpu",
    "--disable-dev-shm-usage",
  ];

  proc = spawn(ELECTRON_BIN, electronArgs, {
    cwd: APP_DIR,
    env,
    stdio: ["ignore", "pipe", "pipe"],
  });

  proc.stdout?.on("data", (d) => process.stdout.write(`[electron:out] ${d}`));
  proc.stderr?.on("data", (d) => process.stderr.write(`[electron:err] ${d}`));

  await waitForCDP(BOOT_TIMEOUT);

  browser = await chromium.connectOverCDP(CDP_URL);
  const context = browser.contexts()[0];
  page = context.pages()[0] || (await context.newPage());
}, BOOT_TIMEOUT + 60000);

test.afterAll(async () => {
  await browser?.close().catch(() => {});
  if (proc) killProcessTree(proc);
});

3 大通用坑点

坑 1: ELECTRON_RUN_AS_NODE=1 环境变量继承

症状: Electron 启动后立即退出(exit code=0),CDP 端点永不可用,无 Chromium 窗口。

根因: VSCode、WorkBuddy 等 Electron IDE 会在宿主环境中设置 ELECTRON_RUN_AS_NODE=1。子进程通过 spawn({ env: { ...process.env } }) 继承此变量后,Electron 以纯 Node.js 模式运行,完全绕过 Chromium runtime,因此不会开启 CDP 端口。

修复:

const env = { ...process.env, E2E_TEST: "1" };
delete env.ELECTRON_RUN_AS_NODE; // 必须 delete,不能设为 '0' 或 ''

坑 2: CI Chromium SIGTRAP 崩溃

症状: CI 日志显示 The SUID sandbox helper binary was found, but is not configured correctly,进程以 SIGTRAP 退出。本地运行正常,仅 CI 失败。

根因: GitHub Actions Ubuntu runner 以 root 用户运行。Chromium 的 sandbox 需要 setuid helper binary(chrome-sandbox)具有 mode 4755 且 owned by root,但 npm 安装的 Electron 中此 binary 权限不正确。

修复: spawn 时传 CLI 参数(不能用 appendSwitch):

const electronArgs = ['.', '--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage']
proc = spawn(ELECTRON_BIN, electronArgs, { ... })

关键: --no-sandbox 必须作为 CLI 参数传递。app.commandLine.appendSwitch('no-sandbox') 不起作用,因为 Chromium 的 SUID 检查在 JavaScript 初始化之前执行。

附加: Ubuntu CI 无显示器,需 xvfb-run 提供虚拟 X server:

- run: xvfb-run --auto-servernum npx playwright test --project=electron

坑 3: CI concurrency 误杀长测试

症状: E2E 测试运行到一半被取消,日志显示 The operation was canceled,quick job 通过但 electron-e2e 显示 cancelled。

根因: concurrency.cancel-in-progress: true + 同一 concurrency group,导致新 push 取消正在运行的 E2E 测试。

修复:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}-${{ github.job }} # 加 job 名
  cancel-in-progress: false # 不取消正在运行的

同时 timeout-minutes 设为 30(E2E + LLM 调用较慢)。

通用原则:测试 mock 与生产代码同步

当子进程管理类生产代码(如 bridge、worker)的启动流程变更后,测试 mock 必须同步:

  1. grep 生产代码中所有对 mock 对象的方法调用(如 this.process.once(...)stdin.writable),确保 mock 补齐所有被调用的方法和属性
  2. 测试 helper 按生产流程顺序 feed 信号(spawn → 就绪信号 → 初始化响应),不能跳步
  3. timing 要匹配:生产代码中的 setTimeout 等待,测试中对应 await new Promise(r => setTimeout(r, ...))

CI 配置参考

name: Desktop CI

on:
  push:
    paths: ["apps/desktop/**", ".github/workflows/desktop-ci.yml"]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}-${{ github.job }}
  cancel-in-progress: false

jobs:
  electron-e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          {
            node-version: 22,
            cache: npm,
            cache-dependency-path: apps/desktop/package-lock.json,
          }
      - run: npm ci
        working-directory: apps/desktop
      - run: npx playwright install chromium --with-deps
        working-directory: apps/desktop
      - run: npm run build
        working-directory: apps/desktop
      - name: Run Electron E2E
        run: xvfb-run --auto-servernum npx playwright test --project=electron
        working-directory: apps/desktop
        timeout-minutes: 30

playwright.config.ts 参考

import { defineConfig } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  projects: [
    {
      name: "electron",
      testMatch: ["full-electron.spec.ts"],
      timeout: 300000, // 5 min — Electron boot 较慢
      // 不需要 headless 配置 —— connectOverCDP 自己处理
    },
  ],
  webServer: {
    command: "python -m http.server 3458 --directory out/renderer",
    url: "http://localhost:3458",
    reuseExistingServer: true, // 避免 port 冲突
  },
});

排查清单

当 Electron E2E 测试失败时,按以下顺序排查:

  1. Electron 是否以 Node.js 模式运行? → 检查 ELECTRON_RUN_AS_NODE 是否已 delete
  2. CDP 端点是否可用? → 本地访问 http://localhost:8315/json/version
  3. CI 是否 SIGTRAP? → 检查 --no-sandbox 是否作为 CLI 参数传递(非 appendSwitch)
  4. 测试是否被取消? → 检查 concurrency group 是否含 job 名、cancel-in-progress 是否 false
  5. mock 是否同步? → 对比生产代码启动流程与测试 mock 的方法签名和信号 feed 顺序

参考资料

https://github.com/stevez/playwright-repl/issues/555 https://github.com/microsoft/playwright/issues/39008

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.