diff --git a/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts b/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts index fc7ebc651..0bea3af72 100644 --- a/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts +++ b/apps/ai-game-creator-shell/tests/uiEditorPage.test.ts @@ -1197,4 +1197,57 @@ describe('UiEditorPage', () => { act(() => hook.result.current.input.selectSprite('panel')); expect(hook.result.current.canvas.activeImageId).toBe('page-2'); }); + it('整树拖动写回一个历史条目,保存后能原样读回', async () => { + const save = vi + .fn() + .mockImplementation(async (_assetId, _revision, state: State) => ({ + status: 'saved' as const, + state, + revision: 1, + committedProjectRevision: 1, + })); + const stateStore: IUiDesignStateStore = { + load: vi.fn().mockResolvedValue({ + revision: 0, + state: stateWithPages(['page-1']), + }), + save, + generateCode: vi.fn().mockRejectedValue(new Error('测试未配置代码生成')), + }; + const hook = renderHook(() => + useUiEditorSession('/tmp/ui-editor-tree-drag', 'ui-resource', stateStore), + ); + await waitFor(() => expect(hook.result.current.save.isLoading).toBe(false)); + + const before = hook.result.current.canvas.uiTrees[0]?.root.offset; + + // 拖动落点一次提交:状态更新并进入历史。 + act(() => { + hook.result.current.canvas.setTreeOffset('page-1', [24, 36]); + }); + expect(hook.result.current.canvas.uiTrees[0]?.root.offset?.min).toEqual([ + 24, 36, + ]); + expect(hook.result.current.history.canUndo).toBe(true); + + // 撤销回到拖动前,重做再回到拖动后。 + act(() => { + hook.result.current.history.undo(); + }); + expect(hook.result.current.canvas.uiTrees[0]?.root.offset).toEqual(before); + act(() => { + hook.result.current.history.redo(); + }); + expect(hook.result.current.canvas.uiTrees[0]?.root.offset?.min).toEqual([ + 24, 36, + ]); + + // 保存把新的树偏移写进快照(下一次加载读回的就是这份 state)。 + await act(async () => { + await hook.result.current.save.save(); + }); + expect(save).toHaveBeenCalledTimes(1); + const savedState = save.mock.calls[0]?.[2] as State; + expect(savedState.ui_trees[0]?.root.offset?.min).toEqual([24, 36]); + }); }); diff --git a/docs/project-memory/plans/【里程碑】UI编辑器多树预览与独立显示开关-2026-09-14.md b/docs/project-memory/plans/【里程碑】UI编辑器多树预览与独立显示开关-2026-09-14.md index 39c752c00..04facdfce 100644 --- a/docs/project-memory/plans/【里程碑】UI编辑器多树预览与独立显示开关-2026-09-14.md +++ b/docs/project-memory/plans/【里程碑】UI编辑器多树预览与独立显示开关-2026-09-14.md @@ -52,7 +52,8 @@ UI 编辑器预览同时展示全部界面树。每棵树使用 root.offset 的 - 三个独立开关在 `view/ui-editor/index.tsx` 里渲染:`显示框线` / `显示原图` / `显示组件`。 - 定向用例:`npx vitest run tests/uiEditorPreview.test.tsx tests/previewWorkspaceZoom.test.tsx tests/previewRightPanGesture.test.ts tests/uiEditorPage.test.ts --root apps/ai-game-creator-shell` → **59 passed / 0 failed**。 - 本轮补齐的验收项(第 1 条):新增 `tests/uiEditorState.test.ts` 的「新树默认落在所有现有树右侧并与最小 top 对齐」——直接跑 `createTree` 入口:第一棵落在 `[0,0]`,第二棵落在「第一棵右边界 + `UI_TREE_PADDING`」且 top 与现有最小 top 对齐;把第二棵整体下移后,第三棵仍然按**最小** top(而不是最后一棵的 top)对齐,横向取所有树最右边界。变异验证:把 `deriveTreeOffset` 的 `minY` 改成取最后一棵树的 top 后,只有这条用例变红(1 failed / 17 passed),改回后恢复绿;本轮四套 UI 编辑器用例合计 **72 passed**。 -- 仍未覆盖的 1 条验收项(不是运行时项,缺的是自动化用例): +- 本轮补齐的验收项(第 2 条的两句):`tests/uiEditorPage.test.ts` 新增「整树拖动写回一个历史条目,保存后能原样读回」——`canvas.setTreeOffset('page-1', [24, 36])` 后断言状态更新且 `history.canUndo` 为真,`undo()` 回到拖动前的 `offset`、`redo()` 再回到 `[24, 36]`,最后 `save.save()` 落盘并断言写入快照的 `state.ui_trees[0].root.offset.min` 就是新值(下一次加载读回的就是这份 state)。变异验证:把 `setTreeOffset` 里的 `commit(result.state)` 换成 `applyState(result.state)`(改状态但不进历史)后,只有这条用例在 `canUndo` 处变红(1 failed / 37 passed)。「拖动结束才写回一次、指针取消不写」由既有 `previewRightPanDrag.test.tsx` 的 `commits the tree offset on pointerup but not on pointercancel` 与 `drops an in-flight tree drag when the window loses focus` 覆盖。 +- 第 2 条还剩一句未单独断言:「原图与组件随 root 一起移动」。缺口很具体——jsdom 里 `getBoundingClientRect` 全为 0,不能直接比绝对坐标;要靠断言树层容器的 `style.left/top` 等于 `root.offset.min`、且根节点是该容器的子节点(从而原图与全部子节点一起随 root 平移)来钉住。 - 「原图与组件随 root 一起移动,拖动结束进入 undo/redo 并可保存恢复」:现有 `uiEditorState.test.ts` 只覆盖 `set-tree-offset` 命令本身,没有断言预览渲染跟随 root 位移与拖动落点进 undo/redo、保存后能读回。 - 本轮补齐的验收项(第 3 条):在同一条 `shows final-preview node frames and resize handles only when requested` 里补上 root 断言——选中 root 时一个 `/调整控制点$/` 都不出现,紧接着的对照是选中 child 时恰好 8 个。变异验证:把 handle 渲染条件里的 `!isRoot &&` 去掉后只有这一处断言变红(1 failed / 10 passed,且证明 `selectedNodeId=root` 确实进入了选中态、断言不是空转),改回后恢复绿。 - 本轮补齐的验收项(第 4 条):`tests/uiEditorPage.test.ts` 新增「默认显示框线与原图、隐藏组件,且三个开关互相独立」——渲染真实 `UiEditorPage` 后按标签取三个复选框,断言初始值 `显示框线=true` / `显示原图=true` / `显示组件=false`,再分别关框线、开组件,确认互不影响。变异验证:把 `view/ui-editor/index.tsx` 的 `showFrame` 初值改成 `false` 后只有这条用例变红(1 failed / 35 passed),改回后四套用例合计 **73 passed**。