styleRuns were updated on the graph during editing (via syncText) but commitTextEdit only captured text changes. Now both text and styleRuns are snapshotted at startTextEditing and restored on undo.
176 lines
5.2 KiB
TypeScript
176 lines
5.2 KiB
TypeScript
import { describe, test, expect } from 'bun:test'
|
|
|
|
import { SceneGraph, TextEditor, UndoManager } from '@open-pencil/core'
|
|
import { createTextActions } from '@open-pencil/core/editor'
|
|
|
|
import type { StyleRun } from '@open-pencil/core'
|
|
import type { EditorContext, EditorState } from '@open-pencil/core/editor'
|
|
|
|
function setup() {
|
|
const graph = new SceneGraph()
|
|
const pageId = graph.getPages()[0].id
|
|
const undo = new UndoManager()
|
|
const textEditor = new TextEditor({} as any)
|
|
|
|
const state = {
|
|
editingTextId: null,
|
|
currentPageId: pageId,
|
|
renderVersion: 0,
|
|
sceneVersion: 0,
|
|
} as EditorState
|
|
|
|
const ctx: EditorContext = {
|
|
graph,
|
|
undo,
|
|
state,
|
|
requestRender: () => { state.renderVersion++; state.sceneVersion++ },
|
|
requestRepaint: () => { state.renderVersion++ },
|
|
getTextEditor: () => textEditor,
|
|
getRenderer: () => null,
|
|
runLayoutForNode: () => {},
|
|
getCk: () => null,
|
|
loadFont: async () => {},
|
|
getViewportSize: () => ({ width: 800, height: 600 }),
|
|
subscribeToGraph: () => {},
|
|
}
|
|
|
|
const textNode = graph.createNode('TEXT', pageId, {
|
|
name: 'Label',
|
|
text: 'Hello',
|
|
width: 100,
|
|
height: 20,
|
|
})
|
|
|
|
const actions = createTextActions(ctx)
|
|
return { graph, undo, textEditor, state, textNode, actions }
|
|
}
|
|
|
|
describe('text edit undo', () => {
|
|
test('commitTextEdit pushes undo entry when text changed', () => {
|
|
const { graph, undo, textEditor, textNode, actions } = setup()
|
|
|
|
actions.startTextEditing(textNode.id)
|
|
expect(textEditor.isActive).toBe(true)
|
|
|
|
textEditor.insert(' World', textNode)
|
|
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
|
|
|
actions.commitTextEdit()
|
|
|
|
expect(undo.canUndo).toBe(true)
|
|
expect(undo.undoLabel).toBe('Edit text')
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello World')
|
|
|
|
undo.undo()
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
|
|
|
|
undo.redo()
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello World')
|
|
})
|
|
|
|
test('commitTextEdit does not push undo when text unchanged', () => {
|
|
const { undo, actions, textNode } = setup()
|
|
|
|
actions.startTextEditing(textNode.id)
|
|
actions.commitTextEdit()
|
|
|
|
expect(undo.canUndo).toBe(false)
|
|
})
|
|
|
|
test('undo restores original text even when graph was synced mid-edit', () => {
|
|
const { graph, undo, textEditor, textNode, actions } = setup()
|
|
|
|
actions.startTextEditing(textNode.id)
|
|
|
|
textEditor.insert(' Beautiful', textNode)
|
|
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
|
|
|
textEditor.insert(' World', textNode)
|
|
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
|
|
|
actions.commitTextEdit()
|
|
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello Beautiful World')
|
|
expect(undo.canUndo).toBe(true)
|
|
|
|
undo.undo()
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
|
|
})
|
|
|
|
test('sequential edits create separate undo entries', () => {
|
|
const { graph, undo, textEditor, textNode, actions } = setup()
|
|
|
|
actions.startTextEditing(textNode.id)
|
|
textEditor.insert('!', textNode)
|
|
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
|
actions.commitTextEdit()
|
|
|
|
actions.startTextEditing(textNode.id)
|
|
textEditor.insert('!', textNode)
|
|
graph.updateNode(textNode.id, { text: textEditor.state!.text })
|
|
actions.commitTextEdit()
|
|
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello!!')
|
|
|
|
undo.undo()
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello!')
|
|
|
|
undo.undo()
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
|
|
})
|
|
|
|
test('undo restores styleRuns when they changed during editing', () => {
|
|
const { graph, undo, textEditor, textNode, actions } = setup()
|
|
|
|
const boldRun: StyleRun = { start: 0, length: 5, style: { fontWeight: 700 } }
|
|
graph.updateNode(textNode.id, { styleRuns: [boldRun] })
|
|
|
|
actions.startTextEditing(textNode.id)
|
|
|
|
textEditor.insert(' World', textNode)
|
|
const newRuns: StyleRun[] = [
|
|
{ start: 0, length: 5, style: { fontWeight: 700 } },
|
|
{ start: 5, length: 6, style: { fontWeight: 400 } },
|
|
]
|
|
graph.updateNode(textNode.id, { text: textEditor.state!.text, styleRuns: newRuns })
|
|
|
|
actions.commitTextEdit()
|
|
|
|
expect(graph.getNode(textNode.id)!.styleRuns).toEqual(newRuns)
|
|
expect(undo.canUndo).toBe(true)
|
|
|
|
undo.undo()
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello')
|
|
expect(graph.getNode(textNode.id)!.styleRuns).toEqual([boldRun])
|
|
|
|
undo.redo()
|
|
expect(graph.getNode(textNode.id)!.text).toBe('Hello World')
|
|
expect(graph.getNode(textNode.id)!.styleRuns).toEqual(newRuns)
|
|
})
|
|
|
|
test('undo entry is pushed when only styleRuns changed', () => {
|
|
const { graph, undo, textNode, actions } = setup()
|
|
|
|
actions.startTextEditing(textNode.id)
|
|
|
|
const newRuns: StyleRun[] = [{ start: 0, length: 5, style: { fontWeight: 700 } }]
|
|
graph.updateNode(textNode.id, { styleRuns: newRuns })
|
|
|
|
actions.commitTextEdit()
|
|
|
|
expect(undo.canUndo).toBe(true)
|
|
|
|
undo.undo()
|
|
expect(graph.getNode(textNode.id)!.styleRuns).toEqual([])
|
|
})
|
|
|
|
test('no undo entry when neither text nor styleRuns changed', () => {
|
|
const { undo, actions, textNode } = setup()
|
|
|
|
actions.startTextEditing(textNode.id)
|
|
actions.commitTextEdit()
|
|
|
|
expect(undo.canUndo).toBe(false)
|
|
})
|
|
})
|