本机全过 CI 全挂:把 Playwright 的环境差异按五层拆开逐层对
现象长什么样
本机 npx playwright test 全绿,推上去以后 CI 上整批红,报错还五花八门:Error: Failed to launch browser、找不到浏览器可执行文件、截图对比全部不匹配、超时。这种「全挂」和「偶尔挂一两个」是两类问题,别混着查——整批挂通常是环境层的差异,不是用例写得不好。
差异其实可以拆成五层,逐层对比是最省事的顺序:浏览器版本、系统依赖与字体、并发度、视口、网络。下面每层都给判定动作、仓库文档给出的处置、以及处置后怎么验证。
第一层:浏览器版本与安装位置
docs/src/browsers.md 开篇写明,每个版本的 Playwright 需要特定版本的浏览器二进制,每次升级 Playwright 都可能要重新跑一次 install。也就是说浏览器是跟着 Playwright 版本走的,本机装过一次不代表 CI 上有。
判定动作。 CLI 自带不改动系统的干跑开关。packages/playwright-core/src/cli/program.ts 里 install 命令注册了 --dry-run(描述是 do not execute installation, only print information)与 --list(prints list of browsers from all playwright installations)。在 CI 脚本里先加一步只打印信息的调用,比直接看测试报错清楚得多。
处置。 docs/src/ci.md 给的三步里,第二步就是安装:
npx playwright install --with-deps
Python 侧对应的是 docs/src/ci.md 里 python 代码块的写法:
pip install playwright
playwright install --with-deps
如果用容器,docs/src/docker.md 的镜像 tag 一节有一条注记写得很直白:建议尽量把镜像固定到具体版本;如果 Docker 镜像里的 Playwright 版本与你项目/测试里的版本不一致,Playwright 将无法定位浏览器可执行文件。ci.md 里容器方案的镜像地址写成 mcr.microsoft.com/playwright:v%%VERSION%%-noble 这种形态,%%VERSION%% 是仓库文档里的版本占位符,实际用要换成与项目匹配的版本。同一份注记在远程连接一节又出现一次:远程执行时要确保测试端与容器里的 Playwright 版本一致。
Windows 侧。 docs/src/browsers.md 的 Managing browser binaries 一节列了缓存目录,Windows 是 %USERPROFILE%\AppData\Local\ms-playwright,Linux 是 ~/.cache/ms-playwright。要改位置用 PLAYWRIGHT_BROWSERS_PATH,文档给了 cmd 与 PowerShell 两种写法,cmd 是 set PLAYWRIGHT_BROWSERS_PATH=%USERPROFILE%\pw-browsers。本机是 Windows、CI 是 Linux 容器时,这两条路径完全不同,缓存策略照搬过去就会落空。
顺带一提,ci.md 的 Caching browsers 一节明说不建议缓存浏览器二进制,理由是恢复缓存的耗时与重新下载相当,而且 Linux 下的系统依赖本身不可缓存。真要缓存,文档要求按 Playwright 版本的哈希做 key。
验证。 干跑那一步不再报缺失、且 CI 日志里安装步骤没有被跳过。
第二层:系统依赖与字体
Linux agent 上最典型的表现是浏览器根本起不来。docs/src/ci.md 的 Debugging browser launches 一节给了对应的开关:
DEBUG=pw:browser npx playwright test
文档写明这个环境变量用于输出调试日志,设成 pw:browser 对排查 Error: Failed to launch browser 有帮助。
判定动作。 install-deps 也有 --dry-run,program.ts 里这条选项的描述值得逐字看:不修改系统;在 Linux 上通过 apt-get 模拟安装,若有必需包缺失则以非零码退出;在 Windows 上打印安装命令。命令的补充说明里还写了它「适合非交互的验证脚本」。这就是一条可以直接塞进流水线的判定动作——缺包时退出码非零,一眼可见。
字体差异在哪。 依赖表在 packages/playwright-core/src/server/registry/nativeDeps.ts,这个文件按发行版分组,每组的 tools 数组里除了 xvfb,剩下的几乎全是字体包:fonts-noto-color-emoji、fonts-liberation、fonts-wqy-zenhei、fonts-ipafont-gothic 之类,还有 libfontconfig1、libfreetype6。换句话说,字体不是浏览器自带的,是 install-deps 装进系统的。CI 上少了这一步,中文与 emoji 的渲染就和本机不是一回事。
同一个文件里的键都是 Ubuntu 与 Debian 系的 x64 / arm64 组合,仓库里没有找到 Windows 或 macOS 的对应条目——这两个平台在 docs/src/ci.md 的 Azure Pipelines 一节被单独说明:Windows 或 macOS agent 不需要额外配置,装好 Playwright 直接跑即可。
字体差异最先咬到的是截图比对。docs/src/test-snapshots-js.md 解释快照文件名时写明,chromium-darwin 这段是浏览器名加平台名,截图会因为渲染、字体等因素在不同浏览器与平台之间存在差异,所以需要为它们准备不同的快照。ci.md 讲容器方案时也提到,用容器的好处之一是为跨操作系统的截图/视觉回归测试提供一致的环境。所以本机 Windows 生成基线、CI Linux 比对,全红是可以预期的——不是用例坏了,是基线本来就不该跨平台复用。
边界。 docs/src/docker.md 的 Alpine 一节写明:Firefox 与 WebKit 的浏览器构建基于 glibc,Alpine Linux 以及其它基于 musl 的发行版不受支持。挑基础镜像时这条是硬约束。
验证。 install-deps --dry-run 退出码为零;截图类用例改成在与基线相同的环境里生成与比对,需要更新基线时用 docs/src/test-snapshots-js.md 给的 npx playwright test --update-snapshots。
第三层:并发度
docs/src/test-api/class-testconfig.md 里 TestConfig.workers 的说明写着:最大并发 worker 进程数,也可以写成逻辑 CPU 核数的百分比(例如 '50%'),默认是逻辑 CPU 核数的一半。这是仓库当前文档里的默认值,随版本可能变动。本机十几个核、CI runner 两个核,同一份配置下的实际并发就完全不同。
docs/src/ci.md 的 Workers 一节(标了 langs: js)给出的建议是在 CI 环境把 workers 设成 1,以稳定性与可复现性优先,并附了配置片段:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// Opt out of parallel tests on CI.
workers: process.env.CI ? 1 : undefined,
});
同一节也写明,自建的高配 CI 可以开并行,更大规模的并行考虑 sharding,把用例分散到多个 CI job。
判定动作。 看 CI 日志开头那行运行摘要——docs/src/browsers.md 里的示例输出形如 Running 7 tests using 5 workers,worker 数直接印在那里,和本机的数字一对就知道差在哪。
CircleCI 一节还有一条容易被忽略的注记:使用 docker agent 定义时资源等级是 medium 档,Playwright 的默认行为是把 worker 数设成检测到的核心数,把 worker 数覆盖成大于该数值会导致不必要的超时与失败。这句是文档原话,不是我们的推测。
顺带把两处放在一起看:class-testconfig.md 里 workers 的说明写的是「默认为逻辑 CPU 核数的一半」,而 ci.md 的 CircleCI 注记写的是「默认行为是把 worker 数设成检测到的核心数」。这两句在仓库里的表述并不一致,我们只把它指出来,不替作者判断哪句才是当前实现——真要确认,以你手上那个版本跑出来的摘要行为准。
容器里另有两条与资源相关的建议,写在 docs/src/docker.md 的 Recommended Docker Configuration 一节:推荐加 --init,避免 PID=1 进程的特殊处理导致僵尸进程;用 Chromium 时推荐 --ipc=host,文档写明没有它 Chromium 可能内存耗尽并崩溃。
验证。 摘要行里的 worker 数变成预期值,且原本成批超时的用例不再集中在同一时间点失败。
第四层:视口
docs/src/api/params.md 里 viewport 参数的说明是:为每个页面模拟一致的视口,默认为 1280x720。这同样是仓库当前文档里的默认值,随版本可能变动。默认值本身是一致的,但只要你用了设备预设,情况就变了。
docs/src/emulation.md 的 Viewport 一节给的配置片段里带了一句注释,说得很清楚:
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
// It is important to define the `viewport` property after destructuring `devices`,
// since devices also define the `viewport` for that device.
viewport: { width: 1280, height: 720 },
},
},
设备预设自己也定义了 viewport,所以展开顺序写反,你以为设了视口其实没设。本机与 CI 如果跑的 project 不同(比如本机只跑 --project=chromium,CI 跑全部 project),生效的视口就不是一回事,响应式布局下的元素可见性、点击位置全会跟着变。上面这段以及本文其它配置片段均抄自仓库文档,未经实测,以仓库最新内容为准。
判定动作。 用 docs/src/browsers.md 里的 --project 参数,在 CI 上先只跑本机跑的那个 project,看还挂不挂;同时把 test.use({ viewport: ... }) 显式写死一组尺寸再跑一次。docs/src/test-api/class-testoptions.md 里 test.use({ viewport: { width: 600, height: 900 } }) 就是这种文件级覆盖的示例写法。
验证。 显式写死视口后失败消失,说明差异来自视口而非用例逻辑。
第五层:网络
CI 上被测应用往往不在 localhost。docs/src/ci.md 的 On deployment 一节用的是环境变量 PLAYWRIGHT_TEST_BASE_URL,值取自部署事件的 target_url;docs/src/test-use-options-js.md 里对应的配置项是 baseURL,作用是让 page.goto('/settings') 这类只写路径的调用能解析。本机 baseURL 指着本地 dev server、CI 指着预览环境,两边的登录态、跨域与证书策略都不同。
如果依赖本地 dev server,docs/src/test-webserver-js.md 的 webServer 表格里有两个字段在 CI 上特别容易出事:reuseExistingServer 文档建议常写成 !process.env.CI,让本地复用已有服务而 CI 上强制新起;gracefulShutdown 的说明里写明 Windows 不支持 SIGTERM 与 SIGINT,该选项在 Windows 上被忽略,并注明关闭 Docker 容器需要 SIGTERM。本机 Windows 上关不干净的进程,在 Linux CI 上行为不一样,反过来也一样。
容器里访问宿主机的本地服务,docs/src/docker.md 的 Network Configuration 一节给的是 --add-host=hostmachine:host-gateway,并写明这会让 hostmachine 指向宿主机的 localhost,测试里要用 hostmachine 而不是 localhost。
第三方接口的抖动也算这一层。docs/src/mock.md 里 page.routeFromHAR / browserContext.routeFromHAR 接受一个 HAR 文件路径,把网络回放固定下来;docs/src/api/class-browsercontext.md 里这个方法带 notFound、update、url 等选项,另有 updateMode、updateContent 两个后加的选项,具体以该文件当前列出的为准。以上为按仓库代码中的接口语义组合的示例,未经实测,以仓库最新代码为准。
判定动作与验证。 把失败用例的 trace 收下来看网络请求。docs/src/test-use-options-js.md 的配置示例里是 trace: 'on-first-retry'(同一段还有 screenshot: 'only-on-failure'),Python 侧 ci.md 的 workflow 直接在命令行上写 pytest --tracing=retain-on-failure,并用 upload-artifact 把 test-results/ 传出来;JS 的 workflow 传的是 playwright-report/。产物拿到本地看,请求打到哪个域名、是不是 401,一目了然。
什么情况说明不是这五层
这一步不能省,否则容易在环境上空耗:
- CI 上只挂一部分、且每次挂的不是同一批:更像用例本身不稳定。
docs/src/test-api/class-testconfig.md写明retries默认不重试,先把重试打开配合 trace 定位,而不是继续改镜像。 - 本机以 headless 方式也能复现:那和 CI 无关。
docs/src/ci.md的 Running headed 一节写明 Playwright 默认以 headless 启动浏览器,Linux agent 上跑 headed 需要装 Xvfb,用xvfb-run前缀执行;文档说明其 Docker 镜像与 GitHub Action 已预装 Xvfb。本机本来就是 headless 跑挂的,方向就不在这里。 - 换成仓库提供的容器镜像、并把版本固定后仍然全挂:五层里的前两层已经被镜像抹平,接着查 baseURL 与被测服务本身。
- 只有 PR 流水线挂、主干不挂:先看是不是启发式筛选造成的。ci.md 的 Fail-Fast 一节写明
--only-changed通过分析依赖图来找受改动影响的测试文件,这是一个启发式方法、可能漏掉测试,所以文档要求在预跑之后仍然执行完整测试套件。
最后提一句边界:Playwright 会启动真实浏览器、执行页面脚本、读写存储状态,docs/src/docker.md 的提示框写明该镜像仅用于测试与开发用途,不建议用它访问不受信任的网站;抓取类场景文档建议在容器内新建用户并配合 seccomp profile,而不是用 root。安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。
本文依据 github.com/microsoft/playwright 仓库于 2026-08-18 的公开内容整理,
事实来自仓库内的文档与源码。我们没有对文中涉及的功能做过实测,
因此不涉及运行速度、稳定性与实际表现的任何描述。
该项目迭代频繁,文中涉及的 API 签名、配置项与默认值随版本变动,请以仓库最新内容为准。
本文不涉及浏览器版本矩阵与版本清单,相关信息请以官方发布说明为准。