- Remove non-null assertions from component, visibility, scene cache, and redo sequence tests - Add explicit node, store, renderer, and canvas bounds guards - Validate check plus affected engine and E2E coverage
149 lines
5 KiB
TypeScript
149 lines
5 KiB
TypeScript
import { describe, test, expect } from 'bun:test'
|
|
|
|
import { expectDefined, getNodeOrThrow } from '#tests/helpers/assert'
|
|
import { createHistoryFrame, setupEditorPage } from '#tests/helpers/editor-history'
|
|
|
|
describe('undo/redo multi-step sequences', () => {
|
|
test('create → move → duplicate → move copy → undo all → redo all', () => {
|
|
const { editor, pageId } = setupEditorPage()
|
|
|
|
const frame = createHistoryFrame(editor, pageId, { x: 100, y: 100 })
|
|
editor.select([frame.id])
|
|
const createSnapshot = structuredClone(getNodeOrThrow(editor.graph, frame.id))
|
|
editor.pushUndoEntry({
|
|
label: 'Create',
|
|
forward: () => {
|
|
const { parentId: _p, childIds: _c, ...rest } = createSnapshot
|
|
editor.graph.createNode('FRAME', pageId, rest)
|
|
},
|
|
inverse: () => editor.graph.deleteNode(frame.id)
|
|
})
|
|
|
|
editor.graph.updateNode(frame.id, { x: 300, y: 50 })
|
|
editor.commitMove(new Map([[frame.id, { x: 100, y: 100 }]]))
|
|
|
|
expect(getNodeOrThrow(editor.graph, frame.id).x).toBe(300)
|
|
expect(getNodeOrThrow(editor.graph, frame.id).y).toBe(50)
|
|
|
|
editor.duplicateSelected()
|
|
const dupIds = [...editor.state.selectedIds]
|
|
expect(dupIds).toHaveLength(1)
|
|
const dupId = expectDefined(dupIds[0], 'duplicate id')
|
|
expect(dupId).not.toBe(frame.id)
|
|
|
|
editor.graph.updateNode(dupId, { x: 500, y: 200 })
|
|
editor.commitMove(new Map([[dupId, { x: 320, y: 70 }]]))
|
|
|
|
// Undo move copy
|
|
editor.undo.undo()
|
|
expect(getNodeOrThrow(editor.graph, dupId).x).toBe(320)
|
|
|
|
// Undo duplicate
|
|
editor.undo.undo()
|
|
expect(editor.graph.getNode(dupId)).toBeUndefined()
|
|
|
|
// Undo move
|
|
editor.undo.undo()
|
|
expect(getNodeOrThrow(editor.graph, frame.id).x).toBe(100)
|
|
|
|
// Undo create
|
|
editor.undo.undo()
|
|
expect(editor.graph.getNode(frame.id)).toBeUndefined()
|
|
|
|
// Redo create
|
|
editor.undo.redo()
|
|
expect(editor.graph.getNode(frame.id)).not.toBeUndefined()
|
|
expect(getNodeOrThrow(editor.graph, frame.id).x).toBe(100)
|
|
|
|
// Redo move
|
|
editor.undo.redo()
|
|
expect(getNodeOrThrow(editor.graph, frame.id).x).toBe(300)
|
|
|
|
// Redo duplicate — must recreate with SAME ID
|
|
editor.undo.redo()
|
|
expect(editor.graph.getNode(dupId)).not.toBeUndefined()
|
|
|
|
// Redo move copy — must find the node by same ID
|
|
editor.undo.redo()
|
|
expect(getNodeOrThrow(editor.graph, dupId).x).toBe(500)
|
|
expect(getNodeOrThrow(editor.graph, dupId).y).toBe(200)
|
|
})
|
|
|
|
test('duplicate with children preserves subtree on redo', () => {
|
|
const { editor, pageId } = setupEditorPage()
|
|
|
|
const frame = createHistoryFrame(editor, pageId, { x: 50, y: 50 })
|
|
editor.graph.createNode('TEXT', frame.id, {
|
|
name: 'Title',
|
|
text: 'Hello',
|
|
x: 10,
|
|
y: 10,
|
|
width: 100,
|
|
height: 20
|
|
})
|
|
|
|
editor.select([frame.id])
|
|
editor.duplicateSelected()
|
|
|
|
const dupFrameId = expectDefined([...editor.state.selectedIds][0], 'duplicated frame id')
|
|
const dupChildren = getNodeOrThrow(editor.graph, dupFrameId).childIds
|
|
expect(dupChildren).toHaveLength(1)
|
|
const dupTextId = expectDefined(dupChildren[0], 'duplicated text id')
|
|
expect(getNodeOrThrow(editor.graph, dupTextId).text).toBe('Hello')
|
|
|
|
// Undo
|
|
editor.undo.undo()
|
|
expect(editor.graph.getNode(dupFrameId)).toBeUndefined()
|
|
expect(editor.graph.getNode(dupTextId)).toBeUndefined()
|
|
|
|
// Redo — must recreate with same IDs
|
|
editor.undo.redo()
|
|
expect(editor.graph.getNode(dupFrameId)).not.toBeUndefined()
|
|
expect(editor.graph.getNode(dupTextId)).not.toBeUndefined()
|
|
expect(getNodeOrThrow(editor.graph, dupFrameId).childIds).toContain(dupTextId)
|
|
expect(getNodeOrThrow(editor.graph, dupTextId).text).toBe('Hello')
|
|
})
|
|
|
|
test('page snapshot restore preserves node IDs', () => {
|
|
const { editor, pageId } = setupEditorPage()
|
|
const frame = createHistoryFrame(editor, pageId)
|
|
const child = editor.graph.createNode('RECTANGLE', frame.id, {
|
|
name: 'Bg',
|
|
width: 200,
|
|
height: 150
|
|
})
|
|
|
|
const snapshot = editor.snapshotPage()
|
|
editor.graph.deleteNode(frame.id)
|
|
editor.restorePageFromSnapshot(snapshot)
|
|
|
|
expect(editor.graph.getNode(frame.id)).not.toBeUndefined()
|
|
expect(editor.graph.getNode(child.id)).not.toBeUndefined()
|
|
expect(getNodeOrThrow(editor.graph, frame.id).childIds).toEqual([child.id])
|
|
})
|
|
|
|
test('delete frame with children → undo restores subtree', () => {
|
|
const { editor, pageId } = setupEditorPage()
|
|
|
|
const frame = createHistoryFrame(editor, pageId)
|
|
const child = editor.graph.createNode('RECTANGLE', frame.id, {
|
|
name: 'Bg',
|
|
x: 0,
|
|
y: 0,
|
|
width: 200,
|
|
height: 150
|
|
})
|
|
|
|
editor.select([frame.id])
|
|
editor.deleteSelected()
|
|
|
|
expect(editor.graph.getNode(frame.id)).toBeUndefined()
|
|
expect(editor.graph.getNode(child.id)).toBeUndefined()
|
|
|
|
editor.undo.undo()
|
|
expect(editor.graph.getNode(frame.id)).not.toBeUndefined()
|
|
expect(editor.graph.getNode(child.id)).not.toBeUndefined()
|
|
expect(getNodeOrThrow(editor.graph, frame.id).childIds).toContain(child.id)
|
|
})
|
|
})
|