playwright-fixture-model

Playwright 测试夹具模型

YouTeacher 的端到端测试跑在 Docker 里的真实服务上,而不是给自己后端造的 mock。让这件事变得可承受的,是那层自定义夹具(tests/e2e/fixtures.ts):它决定浏览器如何共享、每个角色如何登录,以及——很关键的一点——在所有断言都通过时,什么仍然算测试失败。

Worker 级共享 context,测试级独立 page

新建一个 BrowserContext 开销不小(夹具注释里量到大约 500ms 一个),所以整套测试每个 worker 只建一个 context,在这个 worker 跑的所有测试之间复用。这体现为一个 worker 作用域的 sharedContext 夹具,并且 Playwright 内建的 context 夹具被覆盖成返回这个共享的。每个测试仍然拿到自己全新的 page

因为 context 在一个 worker 的多个测试间存活,它的 storage 也一起存活。page 夹具因此要防状态串味:setup 时清掉 sessionStoragelocalStorage(丢掉 OTP 冷却计时器和 passkey 提示),teardown 时如果还有会话,就走真实 UI 把用户登出,像真实用户离开那样。

按角色隔离的 page

有四个角色夹具——adminPageemployerPagetalentPagerecruiterPage——外加一个通用的 authenticatedPage 和一个 jobsPage 入口。每个都通过真实注册 UI 播下一个独一无二的用户,用 worker 索引加时间戳作键,保证并行运行或重跑时邮箱永不撞车。

共享规则是按一个真实约束来切的:一个需要同时用两个角色的测试(同一场景里既有 talent 又有 employer)不能让它们共享 cookie。所以 employerPagetalentPagerecruiterPage 各自建自己的隔离 BrowserContext,而不是那个 worker 共享的——cookie 和 storage 按角色分开。单角色的 adminPage 复用共享 context。播种管理员依赖测试鉴权服务在注册时就赋予管理员类型,所以夹具里不需要单独一步提权。

“goto 只许在夹具里”这条规矩

文件里贯穿着一条明写的不变量:page.goto() 允许出现在夹具里,测试体里绝不允许。测试必须靠点击和滚动去到任何别的界面,像真实用户一样。初始入口(/en/jobs、注册页)在夹具内部完成导航,每处都带一条注明原因的 eslint 豁免注释。意图是让测试走真实的导航路径,而不是瞬移到产品本身根本不会直接链过去的 URL。

导航本身也做了加固。夹具改写了 page.gotopage.waitForURL,让它们等 domcontentloaded 而不是默认的 load 事件:在 E2E 下,第三方资源(分析、网页字体、一个 newsletter 的 CDN)可能永远挂着,它们悬而未决的请求会卡住 load,把导航拖到超时。它还会在 Chromium 的瞬时网络错误(network-changed、connection-reset、aborted)上重试一次导航——这些在 Docker 高并行负载下会冒出来,被当成环境问题而非应用 bug。

把 console 错误当作失败闸门

最有主张的一部分:一个没有任何断言失败的测试,仍可能在 teardown 处失败——只要页面打过 console 错误,或抛过未捕获的 page error。page 夹具收集每一条 console.error/console.warn 和每一个 pageerror,结束时若列表非空就抛错,并把收集到的错误附到报告里。它还会扫描 Next.js 的错误浮层,若存在就判失败。用意是抓住那些从不体现为断言破裂的真实 bug。

这道闸门的成色,取决于它的噪声过滤器。consoleFilter.ts 里有一份中心化的 IGNORED_PATTERNS 列表,装的是预期中的噪声——返回 401/403/404 的鉴权探测、不安全来源上的 FedCM 警告、source-map 提示、限流消息、导航时被中止的在途 fetch、抖动的外部 CDN。文件在自己的注释里立了条硬规矩:未经明确许可不得改这份列表,因为每加一条都可能盖住一个真 bug。逐测试的逃生口是 allowConsoleError(pattern),它只给当前这个测试放行一个模式,并在下一个测试前自动清空。

React 19 Float #418 守卫

有一个专门的守卫(hydration418Guard.ts),用来压掉一个特定的假阳性,而不是笼统地把 hydration 错误都消音。Next.js 15.5+ 配 React 19.2+ 时,React 的 Float 系统会在 hydration commit 期间把异步 <script> 标签从 <head> 里搬走,扰乱 hydration 游标,从而抛出 hydration 错误 #418。此时 <body> 完全一致,只有 <head> 不同。这是上游的 bug,不是应用代码的问题。

守卫在导航前装上一个浏览器端的钩子。它在 DOMContentLoaded 时给 DOM 拍快照;当一个 #418 错误被构造出来时,它同步地把这个快照与当时的活 DOM 做 diff。如果 body 一致(只有 head 不匹配——或者是一种已知的仅导航引起的 hydration 差异),它就把错误改写成一个无害的标记并置一个 window 标志;随后一个测试后的过滤器把这个假阳性 #418 从收集到的错误列表里移除。如果 body 存在真差异,原错误照常传播、测试照理判失败。文件里注明:一旦上游 bug 修复,这整套 workaround 就该删掉。

配置基线

playwright.config.ts 定下外围的信封:单测试 90 秒超时(真实服务慢)、单断言 15 秒超时;无头时全并行、四个 worker(有头时改成串行并放慢,便于观察);重试一次;只跑 Chromium。trace 和截图只在失败时保留。Docker 启动参数关掉 GPU,并把 Chromium 的共享内存指到 /tmp,因为 Docker 默认的 /dev/shm 太小。

本节内容

  • 夹具层是“Docker 里的真实服务”与“读起来像用户旅程的测试”之间的接缝。
  • 两条共享轴:context(worker 共享 vs. 按角色隔离)与 page(永远逐测试、永远清理)。
  • 断言之外的两道失败闸门:console 错误Next.js 浮层——外加一处针对上游 hydration bug 的、狭窄且有据可查的压制。
about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

playwright-fixture-model