Cypress 浏览器自动化:从交互调试到可治理的 CI 证据链
本地交互全绿,CI 为什么仍会随机失败
一个常见现场是:开发者在 cypress open 里点开登录用例,浏览器中的每一步都能回看,连续运行也全绿;提交到 CI 后,第一次报“元素不可见”,自动重试又通过。团队于是把 retries 从 1 调到 3,红灯少了,发布速度似乎恢复了。几周后,同一批用例开始出现账号互踢、测试数据重复、截图含客户姓名、视频占满制品空间,以及某个先运行的 spec 改变了后一个 spec 的结果。此时问题已经不是“选择器偶尔慢”,而是运行环境、命令调度、状态隔离和证据治理没有形成闭环。
Cypress 的价值不只是替人点击页面。它把测试代码装入受控浏览器,在命令日志里保存每一步的主题、断言和页面快照,同时由 Node 侧进程负责配置、插件事件、文件系统和外部任务。cypress open 面向编写与交互调试,cypress run 面向无交互执行和 CI;两者会共用 spec,却在浏览器是否可见、失败截图、视频、资源和退出行为上存在差异。官方浏览器启动说明明确指出,run 默认无头运行,默认浏览器是 Electron,而 open 始终以有界面的方式运行。只在 open 中成功,不能证明无头模式、干净缓存和 CI 资源下也成功。
先把一次执行拆成四层,后面的故障会清楚很多:npm 包中的 CLI 找到匹配版本的 Cypress 二进制;Cypress 进程启动自己的浏览器实例和代理;浏览器内的测试代码把命令加入队列并依次执行;Node 事件进程处理 task、浏览器启动钩子和结果归档。页面应用是被测对象,测试代码可以在浏览器侧观察它,但数据库脚本、密钥读取和文件操作必须留在 Node 或受控测试 API。把这四层混成一个“测试进程”,就会把二进制缺失误判成 spec 错误,也会把浏览器中的秘密误传给日志和应用脚本。
从安装到 verify 建立可诊断基线
Cypress 的 Node、操作系统、包管理器和浏览器支持线会随发行版变化。安装前应从官方安装与系统要求核对准备采用的 Cypress 发行线,而不是把一次查询到的 Node 或 Linux 版本表写成永久条件。系统 Node 负责安装 npm 包和启动 CLI,Cypress 二进制还带 bundled Node 与 Electron,因此排障时要同时记录“系统 Node、npm 包、Cypress 二进制、Electron、目标浏览器”五组版本,不能只贴一行 node --version。
安装是两段式的。npm install 先把较小的 cypress 模块放入项目,随后 postinstall 下载与平台匹配的桌面二进制,并放入用户级全局缓存。企业包管理策略阻止生命周期脚本、代理无法访问下载站、证书链不受信任、缓存目录不可写或磁盘不足时,package.json 里会出现 Cypress,真正执行却失败。官方安装说明分别给出 npm 的 allowScripts、Yarn 的 enableScripts / npmPreapprovedPackages、pnpm 的 build allowlist,以及 Bun 的显式安装或 trusted dependency 入口;团队应只批准 Cypress 所需脚本,或在跳过生命周期脚本后显式执行 cypress install,不要为解决一次下载失败全局放开依赖脚本。官方高级安装说明还给出了 CYPRESS_INSTALL_BINARY、下载镜像、代理和缓存位置等入口;不要用跳过校验来掩盖安装损坏。
安装命令可以解析一个候选版本,但进入团队基线的是 lockfile 中的精确版本。下文使用 cy.env() 与 Cypress.expose() 这组较新的环境变量 API;旧项目升级时先按官方变更记录确认目标发行线具备这些 API,再检查 Node、组件框架适配器与浏览器矩阵:
node --version
npm --version
npm view cypress version engines
npm install --save-dev cypress
npm pkg get devDependencies.cypress
npx cypress version
npx cypress cache path
npx cypress cache list --size
npx cypress verifycypress version 应同时列出 package、binary、Electron 和 bundled Node 版本;package 与 binary 应相互匹配。verify 会检查二进制存在、可以启动并可执行,成功输出包含 Verified Cypress。按照官方命令行参考,open 和 run 本身也会触发 verify;慢磁盘或安全软件扫描导致超时时,可以用 CYPRESS_VERIFY_TIMEOUT 为这个步骤设置显式预算。CYPRESS_SKIP_VERIFY=true 只适合已经由其他门禁校验、且验证结果无法写回的只读二进制目录,不应成为普通可写缓存的长期默认值。
Cypress 二进制自带 Electron,因此 Electron 主门禁不需要再下载 Chrome;选择 Chrome、Edge 或 Firefox 时,Cypress 启动 runner 已安装且能检测到的浏览器。WebKit 是另一条实验链:除 experimentalWebKitSupport: true 外还要安装 playwright-webkit,Linux runner 还要执行 playwright install-deps webkit 或使用已包含依赖的受审查镜像。用 npx cypress info 记录实际检测到的浏览器与系统资源,再执行目标 --browser smoke;“Cypress verify 成功”只证明 Cypress 二进制可启动,不证明每个外部浏览器及其系统库都可用。
生命周期脚本被阻止时,显式安装比全局放开依赖脚本更容易审计。Linux/macOS 可以这样拆开,PowerShell 则用 $env:CYPRESS_INSTALL_BINARY="0" 与 $env:DEBUG="cypress:cli*" 设置同名环境变量:
CYPRESS_INSTALL_BINARY=0 npm ci
DEBUG=cypress:cli* npx cypress install
npx cypress verify如果日志在 Downloading Cypress 后出现 ENOSPC,先执行 npx cypress cache list --size 和系统磁盘检查,清理已确认不用的旧版本可用 npx cypress cache prune;不要删除不明目录或共享 runner 正在使用的缓存。若出现 EPERM、EACCES,检查缓存目录所有者、安全软件占用和 runner 用户,避免把 CI 改成管理员权限。若下载报代理或 CA 错误,应分别检查 npm registry 链路和 Cypress 二进制下载链路,因为 npm 包下载成功并不代表桌面二进制下载成功。
open、run 与项目配置字段怎样协作
第一次执行 npx cypress open 会进入 Launchpad,选择 E2E 或 Component 后生成配置和目录;官方打开 Cypress App 的步骤也建议把命令写入项目 scripts,让所有人使用同一入口。不要把 script 命名为 cypress,部分包管理器会把脚本名和二进制名解析到不同目标。团队常用的入口是:
{
"scripts": {
"cy:verify": "cypress verify",
"cy:open": "cypress open --e2e",
"cy:run": "cypress run --e2e",
"cy:run:chrome": "cypress run --e2e --browser chrome"
}
}open 适合选择单个 spec、回看命令快照、打开 DevTools 和观察热重载;run 默认执行匹配到的全部 spec,按退出码给 CI 结论,并在配置允许时生成截图、视频和报告。定位无头模式差异时,不要直接回到 open,先用 npx cypress run --headed --browser chrome --spec <SPEC> 保留 run 的调度和产物语义,再观察可见浏览器。若 headed 通过、headless 失败,应比较实际浏览器版本、屏幕尺寸、DPR、字体、动画、GPU 和资源限制。
配置文件不是参数仓库,而是测试运行契约。下面是一份适合 E2E 起步的 TypeScript 配置:
import { defineConfig } from "cypress";
export default defineConfig({
viewportWidth: 1280,
viewportHeight: 720,
video: true,
videoCompression: 32,
screenshotOnRunFailure: true,
trashAssetsBeforeRuns: true,
reporter: "junit",
reporterOptions: {
mochaFile: "cypress/results/junit-[hash].xml",
toConsole: true,
},
env: {
e2eUser: process.env.E2E_USER,
e2ePassword: process.env.E2E_PASSWORD,
e2eTenant: process.env.E2E_TENANT,
e2eSessionKey: process.env.E2E_SESSION_KEY,
},
retries: {
openMode: 0,
runMode: 2,
},
e2e: {
baseUrl: process.env.CYPRESS_BASE_URL ?? "http://127.0.0.1:4173",
specPattern: "cypress/e2e/**/*.cy.ts",
supportFile: "cypress/support/e2e.ts",
testIsolation: true,
defaultCommandTimeout: 4000,
requestTimeout: 5000,
responseTimeout: 15000,
setupNodeEvents(on, config) {
on("task", {
log(message: string) {
console.log(`[cy:task] ${message}`);
return null;
},
});
return config;
},
},
});这些字段改变的是不同阶段。baseUrl 给 cy.visit("/path") 提供应用根地址;specPattern 决定收集哪些文件;supportFile 在 spec 前加载通用命令和钩子;testIsolation 控制 E2E 用例之间的页面与存储清理;三个 timeout 分别约束 DOM 命令、请求发出和响应返回,不能用一个超大值吞掉所有慢点。setupNodeEvents 在 Node 侧注册事件和任务。video 在 run 中按 spec 录制,screenshotOnRunFailure 在 run 失败时自动截图,trashAssetsBeforeRuns 会在执行前清空 downloads、screenshots 和 videos 目录。字段默认值与覆盖优先级应以官方配置参考为准。
CYPRESS_BASE_URL 是配置覆盖项,不是测试里的业务环境变量。CI 应把它指向本次作业启动的临时应用,等待健康检查成功后再运行 Cypress。把共享测试环境或生产地址写死在配置里,会让本机命令在错误环境上执行清理和写操作。
用一个正向用例证明页面、网络和断言真的连通
最小验证不应停在“Cypress 窗口打开”。选一个无生产数据的健康页面,给关键元素稳定的 data-testid,同时观察页面结果和对应 API。下面的用例先注册网络监听,再访问页面;别名等待返回真实 request/response 对象,最后断言可见结果:
describe("应用健康链路", () => {
it("加载状态并呈现构建标识", () => {
cy.intercept("GET", "**/api/health").as("health");
cy.visit("/health");
cy.wait("@health").then(({ request, response }) => {
expect(request.method).to.equal("GET");
expect(response?.statusCode).to.equal(200);
expect(response?.body).to.have.property("status", "ok");
});
cy.get('[data-testid="health-status"]')
.should("be.visible")
.and("have.text", "ok");
cy.get('[data-testid="build-id"]').should("not.be.empty");
});
});启动应用后,分别执行交互和无头入口:
npm run build
npm run preview
# 另一个终端
npm run cy:open
npx cypress run --e2e --spec "cypress/e2e/health.cy.ts" --browser electron正向结果应同时出现四份证据:命令退出码为 0;终端显示 1 个 spec、1 个 test 通过;Command Log 中 @health 只命中预期的 GET;页面断言读取到 ok 和非空构建标识。接口 200 但 DOM 不更新,说明故障在应用状态或渲染链;DOM 有 ok 但别名未命中,可能页面使用了缓存、Service Worker、不同方法或不同 URL;只断言文本而不观察请求,会漏掉页面静态占位符掩盖后端失效的情况。
用例结束后不应留下业务数据。健康检查天然只读;涉及创建对象的流程,要给数据附加本次运行的命名空间,在 afterEach 或测试 API 中按 ID 删除,并让删除具备幂等性。清理失败必须进入报告,不能因为主断言通过就忽略污染。
一个反向实验怎样区分等待失败和业务失败
为了证明门禁能抓到错误,把接口改成稳定返回 503 比随意写错选择器更有价值。cy.intercept() 可以构造受控故障,不必关闭真实服务:
describe("降级提示", () => {
it("健康接口失败时阻断继续操作", () => {
cy.intercept("GET", "**/api/health", {
statusCode: 503,
body: { status: "unavailable" },
headers: { "cache-control": "no-store" },
}).as("healthFailure");
cy.visit("/health");
cy.wait("@healthFailure").its("response.statusCode").should("eq", 503);
cy.get('[role="alert"]').should("contain.text", "服务暂不可用");
cy.get('[data-testid="continue"]').should("be.disabled");
});
});正向实现应让这条反向用例通过,因为页面正确处理了 503。为了验证断言不是摆设,可在临时分支把 be.disabled 改成 be.enabled 再执行同一 spec,预期在 defaultCommandTimeout 用尽后失败,失败截图中按钮仍是禁用状态,终端栈指向最后一个断言。修回断言后再跑,失败消失。这样的红绿实验同时证明了 stub 被命中、页面进入降级分支、门禁能阻止错误预期。
若 cy.wait("@healthFailure") 直接超时,先看 Routes 面板中的 matcher 和命中次数。未指定 method 的 intercept 会匹配所有方法,容易把预检或写请求也算进去;注册发生在 cy.visit() 之后时,请求可能已经发出;响应来自浏览器缓存时,网络层没有新请求,intercept 也不会触发。官方cy.intercept() 参考说明 intercept 在每个 test 前自动清除,浏览器缓存命中也不会经过网络拦截层。修复手段是先注册、精确匹配 method 与 pathname,并在测试环境禁用缓存,而不是继续增加 cy.wait 的超时。
命令队列、查询重试和测试重试是三件事
Cypress 代码看起来像同步调用,也有 .then(),但 Cypress 命令不是 Promise,不能用 await cy.get(...)。测试函数先运行,把命令追加到中央队列;函数返回后,Cypress 才按顺序执行队列,并把前一个命令 yield 的 subject 交给下一个命令。官方核心概念用这个模型解释了为什么不能读取命令返回值,也不能让同一个 test 里的 Cypress 命令互相 race。
下面的写法在普通 JavaScript 直觉里很自然,却在 Cypress 中取不到文本:
let status = "unknown";
cy.get('[data-testid="health-status"]').then(($el) => {
status = $el.text();
});
// 这行普通 expect 在命令队列执行前就运行了
expect(status).to.equal("ok");应把依赖 subject 的逻辑留在队列里,优先使用可重试的 .should();确实要转换数据时,再在 .then() 中返回值并继续链:
cy.get('[data-testid="health-status"]')
.should("be.visible")
.invoke("text")
.should("eq", "ok");
cy.get('[data-testid="build-id"]')
.invoke("text")
.then((buildId) => buildId.trim())
.should("match", /^[a-z0-9._-]+$/i);查询重试发生在 get、contains 等 query 及其断言链上。元素暂时不存在或断言尚未满足时,Cypress 会重新执行相关查询,直到成功或 timeout;.then() 回调不会自动重试。动作命令又不同:click 会等待元素通过可见、未遮挡、未禁用等 actionability 检查,但真正的点击只执行一次,因为重复动作可能产生两笔订单。官方Retry-ability明确区分“查询和断言会重试”与“动作执行一次”。因此动作后重新从 cy 查询页面,比沿用可能已经因重渲染失效的 DOM subject 更安全。
测试重试是第三层。retries.runMode: 2 表示首次失败后最多再运行两次 test,beforeEach 和 afterEach 会随每次 attempt 再执行;它不会让某个 click 在同一次 attempt 中执行三次。官方测试重试说明指出默认不启用测试重试。开启的目的应是收集 flaky 证据和降低短暂基础设施抖动影响,而不是把“第一次失败、第二次通过”计成健康。报告必须保存 attempt 数、首次失败原因和重试后状态,发布规则可以对 flaky 单独告警或阻断。
E2E 与组件测试怎样分工
E2E 从 URL 进入,穿过浏览器、前端路由、后端和外部集成,适合验证登录、下单、权限、持久化和发布 smoke;代价是依赖服务、账号、网络和数据准备,执行更慢,也更容易受环境波动影响。组件测试用 cy.mount() 把一个组件挂载进真实浏览器,适合验证表单状态、日期选择器、设计系统组件和交互可访问性;它不证明反向代理、后端权限和整条用户路径正确。官方测试类型对照把两者视为互补能力,而不是互相替代。
团队可以按失败归属拆分:状态组合多、后端无关的行为下沉到组件测试;跨页面、跨服务且具有业务风险的少量主路径保留 E2E。不要把每个输入框的所有校验都经由完整登录和数据库验证,也不要因为组件测试很快就删除关键 E2E。一个“修改邮箱”流程可以让组件测试覆盖空值、格式、禁用和提示,让 E2E 只保留“已登录用户提交后服务端保存,刷新仍可见”这一条契约。
组件配置需要与真实框架和 bundler 对齐,例如 React + Vite、Vue + Vite 或 Angular;Launchpad 会安装对应 adapter 并生成 support 文件。组件应使用生产相同的主题、路由、状态容器和国际化 provider,否则 mount 出来的不是用户实际看到的组件。网络调用可以 stub,但至少保留一条 E2E 验证真实接口契约。Yarn Plug'n'Play 对 Cypress Component Testing 存在兼容限制时,应按官方安装页使用 nodeLinker: node-modules,不要让本机和 CI 各选一种依赖解析模型。
测试隔离也有差别。E2E 的 testIsolation: true 会在每个 test 前访问空白页并清理所有域的 cookies、localStorage 和 sessionStorage;组件测试固定卸载组件并清理浏览器状态,不能配置关闭。官方Test Isolation列出了具体清理行为。关闭 E2E 隔离可能减少重复登录时间,却会让顺序和并发改变结果;优先用 API 登录与 cy.session() 缓存受验证的会话,而不是让 test 继承前一个 test 的页面。
intercept、session、origin 和 task 分别解决哪一层
cy.intercept() 工作在浏览器网络路径上,用来监听、等待、修改或 stub HTTP 请求。它最适合把“页面动作完成了”绑定到可观察的请求和响应,也能稳定构造 4xx、5xx、延迟或断网分支。matcher 要尽可能包含 method、pathname 和必要 query;过宽的 **/users/** 会把背景请求也命中,过窄的完整 host 又会让不同测试环境失效。stub 能证明前端分支,不证明真实后端契约,因此关键路径应同时保留 spy 真实响应的 E2E 或独立契约测试。
intercept 也有自己的生命周期:每个 test 开始前会清除上一条用例注册的规则;同一请求命中多个普通 route 时,默认从后注册的规则开始处理,middleware: true 的规则才会按定义顺序优先运行。通用 header 中间件、局部故障 stub 和只观察不修改的 spy 若层层重叠,后注册的 stub 可能抢先结束请求。官方cy.intercept() 生命周期给出了精确顺序。排查时查看 Routes 面板的 matcher 与命中次数,给一次性故障使用 times: 1,不要靠调整 spec 顺序碰运气。
cy.session() 缓存并恢复 cookies、localStorage 和 sessionStorage,解决重复登录慢的问题。session ID 必须包含会改变授权结果的身份维度,但应使用环境、租户和账号槽位的非敏感别名,不能直接放用户名、密码或 token;validate 要调用 /api/me 或访问受保护页面,同时确认角色与租户,而不只是接受任意 200。启用 test isolation 时,建立或恢复 session 会清空页面,之后仍要显式 cy.visit()。官方cy.session() 说明特别提醒,恢复后的 validate 失败会重新执行 setup,而首次 setup 后立即验证失败会直接让测试失败。
declare global {
namespace Cypress {
interface Chainable {
loginAs(role: "editor" | "viewer"): Chainable<void>;
}
}
}
Cypress.Commands.add("loginAs", (role: "editor" | "viewer") => {
cy.env(["e2eUser", "e2ePassword", "e2eTenant", "e2eSessionKey"]).then(
({ e2eUser, e2ePassword, e2eTenant, e2eSessionKey }) => {
// session id 会进入 reporter,只使用不敏感但能区分账号槽位的别名。
const sessionId = [Cypress.config("baseUrl"), e2eSessionKey, role];
cy.session(
sessionId,
() => {
cy.request({
method: "POST",
url: "/api/test-login",
body: {
tenant: e2eTenant,
username: e2eUser,
password: e2ePassword,
role,
},
log: false,
}).its("status").should("eq", 204);
},
{
validate() {
cy.request({ url: "/api/me", failOnStatusCode: false, log: false })
.then(({ status, body }) => {
expect(status).to.equal(200);
expect(body).to.include({ tenant: e2eTenant, role });
});
},
},
);
},
);
});
export {};这里使用 cy.env() 按需读取敏感值,而不是把全部配置暴露给浏览器同步上下文。官方环境变量与秘密区分了敏感值的 cy.env() 和可公开给应用、第三方脚本与浏览器扩展读取的 Cypress.expose();已使用旧 Cypress.env() 的项目要按迁移指南改造,不能在不支持新 API 的发行线上直接复制示例。session id 会出现在 reporter 和 Sessions 面板,所以示例使用非敏感 e2eSessionKey 区分环境、租户和账号槽位,不能把密码、token 或真实用户名塞进 id。即使值通过 secret store 注入,密码仍可能因为 type()、请求体日志、失败截图或 task 日志被复制,测试命令要关闭不必要的 log,并避免 UI 登录成为每个用例的固定步骤。
session 默认只在当前 spec 生命周期内缓存。cacheAcrossSpecs: true 可以在同一次 cypress run 的同一台机器上跨 spec 复用,但缓存只存在内存中,不会跨机器,也不会延续到下一次 run;Cloud 并行时每台机器仍至少建立一次。所有 spec 还必须通过同一个 custom command 提供一致的 id、setup、validate 和 cacheAcrossSpecs,否则 Cypress 会拒绝复用。它是登录成本优化,不是分布式凭证缓存,更不能替代账号池和服务端数据隔离。
cy.origin() 用于同一个 test 中进入第二个 origin,例如业务站跳到身份提供方,再回到业务站。从 Cypress 14 起,同一 superdomain 下不同 origin 也需要显式处理;回调在隔离的 secondary origin 执行,外部数据必须通过 args 传递且可序列化。官方cy.origin() 参考说明回调内的 baseUrl 会变成 secondary origin。不要把完整 token 作为 args 传入;第三方身份站通常更适合测试环境 API 登录,保留少量跨 origin smoke 验证真实跳转。
cy.task() 是浏览器到 Node 事件进程的逃生口,适合种子数据、文件检查和调用受控外部进程。参数要能被 JSON 序列化,handler 返回 undefined 会被判定失败;无返回值也要显式 return null。官方cy.task() 参考强调 task 可以执行任意 Node 代码,这也意味着它拥有 runner 文件系统、网络和环境变量权限。任务名应白名单化,参数做 schema 校验,数据库账号只授予测试 schema,禁止实现“传入任意 SQL”“传入任意命令”的万能 task。
把 Cypress 接进真实项目而不污染业务代码
一个可维护的目录可以把 E2E、fixture、support、报告和业务组件测试分开:
project/
├─ cypress.config.ts
├─ cypress/
│ ├─ e2e/
│ │ ├─ smoke/
│ │ └─ account/
│ ├─ fixtures/
│ ├─ support/
│ │ ├─ commands.ts
│ │ └─ e2e.ts
│ ├─ downloads/ # 生成物
│ ├─ screenshots/ # 生成物
│ ├─ videos/ # 生成物
│ └─ results/ # 生成物
└─ src/
└─ components/
└─ ProfileForm.cy.tsxspec 只表达用户意图和断言,登录、种子和清理封装为语义清楚的 custom command 或 task;fixture 保存不敏感且稳定的输入,不保存从生产导出的原始响应。data-testid 是测试与 UI 的窄契约,比 CSS 层级和可变化文案稳定,但不能取代可访问语义:按钮仍应通过 role/name 让用户和测试都能理解。删除一个 test ID 应在代码评审中像删除 API 字段一样审查。
测试数据要按运行隔离。每个 CI 作业生成不可猜错的 run ID,创建的数据带 owner=browser-test、runId 和过期策略;账号至少按并发 worker 或角色拆分,不能让两个 spec 同时修改同一用户。种子接口只能在测试环境启用,经服务身份认证,并限制为测试租户。清理按创建返回的 ID 执行,而不是用“删除最近一条”这种并发不安全的语句。
项目脚本应把“启动应用、等待健康、运行测试、停止应用”做成一个原子入口。应用没启动时,Cypress 通常表现为 cy.visit() 连接拒绝;应用启动但迁移未完成时,页面可能加载,接口连续 5xx。健康检查应同时证明 HTTP 可达和必要依赖就绪。进程退出时无论测试成功失败都要终止预览服务,避免自托管 runner 残留端口影响下次作业。
截图、视频和报告怎样成为失败证据
cypress run 在 test 失败时默认截图,cypress open 不会自动生成失败截图;视频默认关闭,启用后按 spec 录制且只在 run 生效。官方截图与视频指南还说明,录制到 Cypress Cloud 时,每个 spec 完成后会处理和上传视频。于是“本地 open 没有截图”不是功能坏了,“CI 开启视频却没有 open 视频”也不是配置漂移。
证据包至少要把 spec、test、attempt、浏览器、系统、viewport、提交标识、配置摘要、错误栈、截图、视频和 JUnit 结果关联到同一个 run ID。内置 junit reporter 无需额外安装,默认 spec reporter 输出到终端;其他 Mocha reporter 需要单独锁版本。官方Reporter 指南说明每个 spec 可能生成独立报告,因此 mochaFile 要包含哈希,后续再合并,不能让并发 runner 覆盖同一个 XML。
截图和视频不是无害日志。密码框遮蔽不代表页面上的姓名、邮箱、订单、内部域名、聊天内容和 Authorization 头都被遮蔽;失败恰好发生在敏感页面时,自动截图会把现场永久复制到 artifact。可采用三层控制:测试租户只放合成数据;截图前对稳定选择器做遮罩或把敏感区域移出测试场景;artifact 存储设置最小读取权限、短保留期和删除审计。不要在报告里打印 config.env、完整 request body、session 详情或 cy.task() 参数。
trashAssetsBeforeRuns: true 会删除 downloads、screenshots 和 videos 三个生成目录的全部内容。单机顺序执行有利于避免旧证据混入新结果,但多个 Cypress 进程共享工作目录时会互相删除。并发 runner 应拥有独立 checkout 或把输出目录按 worker 隔离;上传完成后再统一合并报告。失败 attempt 会产生独立截图,而视频是按 spec 录制,不是按 attempt 分段;全部成功的长视频可在 after:spec 事件中按 results.tests[].attempts 判断后删除,retried/flaky spec 则保留整段视频和各 attempt 截图。
CI、浏览器矩阵和并发如何控制成本
CI 先用 lockfile 安装依赖,再 verify 二进制,启动应用并等待健康,最后执行固定浏览器和 spec。官方CI 指南建议 CI 至少提供 2 CPU 和 4 GB RAM,长时间运行或录像推荐 8 GB 以上;资源不足的信号包括运行中崩溃、视频缺帧和耗时突然上升。提高重试不会修复内存压力,只会让同一资源瓶颈被重复放大。
一个不依赖测试云的基础作业可以这样组织,具体启动命令替换为项目真实脚本:
jobs:
cypress-e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npx cypress version && npx cypress verify
- run: npm run build
- run: npm run test:e2e:ci
env:
CYPRESS_BASE_URL: http://127.0.0.1:4173
E2E_USER: ${{ secrets.CYPRESS_E2E_USER }}
E2E_PASSWORD: ${{ secrets.CYPRESS_E2E_PASSWORD }}
E2E_TENANT: ${{ vars.CYPRESS_E2E_TENANT }}
E2E_SESSION_KEY: ${{ vars.CYPRESS_E2E_SESSION_KEY }}
- uses: actions/upload-artifact@v5
if: always()
with:
name: cypress-evidence
path: |
cypress/screenshots
cypress/videos
cypress/resultstest:e2e:ci 应在内部可靠管理服务进程,不能简单使用一个不会回收的后台 &。制品上传使用 if: always() 才能保存失败现场,但上传前仍要执行敏感数据策略。示例中的 Action 主版本会继续变化,正式流水线应固定受审查的 commit SHA,并用依赖更新机器人维护。二进制缓存的 key 至少包含操作系统、CPU 架构、Cypress package 版本和 lockfile 摘要;缓存命中后仍执行 verify。缓存 node_modules、Cypress 二进制和浏览器下载目录是不同策略,不要把一个 cache hit 当成全部依赖可用。
Cypress Cloud 的 --parallel 不是单机把 test 开成多线程。官方并行说明指出,它要求 --record,由多个 CI 机器向 Cloud 报告 spec 列表,Cloud 根据历史耗时一次分配一个 spec;调度粒度是 spec 文件,顺序不保证。多个机器必须共享同一个 CI build ID,record key 通过 CYPRESS_RECORD_KEY secret 注入,不能出现在命令行、仓库或 artifact。
不开 Cloud 也可以用 CI matrix 手工把互不重叠的 spec 分组,但团队要自己维护分片和合并报告。单机并行启动多个 Cypress 进程通常先争抢 CPU、内存、端口、账号、数据和生成目录,可能更慢。并发收益取决于最慢 spec、应用后端容量和环境隔离;把一个 20 分钟的大 spec 与许多 10 秒 spec 放在一起,文件级调度仍会被长尾拖住。拆 spec 应按业务独立性和可清理的数据边界,不要为了均衡把一个事务流程切成依赖执行顺序的碎片。
浏览器矩阵也应分层:每次提交用 Electron 或团队主浏览器跑快速 smoke;合并门禁扩展用户占比高的 Chrome-family 与 Firefox 关键路径;WebKit 放在独立可观察作业。具体受支持的浏览器和版本窗口要在升级时从官方浏览器启动说明重新确认,并用 npx cypress info、cypress open 的浏览器列表或实际启动结果证明 runner 检测到了目标二进制。WebKit 仍是实验能力,需要显式开启 experimentalWebKitSupport 并安装 playwright-webkit;官方已列出的限制包括不支持 cy.origin() 与 Test Replay,部分 cy.intercept() 行为也受限。实验浏览器失败不能悄悄 continue-on-error 后无人负责;至少要有 owner、已知差异、升级触发和退出条件。
从现象反推故障层
The Cypress App could not be unzipped、ENOSPC 或 verify 超时发生在 spec 加载前,应查二进制下载、缓存目录、磁盘、代理、CA 和安全软件。先运行 npx cypress version、npx cypress cache path、npx cypress cache list --size,再以 DEBUG=cypress:cli* 单独执行 install;package/binary 不一致时清理目标版本缓存并重装,不要直接删除整个用户缓存。
浏览器启动后立即退出,应记录 npx cypress info、选择的浏览器路径、系统依赖和 stderr。Linux 容器缺 GTK、NSS、Xvfb 等依赖时,优先使用带固定 Cypress、Node 和浏览器标签的官方镜像,或按安装页补齐依赖;不要靠 --no-sandbox 作为通用修复。Windows 上出现 EPERM 时先确认杀毒软件和残留进程是否占用二进制,再检查 runner 用户权限。
cy.visit() 连接拒绝说明应用地址或进程有问题;页面能开但 cy.get() 超时,先看选择器、iframe、Shadow DOM、动画和覆盖层;cy.wait('@alias') 超时则查 matcher、注册时机、缓存、Service Worker 和请求是否由浏览器发出。把 defaultCommandTimeout 从 4 秒全局调到 60 秒会让每个真实错误多等 56 秒,且不能修复未命中的 intercept。应对确实慢的单个查询设置局部 timeout,并同时记录后端耗时。
登录后仍在空白页,常见原因是 cy.session() 在 test isolation 下清空页面而用例忘记再次 cy.visit();恢复后立即 401,则看 validate 是否存在、session ID 是否混用了角色或环境、服务端 token 是否已撤销。跨域报错时检查 scheme、host 和 port 是否改变,进入第二 origin 的命令是否放进 cy.origin(),传入 args 是否可序列化。不要关闭 chromeWebSecurity 来绕过所有同源问题,这会改变浏览器安全模型并隐藏真实集成缺陷。
只有并发时失败,优先查共享账号、固定测试数据、相同下载文件名、同一报告路径和后端限流;只有重试才通过,比较每个 attempt 的网络和截图,确认是页面等待不足、环境抖动还是前一次 attempt 留下的数据。固定顺序运行能通过、单独运行失败,几乎总是 test 依赖前序状态。用 --spec 单独执行,再交换 spec 顺序和 worker 数,能快速确认污染来自浏览器状态还是服务端数据。
清理、卸载和回滚要恢复整套状态
一次实验至少会留下 npm 依赖、lockfile、用户级二进制缓存、配置、spec、截图、视频、下载、报告、测试账号会话和后端数据。只执行 npm uninstall cypress 会删除项目包,不会删除全局二进制缓存和应用数据;官方高级安装说明提供 cypress cache prune 与 cypress cache clear。共享开发机和 runner 上优先 prune 已不用版本,clear 前确认没有其他项目依赖缓存。
# 删除项目依赖并更新 lockfile
npm uninstall cypress
# 仍在使用 Cypress 时,仅清理旧缓存版本
npx cypress cache list --size
npx cypress cache prune
# 生成物由项目脚本按明确目录清理
rm -rf cypress/screenshots cypress/videos cypress/downloads cypress/resultsWindows 用 Remove-Item -LiteralPath <目录> -Recurse 前要解析并核对绝对路径,不能把空变量传给递归删除。测试数据回滚由创建接口返回的 ID 驱动,session 回滚包括撤销服务端 token 和删除测试 cookie;Cloud 接入退出时还要撤销 record key、关闭项目访问、导出必要审计并删除不再需要的远端制品。仅删除 spec 不会撤销已经暴露的凭证。
升级回滚要保留旧 lockfile、旧 CI 镜像标签和一组固定正反 spec。先在隔离分支升级 Cypress 与组件 adapter,执行 install、verify、E2E、组件测试、浏览器矩阵和失败证据检查;若新版本改变 origin、test isolation、浏览器支持或 reporter 行为,回滚 package 而不回滚配置仍可能失败。恢复时应让 package、binary cache、配置和镜像重新匹配,再执行 cypress version 与 smoke 证明状态一致。
底层运行模型决定架构取舍
Cypress 命令运行在浏览器内,能直接访问被测页面的 DOM 和浏览器对象;需要服务器、数据库或文件系统能力时,经 cy.request()、cy.exec()、cy.task() 或 Node events 跨出浏览器。官方架构取舍说明也明确指出,Cypress 不是通用自动化工具,不能在一个 test 里同时控制两个浏览器,原生移动应用、爬网和性能压测不是它的优势场景。这个模型换来了强交互调试、命令时间旅行、网络控制和自动等待,也要求团队接受串行命令队列、origin 约束和浏览器/Node 两侧代码分工。
选择 Cypress 的强信号是:团队主要测试自己可控的 Web 应用;前端开发者需要快速组件反馈和可视化 Command Log;关键用户路径可以在单浏览器上下文中表达;后端提供测试 API 或可控 task 来准备数据。弱信号是:任务核心是多标签、多用户实时协作、任意第三方站点自动化、原生移动、复杂下载进程、性能采集或一个脚本同时编排多个浏览器。此时应重新评估专用工具或把场景拆成契约、服务端和少量浏览器验证。
Cypress App 可以在不录制到 Cloud 的情况下本地或在自有 CI 运行;Cypress Cloud 则提供 recorded runs、历史关联、Test Replay、Smart Orchestration 和 flaky 管理等托管能力。是否使用 Cloud 不是“有无 CI”的选择:本地 App + 自有 CI + JUnit/artifact 可以形成完整基础链;Cloud 的价值在跨机器协调、历史关联和调试体验,同时增加外部数据边界、用量成本、供应链和退出责任。Cloud 能力还受浏览器与订阅计划约束,例如官方Cloud FAQ说明 Test Replay 依赖 CDP,只覆盖 Chromium-family,Firefox/WebKit 的 recorded run 仍可保留截图、视频和 CI 日志。采购和升级前应在目标组织的计划页核对数据保留、访问控制、SSO、审计、并行、浏览器覆盖与导出删除能力,不能把产品页的一次能力列表写成永久承诺。
权限、凭证和敏感数据必须在浏览器外收口
自动化账号应是专用、可撤销、最小权限的测试身份,不与开发者个人账号共享,也不使用生产管理员。按环境、租户和角色拆分账号,密钥由 CI secret store 在作业运行时注入;本机使用被 .gitignore 排除且权限受控的秘密文件。cypress.env.json 含敏感数据时必须忽略。Cloud 的两个标识也不能混为一谈:官方项目管理说明允许 projectId 写入 Cypress 配置并提交,record key 才是授权写入 recorded runs 的秘密,泄漏后必须轮换。若团队选择隐藏 projectId,可以通过 OS 级 CYPRESS_PROJECT_ID 提供;CYPRESS_RECORD_KEY 始终只从 CI secret 注入,不能写进 env 配置、命令行、日志或 artifact。
秘密进入 Cypress 后仍会流过多个观察面:命令日志可能显示输入值,请求日志可能显示 header/body,session 面板包含 cookie 与 storage,task 可以打印参数,截图和视频会记录页面,reporter 会写错误对象。UI 输入密码时用 { log: false },网络断言只输出必要字段,task 日志建立字段级脱敏,失败 hook 不转储整个 config。任何访问 artifact 的人都可能看到测试数据,因此制品权限应低于代码库写权限、保留期短于普通构建日志,并支持按 run 删除。
生产环境地址应由硬阻断保护,而不是靠团队记忆。启动测试前检查 baseUrl host、环境标签和测试 API 响应中的租户标识,不满足允许列表就退出;种子 task 再检查数据库 host 和 schema。允许列表不能只写 endsWith("example.com"),否则恶意或误配子域可能通过。测试账号也应带资源配额和操作审计,防止失控 spec 创建大量对象或触发真实通知、支付和外部 webhook。
第三方插件和 reporter 在 Node 或浏览器中运行,能读测试、环境变量和文件。安装前检查维护者、支持 Cypress 版本、依赖树、发布来源和许可,锁定版本并经过依赖审计。setupNodeEvents 中注册的插件权限接近 CI 作业权限,不能因为它叫“测试插件”就降低供应链审查。
容量和成本会从 spec 数量反向塑造设计
一套浏览器测试的成本由安装下载、缓存、应用环境、浏览器 CPU/内存、账号和数据、重试次数、并发 runner、视频压缩、报告存储、Cloud 用量以及人工排障共同组成。最便宜的优化通常不是把并发从 4 加到 16,而是删除重复 E2E、把状态组合下沉到组件测试、用 API 建立登录态、把固定 sleep 改成可观察请求,并拆掉一个超长 spec 的共享数据依赖。
容量基线应记录总 spec、总 test、首轮通过率、重试后通过率、P50/P95 spec 时长、最慢 spec、每次运行截图/视频字节数、二进制缓存大小和 runner 峰值内存。视频默认关闭有利于控制存储,但高风险路径和 flaky 调查可能需要开启;可以按分支、标签或失败状态保留。失败 attempt 会增加截图,重试还会延长该 spec 的整段视频、执行时间和日志,因此不能按“一个 attempt 一段视频”错误估算制品数量。
后端容量也是并发上限。16 个 runner 同时登录、建单、发邮件和清理,可能把共享测试环境压到限流,此时新增机器只会让 flaky 更多。为测试流量设置独立租户、队列和限额,监控 API 错误率、数据库连接、队列积压和外部沙箱配额;超过安全水位时降低并发,而不是继续重试。Cloud 文件级负载均衡依赖 spec 历史时长,频繁重命名或把所有用例塞进一个文件都会降低调度收益。
成本评审还要包含 Cloud 的计量单位与达到限额后的行为。官方Billing & Usage把 --record 上传的 passed/failed test result 作为核心计量单位,计划、包含量、保留期和超额行为都可能变化,预算应从目标组织的实际计划页读取,并对异常录制量告警。与此同时要验证自有 artifact 是否能替代 Cloud 历史、报告是否使用开放格式、spec 是否依赖专有 API、record key 撤销后 CI 能否继续基础运行,以及历史结果如何导出和按期删除。把测试代码、运行证据和调度服务解耦,团队才有能力在预算、合规或供应商策略变化时迁移。
flaky 治理要把第一次失败当成真实信号
flaky 不是“最终绿了”的同义词,而是相同代码和输入得到不同结果。常见来源包括命令队列误用、固定 sleep、动作后沿用失效 subject、过宽 intercept、缓存请求、共享账号、非幂等种子、时区语言差异、动画字体、后端限流、资源不足和第三方沙箱抖动。每类原因都应有证据:Command Log 与栈定位命令,request/response 定位网络,attempt 截图和视频定位页面,run ID 与数据记录定位污染,runner 指标定位容量。
团队至少维护四个比例:首轮失败率、重试后通过率、隔离运行失败率和按 owner 的 flaky 存量。一个 test 首轮失败后第二次通过,发布结果可以按风险决定是否阻断,但必须计入 flaky;若只看最终退出码,指标会主动奖励增加重试。新增 flaky 先隔离还是立即阻断,要由关键程度决定:支付、权限和数据删除路径应阻断;低风险兼容矩阵可以短期隔离,但必须有 owner、原因假设、证据链接和到期处理,不允许永久 skip。
修复顺序从可观测条件开始:删除任意等待毫秒数,改为等待明确请求、DOM 状态或服务事件;让每个 test 独立创建和清理数据;缩小 matcher 与选择器;固定浏览器、viewport、语言、时区和字体;给真正慢的单点设置局部 timeout;最后才评估基础设施重试。每次修复都要做反向验证,例如重新引入旧 matcher 或共享数据能稳定失败,再恢复修复确认通过,避免把偶然绿误当成归因完成。
升级门禁用固定样本保护运行模型:一条查询等待、一条动作后重渲染、一条 503 intercept、一条 session 失效重建、一条 cross-origin、一条 task 参数拒绝、一条失败截图和报告、一条并发数据隔离。升级 Cypress、Node、浏览器、组件框架或 reporter 时先跑这组样本,再扩大业务矩阵。长期 owner 不仅维护 spec,还要维护账号、测试 API、缓存、artifact 权限、版本基线、容量预算和退出演练。这样,重试才是发现不稳定性的放大镜,而不是把发布风险涂成绿色的按钮。
