Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasHistoryModel.test.ts
suzmii 0a1f0e0b26 画布布局收口:整理只动当前栏目、多选拖动一笔提交与一次撤销
- 整理画布重排目标栏目内全部素材(含手动坐标与被筛选隐藏的卡),「所有资源」页覆盖全部栏目
- 栏外坐标逐值保留:不再把其他栏目的自动坐标丢给重算,避免被顺带规整成规范槽位
- 手动标记纳入历史快照与相等判定,撤销整理恢复原坐标与原手动标记
- 新增批量坐标写入入口:多选拖动与撤销都只排一笔 CAS、只压一条历史
- 多选拖动按同一位移整批跟随,取消 / 捕获丢失 / 失焦回滚,切项目丢弃进行中的位移
- 补测试:栏外自动坐标保留、all 页整理范围、批量撤销与取消边界
2026-09-17 17:47:11 +08:00

216 lines
6.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
canRedoResourceCanvasHistory,
canUndoResourceCanvasHistory,
captureResourceCanvasSnapshot,
clearResourceCanvasHistory,
createResourceCanvasHistory,
MAX_RESOURCE_CANVAS_HISTORY_STEPS,
pushResourceCanvasHistory,
redoResourceCanvasHistory,
resolveResourceCanvasRestoreEntries,
resourceCanvasSnapshotsEqual,
undoResourceCanvasHistory,
} from '../src/features/resource-canvas/resourceCanvasHistoryModel';
function positions(
entries: Array<[string, number, number, boolean?]>,
): ReturnType<typeof captureResourceCanvasSnapshot>['entries'] {
return entries.map(([resourceId, x, y, manuallyPlaced = true]) => ({
resourceId,
section: 'document',
x,
y,
manuallyPlaced,
}));
}
describe('resource canvas history model', () => {
it('按 resourceId 排序落快照,map 顺序不影响相等判定', () => {
const left = captureResourceCanvasSnapshot(
positions([
['asset:b', 2, 2],
['asset:a', 1, 1],
]),
);
const right = captureResourceCanvasSnapshot(
positions([
['asset:a', 1, 1],
['asset:b', 2, 2],
]),
);
expect(left.entries.map((entry) => entry.resourceId)).toEqual([
'asset:a',
'asset:b',
]);
expect(left).toEqual(right);
});
it('push 连续相同快照不产生新历史条目', () => {
const snapshot = captureResourceCanvasSnapshot(
positions([['asset:a', 1, 1]]),
);
const first = pushResourceCanvasHistory(createResourceCanvasHistory(), {
label: '移动卡片',
snapshot,
});
const second = pushResourceCanvasHistory(first, {
label: '移动卡片',
snapshot: captureResourceCanvasSnapshot(positions([['asset:a', 1, 1]])),
});
expect(first.undoStack).toHaveLength(1);
expect(second).toBe(first);
});
it('push 后重做分支失效,撤回栈有界', () => {
let history = createResourceCanvasHistory();
for (
let index = 0;
index < MAX_RESOURCE_CANVAS_HISTORY_STEPS + 5;
index += 1
) {
history = pushResourceCanvasHistory(history, {
label: `移动 ${index}`,
snapshot: captureResourceCanvasSnapshot(
positions([['asset:a', index, index]]),
),
});
}
expect(history.undoStack).toHaveLength(MAX_RESOURCE_CANVAS_HISTORY_STEPS);
expect(history.undoStack[0]?.label).toBe('移动 5');
const undone = undoResourceCanvasHistory(
history,
captureResourceCanvasSnapshot(positions([['asset:a', 99, 99]])),
)!;
expect(canRedoResourceCanvasHistory(undone.history)).toBe(true);
const pushed = pushResourceCanvasHistory(undone.history, {
label: '新的移动',
snapshot: captureResourceCanvasSnapshot(
positions([['asset:a', 120, 120]]),
),
});
expect(canRedoResourceCanvasHistory(pushed)).toBe(false);
});
it('撤销与重做互为逆操作,并回填当前快照', () => {
const moved = captureResourceCanvasSnapshot(
positions([['asset:a', 10, 20]]),
);
let history = pushResourceCanvasHistory(createResourceCanvasHistory(), {
label: '移动卡片',
snapshot: moved,
});
const current = captureResourceCanvasSnapshot(
positions([['asset:a', 60, 70]]),
);
const undone = undoResourceCanvasHistory(history, current)!;
expect(undone.snapshot).toEqual(moved);
expect(undone.label).toBe('移动卡片');
expect(canUndoResourceCanvasHistory(undone.history)).toBe(false);
const redone = redoResourceCanvasHistory(undone.history, moved)!;
expect(redone.snapshot).toEqual(current);
expect(canRedoResourceCanvasHistory(redone.history)).toBe(false);
expect(canUndoResourceCanvasHistory(redone.history)).toBe(true);
history = redone.history;
expect(history.redoStack).toHaveLength(0);
});
it('空历史撤销/重做返回 null,清空后两个栈都为空', () => {
const empty = createResourceCanvasHistory();
expect(undoResourceCanvasHistory(empty, { entries: [] })).toBeNull();
expect(redoResourceCanvasHistory(empty, { entries: [] })).toBeNull();
const history = pushResourceCanvasHistory(empty, {
label: '移动卡片',
snapshot: captureResourceCanvasSnapshot(positions([['asset:a', 1, 1]])),
});
const cleared = clearResourceCanvasHistory(history);
expect(cleared.undoStack).toHaveLength(0);
expect(cleared.redoStack).toHaveLength(0);
expect(clearResourceCanvasHistory(cleared)).toBe(cleared);
});
it('只把与当前不一致的位置写回后端', () => {
const snapshot = captureResourceCanvasSnapshot(
positions([
['asset:a', 10, 20],
['asset:b', 30, 40],
]),
);
const restore = resolveResourceCanvasRestoreEntries(
snapshot,
positions([
['asset:a', 10, 20],
['asset:b', 99, 98],
['asset:c', 1, 1],
]),
);
expect(restore.map((entry) => entry.resourceId)).toEqual(['asset:b']);
});
it('坐标完全相同但手动标记不同,仍算两次不同的布局', () => {
const manual = captureResourceCanvasSnapshot(
positions([['asset:a', 10, 20, true]]),
);
const automatic = captureResourceCanvasSnapshot(
positions([['asset:a', 10, 20, false]]),
);
expect(resourceCanvasSnapshotsEqual(manual, automatic)).toBe(false);
const history = pushResourceCanvasHistory(createResourceCanvasHistory(), {
label: '整理画布',
snapshot: manual,
});
expect(
pushResourceCanvasHistory(history, {
label: '整理画布',
snapshot: automatic,
}).undoStack,
).toHaveLength(2);
});
it('坐标一致但手动标记不同时,撤销仍然回写这条标记', () => {
const snapshot = captureResourceCanvasSnapshot(
positions([
['asset:a', 10, 20, true],
['asset:b', 30, 40, false],
]),
);
const restore = resolveResourceCanvasRestoreEntries(
snapshot,
positions([
// 整理画布之后:坐标没变,但标记从手动变成了自动。
['asset:a', 10, 20, false],
['asset:b', 30, 40, false],
]),
);
expect(restore).toEqual([
{
resourceId: 'asset:a',
section: 'document',
x: 10,
y: 20,
manuallyPlaced: true,
},
]);
});
it('快照里不存在的资源不会在撤销时被新建出来', () => {
const snapshot = captureResourceCanvasSnapshot(
positions([['asset:deleted', 10, 20]]),
);
expect(
resolveResourceCanvasRestoreEntries(snapshot, positions([])),
).toEqual([]);
});
});