import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { resolve } from "node:path"; import { moduleGroupSource } from "./helpers/module-group-source"; /** * 生产接线回归:领域函数正确不代表用户能删到关键帧。 * 这里锁住「时间轴入口 -> workbench -> 领域函数」这条链路真的接上了。 */ const dock = readFileSync(resolve(import.meta.dir, "../src/components/canvas/previs/previs-viewport-dock.tsx"), "utf8"); const workbench = moduleGroupSource("components/canvas/previs/canvas-previs-workbench.tsx"); const sequencer = readFileSync(resolve(import.meta.dir, "../src/components/canvas/previs/previs-sequencer.tsx"), "utf8"); const styles = readFileSync(resolve(import.meta.dir, "../src/styles/globals.css"), "utf8"); function slice(source: string, from: string, to: string) { const start = source.indexOf(from); expect(start).toBeGreaterThanOrEqual(0); const end = source.indexOf(to, start + from.length); expect(end).toBeGreaterThan(start); return source.slice(start, end); } describe("workbench 接入时间轴关键帧删除", () => { test("PrevisSequencer 拿到的是 deleteKeyframe,而不是空实现", () => { const element = slice(workbench, ""); expect(element).toContain("onDeleteKeyframe={deleteKeyframe}"); expect(element).not.toContain("onDeleteKeyframe={() =>"); }); test("缓动更新通过 workbench commit 接入,未命中不制造历史", () => { const element = slice(workbench, ""); expect(element).toContain("onSetKeyframeEasing={setKeyframeEasing}"); const handler = slice(workbench, "const setKeyframeEasing = useCallback", "* 快捷键执行器"); expect(handler).toContain("setPrevisSceneKeyframeEasing(current, target, easing) === current"); expect(handler).toContain("commit((scene) => setPrevisSceneKeyframeEasing(scene, target, easing));"); }); test("deleteKeyframe 未命中时不进 commit:不记历史、不产生修订", () => { const handler = slice(workbench, "const deleteKeyframe = useCallback", "* 快捷键执行器"); expect(handler).toContain("removePrevisSceneKeyframe(current, target) === current) return;"); expect(handler).toContain("commit((scene) => removePrevisSceneKeyframe(scene, target));"); }); test("删除走 commit,因此进历史、可撤销、并触发 canonical 保存", () => { // commit 自身的语义:入历史 + writeAndPublish(coordinator 保存 + 镜像项目)。 const commit = slice(workbench, "const commit = useCallback", "/** 暂存型手势"); expect(commit).toContain("setHistory((items) => [...items.slice(-49), structuredClone(current)]);"); expect(commit).toContain("writeAndPublish(touchPrevisScene(updater(current)));"); }); }); describe("workbench 快捷键接线", () => { const effect = slice(workbench, "// 预演台是全屏浮层,快捷键挂在 window", "/** 对象 transform 编辑的唯一入口"); test("监听挂在 window keydown,并随 open 装卸", () => { expect(effect).toContain("if (!open) return;"); expect(effect).toContain('window.addEventListener("keydown", onKeyDown);'); expect(effect).toContain('window.removeEventListener("keydown", onKeyDown);'); }); test("交互控件语境由 blocksPrevisShortcut 判定后交给解析器", () => { expect(effect).toContain("isInteractiveTarget: blocksPrevisShortcut(event.target),"); // 旧的 text-only 判定不得残留:它放过 BUTTON/A/role 型控件。 expect(workbench).not.toContain("isPrevisTextEntryTarget"); }); test("只有动作真的执行了才 preventDefault", () => { expect(effect).toContain("if (runShortcutRef.current(action)) event.preventDefault();"); // 不得无条件吞掉按键。 expect(effect).not.toContain("event.preventDefault();\n if (!action)"); }); test("每个动作都落到已存在的真实执行路径", () => { const runner = slice(workbench, "const runShortcut = (action: PrevisShortcutAction)", "const runShortcutRef"); expect(runner).toContain("setTransformMode(action.mode);"); expect(runner).toContain("removeObject(selectedObject.id);"); expect(runner).toContain("removeLight(selectedLight.id);"); expect(runner).toContain("updateObject(selectedObject.id, { visible: !selectedObject.visible });"); expect(runner).toContain("setPlaying(!playing);"); expect(runner).toContain("undo();"); expect(runner).toContain("redo();"); }); test("没有可操作对象/历史时返回 false,把按键交还浏览器", () => { const runner = slice(workbench, "const runShortcut = (action: PrevisShortcutAction)", "const runShortcutRef"); expect(runner).toContain("if (!history.length) return false;"); expect(runner).toContain("if (!future.length) return false;"); expect(runner).toContain("if (!selectedObject) return false;"); expect(runner).toContain("if (!selectedObjectId && !selectedLightId && !selectedBone) return false;"); }); test("dock 与场景列表点选后释放焦点:否则点完就再按不动 W/E/R/Delete", () => { // 全局快捷键是本轮新增的,交互控件守卫会吃掉聚焦按钮上的按键。 // dock 承载 W/E/R 变换工具;场景列表承载「点选对象 -> 按 Delete」主流程。 const dockButton = slice(dock, "function DockButton(", "function DockDivider("); expect(dockButton).toContain("releasePrevisFocusAfterPointer(event)"); const sceneRow = slice(workbench, "function SceneRow(", "function AddMenuButton("); expect(sceneRow).toContain("releasePrevisFocusAfterPointer(event)"); }); test("焦点释放规则集中在共享 helper,各按钮不自写 blur", () => { // 判据(keyboard detail === 0 保留焦点)必须只有一处,否则迟早走偏。 const dockButton = slice(dock, "function DockButton(", "function DockDivider("); expect(dockButton).not.toContain("event.currentTarget.blur()"); expect(workbench).not.toContain("if (event.detail !== 0) event.currentTarget.blur();"); }); test("时间轴轨道行点选后也释放焦点:与场景列表同源的 select-then-Delete 流程", () => { const row = slice(sequencer, "function SequencerRow(", "* 关键帧渲染为真实 button"); expect(row).toContain("releasePrevisFocusAfterPointer(event)"); }); test("关键帧按钮绝不释放焦点:它自己拥有 Enter/Space/Delete/Backspace", () => { // 关键帧 blur 掉焦点会让键盘连续删除失效,且与它的 stopPropagation 设计冲突。 const trackKeys = slice(sequencer, "function TrackKeys(", "function TrackBar("); expect(trackKeys).not.toContain("releasePrevisFocusAfterPointer"); }); }); describe("时间轴关键帧入口可见、可选择、可键盘删除", () => { const trackKeys = slice(sequencer, "function TrackKeys(", "function TrackBar("); test("可编辑关键帧是真实 button,不是惰性 span", () => { expect(trackKeys).toContain('type="button"'); expect(trackKeys).toContain("previs-sequencer-key is-actionable"); expect(trackKeys).toContain('aria-label={`选择 ${key.label ?? "关键帧"} ${key.time.toFixed(2)}s 的关键帧`}'); expect(trackKeys).toContain("aria-pressed="); }); test("普通点击与 Enter/Space 只选择定位,Delete/Backspace 才删除,且都不冒泡", () => { expect(trackKeys).toContain('if (!["Enter", " ", "Delete", "Backspace"].includes(event.key)) return;'); expect(trackKeys).toContain("event.preventDefault();"); expect(trackKeys).toContain("event.stopPropagation();"); expect(trackKeys).toContain('if (event.key === "Delete" || event.key === "Backspace") onDeleteKey(target);'); expect(trackKeys).toContain("else onSelectKey?.(key);"); expect(trackKeys).toContain("onSelectKey?.(key);"); }); test("没有 target 的轨道保持只读,不渲染删除按钮", () => { expect(trackKeys).toContain("const target = onDeleteKey && key.target;"); expect(trackKeys).toContain("if (!target) {"); }); test("命中区扩大且有 focus-visible 焦点环", () => { expect(styles).toContain(".previs-sequencer-key.is-actionable"); expect(styles).toContain(".previs-sequencer-key.is-actionable::after"); expect(styles).toContain(".previs-sequencer-key.is-actionable:focus-visible"); expect(styles).toContain(".previs-sequencer-key.is-actionable.is-selected"); }); test("hover/focus 只用语义 token,不新增硬编码颜色", () => { const block = slice(styles, "/* 可删除关键帧:", ".previs-sequencer-clip {"); expect(block).toContain("outline: var(--stroke-2) solid var(--control-focus-ring);"); expect(block).toContain("color-mix(in srgb, var(--control-focus-ring) 55%, transparent)"); // rgba / 十六进制字面值一律不允许出现在新增块里。 expect(block).not.toMatch(/rgba?\(/); expect(block).not.toMatch(/#[0-9a-fA-F]{3,8}/); }); }); describe("三类轨道都有删除入口,概览轨保持只读", () => { test("摄影机行可删除,Camera Cut 概览轨只读", () => { const cameraCut = slice(sequencer, 'label="Camera Cut"', ""); expect(cameraCut).toContain(""); expect(cameraCut).not.toContain("onDeleteKey"); const cameraRow = slice(sequencer, "{camera ? : null}"); expect(cameraRow).toContain("onDeleteKey={deleteTrackKey}"); }); test("摄影机关键帧带 camera 删除目标", () => { expect(sequencer).toContain('target: { track: "camera", cameraId: camera.id, keyframeId: key.id }'); }); test("对象 transform 与骨骼帧各自带正确目标", () => { expect(sequencer).toContain('target: { track: "object-transform" as const, objectId: object.id, keyframeId: key.id }'); expect(sequencer).toContain('target: { track: "object-bone" as const, objectId: object.id, bone: track.bone, keyframeId: key.id }'); }); test("折叠状态下的对象汇总轨也可删除,否则收起后就删不掉", () => { const summaryRow = slice(sequencer, ""); expect(summaryRow).toContain("keys={[...transformKeys, ...boneTrackKeys]}"); expect(summaryRow).toContain("onDeleteKey={deleteTrackKey}"); }); test("展开后的 Transform 子轨与骨骼子轨都可删除", () => { const transformRow = slice(sequencer, 'label="Transform" icon="◇"', ""); expect(transformRow).toContain("onDeleteKey={deleteTrackKey}"); const boneRow = slice(sequencer, "label={previsBoneLabel(track.bone)}", ""); expect(boneRow).toContain("onDeleteKey={deleteTrackKey}"); }); test("非演员对象(立方体/模型/立牌)的 transform 轨同样可删除", () => { const plainRow = slice(sequencer, 'icon="□"', ""); expect(plainRow).toContain("onDeleteKey={deleteTrackKey}"); expect(sequencer).toContain('target: { track: "object-transform", objectId: object.id, keyframeId: key.id }'); }); test("所选关键帧提供 step/linear/smooth 缓动切换与显式删除按钮", () => { expect(sequencer).toContain('aria-label="关键帧缓动"'); expect(sequencer).toContain(''); expect(sequencer).toContain(''); expect(sequencer).toContain(''); expect(sequencer).toContain('aria-label="删除所选关键帧"'); }); test("切换摄影机或外部删帧后清除陈旧选择,顶部控件不能误改不可见轨道", () => { expect(sequencer).toContain("previsKeyframeTargetExists(selectedKey.target, camera, objects)"); expect(sequencer).toContain("setSelectedKey((current) => current?.target && !previsKeyframeTargetExists(current.target, camera, objects) ? null : current);"); expect(sequencer).toContain("camera?.id === target.cameraId && camera.keyframes.some"); expect(sequencer).toContain("object.boneTracks?.some"); }); });