import { describe, expect, it } from 'vitest'; import { canRedoResourceCanvasHistory, canUndoResourceCanvasHistory, captureResourceCanvasSnapshot, clearResourceCanvasHistory, createResourceCanvasHistory, MAX_RESOURCE_CANVAS_HISTORY_STEPS, pushResourceCanvasHistory, redoResourceCanvasHistory, resolveResourceCanvasRestoreEntries, undoResourceCanvasHistory, } from '../src/features/resource-canvas/resourceCanvasHistoryModel'; function positions( entries: Array<[string, number, number, boolean?]>, ): ReturnType['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 snapshot = captureResourceCanvasSnapshot( positions([['asset:deleted', 10, 20]]), ); expect( resolveResourceCanvasRestoreEntries(snapshot, positions([])), ).toEqual([]); }); });