跳转到主要内容
用可重复的端到端测试在多个浏览器中验证扩展行为。 Extension.js 项目可以使用 Playwright 来测试扩展流程、UI 渲染与集成行为,适用于持续集成 (CI) 与本地环境。

Playwright 测试能力

能力给你带来什么
跨浏览器运行时检查在 Chromium 与 Firefox 引擎上验证核心流程
UI 与交互覆盖在真实浏览器上下文中测试 popup/options/content-script 行为
适合 CI 的报告为失败的测试捕获 trace/截图/录像
回归安全捕获单元测试通常错过的集成 bug

为什么使用

  • 捕获单元测试错过的运行时回归。
  • 在真实浏览器引擎上验证扩展行为。
  • 在发布前验证多浏览器变更。

典型设置

在项目中安装 Playwright 测试依赖: 创建 playwright.config.ts,定义浏览器项目与报告方式。

推荐基线

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

export default defineConfig({
  testDir: "e2e",
  retries: process.env.CI ? 2 : 0,
  reporter: [["html"], ["list"]],
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
  ],
});

自动化契约(推荐)

为了确定性自动化,不要解析终端文本。请使用 Extension.js 生成的元数据文件:
  • dist/extension-js/<browser>/ready.json
  • dist/extension-js/<browser>/events.ndjson(watch/重建事件的换行分隔 JSON)
ready.json 包含为脚本/Agent 设计的稳定字段:
  • statusstarting | ready | error
  • commanddev | start | preview
  • browser
  • distPath
  • manifestPath
  • port
  • pid
  • runId
  • startedAt
  • compiledAt
  • errors
把这份契约作为就绪和失败的真相来源。

运行测试

npx playwright test

标准 Playwright 流程(对 AI 友好)

  1. 以 no-browser 模式启动 Extension.js。
  2. 等到 ready.json 报告 status: "ready"
  3. distPath 中的扩展产物启动 Playwright。
  4. 运行测试并关闭。

开发模式 vs 测试模式

  • dev 用于 watch 模式迭代(extension dev --no-browser + extension dev --wait)。
  • start 用于生产风格的检查(extension start --no-browser + extension start --wait)。

重要区分:运行模式 vs 就绪门

  • --no-browser运行模式:在不启动浏览器的情况下启动扩展流水线。
  • 就绪门(extension dev --wait --browser=<browser>) 是同步步骤,告诉 Playwright 扩展何时就绪。
--no-browser 生成构建产物。wait 步骤在测试继续前确认就绪。如果你的环境无法运行第二个 CLI 进程,请直接轮询 ready.json 使用如下双进程模式:
  1. 进程 A:extension dev --no-browser
  2. 进程 B:extension dev --wait --browser=<browser> --wait-format=json
  3. 仅在 wait 步骤成功退出后再启动 Playwright
面向生产的变体:
  1. 进程 A:extension start --no-browser
  2. 进程 B:extension start --wait --browser=<browser> --wait-format=json
  3. 仅在 wait 步骤成功退出后再启动 Playwright
import { spawn } from "node:child_process";
import { chromium } from "@playwright/test";

const browserName = "chromium";

const child = spawn(
  "pnpm",
  ["extension", "dev", "--no-browser", `--browser=${browserName}`],
  {
    stdio: "inherit",
    env: process.env,
  },
);

async function waitForReady() {
  return await new Promise<any>((resolve, reject) => {
    let stdout = "";
    let stderr = "";
    const wait = spawn(
      "pnpm",
      [
        "extension",
        "dev",
        "--wait",
        `--browser=${browserName}`,
        "--wait-timeout=60000",
        "--wait-format=json",
      ],
      { stdio: ["ignore", "pipe", "pipe"], env: process.env },
    );
    wait.stdout.on("data", (chunk) => (stdout += chunk.toString()));
    wait.stderr.on("data", (chunk) => (stderr += chunk.toString()));
    wait.on("error", reject);
    wait.on("close", (code) => {
      if ((code ?? 1) !== 0) {
        reject(
          new Error(stderr || `wait command failed with code ${String(code)}`),
        );
        return;
      }
      const payload = JSON.parse(stdout.trim());
      resolve(payload.results[0]);
    });
  });
}

const ready = await waitForReady();

const context = await chromium.launchPersistentContext("", {
  headless: false,
  args: [
    `--disable-extensions-except=${ready.distPath}`,
    `--load-extension=${ready.distPath}`,
  ],
});

// run tests using context/pages...

await context.close();
child.kill("SIGTERM");

针对扩展的实用建议

  • 让测试夹具保持确定性;扩展启动可能对 profile 状态敏感。
  • 优先在扩展 UI 状态上做显式等待,而不是固定超时。
  • 在 CI 中同时跑 Chromium 与 Firefox 项目,获得跨引擎信心。
  • 在失败时捕获 trace/截图/录像,方便加速调试。
  • 在机器可靠性上,优先使用 ready.json/events.ndjson,而不是解析 stdout。

常见陷阱

  • 用固定超时取代基于状态的等待
  • 在 CI 中只跑一个浏览器目标
  • 失败运行未上传产物
  • 让测试耦合于本地 profile 或环境的假设

仓库参考

你的仓库中包含一份 Playwright 配置:
  • playwright.config.ts

下一步