SurfSense 的 Playwright E2E 与 CI/CD 集成实战:GitHub Actions、Docker、Sharding 与报告体系
【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense
本文为 SurfSense 的 E2E 测试基础设施指南:系统讲解如何将 Playwright 接入 GitHub Actions 与 Docker 环境,覆盖分片(sharding)、多 Reporter 配置、环境变量与 Secrets 管理、浏览器/依赖缓存与基于标签的测试过滤。读完之后,你既能掌握一套可复制到任何项目的 Playwright CI/CD 标准方案,也能从 SurfSense 仓库的真实工作流(.github/workflows/e2e-tests.yml、surfsense_web/playwright.config.ts、docker/docker-compose.e2e.yml)看到这套方案在「Next.js + FastAPI + Celery + Postgres + Redis」全栈上的落地细节。
一、GitHub Actions 工作流
Playwright 官方推荐的 CI 接入方式是 GitHub Actions。SurfSense 仓库中的 E2E 测试工作流 就是在该模式基础上、针对全栈 E2E 场景做了扩展的实例。
1.1 基础工作流
最小可用的工作流包含:检出代码、安装 Node 依赖、安装 Playwright 浏览器、运行测试、上传 HTML 报告。标准形态如下:
# .github/workflows/playwright.yml name: Playwright Tests on: push: branches: [main] pull_request: branches: [main] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: "npm" - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test - uses: actions/upload-artifact@v4 if: ${{ !cancelled() }} with: name: playwright-report path: playwright-report/ retention-days: 30几个关键点的说明:
timeout-minutes: 60防止测试卡死无限占用 runner;cache: "npm"让setup-node自动缓存node_modules(pnpm 项目则改用pnpm/action-setup+ store 缓存,见 2.4 节);npx playwright install --with-deps会同时安装浏览器二进制与系统依赖库;- 最后的
upload-artifact使用if: ${{ !cancelled() }},确保被手动取消的运行也能保留报告用于排查。
1.2 带 Sharding 的工作流
测试套件变大后,可将用例切分为多个 shard 并行执行,每个 shard 独立输出 blob 报告,最后由merge-reports作业合并:
name: Playwright Tests on: push: branches: [main] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest strategy: fail-fast: false matrix: shardIndex: [1, 2, 3, 4] shardTotal: [4] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: "npm" - name: Install dependencies run: npm ci - name: Install Playwright browsers run: npx playwright install --with-deps - name: Run Playwright tests run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} - name: Upload blob report if: ${{ !cancelled() }} uses: actions/upload-artifact@v4 with: name: blob-report-${{ matrix.shardIndex }} path: blob-report retention-days: 1 merge-reports: if: ${{ !cancelled() }} needs: [test] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: "npm" - name: Install dependencies run: npm ci - name: Download blob reports uses: actions/download-artifact@v4 with: path: all-blob-reports pattern: blob-report-* merge-multiple: true - name: Merge reports run: npx playwright merge-reports --reporter html ./all-blob-reports - name: Upload HTML report uses: actions/upload-artifact@v4 with: name: html-report path: playwright-report retention-days: 14要点:
fail-fast: false让一个 shard 失败不取消其他 shard,保证能拿到完整的失败清单;- shard 上传的 blob 报告
retention-days: 1即可(中间产物),合并后的 HTML 报告保留更久; merge-reports作业通过needs: [test]等待全部 shard 完成后,用download-artifact的pattern: blob-report-*+merge-multiple: true把所有 shard 的 blob 归拢到一个目录再合并。
1.3 容器化运行
如果不想在 runner 上安装系统依赖,可以直接在 Playwright 官方镜像中运行,镜像版本应与package.json中的@playwright/test版本匹配:
jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest container: # Use latest or more appropriate playwright version (match package.json) image: mcr.microsoft.com/playwright:v1.40.0-jammy steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: "npm" - name: Install dependencies run: npm ci - name: Run tests run: npx playwright test env: HOME: /root注意HOME: /root这一行:容器内 Playwright 把浏览器装到$HOME/.cache/ms-playwright,而 GitHub Actions 的 workspace 与缓存目录路径默认假设与裸 runner 一致,显式设置HOME可避免浏览器路径解析问题。
二、Docker 方案
2.1 Dockerfile
官方镜像自带浏览器与系统依赖,是 E2E 容器的首选基座:
FROM mcr.microsoft.com/playwright:v1.40.0-jammy WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . CMD ["npx", "playwright", "test"]先把 lockfile 与包清单单独 COPY 并安装,利用 Docker 层缓存让「依赖未变时不重装」成立,是标准的多层优化。
2.2 Docker Compose:测试容器与被测应用同栈
当 E2E 需要访问本地起的应用服务时,可以让测试容器与应用容器组成一个 compose 栈:
# docker-compose.yml version: "3.8" services: playwright: build: . volumes: - ./playwright-report:/app/playwright-report - ./test-results:/app/test-results environment: - CI=true - BASE_URL=http://app:3000 depends_on: - app app: build: ./app ports: - "3000:3000"两个细节值得注意:
- 报告与
test-results(trace、截图、视频)通过 volume 挂载回宿主机,容器--rm删除后产物不丢; BASE_URL=http://app:3000使用容器网络内的服务名而非localhost,CI=true则触发 Playwright 的 CI 模式(headless、禁用交互等)。
2.3 运行命令
# Build and run docker build -t playwright-tests . docker run --rm -v $(pwd)/playwright-report:/app/playwright-report playwright-tests # With docker-compose docker-compose run --rm playwright2.4 SurfSense 的实际做法:Docker 负责后端栈,Playwright 跑在宿主机
SurfSense 的 E2E 场景比单容器更复杂:被测对象是「Next.js 前端 + FastAPI 后端 + Celery worker + Postgres + Redis」的全链路。仓库的 E2E 工作流 采取的策略是:用 Docker Compose 起一个隔离的(hermetic)后端栈,Playwright 本体跑在宿主机上。
后端栈定义在 docker/docker-compose.e2e.yml,其核心设计包括:
- 网络级断网:
db/redis/celery_worker只挂在internal: true的 bridge 网络上,容器在 L3 层面就无法访问外网;backend额外挂一个普通ingress桥接网,仅供宿主机访问:8000。这与 1.3 节的单容器方案不同——这里用「网络隔离 + 哨兵密钥(COMPOSIO_API_KEY: e2e-deny-real-call-sentinel等)+HTTPS_PROXY=http://127.0.0.1:1」三层防线,确保测试永远不会触发真实的第三方 API 调用; - 健康检查门控:
db用pg_isready、redis用redis-cli ping、backend用容器内 Python 请求/openapi.json、celery_worker用celery inspect ping。工作流里docker compose up -d --build --wait --wait-timeout 300会阻塞到所有健康检查变绿,替代了脆弱的curl轮询脚本; - 临时存储:Postgres 数据目录用
tmpfs挂载,每次 CI 运行都是干净数据库,无需清理 volume; - 构建缓存:后端镜像声明了
cache_from/cache_to: type=gha(GitHub Actions 构建层缓存),跨运行复用~9 GB量级的多阶段构建结果; - ephemeral 数据库名:
DATABASE_URL指向独立的surfsense_e2e库,与开发环境隔离。
工作流中与缓存相关的步骤(与原文档 Caching 一节逐行对应,只是把npm换成了 pnpm 生态):
- name: Get pnpm store directory id: pnpm-cache run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT - name: Cache pnpm store uses: actions/cache@v5 with: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: pnpm-${{ runner.os }}-${{ hashFiles('surfsense_web/pnpm-lock.yaml') }} restore-keys: pnpm-${{ runner.os }}- - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v5 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('surfsense_web/pnpm-lock.yaml') }} - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' working-directory: surfsense_web run: pnpm exec playwright install --with-deps chromium - name: Install Playwright system deps (cache hit) if: steps.playwright-cache.outputs.cache-hit == 'true' working-directory: surfsense_web run: pnpm exec playwright install-deps chromium可以注意到两点实际工程细节:一是只安装chromium单浏览器(配置里只定义了 chromium 项目),比--with-deps全量安装更快;二是浏览器缓存命中时仍要跑install-deps,因为系统库依赖无法用文件哈希精确缓存,而浏览器二进制可以。
前端则由 Playwright 的webServer字段托管(见 5.3 节),CI 下执行pnpm build && pnpm start起生产模式服务,与线上构建完全一致;本地开发则用pnpm exec next dev快速迭代。surfsense_web/tests/README.md 中给出了与 CI 完全对齐的本地复现路径:docker compose -f docker/docker-compose.e2e.yml up -d --build --wait起后端栈、注册测试用户,然后在surfsense_web/下执行pnpm test:e2e:prod(该脚本即 package.json 中的cross-env CI=1 playwright test)。
三、Reporting 报告体系
3.1 多 Reporter 组合配置
Playwright 允许同时启用多个 reporter,各司其职:
// playwright.config.ts export default defineConfig({ reporter: [ // Always generate ["html", { outputFolder: "playwright-report" }], // Console output ["list"], // CI-friendly ["github"], // GitHub Actions annotations // JUnit for CI integration ["junit", { outputFile: "results.xml" }], // JSON for custom processing ["json", { outputFile: "results.json" }], // Blob for merging shards ["blob", { outputDir: "blob-report" }], ], });各 reporter 的职责划分:html供人看(配合 artifact 上传后浏览器打开);list提供实时控制台输出;github把失败信息以 annotation 形式直接标在 PR 的 diff 上;junit/json供外部 CI 系统集成或自定义脚本处理;blob是 shard 合并的中间格式(人类不可读,只喂给merge-reports)。
3.2 按 CI 环境切换 Reporter
export default defineConfig({ reporter: process.env.CI ? [["github"], ["blob"], ["html"]] : [["list"], ["html"]], });process.env.CI在 GitHub Actions、GitLab CI 等主流平台上都会被自动设置为真值,因此无需额外配置即可区分「本地调试」与「流水线」。
SurfSense 的 playwright.config.ts 采用的是同一模式的变体,并额外通过open参数控制 HTML 报告的打开行为:
reporter: process.env.CI ? [["html", { open: "never" }], ["github"], ["list"]] : [["html", { open: "on-failure" }], ["list"]],即本地运行失败时自动打开 HTML 报告页方便排查,CI 上永远不尝试打开(headless 环境也没有图形界面)。
四、Sharding 分片执行
4.1 命令行分片
# Split into 4 shards, run shard 1 npx playwright test --shard=1/4 # Run shard 2 npx playwright test --shard=2/4--shard=N/M表示「M 个 shard 中的第 N 个」。Playwright 按测试文件维度做确定性切分,保证同一套用例在不同 shard 数下都能完整覆盖且不重复。
4.2 配置侧配合
// playwright.config.ts export default defineConfig({ // Evenly distribute tests across shards fullyParallel: true, // For blob reporter to merge later reporter: process.env.CI ? [["blob"]] : [["html"]], });fullyParallel: true让文件间并行调度更均衡,配合分片可获得更均匀的耗时分布。SurfSense 的配置同样设置了fullyParallel: true(playwright.config.ts),但当前 CI 采用workers: 1串行执行(全栈 journey 测试对数据库状态强敏感,串行换取确定性),分片能力在套件规模扩大后再开启——这也是原文档「Use sharding for large suites」一行的实践注脚。
4.3 合并分片报告
# After all shards complete, merge blob reports npx playwright merge-reports --reporter html ./all-blob-reportsmerge-reports是独立命令(不需要再跑测试),读取各 shard 产出的blob-report/目录并重新生成任意格式的报告,如 1.2 节工作流中的--reporter html。
五、Environment 环境管理
5.1 按 NODE_ENV 加载 .env 文件
// playwright.config.ts import { defineConfig } from "@playwright/test"; import dotenv from "dotenv"; // Load env file based on environment dotenv.config({ path: `.env.${process.env.NODE_ENV || "development"}` }); export default defineConfig({ use: { baseURL: process.env.BASE_URL || "http://localhost:3000", }, });5.2 多环境矩阵
同一份用例通过矩阵变量指向不同环境(staging / production):
# .github/workflows/playwright.yml jobs: test: strategy: matrix: environment: [staging, production] steps: - name: Run tests run: npx playwright test env: BASE_URL: ${{ matrix.environment == 'staging' && 'https://staging.example.com' || 'https://example.com' }} TEST_USER: ${{ secrets[format('TEST_USER_{0}', matrix.environment)] }}5.3 Secrets 管理与 Playwright webServer 注入
测试账号等敏感值放在仓库 Secrets 中,仅通过env:注入运行步骤,不写入配置或测试代码:
# GitHub Actions secrets - name: Run tests run: npx playwright test env: TEST_EMAIL: ${{ secrets.TEST_EMAIL }} TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}// tests use environment variables test("login", async ({ page }) => { await page.getByLabel("Email").fill(process.env.TEST_EMAIL!); await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!); });SurfSense 对「环境值如何穿透到被测应用」这一问题给出了更完整的样板:playwright.config.ts 在模块顶层集中解析并回写环境,然后透传给webServer启动的 Next.js 进程:
const PORT = process.env.PORT || "3000"; const BACKEND_PORT = process.env.BACKEND_PORT || "8000"; const baseURL = process.env.PLAYWRIGHT_BASE_URL || `http://localhost:${PORT}`; process.env.PLAYWRIGHT_TEST_EMAIL ??= "e2e-test@surfsense.net"; process.env.PLAYWRIGHT_TEST_PASSWORD ??= "E2eTestPassword123!"; process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL ??= backendURL; process.env.SURFSENSE_BACKEND_INTERNAL_URL ??= backendURL; process.env.AUTH_TYPE ??= "LOCAL"; webServer: { // Local stays on webpack dev (Turbopack caused stale-lock panics in E2E). command: process.env.CI ? "pnpm build && pnpm start" : "pnpm exec next dev", url: `http://localhost:${PORT}`, reuseExistingServer: !process.env.CI, timeout: process.env.CI ? 300_000 : 180_000, stdout: "pipe", stderr: "pipe", env: { /* 透传后端地址等变量给 Next.js */ }, }值得注意的设计取舍:
??=提供默认值,使本地「零环境变量」即可运行,覆盖时只需显式导出;reuseExistingServer: !process.env.CI:本地复用已存在的 dev server 加速迭代,CI 上强制新起,避免陈旧进程导致的不确定结果;- 注释里记录了「本地保留 webpack dev(Turbopack 曾导致 stale-lock panic)」这类经验性决策,这是生产级 E2E 配置中很典型的知识沉淀;
- 测试账号在 CI 工作流中由后端
/auth/register接口真实注册(e2e-tests.yml 的「Register E2E test user」步骤),并在注册后清空 Redis 中surfsense:auth_rate_limit:*限流计数,保证登录链路每次都从干净状态开始。测试登录会话的复用则交给auth.setup.ts写入playwright/.auth/user.json,供chromium项目通过storageState加载(见 auth.setup.ts 与配置中的projects依赖关系)。
六、Caching 缓存策略
6.1 缓存 Playwright 浏览器
浏览器二进制是 CI 里最大的可缓存资产(数百 MB),用 lockfile 哈希作为 key:
- name: Cache Playwright browsers uses: actions/cache@v4 id: playwright-cache with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} - name: Install Playwright browsers if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps - name: Install system deps only if: steps.playwright-cache.outputs.cache-hit == 'true' run: npx playwright install-depscache-hit输出驱动的三段式(命中则跳过安装 / 未命中全装 / 命中补装系统依赖)在 e2e-tests.yml 中有逐行对应的真实实现,差异仅在于 key 使用hashFiles('surfsense_web/pnpm-lock.yaml')且只装 chromium。
6.2 缓存 Node 依赖
npm 项目直接用setup-node内置缓存:
- uses: actions/setup-node@v4 with: node-version: 22 cache: "npm" - name: Install dependencies run: npm cipnpm 项目则缓存pnpm store path输出的 store 目录(2.4 节示例),配合--frozen-lockfile安装。此外 SurfSense 还缓存了surfsense_web/.next/cache(Next.js 构建缓存,key 含github.sha并配多级restore-keys回退),进一步压缩 CI 时间。
七、Tag-Based 测试过滤
7.1 在 CI 中按标签运行
# Run smoke tests on PR - name: Run smoke tests run: npx playwright test --grep @smoke # Run full regression nightly - name: Run regression run: npx playwright test --grep @regression # Exclude flaky tests - name: Run stable tests run: npx playwright test --grep-invert @flaky7.2 PR 快反馈 vs 夜间全量
# .github/workflows/pr.yml - Fast feedback - name: Run critical tests run: npx playwright test --grep "@smoke|@critical" # .github/workflows/nightly.yml - Full coverage - name: Run all tests run: npx playwright test --grep-invert @flaky这是典型的「快慢分层」策略:PR 上只跑冒烟级用例换反馈速度,夜间定时任务承担全量回归与 flaky 暴露。
7.3 在配置中做标签过滤
// playwright.config.ts export default defineConfig({ grep: process.env.CI ? /@smoke|@critical/ : undefined, grepInvert: process.env.CI ? /@flaky/ : undefined, });7.4 按 Project 划分标签
// playwright.config.ts export default defineConfig({ projects: [ { name: "smoke", grep: /@smoke/, }, { name: "regression", grepInvert: /@smoke/, }, ], });projects级别的grep/grepInvert可以把同一批文件组织成两个逻辑套件,pnpm test:e2e smoke之类的按项目运行方式(playwright test --project=smoke)随之成为可能。SurfSense 的 projects 划分则基于「setup → chromium」的依赖关系(playwright.config.ts):setup项目只匹配*.setup.ts(认证状态生成),chromium项目声明dependencies: ["setup"]并复用storageState,是 project 机制「先鉴权、再测试」的经典用法。
八、Best Practices 与 CI 优化配置
8.1 实践清单
| Practice | Benefit |
|---|---|
Usenpm ci | Deterministic installs |
| Run headless in CI | Faster, no display needed |
| Set retries in CI only | Handle flakiness |
| Upload artifacts on failure | Debug failures |
| Use sharding for large suites | Faster execution |
| Cache browsers | Faster setup |
| Use blob reporter for shards | Merge reports correctly |
| Use tags for PR vs nightly | Fast feedback + coverage |
| Exclude @flaky in CI | Stable pipeline |
「只在 CI 设置 retries」是重点:本地重跑会掩盖真实缺陷,而 CI 偶发抖动的代价是流水线失败,两者权衡后retries: process.env.CI ? N : 0是通行做法。SurfSense 当前取值为retries: process.env.CI ? 1 : 0(playwright.config.ts),并配合trace: "on-first-retry"——只在首次重试失败时保留 trace,兼顾诊断能力与产物体积。
8.2 CI 优化后的完整配置参考
// playwright.config.ts - CI optimized export default defineConfig({ testDir: "./tests", fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, reporter: process.env.CI ? [["github"], ["blob"], ["html"]] : [["list"], ["html"]], use: { baseURL: process.env.BASE_URL || "http://localhost:3000", trace: "on-first-retry", screenshot: "only-on-failure", video: "on-first-retry", }, });逐项解析 CI 相关开关:
forbidOnly: !!process.env.CI:防止test.only被误提交后在 CI 上静默跳过大量用例——这是 E2E 流水线最常见的「假绿」来源;screenshot: "only-on-failure"、video/trace按需录制:失败产物自动进入test-results/,工作流再用actions/upload-artifact上传。SurfSense 的 CI 把video直接设为off(容器化后端 + 确定性 API 驱动使视频价值有限),但保留 trace 并在失败时上传test-results/作为playwright-tracesartifact;- 失败诊断不止前端:e2e-tests.yml 在
failure() || cancelled()时还会 dump 各 compose 服务的日志到compose-logs/并作为 artifact 上传,覆盖「测试失败到底是前端、后端还是 worker 的问题」的归因需求。
8.3 失败产物与 artifact 生命周期
SurfSense 工作流对产物做了分级保留:HTML 报告与 trace 保留 14 天(retention-days: 14),后端栈日志保留 7 天;playwright-report在always()条件下上传(成功也要可回看),traces 仅在failure()时上传。配合concurrency: cancel-in-progress: true(同一 ref 的新推送取消旧运行),可以既保证最新代码的反馈速度,又不丢失被取消运行的报告。
九、延伸参考
围绕本文的 CI/CD 主题,仓库内的 Playwright 技能文档还提供了配套深读材料:
- 测试标签:test-tags.md —— 标签定义与
--grep过滤的完整模式; - 性能优化:performance.md —— workers/sharding 与并行化的进阶调优;
- GitHub Actions 细节:github-actions.md 与 docker.md、parallel-sharding.md;
- CI 失败调试:debugging.md —— trace viewer 用法与常见失败归因。
项目侧的落地入口则是 surfsense_web/tests/README.md(本地/CI 两种运行方式)、surfsense_web/playwright.config.ts(唯一事实来源的 E2E 配置)与 docker/docker-compose.e2e.yml(隔离后端栈定义),三者与本文各节一一对应。
【免费下载链接】SurfSenseOpen-source NotebookLM alternative. Research the open web with live data(Reddit, YT, IG, TikTok, Indeed, Google Search, Maps etc) through one platform, API or MCP server. Join our Discord: https://discord.gg/ejRNvftDp9项目地址: https://gitcode.com/GitHub_Trending/su/SurfSense
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考