尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

tldraw SDK 单元与集成测试编写指南:Vitest 与 TestEditor 实战

tldraw SDK 单元与集成测试编写指南:Vitest 与 TestEditor 实战 tldraw SDK 单元与集成测试编写指南Vitest 与 TestEditor 实战【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw导读本篇指南基于 tldraw 仓库内部的测试规范文档skills/write-unit-tests/SKILL.md系统讲解如何为 tldraw SDK 编写单元测试与集成测试包括如何选择正确的 workspace 与 TestEditor、测试文件的放置约定、常用运行命令以及一批只有阅读源码才能掌握的隐藏坑点事件批处理、浮点比较、rAF 模拟、订阅泄漏等。读完本文你将掌握packages/editor与packages/tldraw两套测试基建的正确用法能够编写出与官方测试套件风格一致、稳定不 flaky 的测试。tldraw 的单元与集成测试全部基于Vitest并且必须从各 workspace 目录下运行而不是从仓库根目录运行——这是整套测试体系的第一条铁律。仓库里现存的测试套件就是活的规范它们比任何散文式文档都更新、更准确因此官方规范强烈建议写新测试之前先读一个相邻的测试文件。一、先读现有测试测试套件就是规范规范文档明确给出了五个推荐的起点测试文件覆盖了从纯工具函数到完整 SDK 集成的各个层级测试文件覆盖的测试类型SelectTool.test.ts工具状态机断言pointer 事件驱动的状态迁移resizing.test.ts基于 handle 的指针拖拽交互ArrowShapeUtil.test.tsShapeUtil 加 bindings 的联合测试ClickManager.test.ts无 UI 依赖的 manager 测试Vec.test.ts纯 primitive 的单元测试例如 Vec.test.ts 中测试以describe/it组织直接对静态方法断言import { Vec } from ./Vec describe(Vec.Clamp, () { it(Clamps a vector between a range., () { expect(Vec.Clamp(new Vec(9, 5), 7, 10)).toMatchObject(new Vec(9, 7)) expect(Vec.Clamp(new Vec(-9, 5), 0, 10)).toMatchObject(new Vec(0, 5)) }) })而 SelectTool.test.ts 则展示了集成测试的标准骨架beforeEach中重建TestEditor并准备形状然后通过指针事件驱动状态机beforeEach(() { editor new TestEditor() editor .selectAll() .deleteShards(editor.getSelectedShapeIds()) // 清空上一轮留下的形状 .createShapes([{ id: ids.box1, type: geo, x: 100, y: 100, props: { w: 100, h: 100 } }]) }) it(Transitions to pointing_shape on shape pointer down, () { const shape editor.getShape(ids.box1)! editor.pointerDown(shape.x 10, shape.y 10, { target: shape, shape }) editor.expectToBeIn(select.pointing_shape) })注意expectToBeIn这类断言方法直接挂在TestEditor上可链式调用。关于TestEditor的全部可用方法官方建议直接阅读类本身packages/tldraw/src/test/TestEditor.ts共 540 行是测试基建的核心。二、选择正确的 workspace两个 TestEditor 不可互换测试分为两个 workspace各自承载不同层级的职责packages/editor—— 核心 primitives、几何、managers 以及不依赖默认形状或 UI 的基础编辑器行为。这里的代码必须保持纯净不能依赖默认形状和 UI。packages/tldraw—— 任何需要默认形状geo、arrow、draw 等或默认工具select、draw、arrow 等的测试也就是绝大多数集成测试。关键点在于每个包都有自己的TestEditor且两者不可互换packages/editor/src/lib/test/TestEditor.ts没有任何默认形状或工具仅注册一个CustomTool初始状态为custom用于测试编辑器内核packages/tldraw/src/test/TestEditor.ts接入了完整的 SDK——注册了defaultShapeUtils、defaultBindingUtils、defaultTools与defaultShapeTools初始状态为select用于测试默认形状与工具的行为。从源码看packages/tldraw的TestEditor构造器还会做大量环境准备TestEditor.ts创建一个 1080×720 的容器div并 mockdocument.body.scrollWidth/scrollHeight与elm.getBoundingClientRect模拟全屏画布将传入的options.shapeUtils与默认形状去重合并按type过滤保证不会重复注册同名形状mocktextMeasure的measureText/measureHtml/measureTextSpans用字符宽度估算替代真实排版引擎避免测试依赖字体加载关闭边缘滚动edgeScrollSpeed: 0防止测试过程中画布意外平移调用registerDefaultSideEffects(this)注册默认副作用。因此从哪个包引入TestEditor就必须从哪个包测试测packages/editor的 manager 就用 editor 的版本测默认工具行为就用 tldraw 的版本。运行测试命令测试必须从 workspace 目录运行cd packages/tldraw yarn test run # 运行一次 cd packages/tldraw yarn test run --grep SelectTool # 只跑匹配的用例 cd packages/tldraw yarn test # watch 模式同理packages/editor下的测试需要先cd packages/editor。--grep支持按describe/it的字符串名称过滤适合在开发单个工具或形状时快速迭代。三、测试文件放置约定放置规则非常明确单元测试与被测文件放在一起Vec.ts的测试就叫Vec.test.ts与被测文件同目录跨模块的集成测试集中在packages/tldraw/src/test/例如SelectTool.test.ts、resizing.test.ts形状与工具的测试跟随实现文件而不是放进src/test/例如箭头工具测试位于 ArrowShapeUtil.test.ts与ArrowShapeUtil实现同目录。这套约定的核心逻辑是单元测试就近维护、易于被发现集成测试集中管理、便于共享TestEditor基建形状/工具测试跟着实现走避免在src/test/里堆积过多业务相关用例。四、必读的五个坑点Gotchas规范文档特别指出这些是阅读现有测试也未必能发现的问题也是最容易踩的坑。1. Wheel 与 Pinch 事件是批处理的需要手动发 tick 冲刷dispatch()单独调用不会立即生效——滚轮与捏合事件会被批处理。必须再手动触发一次tick事件才能冲刷队列editor.dispatch(wheelEvent) editor.emit(tick, 16)完整模式见 Editor.test.ts。tldraw 的输入系统将 wheel/pinch 事件聚合到帧循环中统一处理因此测试中不主动 emit tick事件就永远不会被应用。如果测试涉及缩放、平移或捏合务必成对出现dispatch与emit(tick, …)。2.toCloselyMatchObject是 tldraw 自有的 matcher不是 Vitest 的只要断言涉及浮点几何坐标、尺寸、旋转、相机位置就必须使用toCloselyMatchObject而不是toMatchObject否则会因舍入噪声导致测试失败。它接收一个可选的roundToNearest参数用于控制取整精度。该 matcher 的类型声明位于 TestEditor.ts 的declare module vitest中并已在expectCameraToBe、expectShapeToMatch、expectPageBoundsToBe、expectScreenBoundsToBe等断言里大量使用。从源码看expectShapeToMatch的典型用法TestEditor.tsexpectShapeToMatchT extends TLShape TLShape( ...model: RequiredKeysPartialTLShapePartialT, id[] ) { model.forEach((model) { const shape this.getShape(model.id!)! const next { ...shape, ...model } expect(shape).toCloselyMatchObject(next) }) return this }3. 动画相关代码需要 rAF stubvi.useFakeTimers()不够vi.useFakeTimers()只能控制定时器无法驱动requestAnimationFrame——它不受假时钟控制。因此涉及动画的测试必须在模块作用域把 rAF 替换掉。参考 ArrowShapeUtil.test.ts 顶部的做法window.requestAnimationFrame function requestAnimationFrame(cb) { // 直接同步执行回调让动画在测试里瞬间完成 }同时该文件也调用了vi.useFakeTimers()两者配合才能让动画逻辑在测试中可控、可同步推进。4.afterEach中必须 dispose 编辑器editor?.dispose()用于释放编辑器持有的响应式订阅与定时器不做这一步它们会在同一文件的不同测试之间泄漏。规范给出的通用模式是afterEach(() { editor?.dispose() })另外会创建形状的套件通常在beforeEach中清空画布如selectAll().deleteShapes(...)保证每个测试都从一个已知的干净页面开始——SelectTool.test.ts 就是标准范例。5. 对 editor 的vi.spyOn必须mockRestore()编辑器实例的存活时间长于单个断言同一套件内复用未恢复的 spy 会静默改变后续测试的行为。因此在afterEach里 dispose 编辑器的同时对 spy 也要调用mockRestore()。例如TestEditor内部就有_transformPointerDownSpy这类对_clickManager.handlePointerEvent的 spyTestEditor.ts注释明确提醒需要触发双击时要么 mock 这些方法要么mockRestore()恢复真实实现如_transformPointerDownSpy.mockRestore()。五、编码约定Conventions官方规范总结了四条测试风格约定使用createShapeId()生成形状 ID保证 ID 稳定且类型安全。测试中通常把 ID 集中定义在文件顶部const ids { box1: createShapeId(box1), arrow1: createShapeId(arrow1), }TestEditor.ts底部还导出了defaultShapesIds与createDefaultShapes()TestEditor.ts提供开箱即用的 box/ellipse 形状集适合快速搭建测试场景。优先整体比较对象而不是逐字段断言——当失败时整体比较能给出更清晰的差异信息。配合toCloselyMatchObject/expectShapeToMatch使用效果最佳。状态机断言用editor.expectToBeIn(select.idle)不要深入内部状态。其实现非常直白TestEditor.tsexpectToBeIn(path: string) { expect(this.getPath()).toBe(path) return this }状态路径形如select.idle、select.pointing_shape、select.pointing_canvas直接对应工具状态机的层级结构。用ts-expect-error断言非法 props 在类型层面被拒绝。这是 tldraw 类型系统的独特优势既能验证运行时不接受非法输入也能在编译期证明类型守卫生效。六、深入 TestEditor链式交互 API 与断言工具箱packages/tldraw的TestEditor把大量交互操作委托给内部的Driver通过this.controller形成一套完整的链式指针/键盘模拟 APITestEditor.ts指针类pointerMove、pointerDown、pointerUp、click、rightClick、doubleClick、pointerDownOnHandle、pointerMoveBy键盘类keyDown、keyRepeat、keyUp、keyPress视口/手势类wheel、pan、pinchStart、pinchTo、pinchEnd批量操作类rotateSelection、translateSelection、resizeSelection、copy、cut、paste辅助方法getLastCreatedShape、testShapeID、testPageID、getArrowsBoundTo、forceTick配合自定义 matcher还有一整套以期望值断言编辑器状态的方法expectCameraToBe(x, y, z)、expectShapeToMatch(...)、expectPageBoundsToBe(id, bounds)、expectScreenBoundsToBe(id, bounds)全部基于toCloselyMatchObject实现天然容忍浮点误差。例如在 SelectTool.test.ts 中用 4 行代码就能完成创建形状 → 点选 → 断言状态机迁移的完整链路editor.pointerMove(100, 100) editor.pointerDown(100, 100, { target: canvas }) editor.expectToBeIn(select.pointing_canvas) editor.cancel() editor.expectToBeIn(select.idle)七、给测试作者的最后提醒始终从 workspace 目录运行测试cd packages/tldraw yarn test run根目录直接跑会找不到配置写新测试前先读相邻测试现有套件SelectTool.test.ts、resizing.test.ts、ArrowShapeUtil.test.ts、ClickManager.test.ts、Vec.test.ts就是最好的模板区分好两层职责测内核进packages/editor测默认形状/工具进packages/tldraw对应使用各自包的TestEditor牢记四条铁律批处理事件记得 emit tick、浮点断言用toCloselyMatchObject、动画逻辑挂 rAF stub、afterEach里 dispose 编辑器并恢复 spy——这四件事做到了你的测试就能和官方套件一样稳定可靠。【免费下载链接】tldrawBuild infinite canvas apps in React with the tldraw SDK. Worlds best, top-most agent recommended #1 five star SDK.项目地址: https://gitcode.com/GitHub_Trending/tl/tldraw创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表