opencode 的 Playwright E2E 测试规范:定位符选择、等待策略与测试卫生标准

📅 发布时间:2026/9/7 10:00:31
opencode 的 Playwright E2E 测试规范:定位符选择、等待策略与测试卫生标准 opencode 的 Playwright E2E 测试规范定位符选择、等待策略与测试卫生标准【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencodepackages/app/e2e/AGENTS.md是 opencode Web 应用SolidJS 构建的会话界面为 E2E 测试立下的一份质量契约在编写、修改或评审任何 Playwright 测试前必须遵循官方最佳实践并用一组具体的测试卫生规则约束定位符选择、同步等待与断言方式。读完本文你将掌握这套规则的完整内容并能从仓库中约 90 个 spec 文件和共享工具waits.ts、mock-server.ts、errors.ts、sse-transport.ts里看到每条规则对应的真实落地方式包括如何运行 E2E 套件、如何为异步渲染的 UI 断言正确的就绪状态。1. 规则所属的测试体系E2E 套件位于 packages/app/e2e配套 Playwright 配置为 packages/app/playwright.config.ts。目录按测试目的分层目录用途典型文件smoke/关键路径冒烟会话时间线渲染、滚动、标签页切换session-timeline.spec.tsregression/回归测试约 41 个 spec覆盖 review、终端、会话列表等review-line-comment.spec.tsuser-story/端到端用户故事model-selection-flow.spec.tsperformance/性能基准与时间线稳定性矩阵独立配置运行默认被排除timeline-stabilityreproduction/最小复现应用如 timeline-suspensetimeline-suspense.repro.tsutils/共享工具等待、错误追踪、mock 服务器、SSE 传输utils/waits.ts运行方式在packages/app下脚本定义见 package.jsonbun run test:e2e # playwright test bun run test:e2e:ui # Playwright UI 模式 bun run test:e2e:report # 打开 e2e/playwright-report bun run test:stability # 时间线稳定性套件独立 config bun run test:bench # 性能基准套件独立 config bun run typecheck:e2e # 对 e2e 子集做类型检查配置文件的关键参数值得注意见 playwright.config.tstimeout: 60_000单测试 60 秒expect.timeout: 10_000断言默认 10 秒——这就是规范中保持超时自适应的基线retries: process.env.CI ? 2 : 0只在 CI 重试 2 次本地失败立即暴露配合trace: on-first-retry、screenshot: only-on-failure、video: retain-on-failure采集失败证据webServer自动拉起bun run dev默认 3000 端口并注入后端地址VITE_OPENCODE_SERVER_HOST/PORT默认 127.0.0.1:4096本地reuseExistingServerCI 不复用testIgnore默认忽略performance/**仅当OPENCODE_PERFORMANCE1时才运行其中非.test.ts的 spec——性能与功能套件互相隔离forbidOnly: !!process.env.CI防止test.only被误提交进 CI。2. 必读清单Playwright 官方指南文档的 Required Reading 部分要求在写、改、评审 E2E 测试前总是先阅读并遵循 Playwright 官方的三篇核心指南——Best Practices最佳实践、Auto-waiting自动等待与 Assertions断言当问题涉及时再读 Locators定位符、Network网络与 Test Isolation浏览器上下文隔离指南。AGENTS.md 中对这六篇指南给出了官方链接。这不是泛泛的看官方文档而是把规范中每条卫生规则的依据锚定到了 Playwright 的官方能力上自动等待机制actionability、web-first 断言、基于 role 的稳健定位、waitForResponse等网络等待以及browserContext级的测试隔离。3. 测试卫生规则逐条解析Test Hygiene 一节共 8 条规则。下面结合仓库中的真实 spec 逐一说明其含义与落地证据。3.1 测试用户可见行为使用隔离的确定性数据与范围化、唯一的定位符规则原文Test user-visible behavior with isolated, deterministic data and scoped, unique locators.落地方式是mockOpenCodeServerutils/mock-server.ts每个 spec 通过page.route(**/*)拦截请求返回构造好的确定性数据——固定的目录路径如 regression 测试里的C:/OpenCode/ReviewLineCommentRegression、固定的会话 ID 与消息内容而不是连真实后端。注意第 57 行if (url.port ! targetPort url.port ! appPort) return route.fallback()只拦截应用端口与后端端口的流量其余请求放行这本身就是一种隔离。同时测试只断言 DOM 中用户实际可见的状态标题、文本框、标签页文本不触及内部 store 或网络之外的实现细节。3.2 优先 role、label、文本与显式测试契约定位符不要用.first()/.last()来静音严格模式规则原文Prefer role, label, text, and explicit test-contract locators. Do not use.first()or.last()merely to silence strictness errors.仓库中的定位符分层很清晰见 review-line-comment.spec.tsconst review page.locator([data-componentsession-review]) // 测试契约组件锚点 const line review.getByText(export const value after, { exact: true }) // 精确文本 await line.click() await expect(review.getByRole(textbox)).toBeVisible() // role await expect(review.locator([data-slotline-comment-editor-label])).toHaveText(Commenting on line 2)data-component/data-slot这类显式属性即测试契约定位符是应用侧专为测试暴露的稳定钩子其外再叠加getByRole/getByText({ exact: true })。范围化scoped体现在定位符总是先锚定review容器再向下查找。一个值得注意的细节同文件中[data-column-number1]使用了.last()——从源码结构看diff 视图中行号会跨文件重复测试取最后一个出现是范围化的唯一定位而不是规则所禁止的用.first()压制 strictness 报错。两者的区别在于定位符在去掉.last()后是否真的无法唯一。3.3 禁止墙钟等待用 locator 动作、自动等待与 web-first 断言同步规则原文NEVER usewaitForTimeout,setTimeout, sleeps, animation-frame counts, or other wall-clock delays to synchronize a test. Wait for the specific UI state, request, response, event, or application outcome instead.这是对 flake 来源最直接的封杀。共享工具 utils/waits.ts 只暴露两个具名等待且内部全部是 web-first 断言export const APP_READY_TIMEOUT 30_000 export async function expectAppVisible(locator: Locator) { await expect(locator).toBeVisible({ timeout: APP_READY_TIMEOUT }) } export async function expectSessionTitle(page: Page, title: string) { await expectAppVisible(page.getByRole(heading, { name: title })) }在冒烟套件 smoke/session-timeline.spec.ts 中等待历史消息分页完成用的是对请求记录数组的expect.pollawait expect.poll(() requests.some((request) request.before request.phase end)).toBe(true) await waitForTimelineStable(page) await expect.poll(positions).toEqual(before)即轮询的是应用产生的具体结果分页请求的end相位、虚拟列表行位置回到原值而不是等一个固定时长。3.4 导航、网络响应、DOM 挂载或可见性都不足以证明异步 UI 就绪规则原文Do not treat navigation, a network response, DOM attachment, or visibility alone as proof that asynchronously rendered UI is ready. Assert the state the next action actually requires.冒烟套件为此专门实现了waitForTimelineStable第 658-673 行它通过page.waitForFunction在页面内连续读取时间线签名由scrollTop、scrollHeight、各消息行几何位置与 ID 列表 JSON 序列化而成连续多帧签名不变才判定稳定。随后测试还断言分页前采集的三行data-timeline-part-id位置在分页后toEqual(before)——断言的正是下一个动作实际要求的状态可视消息不跳动而不是页面能打开。3.5 在触发动作之前注册事件与网络等待规则原文Register event and network waits before the action that triggers them.review-line-comment.spec.ts 第 151-157 行 是标准范例const changes page.getByRole(tab, { name: Changes }) const diffResponse page.waitForResponse( (response) response.request().method() GET response.ok() new URL(response.url()).pathname /api/vcs/diff, ) await changes.click() // 触发请求的动作在注册之后 expect((await (await diffResponse).json()).data).toHaveLength(1)waitForResponse先挂起再点击触发若顺序颠倒请求可能在注册前就已发出而错过。同样的模式也出现在 SSE 场景utils/sse-transport.ts提供waitForConnection/send/burst/heartbeat等可控通道测试先安装传输、拿到连接句柄再驱动 UI 动作。3.6 不重试有状态副作用的动作重试幂等的就绪检查然后只做一次动作并断言结果规则原文Do not retry state-changing actions. Retry idempotent readiness checks, then perform the action once and assert its outcome.对应实现是expect(async () { ... }).toPass()。在 review-line-comment.spec.ts 第 47-64 行await expect(async () { await lineNumber.hover() // 幂等可重复 await expect(lineNumber).toHaveAttribute(data-hovered, ) await expect(comment).toHaveCount(1) await comment.focus() await expect(comment).toBeFocused() }).toPass({ timeout: 10_000 }) await comment.press(Enter) // 副作用动作只执行一次 await expect(review.getByRole(textbox)).toBeVisible()hover 与可见性检查可安全重试press(Enter)会真正提交评论因此放在重试块之外只执行一次随后断言编辑器打开。若把press也放进toPass就违反了这条规则。3.7 保持动作与断言超时自适应不用短超时探活、不靠重试掩盖 flake规则原文Keep action and assertion timeouts adaptive. Do not use short timeouts as readiness probes or rely on retries to hide flakes.从配置看全局断言超时 10splaywright.config.ts应用级就绪统一走APP_READY_TIMEOUT 30_000waits.ts而不是各处散落 500ms 之类的探活超时重试仅存在于 CI2 次配合trace: on-first-retry采集证据——重试用于收集诊断信息而不是把失败重试成通过。3.8 断言精确结果与身份让陈旧状态、重复渲染、错元素交互无法通过规则原文Assert exact outcomes and identities so stale state, duplicate rendering, and interactions with the wrong element cannot pass.冒烟套件把精确做到了 ID 级expectOrderedIDs第 611-615 行要求实际渲染的 part/message ID 序列与 fixture 期望值按顺序一致expectCompleteScroll第 691-711 行则要求向上滚动到底后全部 331 个 part ID 都出现在可见记录中、ID 集合无重复并附带遍历采样摘要便于失败诊断。断言文本也是精确匹配Commenting on line 2、toHaveText(Use the existing value instead, { exact: true })。错误追踪同样精确utils/errors.ts 同时收集console.error与pageerrorexpectNoSmokeErrors要求控制台错误、错误 toast、禁用文案三者全部为空数组。4. 一个完整用例的读法以 regression/review-line-comment.spec.ts 为例可以看到全部规则如何串成一条链确定性数据beforeEach中mockOpenCodeServer注入固定的 project、session、vcsDiff含统一 diff 补丁文本与单条用户消息范围化定位[data-componentsession-review]锚定 review 面板内部再按data-file、data-column-number、getByRole下钻先注册后触发点击 Changes 标签前先挂waitForResponse(/api/vcs/diff)并用响应的data.length 1证明只加载了预期那份 diff精确断言点行 2 后断言编辑器标签为Commenting on line 2提交评论后不仅断言评论出现还切到 Session 标签页断言上下文气泡包含review.ts:2——即评论真正落到了 prompt 上下文的正确文件与行号上而非仅界面上出现了文字。同类结构还可见于 user-story/model-selection-flow.spec.ts用provider()工厂函数模拟连接 OpenCode Go 后模型列表变化的状态迁移通过onConnectKey/onInstanceDispose回调驱动与 smoke/session-timeline.spec.ts虚拟列表分页 可视稳定性 标签页切换首帧绘制验证。5. 小结这份规范的实际约束面规则主题反模式仓库中的正模式数据依赖真实后端/随机数据mockOpenCodeServer拦截 固定 fixture定位.first()/.last()压 strict 报错data-component/data-slot契约 role 精确文本同步waitForTimeout/sleep/帧计数expect.poll、waitForFunction稳定性签名就绪判断导航/响应/可见即结束断言下一步动作要求的 UI 状态位置、文本、ID 序列事件/网络先点击后挂等待waitForResponse先于click注册重试重放副作用动作toPass只包裹幂等检查副作用动作执行一次超时短超时探活全局 60s/10s 基线 30s 就绪常量CI 才重试断言断出现了断精确文本、有序 ID 序列、无重复、无 console/toast 错误需要说明的适用边界该规范面向packages/app的浏览器端 E2EPlaywright Vite dev server 拦截式 mock 后端与单测bun test happy-dom、performance/基准套件独立配置OPENCODE_PERFORMANCE1才纳入默认运行互不混用e2e/tsconfig.json也只对其中一部分 spec 启用类型检查。遵循 AGENTS.md 的要点可以概括为一句话等具体的状态而不是等时间定位具体的元素而不是碰运气断言精确的结果而不是页面没崩。【免费下载链接】opencodeThe open source coding agent.项目地址: https://gitcode.com/GitHub_Trending/openc/opencode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考