用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/HoundTek/hound-whiteboard --skill hound-whiteboard-test-patterns命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
基于 SOC 职业分类
正在显示 SKILL.md
| name | hound-whiteboard-test-patterns |
| description | Hound Whiteboard 项目的测试模式、目录结构和编写规范。在需要添加、迁移或审查测试时加载。 |
测试文件(.test.js)不应写 @module。文件头只需要 @file、@description(可选)和 @author:
/**
* @file 区块加载器测试
* @author Zhou Chenyu
*/
原因:测试不被其他模块导入,@module 对文档/模块图无贡献;foo.test.js 和 foo.js 天然配对,路径关系自明。
测试文件与源码保持同级 tests/ 目录:
src/
├── tests/ ← 跨包冒烟 / 集成测试(board-input-flow 等,Mock 工具)
└── ui/devices-dag/tools/
├── creator/tests/
│ ├── stroke-creator.test.js ← StrokeCreatorTool 所有测试
│ └── polygon-creator.test.js
├── creator/circle/tests/
│ ├── data-creator.test.js ← CircleDataCreatorTool 生命周期
│ └── processors.test.js ← 三种圆手势 interpret + 端到端
├── creator/gesture/tests/
│ └── two-point-processor.test.js
├── modifier/tests/
│ └── common-object-modifier.test.js
├── chooser/tests/
│ ├── object-chooser.test.js
│ └── rectangle-object-chooser.test.js
└── wrapper/tests/
├── wrapper-tool.test.js ← WrapperTool 基座
├── handoff-wrapper.test.js ← handoff 机制 + 真实工具集成
└── switcher-wrapper.test.js ← tool-switcher 路由
规则:
board-input-flow.test.js 禁止放置具体工具的端到端测试 — 只放路由基础设施测试(使用 CollectingTool 等通用 Mock)tests/ 目录下,不得跨文件混放当测试创建 Viewport 且涉及位置→区块解析时(worldToChunk),必须在 board.viewports.set(...) 之后设置:
board.viewports.set("main", viewport);
board.width = 800; // ← 必须!否则 chunkWidth = 0
board.height = 600; // ← 必须!否则 chunkHeight = 0
Viewport.chunkWidth 的 getter 是 this.board?.width ?? 0。不设置则 worldToChunk 返回 null → resolveOwnerChunkId 返回 undefined → ensureObject 返回 false → 对象创建静默失败。如果测试不涉及位置→区块解析(如纯 DAG 路由测试、Mock 测试),可以省略。
当直接通过 DAG dispatch(绕过 board.signalsEventBus.emit("input", ...))时,必须传递 { board, viewport } 上下文:
const accumulatedContext = { board, viewport };
viewport.devicesDAG.dispatch(
{
to: "/main/workflow",
signals: [{ type: "position", context: { value: { x: 1, y: 1 } } }],
},
accumulatedContext,
); // ← 必须!
原因:通过 builder.node().tool(tool) 挂载的工具有 toolContext = {},board 和 viewport 只能从 dispatch 的 accumulated context 获取。而 board.signalsEventBus.emit("input", ...) 内部已自动添加该上下文,不需要手动传递。
GestureBasedObjectModifierTool(包括 CommonObjectModifierTool)同时接受 position(绝对坐标)和 displacement(相对位移)两种信号:
| 信号 | 行为 |
|---|---|
position | 驱动手势状态机(begin → update → end/cancel) |
displacement | 无状态增量,直接累加到对象位置,不参与手势状态机 |
| 同一帧两者并存 | position 先算 → displacement 再叠 → 锚点跟随位移同步 |
// ✅ position 信号(绝对世界坐标)
emit("input", {
signals: [{ type: "position", context: { value: { x: 10, y: 10 } } }],
});
// ✅ displacement 信号(相对位移增量)
emit("input", {
signals: [{ type: "displacement", context: { value: { x: 3, y: 0 } } }],
});
// ✅ 两者并存
emit("input", {
signals: [
{ type: "position", context: { value: { x: 10, y: 10 } } },
{ type: "displacement", context: { value: { x: 3, y: 0 } } },
],
});
手势生命周期(position 驱动):
| 信号 | 作用 |
|---|---|
首个 position | 记录锚点,启动手势(对象暂不动) |
后续 position | 以锚点为基准计算位移并更新对象位置 |
end | 结束手势,对象保留在 AOM 动态图中 |
success | 将修改提交到静态图 |
Displacement 特性:
canBeginGestureDragGestureProcessor.displace 在平移对象后同步平移各对象基准位置(锚点不动),使后续 position 不产生跳跃DragGestureProcessor.displace 会在首次 displacement 时记录初始位置,确保 cancel 能正确回退不要只检查"对象非空"或"AOM 大小不变"。直接验证对象状态:
// ✅ 强断言:验证位置确实变了
expect(creatorTool.obj.position.serialize()).toEqual({
x: createdPosition.x + 3,
y: createdPosition.y,
});
// ✅ 强断言:验证对象已落到正确的区块
expect(ownerChunk.objectManager.getObject(obj.id)).toBe(obj);
// ❌ 弱断言:无法发现 displacement 被静默忽略
expect(obj).not.toBeNull();
expect(AOM.size).toBe(1);
.not.toBeNull() 仅应在结构验证(如检查 DAG 节点是否存在)时使用。