首页
/ tldraw dotcom E2E 场景测试规范:从冒烟测试走向多角色用户场景

tldraw dotcom E2E 场景测试规范:从冒烟测试走向多角色用户场景

2026-09-06 22:51:09作者:秋泉律Samson

本篇介绍 tldraw 生产应用(dotcom)的 E2E 测试规范。tldraw 正在把 dotcom 的 Playwright 测试从孤立的 UI 冒烟检查逐步迁移为贴近真实用户流程的"场景测试"(scenario tests),覆盖共享链接、工作区协作、访客权限等端到端行为。读完后你将掌握:如何按仓库约定编写 *.scenario.spec.ts 场景测试、如何使用多角色 actor fixture 与场景命名空间做数据隔离、如何选择合适的就绪等待与断言方式,以及如何正确运行/调试各套测试命令。

背景:为什么从冒烟测试迁移到用户场景

规范文档 README 开宗明义:dotcom 的测试正在向"用户场景"靠拢,而不是孤立的 UI 冒烟测试。新增的协同(dotcom)覆盖应当优先使用 fixtures/scenario-test.ts 中的场景 fixture。动机在于:共享、协作、成员权限这类行为需要"多个用户同时操作、互相观察"才能验证,单窗口、单用户的冒烟断言无法证明协作语义是否正确。

仓库中的现状与规范一致:apps/dotcom/client/e2e/tests 下既有新式场景测试(auth.scenario.spec.tssharing-live.scenario.spec.tslegacy-routes.scenario.spec.tsworkspaces-feature-coverage.scenario.spec.ts 等),也保留着位于 tests/smoke 子目录的旧式冒烟套件。

测试项目划分:chromium-scenarios 与 legacy chromium

两套套件由 playwright.config.ts 中的文件名匹配规则区分:

const scenarioTestMatch = /.*\.scenario\.spec\.ts/
// Legacy smoke specs live in e2e/tests/smoke and are intentionally separate from the default
// scenario runner. See e2e/README.md.
const smokeTestMatch = /tests\/smoke\/.*\.spec\.ts/

项目配置对应两个 runner:

  • chromium-scenarios:匹配 *.scenario.spec.ts,并显式开启 fullyParallel: true(见 playwright.config.ts)。这是规范要求的"场景文件跑在 chromium-scenarios 项目下、全并行"的落地点;
  • chromium:仅匹配 tests/smoke 下的旧式 spec,使用基于重置(reset-based)的流程,有意与默认 runner 分开,并且不在 CI 上运行。规范明确要求:随着后续工作解锁,应把 tests/smoke 中的覆盖逐步迁移到场景测试中;
  • 两个项目都依赖 global-setup 项目。该 setup(global.setup.ts)调用 @clerk/testing/playwrightclerkSetup(),为测试准备 Clerk 测试账号环境。

配置文件还有几个与场景测试直接相关的全局设置:CI 下 retries: 2forbidOnly: true;webServer 在 CI 中用 VITE_PREVIEW=1 yarn dev-app 启动完整 dotcom 开发栈(process-compose 拉起 postgres → migrate → zero-cache → workers → client,见 apps/dotcom/process-compose.yaml),本地则复用已运行的 yarn preview-app;globalTeardown 仅在 CI 生效,用于测试后拆除容器和端口。

运行命令速查

命令定义在 apps/dotcom/client/package.jsonscripts 中,仓库根目录 package.json 也提供了转发入口:

命令(仓库根目录) 等价命令(dotcom workspace) 说明
yarn e2e-dotcom yarn workspace dotcom e2e 常规本地/CI 套件,实际只运行 chromium-scenarios 项目;执行前会 rm -rf 'e2e/.auth' 清除已存储的登录态
yarn workspace dotcom e2e-scenarios 只运行场景套件,但不清除已存储的 auth(增量调试更快)
yarn e2e-dotcom-smoke yarn workspace dotcom e2e-smoke 运行保留的 legacy 冒烟套件(chromium 项目,tests/smoke 下的 spec),本地专用,CI 不跑
yarn e2e-dotcom-all yarn workspace dotcom e2e-all 同时运行新旧两个项目,用于对比新旧覆盖;同样先清 auth

workspace 脚本的实际实现示例(节选自 apps/dotcom/client/package.json):

"e2e": "rm -rf 'e2e/.auth' && NODE_OPTIONS='--no-strip-types' yarn playwright test --project=chromium-scenarios",
"e2e-scenarios": "NODE_OPTIONS='--no-strip-types' yarn playwright test --project=chromium-scenarios",
"e2e-smoke": "rm -rf 'e2e/.auth' && NODE_OPTIONS='--no-strip-types' yarn playwright test --project=chromium",
"e2e-all": "rm -rf 'e2e/.auth' && NODE_OPTIONS='--no-strip-types' yarn playwright test --project=chromium --project=chromium-scenarios"

此外还有配套脚本:e2e-debug / e2e-all-debug(Playwright Inspector 调试)、e2e-ui / e2e-all-ui(UI 模式)、e2e-x10 系列(通过 --repeat-each=10 重复 10 遍,用于暴露并发下的偶发失败)。注意 e2ee2e-all 都会先删掉 e2e/.auth 目录——该目录存放 actor 的登录态(storageState)文件,删除后首次运行会重新登录(见下文"账号池")。

场景 fixture:命名 actor 与场景命名空间

场景 fixture 定义在 fixtures/scenario-test.ts,它扩展 Playwright 的 base 测试,暴露的 fixture 与规范逐条对应:

  • 命名 actor:ownermembervisitor 三个 fixture。每个 actor 是一个 DotcomActor 实例,封装了独立的 BrowserContextPage,以及一组页面对象(SidebarEditorHomePageShareMenuDeleteFileDialogErrorPageImportHelperSignInDialogWorkspaceInviteDialog,见 scenario-test.ts)。ownermember 是已登录账号,visitor 是未登录的无痕上下文;
  • 数据按场景命名空间隔离:scenario.name('label') 返回 `${this.id} ${label}`,其中 idgetScenarioId 生成——取测试完整标题的 slug(截断到 24 字符)+ 全标题的 sha1 前 6 位哈希,再拼上 CI 运行号 GITHUB_RUN_ID、并行 worker 下标、repeatEachIndex 与重试次数(见 scenario-test.ts)。因此规范强调"文件和/工作区要用 scenario.name 命名":不同运行、不同并行 worker 之间数据天然互不冲突;
  • 上下文生命周期归 fixture 所有:actors fixture 在测试结束后自动调用 actors.closeAll() 关闭所有 actor 上下文,owner/member/visitor 这三个便捷 fixture 也各自包在 testUse 中,测试代码不需要手动清理浏览器上下文;
  • 账号与 worker 绑定:每个并行 worker 有自己的"场景用户池下标"。getScenarioUserIndex(parallelIndex) 返回 4 + parallelIndex(SCENARIO_USER_POOL_START = 4),超出 NUMBER_OF_USERS(见 consts.ts,共 8 个 Clerk 测试账号)时会直接抛错提示"增加测试账号或降低 worker 数"。

这正是规范中两条约定在源码中的体现:

  1. "场景 actor 使用比 legacy chromium 项目更靠后的 Clerk 测试账号"——SCENARIO_USER_POOL_START = 4 让两个项目即使在同一命令里一起跑(e2e-all),也各自使用不相交的账号;
  2. "场景测试中不要重置整个数据库或共享用户"——setupAndCleanup 自动 fixture 只调用 Database.reset(),而 fixtures/Database.tsreset() 仅清理"本 worker 绑定的两个 Clerk 测试账号"(通过调用 worker 提供的测试接口 POST /api/app/__test__/user/:id/prepare-for-test),而非清空全库。工作区/文件则靠 scenario.name 的命名空间天然隔离。

一个典型测试的开头大致如下(按 fixture 约定编写):

import { test, expect } from '../fixtures/scenario-test'

test('owner 与 member 在同一文件中协作', async ({ owner, member, scenario }) => {
  const file = await scenario.createSharedFile(owner, 'edit')
  await member.goto(file.sharedUrl)
  await scenario.createRectangle(owner)
  // 在已打开的窗口之间做实时断言,而不是 reload
  await member.expectCollaboratorCount(2)
})

场景 fixture 的常用搭建 API

规范要求用 scenario 对象的方法完成常见前置搭建,而不是在每个测试里手写一遍 UI 流程。从 DotcomScenario 类 可以确认全部方法签名:

  • scenario.createPersonalFile(actor, fileName?):确保侧边栏打开、切回主工作区(如可用),新建文档并断言其激活,返回 { fileName, url };
  • scenario.createSharedFile(actor, linkType?, fileName?):先建个人文件,再通过共享菜单设置链接类型('edit' | 'view')并复制共享链接,返回附带 sharedUrllinkType 的结果;
  • scenario.createGuestEditFile(owner, guest, fileName?) / scenario.createGuestViewFile(owner, guest, fileName?):owner 建共享文件后,让 guest actor 直接打开共享 URL——这是"已登录访客以共享链接身份编辑/查看"场景的标准入口;
  • scenario.createPublishedFile(actor, fileName?):创建文件并发布,返回 publishedUrl;
  • scenario.importFileFromUrl(actor, url?):通过 ImportHelper mock 远程 URL 并导航,验证 /f/ 路由加载完成后返回文件名与 URL;
  • scenario.downloadFileFromSidebar(actor, fileName):悬停侧边栏文件项、点 Download 菜单项,捕获 Playwright 的 Download 事件;
  • scenario.setSharedLinkType(actor, linkType):在邀请页签切换共享开关,支持 'edit''view''no-access'(关闭共享开关),并通过下拉框断言最终标签为 Editor/Viewer;
  • scenario.createWorkspaceWithMember({ owner, member, workspaceName?, fileName? }):owner 侧通过应用内 mutator 建工作区与文件(等待 server 确认,见 scenario-test.ts),复制工作区邀请链接,member 打开邀请链接接受,并断言侧边栏出现工作区与文件;注意工作区名会按 MAX_WORKSPACE_NAME_LENGTH 截断,与生产 mutator 的钳制行为保持一致;
  • scenario.createWorkspaceWithRemovedMember(...):在前者基础上,通过工作区设置菜单把 member 移除,用于验证"成员被移除后访问受限"的行为;
  • scenario.createLegacyRouteFixture(actor):规范单独点名的遗留路由(/r/ro/v/s 及 history 路由)搭建方法。它向后端的 POST /api/app/__test__/legacy-room 调试接口发请求(带 30 次重试),创建命名空间化的、仅调试用的 worker 数据,而不是依赖共享的类生产 fixture,最后返回 roomreadonlylegacyReadonlysnapshothistoryhistorySnapshot 六个可直接访问的 URL。

数据准备的边界:直接写库只在必要时

规范对"直连数据库"的用法划了明确边界:仅当前置条件通过 UI 做成本高或根本做不到时(例如开启某个工作区 feature flag),才允许直接做数据库搭建;被测行为本身必须走 UI 操作完成

从源码结构看这一边界是有支撑的:fixture 里确实保留了数据库直连能力——Database 类用 Kysely + pg 连接本地 Postgres(连接串指向 127.0.0.1:6432,见 fixtures/Database.ts),提供 getUserIdByEmail 等查询能力;createPendingWorkspaceInvite 也需要它按 email 查 member 的用户 ID。同时 createLegacyRouteFixture 走的是 worker 暴露的 __test__ 调试端点,而非手工插库——"用测试专用接口创建最小调试数据"与"用 UI 驱动被测行为"的组合,正是这条约定的具体形态。

同理,actor 的登录态也通过文件缓存:每个并行 worker 首次运行时,ensureStorageState(scenario-test.ts)会真实登录 huppy+clerk_testN@tldraw.com / suppy+clerk_testN@tldraw.com 并把 storageState 写入 e2e/.auth/ 目录(文件名规则见 fixtures/helpers.ts),后续 worker 直接复用。这就是规范中"稳定的 Clerk 测试账号被 worker 复用"的实现。

就绪等待:用 actor 助手替代 sleep

规范要求"通过 actor 助手和 page objects 等待就绪,避免新增 sleep(除非产品本身有意的等待)"。actor.goto(url) 的默认行为(scenario-test.ts)是调用 waitForAppReady(),它串行等待五个条件:

async waitForAppReady() {
  await this.homePage.isLoaded()
  await this.waitForEditorReady()
  await this.waitForAuthLoaded()
  await this.waitForAppStoreHydrated()
  await this.waitForFileRoomConnected()
  await this.waitForVisitorAccessMetadata()
}

各项含义(均通过 page.waitForFunction 轮询页面内 window.app / window.editor 状态):

  • waitForAuthLoaded:登录态已加载。签名用户要求 app.getUser() 存在;未登录则要求页面上出现登录按钮或侧边栏开关等 DOM 锚点,并区分 app 路由(//f/)与遗留只读路由(/ro//v/);
  • waitForAppStoreHydrated:仅对签名 actor 有意义,等待 app.getUserFileStates() 返回数组,即应用级 store 已从远端水合;
  • waitForEditorReady:editor 已挂载并拥有当前页(editor.getCurrentPageId()),遗留只读路由下则退化为"画布已挂载 + 路由前缀匹配";
  • waitForFileRoomConnected:editor store 的连接状态为 undefined'synced-remote',即文件房间(sync room)已连上远端;
  • waitForVisitorAccessMetadata:以 editor.getIsReadonly() 能返回布尔值作为探针,证明挂载的 editor 已经拿到了当前访问模式(对未登录访客与已登录 guest 均适用);
  • 另有 waitForSessionClosed(timeout = 20000):轮询 editor.getCollaborators().length 直到为 0,用于断言"对方已离线/会话已关闭"。

规范建议:当测试需要证明某个特定状态迁移时,应使用更窄的单一助手(waitForAuthLoadedwaitForAppStoreHydratedwaitForEditorReadywaitForFileRoomConnectedwaitForVisitorAccessMetadatawaitForSessionClosed),而不是每次都用全量的 waitForAppReady

断言风格:优先跨窗口实时断言

规范在断言上给了两条原则:

  1. 优先在已经打开的多个窗口之间做实时断言(live assertions)。reload 型检查对验证持久性仍然有用,但不应是协作、权限、成员行为的唯一证据。fixture 为此提供了配套工具:getCollaboratorCount() / expectCollaboratorCount(count, timeout)getIsReadonly() / expectReadonly(readonly, timeout)(scenario-test.ts)都用 expect.poll 轮询真实 editor 状态——例如 owner 在一个窗口画矩形、member 在另一个已打开的窗口轮询协作者数量或形状,不需要各自刷新页面;
  2. 旧式辅助 expectBeforeAndAfterReload(fixtures/helpers.ts)仍然保留:它在 reload 前后各执行一次断言,中间留了 100ms 让乐观更新传播到服务端。这是"reload 作为持久性证据"的规范工具,但它不携带登录上下文切换,适合单窗口场景。

Legacy 冒烟套件与迁移策略

规范把 legacy 套件的定位说得非常明确:

  • tests/smoke 下的 spec 仍然使用基于重置的 chromium 项目,与默认 runner 有意分离;
  • 不在 CI 上运行,避免拖慢主流程;
  • 迁移方向是:把 tests/smoke 的覆盖逐个改写成场景测试("as the follow-ups below are unblocked")。

对实际维护者的含义是:新增覆盖永远写到 *.scenario.spec.ts;只有当某条冒烟覆盖暂时无法在场景框架下表达(通常卡在 auth 边界,见下节)时,才留在 tests/smoke,并把它视为待迁移项而非稳定资产。新旧对比可用 yarn e2e-dotcom-all 同时跑两个项目。

Auth 后续迁移清单

文档最后列出仍由 issue #9185 跟踪的 auth 边缘场景,并明确要求在具备 Clerk 层 mock 或无需逐测试重置共享账号的稳定账号状态之前,不要把它们放进场景项目。迁移分组有四项,值得作为"哪些场景测试写法不成熟"的参照:

  1. 法务同意与 analytics 同意(auth legal acceptance and analytics consent):当前依赖对路由层 Clerk 响应的 patch;应替换为稳定的 Clerk 测试账号状态,或在 Clerk client 边界做 mock;
  2. 验证与重发行为(verification and resend):Clerk 网络故障、畸形响应、冷却(cooldown)行为、验证码输入行为,需要在不依赖"共享已登录用户清理"的前提下隔离测试;
  3. OAuth 流程:需要一个可演练 Google 登录与法务同意的 Clerk/OAuth 边界,且不触发外部重定向;
  4. Auth 响应边缘情况:完整会话、不一致的 missing_fields payload、缺少 email-code factor 数据等,需要等到能在路由层之下 mock Clerk 响应后才可纳入。

小结:编写一个新场景测试的检查清单

综合规范与源码,一个新 *.scenario.spec.ts 应满足:

  1. 文件名以 .scenario.spec.ts 结尾,从 fixtures/scenario-test.ts 导入 test / expect;
  2. 使用 owner / member / visitor 命名 actor,文件与工作区一律用 scenario.name('label') 命名;
  3. 前置搭建优先用 scenario.createSharedFile / createGuestEditFile / createWorkspaceWithMember / createLegacyRouteFixture 等现成方法;
  4. 不重置全库、不重置共享用户;直连数据库只用于"UI 做不了或成本过高"的前置条件(如工作区 flag);
  5. 就绪等待走 actor.goto / 细粒度 waitFor* 助手,不写裸 sleep;
  6. 协作、权限、成员行为优先跨窗口实时断言,expectBeforeAndAfterReload 只作为持久性的补充证据;
  7. yarn workspace dotcom e2e-scenarios 验证(不清 auth),交付前用 yarn e2e-dotcom 走完整 CI 等价路径。
登录后查看全文
热门项目推荐
相关项目推荐