openpencil/tests/engine/text-edit-undo.test.ts
Danila Poyarkov f77c42d798 Fix text edit not pushing to undo history
commitTextEdit compared prevText from the scene graph node, but
syncText already updates the node on every keystroke during editing.
This made prevText === newText, so the undo entry was never pushed.

Fix: capture the original text when startTextEditing is called and
use that snapshot for the undo comparison.
2026-04-06 15:57:11 +03:00

121 lines
3.5 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 { 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')
})
})