从覆盖度队列到执行纪律:Plate 仓库非 React 测试覆盖路线图 Phase 2 复盘

发布时间:2026/9/15 15:55:58
从覆盖度队列到执行纪律:Plate 仓库非 React 测试覆盖路线图 Phase 2 复盘 从覆盖度队列到执行纪律Plate 仓库非 React 测试覆盖路线图 Phase 2 复盘【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate导读本文以 docs/plans/2026-03-24-non-react-coverage-roadmap-phase-2.md 为骨架复盘 Plate 编辑器仓库在测试覆盖率治理中如何对非 React 侧代码执行最后一轮收敛先冻结队列、再按 Tier 分层执行、最后用明确规则拒绝无谓重排。你将看到一套可复用的覆盖度队列治理方法论以及htmlDeserializerCodeBlock、ParserPlugin、pipeDecorate、withScrolling、upsertLink、convertNodesSerialize等 17 个核心文件的真实实现与测试切入点并顺带发现该批次暴露出的一个真实运行时缺陷。背景为什么需要冻结非 React 覆盖度队列在大型 monorepo 中覆盖率治理最常见的问题是反复给同一批遗留文件重新打分、重新排序却迟迟不真正动手补测试。Phase 2 文档开宗明义地给出了它的目标Freeze the last worthwhile non-React cleanup batch so future passes stop re-ranking the same leftovers and just burn down the queue.即冻结最后一轮值得做的非 React 清理批次让后续的 pass 不再对同一批残羹剩饭反复重排而是直接消耗队列。这个目标拆解为三条 Lock Rules锁定规则Phase 为临时性非 React 削减temporary non-React cut only只针对非 React 侧代码不扩散到 React 组件测试冻结阈值以 2026-03-24-coverage-priority-files-testing-review-non-react-post-roadmap.tsv 这份新鲜生成的 TSV 覆盖度优先级清单为起点但不盲目执行所有score 5的文件——分数只是起点最终取舍仍要人工判断队列以文件为单位、文件优先file-first不轻易重排除非文件被删除、文件已被直接测试完全覆盖、或文件被证明是虚假 ROI而有意延期。配套执行文档 2026-03-24-non-react-coverage-roadmap-phase-2-execution.md 进一步明确了工作流先检查现有实现与邻近 spec再补 Tier 1 覆盖、补 Tier 2 覆盖、更新路线图状态最后对受影响的包运行定向测试与 build/typecheck/lint。它特别提醒ViewPlugin、onDropNode、upsertLink、convertNodesSerialize在 Tier 2 中已有不错的测试脚手架应当扩展现有套件而非新建重复套件。Tier 1立即执行的 8 个文件Tier 1 是本次路线的必做项8 个文件全部标记为[done]覆盖分数为 67 分。它们集中在 HTML 反序列化、静态渲染管道与核心插件体系上。1. 代码块 HTML 反序列化htmlDeserializerCodeBlock.ts7 分htmlDeserializerCodeBlock.ts 负责把粘贴/导入的 HTML 中的pre与font-family: Consolas段落转换为 Slate 的代码块结构。其HtmlDeserializer结构包含两条规则validNodeName: PRE直接匹配pre元素validNodeName: P且validStyle.fontFamily: Consolas兼容某些富文本编辑器导出为普通段落但字体设为等宽字体的代码片段。parse回调的细节值得注意它先扫描子节点中是否存在select语言选择器一些在线代码编辑器会附带该元素将其textContent从整体文本中剔除再按换行符切分最终产出一组KEYS.codeLine子节点包裹在KEYS.codeBlock中。这个剥离语言选择器文本的逻辑是测试中必须覆盖的关键路径因为它直接决定粘贴后的文本是否干净。2. HTML 插件HtmlPlugin.ts7 分HtmlPlugin.ts 是整个 HTML 粘贴/导出能力的入口通过createSlatePlugin({ key: html })注册extendApi将deserializeHtml以bindFirst绑定到 editor 上暴露为editor.api.html.deserializeextend.parser声明format: text/htmldeserialize中先用parseHtmlDocument(data)解析 DOM再以document.body为根调用api.html.deserialize。这里的 parser 声明格式被下游 ParserPlugin.ts 消费是粘贴链路的第一环测试时既可以直接构造 HTML 字符串验证反序列化产物也可以验证 parser 的format/mimeTypes契约。3. HTML 字符串到编辑器 DOMhtmlStringToEditorDOM.ts7 分htmlStringToEditorDOM.ts 提供getEditorDOMFromHtmlString(html)用DOMParser解析 Plate 导出的 HTML再querySelector([data-slate-editortrue])取出编辑器根元素。它是编辑器导出 HTML → 重新取回 DOM这条闭环中的纯函数工具非常适合无 DOM 依赖的单元测试jsdom 环境下即可验证选择器与返回值。4. 静态文本渲染pluginRenderTextStatic.tsx6 分pluginRenderTextStatic.tsx 是静态服务端/无交互渲染体系的一部分导出两个函数pluginRenderTextStatic(editor, plugin)为单个文本类型插件生成渲染函数命中text[plugin.node.type ?? plugin.key]时选择组件缺省回退SlateText通过getRenderNodeStaticProps组装上下文属性pipeRenderTextStatic(editor, { renderText })遍历editor.meta.pluginCache.node.isText与node.textProps两组插件缓存逐个叠加渲染结果与文本属性最后统一用getNodeDataAttributes输出data-*属性。pluginCache的分组缓存是本文件性能设计的核心它把文本插件与文本属性插件分开遍历避免每次渲染都全量扫描插件列表。测试重点可放在多插件叠加时的 children 传递顺序与className合并内部用clsx。5. 粘贴解析内核ParserPlugin.ts6 分ParserPlugin.ts 通过overrideEditor重写了insertData变换是整个粘贴链路的中枢。其执行顺序为逆序遍历插件列表寻找第一个声明了parser的插件将parser.format规范化为text/${format}形式的 MIME 列表若直接声明mimeTypes则优先使用从dataTransfer取数据经pipeInsertDataQuery查询放行、pipeTransformData转换数据调用插件deserialize得到 fragment经pipeTransformFragment转换用pipeInsertFragment插入命中即返回true避免后续插件重复处理。从源码结构看这一设计保证第一个能解析的插件赢得插入权且每个环节都留了插件可注入的钩子pipe*系列是典型的管道-过滤器架构。测试应覆盖无 parser 插件时的回退、format与mimeTypes两种声明方式、以及insertData失败时回落到原始insertData(dataTransfer)。6. 装饰管道pipeDecorate.ts6 分pipeDecorate.ts 实现pipeDecorate(editor, decorateProp?)把editor.meta.pluginCache.decorate中所有插件的decorate输出与外部decorateProp合并成TRange[]。文件头注释点明了一个刻意优化Optimization: return undefined if empty list so Editable uses a memo.即当没有任何装饰插件且未传入decorateProp时直接返回undefined让Editable走 memo 路径避免空函数造成的多余渲染。测试时应当断言空列表返回undefined、多插件范围拼接、decorateProp追加这三个分支。7. Excalidraw 基础插件BaseExcalidrawPlugin.ts6 分BaseExcalidrawPlugin.ts 只声明了两件事key: KEYS.excalidraw以及node: { isElement: true, isVoid: true }并导出了TExcalidrawElement类型携带可空的data.elements/data.appState。作为一个极简插件其测试价值主要在于验证createSlatePlugin配置的元数据正确性——isElement/isVoid决定它在编辑、序列化、光标导航中的行为是典型的小而关键覆盖对象。8. 视图插件ViewPlugin.ts6 分ViewPlugin.ts 继承DOMPlugin干两件事extendEditorApi暴露getFragment()内部委托getSelectedDomFragmentoverrideEditor重写setFragmentData当originEvent copy且选区跨多个块fragment.length 0时向剪贴板写入三份数据application/x-slate-fragmentbtoa(encodeURIComponent(JSON.stringify(fragment)))编码、text/htmlDOM 片段 innerHTML、text/plain纯文本。它还通过isSelectOutside拦截选区在编辑器外的复制避免污染剪贴板。这块逻辑与 getSelectedDomFragment.tsxTier 2 第 1 项是同一批测试的天然组合后者负责从 DOM 选区重建 Slate fragment。Tier 2仍然值得做的 9 个文件Tier 2 是有余力再做的批次9 个文件全部[done]覆盖分数 5 分。执行文档特别强调其中有 4 个文件已有测试脚手架应以扩展为主。1. 选区 DOM 片段重建getSelectedDomFragment.tsx5 分getSelectedDomFragment.tsx 从window.getSelection()取范围、cloneContents()克隆 DOM再querySelectorAll([data-slate-nodeelement][data-slate-id])找出块级元素。对每个块通过dataset.slateId调editor.api.node({ id })找回 Slate 节点并用block[1].length ! 1排除内联元素如链接、表格单元格若首尾块未被选区完整覆盖文本内容与NodeApi.string(block[0])不一致且非 void 元素则回退到editor.api.html.deserialize做局部反序列化保证拿到的是选区真实覆盖的片段而非整块。这个局部选中时回退反序列化的分支是测试设计的难点与重点。2. 静态叶子渲染pluginRenderLeafStatic.tsx5 分pluginRenderLeafStatic.tsx 与 Tier 1 的pluginRenderTextStatic对称处理叶子leaf/mark渲染pluginRenderLeafStatic命中leaf[plugin.node.type]时选择组件优先plugin.render.leaf其次editor.meta.components最后SlateLeafpipeRenderLeafStatic遍历pluginCache.node.isLeaf与node.leafProps叠加渲染与属性className同样用clsx合并。测试应覆盖 mark 命中/未命中、多 mark 叠加顺序、leafProps函数式与对象式两种形态。3. 滚动控制withScrolling.ts5 分withScrolling.ts 导出一个包装函数调用前把临时scrollMode/scrollOperations/scrollOptions写入DOMPlugin的 store置位AUTO_SCROLL标记try/finally中执行回调并恢复原配置。执行记录2026-03-24-non-react-coverage-roadmap-phase-2-execution.md披露了一个由该批次测试暴露的真实 bugwithScrolling 之前把mode/operations写进了 DOMPlugin store而不是scrollMode/scrollOperations并且在回调抛出异常时没有恢复状态。也就是说这次覆盖工作不只是补测试还真实修复了运行时缺陷——这正是覆盖度队列驱动出实际价值的典型案例。测试应断言正确键名写入、异常路径下 finally 恢复、嵌套调用时配置还原。4. 静态元素渲染pluginRenderElementStatic.tsx5 分pluginRenderElementStatic.tsx 是元素级静态渲染先取editor.meta.components?.[plugin.key]缺省SlateElement依次执行pluginCache.render.belowNodes的 HOC 包裹 children、渲染belowRootNodes、最后用aboveNodes的 HOC 包裹整个组件树。这个below → root → above三层 HOC 顺序是插件扩展渲染能力的标准接缝测试应锁定该顺序与 props 传递的一致性。5. 拖放落点计算onDropNode.ts5 分onDropNode.ts 提供getDropPath基于getHoverDirection判断方向把right视作bottom插入到悬停节点之后、left视作top插入到之前若拖动节点已在目标位置则直接返回空操作还支持从文件系统拖入dragItem不含element时以[]作为默认路径。canDropNode与monitor.canDrop()双重校验、editorId匹配检查都是测试要点。该文件在 Tier 2 中被点名已有测试脚手架应在其上扩展边界场景。6. HTML 元素到叶子htmlElementToLeaf.ts5 分htmlElementToLeaf.ts 实现 HTML 反序列化的叶子构建先pipeDeserializeHtmlLeaf聚合各插件的叶子反序列化结果再递归deserializeHtmlNodeChildren处理子节点对元素子节点用mergeDeepToNodes把 mark 深合并进文本节点query.filter限定只合并TextApi.isText对文本子节点用slate-hyperscript的jsx(text, ...)重建并保证子节点已有属性优先、不覆盖。测试应覆盖 mark 深合并与属性覆盖优先级这两个分支。7. Slate AST 反序列化AstPlugin.ts5 分AstPlugin.ts 注册application/x-slate-fragmentMIME 的 parserwindow.atob(data)解码先decodeURIComponent再JSON.parse。它对应ViewPlugin写入剪贴板的x-slate-fragment格式构成复制 → 粘贴闭环的另一半。测试要覆盖合法 base64 JSON、非法数据JSON.parse抛错时静默吞掉返回undefined两个分支。8. 链接增改upsertLink.ts5 分upsertLink.ts 是链接编辑的核心变换行为分四路光标在链接内且insertTextInLink为真直接insertText(url)插入文本skipValidation为假时先validateUrl校验光标在链接上linkAbove命中URL 或 target 变化则setNodes更新并upsertLinkText同步文本选区展开先unwrapLinksplit: true再wrapLink重包最后更新文本。从源码结构看RangeApi.isExpanded分支对拖选文字转链接这一最常见交互至关重要测试覆盖应包含光标在链接内、编辑已有链接、展开选区转链接、空文本回退为 URL 等场景。9. 节点序列化为 Markdown ASTconvertNodesSerialize.ts5 分convertNodesSerialize.ts 是 Slate 节点 → mdast 的转换器按顺序消费节点数组连续文本节点进入textQueue由convertTextsSerialize处理遇到带listStyleType的段落节点时累积进listBlock在下一项不是同级缩进或列表样式切换时用listToMdastTree生成列表节点支持fragment类型展开普通节点走buildMda*分支并按getSerializerByKey分发。过滤函数shouldIncludeNode/shouldIncludeText保证序列化可受SerializeMdOptions控制。测试重点相邻文本合并、同级/嵌套列表、列表样式切换、被过滤节点的跳过。Deferred By Design有意延期的 8 个文件被标记[deferred]的文件不是不测了而是按 ROI 判断暂不投入。文档为每个文件都给了明确理由这是队列治理纪律性的体现文件分数延期理由isEditOnlyDisabled.ts5仅一行局部缺口不值得单独跑一趟除非邻近 Tier 1 spec 顺带覆盖pipeInjectNodeProps.tsx5同样是微小残留不是真正的阶段驱动项html-to-docx.ts4巨大的序列化烂泥对最后一轮非 React pass 而言 ROI 很低font-table.ts4纯 schema 样板content-types.ts4纯 schema 样板focus.ts4DOM 风格的工具碎屑若将来值得再做留给 DOM 专项阶段useRecordHotkeys.ts4虽不在/react目录但行为接近 React 侧可等 React 阶段AutoformatPlugin.ts4仅小部分缺口不值得提前于更严格的队列处理这 8 条理由本身就是一个很好的延期决策清单微小缺口、schema 样板、低 ROI 巨型文件、跨阶段归属、临近 React 行为——每一条都是可复用的延期判据。状态更新规则与纪律执行路线图用[done]/[deferred]/[removed]三个状态标记维护队列更新规则被刻意收紧文件获得直接测试 → 翻为[done]文件被证明是虚假 ROI → 翻为[deferred]并附理由文件消失 → 翻为[removed]禁止因为新一轮 pass 有了新感觉而重排队列。配合 2026-03-24-coverage-priority-map-testing-review-non-react.md 等系列文档可以看到这套方法论在同批次的多个阶段phase-2、phase-3、phase-4中被持续复用最终沉淀为非 React 覆盖率治理的稳定工作流TSV 分数表定优先级 → 分 Tier 执行 → 状态翻转更新 → 禁止无理由重排。给团队的实践启示冻结阈值但不冻结判断score 5只是候选集html-to-docx.ts的 4 分与isEditOnlyDisabled.ts的 5 分都说明分数高不一定要做分数低也可能值得做文件优先测试就近扩展已有脚手架的文件ViewPlugin、onDropNode、upsertLink、convertNodesSerialize优先扩展避免套件重复延期必须带理由8 个 deferred 文件每一条都有可审计的原因这让不做什么和做什么一样透明覆盖工作能暴露真实 bugwithScrolling的键名错误与状态恢复缺失正是被测试逼出来的这是覆盖度队列驱动质量最有说服力的回报状态机要简单且强制[done]/[deferred]/[removed]三态 禁止无理由重排一条铁律足以让队列长期保持可消耗状态。结语Phase 2 的价值不在又补了 17 个文件的测试而在于它示范了一种可持续的覆盖率治理姿势用冻结的优先级清单代替反复重排用 Tier 分层控制投入节奏用带理由的延期保持队列诚实用严格的状态翻转规则防止队列腐烂。配合 Phase 2 Execution 文档中withScrolling的缺陷修复这轮非 React 覆盖工作同时交付了测试资产、缺陷修复与一套可复制到其他模块的治理模板——对于任何面临覆盖率数字好看但队列永远清不完的团队这都是一份可直接借鉴的实操范本。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考