Slate v2 Integration-Local Triage 实战:Playwright 集成测试从 676 行精简到 588 行的去重与 fail-fast 修复指南
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
导读
本文以 docs/plans/2026-04-29-slate-v2-integration-local-triage-plan.md 为主体,完整还原 Slate v2 对bun test:integration-local这一本地集成测试通道的一次"分诊(triage)"实践:在不动摇浏览器回归覆盖的前提下,把超过 10 分钟的测试通道从 676 行裁减到 588 行,并依次修复了四簇 fail-fast 红色用例。读完本文,你将掌握一套可复用的 Playwright 用例去重决策框架(安全删除候选 / 风险候选 / 保留清单),以及"单条失败即停 + 逐簇修复"的高效回归修复流程,并了解这些做法在本仓库源码中的对应依据。
一、背景与目标:bun test:integration-local的 10 分钟困境
在 Slate v2 的浏览器回归体系中,bun test:integration-local是开发者日常必跑的本地集成通道。但它有两个痛点:
- 耗时过长:整套本地集成测试超过 10 分钟,严重拖慢开发反馈闭环;
- 职责重叠:通道里混入了大量与其他测试通道重复、低价值、甚至仅做路由冒烟(route smoke)的用例。
本次分诊的核心目标,正如文档所述:
Identify
.tmp/slate-v2Playwright integration rows that are redundant, low-value, or better covered by generated stress / package contracts, sobun test:integration-localcan shrink without gutting browser-regression coverage.
即:找出冗余、低价值、或已被生成式压力测试(stress)与包级契约(package contracts)更好覆盖的行,让本地集成通道瘦身,但绝不能掏空浏览器回归覆盖。
这里的.tmp/slate-v2是 Slate v2 研发流程中的独立工作区(独立于本仓库的 packages/playwright 生态),集成测试代码位于其playwright/integration/**目录下,测试工程由该工作区的package.json管理。
分诊的三个硬约束
文档明确了三条约束,这是整个决策过程的"宪法":
- Do not remove tests by vibe:禁止凭感觉删除测试,每一次删除都必须有更强者(stronger scenario)或明确的重叠依据;
- 优先删除两类行:重复了更强生成式场景的行,以及只断言路由冒烟的行;
- 必须保留的回归类别:用户上报的回归类别(user-reported regression classes)、浏览器选区(browser selection)、DOM/模型一致性(DOM/model parity)、void 导航、表格导航、IME、剪贴板(clipboard)、工具栏选区修复(toolbar selection repair)——除非附近存在严格更强的行,否则一律保留。
这条"保留红线"很重要:它把浏览器特有的行为(IME、剪贴板、选区修复)与纯逻辑行为区分开,防止为省时间而牺牲真正只能在浏览器里暴露的缺陷。
二、工作流程:5 步分诊法
文档给出了一个清晰的分诊执行流程:
- Inventory:盘点 Playwright 工程与测试行(rows);
- Cluster:按路由与场景族(route and scenario family)聚类;
- Compare:将重复行与生成的 stress / slate-browser 契约逐条对比;
- Produce:产出安全删除候选(safe-cut candidates)、风险候选(risky candidates)与保留清单(keepers);
- Verify:用户批准后删除第一批,并用
bun check+ 定向 Playwright 行验证。
关键方法论点是第 5 步之前的每一步都不依赖bun test:integration-local的全量运行——先用列出行、静态聚类、定向行来分诊。这避免了"为了分诊而先跑 10 分钟全量"的循环依赖,是文档明确写出的约束:
Do not require
bun test:integration-localjust to triage; use listing, static clustering, and targeted rows first.
基线盘点:676 → 588 行的量化依据
文档的证据日志(Evidence Log)记录了完整的量化过程:
- 清理前执行
bunx playwright test --list:676 行、30 个文件、每个项目 169 行,横跨 Chromium、Firefox、mobile(移动端)与 WebKit 四个项目; - 最大的一笔浪费:
playwright/stress/**被包含进了test:integration-local,而test:stress与test:stress:replay本就独占这条 lane; - 修改
.tmp/slate-v2/package.json,让test:integration与test:integration-local只跑playwright/integration; - 清理后
bunx playwright test playwright/integration --list:588 行、28 个文件、每个项目 147 行; - 脚本变更后
bun check通过。
也就是说,仅"把 stress 通道移出集成通道"这一步就净减了88 行(84 条生成式 stress 行 + 4 条跳过状态的 replay 行),而 stress 覆盖本身并未丢失——它仍然通过显式的test:stress/test:stress:replay命令可用。这与 benchmarks/targets/slate-v2.json 中大量以PLAYWRIGHT_RETRIES=0 PLAYWRIGHT_WORKERS=1 bun playwright test playwright/integration/examples/...形式出现的正确性命令互相印证:集成通道负责浏览器级正确性,性能与压力场景由独立通道承载。
三、安全删除候选(Safe Cuts Found)
文档把可安全裁剪的行分成四类,每一类都有明确的判定标准与具体文件。
3.1 把playwright/stress/**移出test:integration*(已应用)
这是收益最大、风险最低的一刀:集成通道不再承担压力生成场景的重复执行,88 行从本地通道消失,覆盖由test:stress/test:stress:replay显式接管。
3.2 删除严格弱于同文件或 stress 的行(route-smoke)
以下行只做"页面能渲染 / 元素存在"级别的冒烟断言,属于典型的路由冒烟(route smoke),被同文件更强用例或生成式 stress 覆盖,可安全删除:
- [playwright/integration/examples/richtext.test.ts]:
renders rich text、inserts text through browser input、runs a traced slate-browser scenario - [playwright/integration/examples/markdown-shortcuts.test.ts]:
contains quote - [playwright/integration/examples/forced-layout.test.ts]:
checks for the elements - [playwright/integration/examples/images.test.ts]:
contains image - [playwright/integration/examples/hovering-toolbar.test.ts]:
hovering toolbar appears
判定的核心原则是"严格更强者优先":如果同一文件里已经存在覆盖同一行为的更深用例,或者生成的 stress 场景已经覆盖该交互路径,那么仅断言"元素存在"的行就是重复。
3.3 删除已被包级应用自定义测试覆盖的行
这一类行的独特之处在于:更强的归属者不在浏览器层,而在包级(package-level)测试:
- [playwright/integration/examples/markdown-preview.test.ts] 的
checks for markdown,更强归属者是packages/slate-react/test/app-owned-customization.tsx中的Editable supports app-owned markdown preview projections——Markdown 预览的"应用自有投影"契约在 React 包层已被直接验证,浏览器层再跑一遍属于重复; - [playwright/integration/examples/forced-layout.test.ts] 剩余的行,需先确认包级 normalization / app-owned customization 测试覆盖了完全一致的契约后再决定。
这里的工程智慧是:"谁拥有这个契约,谁就该测试它"。应用自有(app-owned)的定制行为如果能在包级单元测试中精确断言模型/投影结果,就不必让四个浏览器项目各跑一遍。
3.4 DRY 或限定 Chromium,而非盲目删除
- [playwright/integration/examples/styling.test.ts] 的两行是纯 prop / 样式渲染断言,应转化为 React/包级测试,或降级为 Chromium-only 的集成行;
- 一组 "richtext kernel trace metadata" 行(记录浏览器编辑的 kernel 命令元数据)被视为重要契约,但在所有项目上重复跑是浪费:
records kernel commands for structural browser editsrecords kernel commands for proof-handle editsrecords allowed kernel transitions for movement commandsrecords core command metadata for keydown movementrecords kernel policies for browser command and repair tracesrecords core command metadata for text input and deleterecords selectionchange and repair kernel results
处理建议:保留一条浏览器级行,其余下沉到包级 core 测试,或改为 Chromium-only。这体现了一个通用原则:契约型断言只需在浏览器里验证一次"真实环境成立",剩余的差异化验证交给更快的包级通道。
四、保留清单(Keepers):不可触碰的回归红线
文档明确列出了必须保留的行族,它们是浏览器回归覆盖的"压舱石":
- Void / inline / table 导航行:与近期用户回归(recent user regressions)直接对应,如 docs/plans/2026-05-03-slate-v2-mentions-void-arrow-selection-regression.md 所记录的 void 箭头选区回归;
- Decoration 焦点与渲染预算行:装饰(decoration)系统的焦点行为与渲染开销控制;
- IME / composition 行:输入法合成场景,只能在真实浏览器验证;
- Clipboard / paste 行:桌面端使用真实浏览器剪贴板、移动端使用语义回退(semantic fallback);
- Shadow DOM 与 iframe 行:跨文档边界行为;
- 生成式 stress lane 本身:保留,但只通过
test:stress访问。
这些类别与 packages/playwright 插件的设计互相呼应:该包提供getEditorHandle、setSelection、getSelection、getTypeAtPath、clickAtPath等浏览器端助手(见 packages/playwright/src),正是为了让这些"选区 / 导航 / IME"类浏览器行为可以被稳定、精确地驱动和断言。
五、Fail-Fast 红色簇修复执行:四轮"红→绿"实战
分诊不止于删行。文档记录了集成通道中实际存在的四簇红色用例(red cluster),并采用fail-fast 策略逐一修复:--max-failures=1让 sweep 在第一条失败时立即停止,修完当前红色后再继续。
5.1 第一簇:inlines 的 URL 自动包装
- 失败点:[playwright/integration/examples/inlines.test.ts] /
wraps typed URL text as a link command(Chromium); - 根因:示例把 URL 包装逻辑放在
onDOMBeforeInput中,而 slate-browser handle 的insertText()路径只应用Editable的inputRules——导致应用行为在"原生浏览器事件"与"命令式文本插入"两条路径之间分裂(app policy split); - 补丁:把 URL 包装规则迁移到
Editable inputRules,测试继续走editor.insertText(...),该入口跨浏览器且经过共享的命令 / input-rule 路径; - 连带构建红:
EditableInputRule未导出、EditableTextBlocks未把inputRules转发给EditableDOMRoot——补丁同时导出了 input-rule 类型,并在公开的Editable包装层转发inputRules; - 验证:定向命令
PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/integration/examples/inlines.test.ts -g "wraps typed URL text as a link command" --reporter=line在 Chromium、Firefox、mobile、WebKit 全部通过。
5.2 第二簇:markdown-shortcuts 列表项
- 失败点:[playwright/integration/examples/markdown-shortcuts.test.ts] /
can add list items(Chromium); - 根因:与 inlines 相同的策略分裂——Markdown 快捷键转换只存在于
onDOMBeforeInput,而命令式insertText()路径只跑Editable inputRules; - 补丁:把 Markdown 文本快捷键转换移入
Editable inputRules,onDOMBeforeInput仅保留 Android 待定差异(pending-diff)微任务所需的部分; - 验证:
bunx playwright test playwright/integration/examples/markdown-shortcuts.test.ts --reporter=line在四个项目上 16/16 通过。
5.3 第三簇:持久化注释锚点
- 失败点:[playwright/integration/examples/persistent-annotation-anchors.test.ts] /
keeps the annotation anchor attached across fragment insert, text insert, and clear(Chromium); - 根因:示例里 "Insert fragment before anchor" 按钮在文本点(text point)上使用了原始
tx.nodes.insert(...),而契约期望的是 Slate 片段插入(fragment insertion)语义——插入片段的尾部块应与当前文本块合并; - 补丁:先选中插入点,再调用
tx.fragment.insert(...); - 验证:定向命令四个项目 4/4 通过。
5.4 第四簇:大文档的富 HTML 粘贴
- 失败点:[playwright/integration/examples/large-document-runtime.test.ts] /
preserves app-owned rich HTML paste over shell-backed selection(Firefox); - 根因链:测试粘贴传输先派发合成 paste 事件,再强制走浏览器 handle 回退;合成的 Firefox 事件可能在回退插入数据前改动 shell-backed 选区。更深层根因是:模型选区导入正确,但应用自有的富 HTML
insertData处理器对"应遵循片段替换语义"的内容使用了原始节点插入,把"是否发生替换"的决定权交给了浏览器/传输差异; - 补丁:需要时直接使用浏览器 handle 的
insertData回退、在回退前导入 DOM 选区,并让自定义富 HTML 粘贴处理器用反序列化得到的段落调用tx.fragment.insert(...); - 验证:定向命令
-g "preserves.*paste"四个项目 12/12 通过。
5.5 四簇修复的共同规律
把四个根因放在一起,能提炼出 Slate v2 运行时的一个核心原则:文本插入与自动转换应当收敛到共享的命令 / input-rule 路径,而不是散落在onDOMBeforeInput等原生事件处理器中。onDOMBeforeInput是浏览器事件侧的唯一入口,但命令式insertText()(例如来自 slate-browser handle 的调用)必须与之一致;两处分裂,就会产生"同一输入、不同结果"的跨浏览器差异。这正是把 URL 自动链接、Markdown 快捷键全部迁移到Editable inputRules的原因。
六、完整命令速查表
| 用途 | 命令 |
|---|---|
| 清理前列出行数 | bunx playwright test --list |
| 清理后列集成行 | bunx playwright test playwright/integration --list |
| fail-fast 全量扫描 | PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/integration --max-failures=1 --reporter=line |
| 定向验证单个标题 | PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/integration/examples/<file>.test.ts -g "<title>" --reporter=line |
| 定向验证整个文件 | PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/integration/examples/<file>.test.ts --reporter=line |
| 静态检查 | bun check |
所有浏览器扫描命令均以PLAYWRIGHT_RETRIES=0禁用重试,保证失败真实可见;结合--max-failures=1即可实现"第一条红就停、逐簇修复"。同理的浏览器级正确性命令也出现在 benchmarks/targets/slate-v2.json 与 benchmarks/targets/history/slate-v2-latest.json 中(如--project=chromium限定项目 +-g限定标题的 perf 正确性门禁),说明这套运行约定已被项目其他通道复用。
七、可复用的分诊决策清单
把本次实践抽象成一份通用清单,供其他测试通道瘦身时直接套用:
- 先列行,再聚类:用
--list获得基线(676 行 / 30 文件 / 169 行每项目),按路由与场景族聚类; - 识别通道重叠:检查本通道是否包含了已被专门命令(如
test:stress/test:stress:replay)独占的 lane,这是最大、最安全的一刀; - 三类删除优先级:① 只做 route smoke 且同文件有更强用例的行;② 已被包级契约测试(如 app-owned customization)精确覆盖的行;③ 纯 prop/样式渲染、本应下沉到 React/包级测试的行;
- 契约型行降维:对 kernel trace metadata 这类重要但重复的契约,保留一条浏览器级行,其余下沉包级或限定 Chromium;
- 守住红线:用户回归类、选区、DOM/模型 parity、void/表格导航、IME、剪贴板、工具栏选区修复——没有严格更强者就不删;
- fail-fast 修复:
--max-failures=1+ 禁用重试,先修第一条红,定向复跑通过后再继续 sweep; - 以
bun check收口:脚本变更后必须通过静态检查。
最终,本次分诊在 docs/plans/2026-04-29-slate-v2-integration-local-triage-plan.md 中标记为Status: complete:test:integration-local从 676 行收敛到 588 行(每项目 169 → 147),四簇红色全部转绿,bun check通过,浏览器回归覆盖完整保留——这套"先分诊、后瘦身、再 fail-fast 修复"的流程,值得在任何大型富文本编辑器的浏览器测试工程中复用。
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考