0a1f0e0b26
- 整理画布重排目标栏目内全部素材(含手动坐标与被筛选隐藏的卡),「所有资源」页覆盖全部栏目 - 栏外坐标逐值保留:不再把其他栏目的自动坐标丢给重算,避免被顺带规整成规范槽位 - 手动标记纳入历史快照与相等判定,撤销整理恢复原坐标与原手动标记 - 新增批量坐标写入入口:多选拖动与撤销都只排一笔 CAS、只压一条历史 - 多选拖动按同一位移整批跟随,取消 / 捕获丢失 / 失焦回滚,切项目丢弃进行中的位移 - 补测试:栏外自动坐标保留、all 页整理范围、批量撤销与取消边界
216 lines
6.5 KiB
TypeScript
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([]);
|
|
});
|
|
});
|