Plate 仓库测试覆盖率阈值地图:用 `bun test --coverage` 数据驱动非 React 测试工作的批量排期
2026/9/15 18:23:57 网站建设 项目流程

Plate 仓库测试覆盖率阈值地图:用bun test --coverage数据驱动非 React 测试工作的批量排期

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

覆盖率地图(Coverage Threshold Map)是大型开源仓库中一种非常实用的“测试工作排期工具”:它不是简单报告“哪些文件覆盖率低”,而是把覆盖率数据加工成可执行的任务队列。本文以 plate 仓库在 2026-03-23 产出的《Coverage Threshold Map》计划文档(docs/plans/2026-03-23-coverage-threshold-map.md)为骨架,完整还原它的覆盖率运行命令、评分规则、阈值取舍逻辑与包级排名,并结合仓库当前源码深入讲解table包中得分最高的查询与变换实现,以及docx-iolist-classic两个边界文件的真实代码。读完本文,你将掌握一套可复用的“先打分、再定阈值、按包批量推进”的测试补齐方法论,并能在自己的仓库里直接套用。

一、这份地图要解决什么问题

plate 是一个以 shadcn/ui 风格组件和 AI 能力见长的富文本编辑器项目,仓库采用 pnpm workspace 管理几十个packages/*包(见 package.json 的workspaces配置)。到 2026-03-23 为止,项目已经连续执行了 3 月 6 日、3 月 9 日、3 月 17 日的多轮测试计划,以及 3 月 22~23 日的执行批次(可对照 docs/plans/2026-03-17-core-contract-lane.md、docs/plans/2026-03-23-coverage-priority-map.md 等系列文档)。

在这样高频迭代的背景下,作者面临一个典型困境:剩余的测试工作越来越琐碎,如果继续“一次只补一个小包”,既低效又难以评估优先级。这份地图的目标非常明确:

重新跑一遍仓库级覆盖率,与既有计划对齐,然后选定一个阈值,把剩余值得做的非 React 测试工作打包成批量任务,而不是一个包一个包地零散推进。

也就是说,本文档是一份“决策输入 + 决策输出”:输入是覆盖率原始数据,输出是阈值建议>= 6>= 5两档)与具体的文件级任务清单

二、覆盖率运行:命令、结果与产物

2.1 运行命令

文档记录的原始命令是:

bun test --coverage --coverage-reporter=lcov --coverage-dir=.coverage-repo-2026-03-23b --reporter=dots

参数拆解如下:

参数作用
bun test使用 Bun 测试运行器执行全部测试(仓库根package.json中亦提供test:coverage脚本,等价于bun test --coverage
--coverage开启覆盖率统计
--coverage-reporter=lcov以 LCOV 格式输出,便于后续解析每个文件的命中行
--coverage-dir=.coverage-repo-2026-03-23b把覆盖率产物写入独立目录,避免与历史运行混在一起
--reporter=dots终端输出使用点阵进度,降低长跑日志噪音

2.2 运行结果

指标数值
通过用例2599 pass
失败用例0 fail
覆盖文件478 files
总耗时3.35s

注意:该 LCOV 产物目录(.coverage-repo-2026-03-23b/lcov.info)属于历史运行记录,当前仓库已不再保留该目录(覆盖率产物通常会被.gitignore排除);文档中的路径 .coverage-repo-2026-03-23b/lcov.info 记录的是当时生成的相对位置。如果你要在当前仓库复现,直接运行根目录脚本bun run test:coverage(对应 package.json 第 96 行"test:coverage": "bun test --coverage")即可。

三、评分规则:如何把“覆盖率”变成“分数”

裸的覆盖率数字不足以排序,文档设计了一套人工加权评分规则,这是整份地图的灵魂:

  • 统计范围packages/*/src/**,即只统计各包源码目录,排除应用层与测试自身。
  • 单文件分数:0~10 分。
  • 强制清零项/react子目录下的文件、测试文件、桶文件(barrel,即只做 re-export 的index.ts)、纯类型文件一律记 0 分。原因很清晰:React 渲染层和类型声明不适合用“补单测”的方式刷覆盖,桶文件没有业务逻辑。
  • 高分倾向:确定性变换(deterministic transforms)、查询函数(queries)、解析器/序列化器辅助函数、真实插件契约(real plugin contracts)更值得拿高分——因为它们逻辑密集、可离线单测、回归价值高。
  • 近期完成惩罚:3 月 17 日与 3 月 22~23 日批次已经扫过的文件被刻意压分,避免已完工的泳道反复浮出水面、挤占新任务。
  • 反“cosplay”惩罚:所谓 coverage cosplay,指“只差一两行没覆盖、看似简单实则价值不高”的文件。文档规定这类文件即使身处高分目录也要压分,防止团队为了凑数去补毫无意义的边角。
  • 包分数:取该包内最高 8 个文件的分数之和,作为包级排序依据。

这套规则本质上是在回答三个问题:哪些文件值得补(确定性逻辑)、哪些不值得(React/类型/桶文件)、以及怎么防止表面繁荣(cosplay 惩罚)。

四、阈值取舍:>= 6>= 5两档建议

在打分基础上,文档给出两档阈值及其产出:

阈值命中文件数涉及包数定位
score >= 6(严格档)13 个文件1 个包“诚实的高价值批次”,最干净的价值标尺
score >= 5(放宽档)18 个文件3 个包仍可辩护的最大批次,不沦为 cosplay

文档给出的强建议(Strong take)是:

想要最干净的价值标尺就用>= 6;只有当你明确要把 table 包整条泳道做完、并且顺手补掉最后两个勉强及格的确定性遗留文件时,才用>= 5

这是一个非常务实的排期建议:宁可严格,也不要让“边界文件”污染批次质量。

五、严格档(>= 6):table包是唯一的高价值聚集地

严格阈值下命中的 13 个文件全部位于table,包分数 64 分,是第二名core(17 分)的近 4 倍。下表按分数列出全部命中文件:

文件分数归类
packages/table/src/lib/transforms/deleteRow.ts10变换
packages/table/src/lib/transforms/deleteTable.ts9变换
packages/table/src/lib/queries/getTableCellBorders.ts8查询
packages/table/src/lib/queries/getTableCellSize.ts8查询
packages/table/src/lib/queries/getTableEntries.ts8查询
packages/table/src/lib/merge/deleteRow.ts7合并单元格
packages/table/src/lib/queries/getCellInNextTableRow.ts7查询
packages/table/src/lib/queries/getCellInPreviousTableRow.ts7查询
packages/table/src/lib/queries/getPreviousTableCell.ts7查询
packages/table/src/lib/queries/getTableColumnIndex.ts7查询
packages/table/src/lib/transforms/setTableMarginLeft.ts7变换
packages/table/src/lib/queries/getNextTableCell.ts6查询
packages/table/src/lib/transforms/moveSelectionFromCell.ts6变换

可以清晰看到高分文件的构成规律:查询(queries)占 8 个,变换(transforms)占 4 个,合并单元格(merge)占 1 个,与评分规则中“确定性查询和变换拿高分”的取向完全一致。下面结合当前仓库源码深入讲解几个代表性文件。

5.1 满分文件:deleteRow(删除行变换)

packages/table/src/lib/transforms/deleteRow.ts 拿到 10 分满分,它的逻辑是典型的“插件选项分流”:

  1. 通过getEditorPlugin<TableConfig>(editor, { key: KEYS.table })读取表格插件配置;
  2. disableMergefalse(即启用合并单元格支持),直接委托给 packages/table/src/lib/merge/deleteRow.ts 中的deleteTableMergeRow——合并模式下的行删除必须处理合并单元格带来的边界;
  3. 非合并模式下,先检查文档中是否存在表格节点,再用editor.api.above向上定位当前表格与当前行;
  4. 若选择是展开状态(editor.api.isExpanded()),转入deleteRowWhenExpanded
  5. 常规情况下调用editor.tf.removeNodes({ at: currentRowItem[1] })删除当前行,并显式保护最后一行currentTableItem[0].children.length > 1才允许删)。

该文件的测试位于 packages/table/src/lib/transforms/deleteRow.spec.tsx,使用jsxtgetTestTablePlugins({ disableMerge })构造表格编辑器,覆盖了“合并模式委托”“展开选择委托”“常规删除”等分支,其中对deleteTableMergeRow的调用使用spyOn做了断言——这正是文档评分中“真实插件契约”的体现。

5.2 查询代表:getTableCellBorders(单元格边框查询)

packages/table/src/lib/queries/getTableCellBorders.ts 负责计算单元格四边边框样式,是表格边框渲染的数据源:

  • 通过editor.api.findPath(element)拿到单元格路径,再沿editor.api.parent向上找到行节点与表格节点;
  • 使用getCellIndices(editor, element)得到单元格的列索引col
  • 第一列col === 0)才输出left边框,第一行tableNode.children?.[0] === rowNode)才输出top边框,其余情况输出undefined——避免内部单元格重复绘制边框;
  • 边框值合并单元格自身的borders覆盖项与defaultBorder(默认{ size: 1 }),返回{ bottom, left, right, top }结构。

对应的测试 packages/table/src/lib/queries/getTableCellBorders.spec.tsx 位于同目录,验证了不同行列位置下边框对象的具体形态。

5.3 低分档代表:moveSelectionFromCell(按单元格移动光标)

packages/table/src/lib/transforms/moveSelectionFromCell.ts(6 分)实现了表格内“按单元格为单位”的光标移动:

  • 传入edge'bottom' | 'left' | 'right' | 'top')时,通过getTableGridAbove获取选中的单元格网格,把选择向指定边缘扩展一格,并对anchorPath/focusPath做对应维度的+/-1调整;
  • 不传edge时,先定位当前单元格cellPath,在行维度上按reverse决定偏移-1/1,若下一行不存在则回退到表格首尾并用editor.tf.move微调光标;
  • 所有路径都先经NodeApi.has校验路径存在,保证不产生悬空选择。

六、放宽档(>= 5):三个包的最后五个边界文件

在严格阈值之外,文档明确点名了 5 个“仍值得辩护”的 5 分文件:

文件分数
packages/docx-io/src/lib/internal/utils/image-dimensions.tsdocx-io5
packages/list-classic/src/lib/transforms/moveListSiblingsAfterCursor.tslist-classic5
packages/table/src/lib/transforms/deleteColumn.tstable5
packages/table/src/lib/transforms/overrideSelectionFromCell.tstable5
packages/table/src/lib/withSetFragmentDataTable.tstable5

docx-io的 image-dimensions.ts 为例:这是一个浏览器兼容的图片尺寸解析器,明确注释“Parses image dimensions from a Buffer without using Node.js fs module”。它通过魔数(magic number)识别格式:

  • PNG89 50 4E 47头,从固定偏移(16~23 字节)读取宽高;
  • JPEGFF D8 FF头,遍历标记段寻找 SOF0/SOF1/SOF2(0xC0/0xC1/0xC2)读取宽高,畸形 JPEG 回退100x100
  • GIF47 49 46 38头,读取第 6~9 字节的小端宽高;
  • BMP42 4D头,读取 18~25 字节的宽高;
  • WebPRIFF....WEBP头,区分 VP8(有损)与 VP8L(无损)两种子格式,并各自解析位域;
  • 未知格式统一回退{ width: 100, height: 100, type: 'unknown' }

这类“无依赖、纯字节解析、输入输出确定”的工具函数,正是评分规则里最值得补测试的文件类型——给它 5 分意味着文档认为它值得收尾,但不值得为它单独开一条泳道。

七、包级排名快照与全局结论

文档给出了完整的前 15 名包排名:

RankPackageScore>=6files>=5files
1table641316
2core1700
3list-classic1001
4docx900
5docx-io701
6media500
7markdown500
8mention400
9selection400
10diff400
11list300
12code-drawing200
13basic-nodes200
14suggestion200
15dnd100

由此可以得出三个直接可执行的结论:

  1. table是唯一仍有成簇高价值非 React 测试工作的包——严格档 13 个文件全部落在它身上,放宽档 16 个文件中的 15 个也在它身上;
  2. 其余包要么已基本覆盖,要么刚被扫过,要么只剩一两个边界缝隙——core虽有 17 分包分数,但没有任何文件达到 5 分以上,说明其高价值逻辑已完成;
  3. “想要更大的任务,就停止跨包跳跃,把table的整条高价值泳道当做一个独立项目来执行”;完成严格档table批次后,只有docx-io的图片尺寸工具与list-classic的列表兄弟节点移动变换这两个文件仍超过放宽阈值。

八、完整数据产物与复现方式

文档末尾声明了配套的完整数据文件:

  • docs/plans/2026-03-23-coverage-threshold-packages.tsv(包级明细)
  • docs/plans/2026-03-23-coverage-threshold-files.tsv(文件级明细)

这两个 TSV 在文档中用于支撑上文的全部排序与阈值统计;截至当前仓库状态,这两份 TSV 文件本身未被保留(文档记录的是当时的完整快照)。若要在当前仓库复现类似统计,可以:

# 1. 运行全仓覆盖率 bun run test:coverage # 2. 解析 lcov 产物,按“packages/*/src/**”范围过滤, # 排除 /react、测试文件、桶文件与纯类型文件后, # 对剩余文件按确定性逻辑价值打分(0-10)

结语

这份《Coverage Threshold Map》的价值不在于“报告覆盖率”,而在于它把**人工经验(哪些逻辑值得补)机器数据(覆盖率与代码结构)**结合成一套可重复的排期机制:强制清零低价值文件、惩罚近期已完成泳道、惩罚 cosplay 式补覆盖,最终收敛出一个“该干且值得干”的精确文件清单。对任何正在为“测试覆盖率还剩多少、下一步补哪里”发愁的仓库维护者来说,这套打分规则与阈值取舍方法都是可以直接照搬的实战模板;而对 plate 仓库而言,它清晰地指向了一个事实——表格包table的查询与变换层,就是当下唯一值得整条推进的高价值测试泳道。

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询