关于 write-unit-tests
write-unit-tests 是面向 tldraw 工作区的测试技能,指导贡献者使用 Vitest 编写单元和集成测试,涵盖工作区选择、测试放置、编辑器注意事项与测试约定。
主要功能
- 01使用 Vitest 编写单元和集成测试
- 02工作区选择指南
- 03测试文件放置约定
- 04编辑器事件与动画注意事项
- 05清理资源和恢复模拟规则
- 06断言与测试可靠性指南
适用场景
- 01为编辑器基础类型添加测试
- 02测试 tldraw 图形或工具
- 03编写无 UI 的管理器测试
- 04排查事件或动画相关的不稳定测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ tldraw/ tldraw --skill write-unit-tests安装后在 Claude Code 中输入/write-unit-tests确认已加载。
详细介绍
编写测试
单元测试和集成测试使用 Vitest,并从工作区目录运行,而不是从仓库根目录运行。
编写新测试前先阅读一个相邻的测试——现有测试套件就是我们测试方式的规范,而且它们始终保持最新,这一点不是 prose 能做到的。以下是几个不错的起点:
packages/tldraw/src/test/SelectTool.test.ts— 工具状态机断言packages/tldraw/src/test/resizing.test.ts— 由指针驱动的控点交互packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts— 形状工具类及绑定packages/editor/src/lib/editor/managers/ClickManager/ClickManager.test.ts— 无 UI 的管理器packages/editor/src/lib/primitives/Vec.test.ts— 普通基元
如需了解可用的 TestEditor 方法,请直接阅读类本身,而不是查阅此处的列表:packages/tldraw/src/test/TestEditor.ts。
使用哪个工作区
packages/editor— 核心基元、几何、管理器,以及不得依赖默认形状或 UI 的基础编辑器行为。packages/tldraw— 任何需要默认形状或工具的内容,大多数集成测试都属于此类。
每个包都有自己的 TestEditor,两者不能互换:packages/editor/src/lib/test/TestEditor.ts 不包含默认形状或工具,而 packages/tldraw/src/test/TestEditor.ts 会接入完整的 SDK。请从你要测试的包中导入。
cd packages/tldraw && pnpm test run
cd packages/tldraw && pnpm test run -t "SelectTool"
cd packages/tldraw && pnpm test # watch mode
放置位置
单元测试与其覆盖的文件放在一起(Vec.ts → Vec.test.ts)。跨领域集成测试放在 packages/tldraw/src/test/。形状和工具测试应与实现放在一起,而不是放在 src/test/ 中。
注意事项
阅读现有测试时不会了解到以下内容。
滚轮和捏合事件会批量处理。 单独调用 dispatch() 不会应用它们——请发送一个 tick 来刷新:
editor.dispatch(wheelEvent)
editor.emit('tick', 16)
完整模式请参见 packages/editor/src/lib/editor/Editor.test.ts。
toCloselyMatchObject 是我们自己的,不是 Vitest 的。 涉及浮点几何时,请用它代替 toMatchObject,否则测试可能因舍入误差失败。它接受可选的 roundToNearest 参数。定义于 packages/tldraw/src/test/TestEditor.ts。
依赖动画的代码需要 rAF 替身。 仅调用 vi.useFakeTimers() 不够,因为 requestAnimationFrame 不受虚拟时钟驱动。包含动画的测试会在模块作用域替换它——参见 packages/tldraw/src/lib/shapes/arrow/ArrowShapeUtil.test.ts 的开头。
在 afterEach 中释放编辑器。 editor?.dispose() 会释放编辑器持有的响应式订阅和定时器;若不释放,它们会泄漏到同一文件中的后续测试。会创建形状的测试套件通常也会在 beforeEach 中清除形状,确保每个测试都从已知页面状态开始。
对编辑器使用 vi.spyOn 后,务必调用 mockRestore()。 编辑器实例的生命周期会跨越测试套件中的各项断言,因此未恢复的 spy 会悄无声息地影响后续测试。
在 finally 或 afterEach 中恢复所有全局状态。 process.env.NODE_ENV、vi.useFakeTimers()(恢复为 vi.useRealTimers())、Postgres search_path、window 全局变量。若在测试主体末尾恢复,当断言抛出异常时,状态就会泄漏到所有后续测试中。
不要将相对于当前时间的日期与固定截止日期比较。 如果测试用 Date.now() - 1 day 构造日期,再与硬编码的发布日期比较,它迟早会自行失败。请使用固定日期或 vi.setSystemTime。
确保测试在出错时会失败。 断言具体结果(数量、哪个元素获得焦点、重新连接后消息仍会到达),而不只是断言某个东西存在。测试生产路径,而不是为测试搭建的替代路径;新增代码还要覆盖失败分支,而不只是正常分支。如果测试只因在异步验证完成前就执行断言而通过,那就不算测试。
约定
- 使用
createShapeId()生成形状 ID,以确保其稳定且类型安全。 - 如果能让失败原因更清晰,优先比较整个对象,而不是逐个断言字段。
- 对状态机进行断言时,使用
editor.expectToBeIn('select.idle'),而不是深入访问内部实现。 - 使用
@ts-expect-error断言无效属性会在类型层面被拒绝。