Discard provisional undo batches on graph replacement without replaying old document edits or clearing committed history. Reset failed preview controllers and refresh selected projections for plain-state SDK consumers while pausing inactive subscriptions. Cover the verified review findings with negative controls, fix static Vue host insertion, and preserve the shared exact renderer oracle with its independently verified sRGB selection color.
162 lines
5.4 KiB
TypeScript
162 lines
5.4 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { createEditor, type Editor, type Tool } from '@open-pencil/core/editor'
|
|
import { SceneGraph } from '@open-pencil/scene-graph'
|
|
|
|
import { handleDrawMove, startShapeDraw, startTextDraw } from '#vue/shared/input/draw'
|
|
import type { DragState } from '#vue/shared/input/types'
|
|
|
|
function start(editor: Editor, tool: Tool = 'FRAME') {
|
|
const state: { drag: DragState | null } = { drag: null }
|
|
editor.setTool(tool)
|
|
const setDrag = (drag: DragState) => {
|
|
state.drag = drag
|
|
}
|
|
if (tool === 'TEXT') startTextDraw(100, 100, editor, setDrag)
|
|
else startShapeDraw(100, 100, editor, setDrag)
|
|
const drag = state.drag
|
|
if (drag?.type !== 'draw') throw new Error('Expected drawing interaction')
|
|
return drag
|
|
}
|
|
|
|
describe('draw creation previews', () => {
|
|
test('graph replacement discards pending draw batches without replaying or deleting history', () => {
|
|
const editor = createEditor()
|
|
try {
|
|
editor.createShape('RECTANGLE', 0, 0, 20, 20)
|
|
const previousLabel = editor.undo.undoLabel
|
|
const drag = start(editor)
|
|
const graph = new SceneGraph()
|
|
const node = graph.createNode('RECTANGLE', graph.getPages()[0].id, { x: 10 })
|
|
editor.replaceGraph(graph)
|
|
expect(editor.undo.isBatching).toBe(false)
|
|
expect(editor.undo.undoLabel).toBe(previousLabel)
|
|
drag.commit()
|
|
editor.updateNodeWithUndo(node.id, { x: 50 }, 'New graph move')
|
|
expect(editor.undo.undoLabel).toBe('New graph move')
|
|
editor.undoAction()
|
|
expect(node.x).toBe(10)
|
|
expect(editor.undo.undoLabel).toBe(previousLabel)
|
|
} finally {
|
|
editor.dispose()
|
|
}
|
|
})
|
|
test('is interactive immediately, previews geometry, and creates one undo step', () => {
|
|
const editor = createEditor()
|
|
try {
|
|
const drag = start(editor)
|
|
expect(editor.isInteractiveEditing()).toBe(true)
|
|
const version = editor.state.sceneVersion
|
|
let committed = 0
|
|
editor.onEditorEvent('node:updated', () => {
|
|
committed++
|
|
})
|
|
handleDrawMove(drag, 180, 150, false)
|
|
handleDrawMove(drag, 220, 170, false)
|
|
expect(editor.graph.getNode(drag.nodeId)).toMatchObject({
|
|
x: 100,
|
|
y: 100,
|
|
width: 120,
|
|
height: 70
|
|
})
|
|
expect(editor.state.sceneVersion).toBe(version)
|
|
expect(committed).toBe(0)
|
|
expect(editor.undo.canUndo).toBe(false)
|
|
drag.commit()
|
|
expect(editor.isInteractiveEditing()).toBe(false)
|
|
expect(editor.undo.isBatching).toBe(false)
|
|
expect(editor.state.activeTool).toBe('SELECT')
|
|
editor.undoAction()
|
|
expect(editor.graph.getNode(drag.nodeId)).toBeUndefined()
|
|
expect(editor.undo.canUndo).toBe(false)
|
|
editor.redoAction()
|
|
expect(editor.graph.getNode(drag.nodeId)).toMatchObject({ width: 120, height: 70 })
|
|
drag.cancel()
|
|
expect(editor.graph.getNode(drag.nodeId)).toBeDefined()
|
|
} finally {
|
|
editor.dispose()
|
|
}
|
|
})
|
|
|
|
test('cancel removes the provisional node, releases history, and ignores trailing input', () => {
|
|
const editor = createEditor()
|
|
try {
|
|
const drag = start(editor)
|
|
handleDrawMove(drag, 50, 70, true)
|
|
expect(editor.graph.getNode(drag.nodeId)).toMatchObject({
|
|
x: 50,
|
|
y: 50,
|
|
width: 50,
|
|
height: 50
|
|
})
|
|
drag.cancel()
|
|
drag.cancel()
|
|
handleDrawMove(drag, 250, 250, false)
|
|
drag.commit()
|
|
expect(editor.graph.getNode(drag.nodeId)).toBeUndefined()
|
|
expect(editor.undo.isBatching).toBe(false)
|
|
expect(editor.undo.canUndo).toBe(false)
|
|
expect(editor.isInteractiveEditing()).toBe(false)
|
|
} finally {
|
|
editor.dispose()
|
|
}
|
|
})
|
|
|
|
test('click defaults and text box sizing survive the preview transaction', () => {
|
|
for (const tool of ['FRAME', 'TEXT'] as const) {
|
|
const editor = createEditor()
|
|
try {
|
|
const drag = start(editor, tool)
|
|
drag.commit()
|
|
const node = editor.graph.getNode(drag.nodeId)
|
|
if (tool === 'FRAME') expect(node).toMatchObject({ width: 100, height: 100 })
|
|
else expect(node).toMatchObject({ text: '', textAutoResize: 'WIDTH_AND_HEIGHT' })
|
|
expect(editor.isInteractiveEditing()).toBe(false)
|
|
expect(editor.undo.isBatching).toBe(false)
|
|
} finally {
|
|
editor.dispose()
|
|
}
|
|
}
|
|
const editor = createEditor()
|
|
try {
|
|
const drag = start(editor, 'TEXT')
|
|
handleDrawMove(drag, 300, 180, false)
|
|
drag.commit()
|
|
expect(editor.graph.getNode(drag.nodeId)).toMatchObject({
|
|
width: 200,
|
|
height: 80,
|
|
textAutoResize: 'NONE'
|
|
})
|
|
} finally {
|
|
editor.dispose()
|
|
}
|
|
})
|
|
|
|
test('section adoption is included in the creation undo step', () => {
|
|
const editor = createEditor()
|
|
try {
|
|
const page = editor.state.currentPageId
|
|
const child = editor.graph.createNode('RECTANGLE', page, {
|
|
x: 120,
|
|
y: 120,
|
|
width: 20,
|
|
height: 20
|
|
})
|
|
const drag = start(editor, 'SECTION')
|
|
handleDrawMove(drag, 300, 300, false)
|
|
drag.commit()
|
|
expect(child.parentId).toBe(drag.nodeId)
|
|
editor.undoAction()
|
|
expect(child.parentId).toBe(page)
|
|
expect(child.x).toBe(120)
|
|
expect(editor.graph.getNode(drag.nodeId)).toBeUndefined()
|
|
expect(editor.undo.canUndo).toBe(false)
|
|
editor.redoAction()
|
|
expect(child.parentId).toBe(drag.nodeId)
|
|
expect(editor.graph.getNode(drag.nodeId)?.width).toBe(200)
|
|
} finally {
|
|
editor.dispose()
|
|
}
|
|
})
|
|
})
|