Files
svgedit/tests/unit/history.test.js
JFH 97386d20b5 Jan2026 fixes (#1077)
* fix release script
* fix svgcanvas edge cases
* Update path-actions.js
* add modern js
* update deps
* Update CHANGES.md
2026-01-11 00:57:06 +01:00

594 lines
19 KiB
JavaScript

import { NS } from '../../packages/svgcanvas/core/namespaces.js'
import * as utilities from '../../packages/svgcanvas/core/utilities.js'
import * as history from '../../packages/svgcanvas/core/history.js'
describe('history', function () {
// TODO(codedread): Write tests for handling history events.
utilities.mock({
getHref () { return '#foo' },
setHref () { /* empty fn */ },
getRotationAngle () { return 0 }
})
// const svg = document.createElementNS(NS.SVG, 'svg');
let undoMgr = null
let divparent
let div1
let div2
let div3
let div4
let div5
let div
class MockCommand extends history.Command {
constructor (optText) {
super()
this.text = optText
}
apply (handler) {
super.apply(handler, () => { /* empty fn */ })
}
unapply (handler) {
super.unapply(handler, () => { /* empty fn */ })
}
elements () { return [] }
}
/*
class MockHistoryEventHandler {
handleHistoryEvent (eventType, command) {}
}
*/
/**
* Set up tests (with undo manager).
* @returns {void}
*/
beforeEach(function () {
undoMgr = new history.UndoManager()
document.body.textContent = ''
divparent = document.createElement('div')
divparent.id = 'divparent'
divparent.style.visibility = 'hidden'
div1 = document.createElement('div'); div1.id = 'div1'
div2 = document.createElement('div'); div2.id = 'div2'
div3 = document.createElement('div'); div3.id = 'div3'
div4 = document.createElement('div'); div4.id = 'div4'
div5 = document.createElement('div'); div5.id = 'div5'
div = document.createElement('div'); div.id = 'div'
divparent.append(div1, div2, div3)
div4.style.visibility = 'hidden'
div4.append(div5)
document.body.append(divparent, div)
})
/**
* Tear down tests, destroying undo manager.
* @returns {void}
*/
afterEach(() => {
undoMgr = null
})
it('Test svgedit.history package', function () {
assert.ok(history)
assert.ok(history.MoveElementCommand)
assert.ok(history.InsertElementCommand)
assert.ok(history.ChangeElementCommand)
assert.ok(history.RemoveElementCommand)
assert.ok(history.BatchCommand)
assert.ok(history.UndoManager)
assert.equal(typeof history.MoveElementCommand, typeof function () { /* empty fn */ })
assert.equal(typeof history.InsertElementCommand, typeof function () { /* empty fn */ })
assert.equal(typeof history.ChangeElementCommand, typeof function () { /* empty fn */ })
assert.equal(typeof history.RemoveElementCommand, typeof function () { /* empty fn */ })
assert.equal(typeof history.BatchCommand, typeof function () { /* empty fn */ })
assert.equal(typeof history.UndoManager, typeof function () { /* empty fn */ })
})
it('Test UndoManager methods', function () {
assert.ok(undoMgr)
assert.ok(undoMgr.addCommandToHistory)
assert.ok(undoMgr.getUndoStackSize)
assert.ok(undoMgr.getRedoStackSize)
assert.ok(undoMgr.resetUndoStack)
assert.ok(undoMgr.getNextUndoCommandText)
assert.ok(undoMgr.getNextRedoCommandText)
assert.equal(typeof undoMgr, typeof {})
assert.equal(typeof undoMgr.addCommandToHistory, typeof function () { /* empty fn */ })
assert.equal(typeof undoMgr.getUndoStackSize, typeof function () { /* empty fn */ })
assert.equal(typeof undoMgr.getRedoStackSize, typeof function () { /* empty fn */ })
assert.equal(typeof undoMgr.resetUndoStack, typeof function () { /* empty fn */ })
assert.equal(typeof undoMgr.getNextUndoCommandText, typeof function () { /* empty fn */ })
assert.equal(typeof undoMgr.getNextRedoCommandText, typeof function () { /* empty fn */ })
})
it('Test UndoManager.addCommandToHistory() function', function () {
assert.equal(undoMgr.getUndoStackSize(), 0)
undoMgr.addCommandToHistory(new MockCommand())
assert.equal(undoMgr.getUndoStackSize(), 1)
undoMgr.addCommandToHistory(new MockCommand())
assert.equal(undoMgr.getUndoStackSize(), 2)
})
it('Test UndoManager.getUndoStackSize() and getRedoStackSize() functions', function () {
undoMgr.addCommandToHistory(new MockCommand())
undoMgr.addCommandToHistory(new MockCommand())
undoMgr.addCommandToHistory(new MockCommand())
assert.equal(undoMgr.getUndoStackSize(), 3)
assert.equal(undoMgr.getRedoStackSize(), 0)
undoMgr.undo()
assert.equal(undoMgr.getUndoStackSize(), 2)
assert.equal(undoMgr.getRedoStackSize(), 1)
undoMgr.undo()
assert.equal(undoMgr.getUndoStackSize(), 1)
assert.equal(undoMgr.getRedoStackSize(), 2)
undoMgr.undo()
assert.equal(undoMgr.getUndoStackSize(), 0)
assert.equal(undoMgr.getRedoStackSize(), 3)
undoMgr.undo()
assert.equal(undoMgr.getUndoStackSize(), 0)
assert.equal(undoMgr.getRedoStackSize(), 3)
undoMgr.redo()
assert.equal(undoMgr.getUndoStackSize(), 1)
assert.equal(undoMgr.getRedoStackSize(), 2)
undoMgr.redo()
assert.equal(undoMgr.getUndoStackSize(), 2)
assert.equal(undoMgr.getRedoStackSize(), 1)
undoMgr.redo()
assert.equal(undoMgr.getUndoStackSize(), 3)
assert.equal(undoMgr.getRedoStackSize(), 0)
undoMgr.redo()
assert.equal(undoMgr.getUndoStackSize(), 3)
assert.equal(undoMgr.getRedoStackSize(), 0)
})
it('Test UndoManager.resetUndoStackSize() function', function () {
undoMgr.addCommandToHistory(new MockCommand())
undoMgr.addCommandToHistory(new MockCommand())
undoMgr.addCommandToHistory(new MockCommand())
undoMgr.undo()
assert.equal(undoMgr.getUndoStackSize(), 2)
assert.equal(undoMgr.getRedoStackSize(), 1)
undoMgr.resetUndoStack()
assert.equal(undoMgr.getUndoStackSize(), 0)
assert.equal(undoMgr.getRedoStackSize(), 0)
})
it('Test UndoManager.getNextUndoCommandText() function', function () {
assert.equal(undoMgr.getNextUndoCommandText(), '')
undoMgr.addCommandToHistory(new MockCommand('First'))
undoMgr.addCommandToHistory(new MockCommand('Second'))
undoMgr.addCommandToHistory(new MockCommand('Third'))
assert.equal(undoMgr.getNextUndoCommandText(), 'Third')
undoMgr.undo()
assert.equal(undoMgr.getNextUndoCommandText(), 'Second')
undoMgr.undo()
assert.equal(undoMgr.getNextUndoCommandText(), 'First')
undoMgr.undo()
assert.equal(undoMgr.getNextUndoCommandText(), '')
undoMgr.redo()
assert.equal(undoMgr.getNextUndoCommandText(), 'First')
undoMgr.redo()
assert.equal(undoMgr.getNextUndoCommandText(), 'Second')
undoMgr.redo()
assert.equal(undoMgr.getNextUndoCommandText(), 'Third')
undoMgr.redo()
assert.equal(undoMgr.getNextUndoCommandText(), 'Third')
})
it('Test UndoManager.getNextRedoCommandText() function', function () {
assert.equal(undoMgr.getNextRedoCommandText(), '')
undoMgr.addCommandToHistory(new MockCommand('First'))
undoMgr.addCommandToHistory(new MockCommand('Second'))
undoMgr.addCommandToHistory(new MockCommand('Third'))
assert.equal(undoMgr.getNextRedoCommandText(), '')
undoMgr.undo()
assert.equal(undoMgr.getNextRedoCommandText(), 'Third')
undoMgr.undo()
assert.equal(undoMgr.getNextRedoCommandText(), 'Second')
undoMgr.undo()
assert.equal(undoMgr.getNextRedoCommandText(), 'First')
undoMgr.redo()
assert.equal(undoMgr.getNextRedoCommandText(), 'Second')
undoMgr.redo()
assert.equal(undoMgr.getNextRedoCommandText(), 'Third')
undoMgr.redo()
assert.equal(undoMgr.getNextRedoCommandText(), '')
})
it('Test UndoManager.undo() and redo() functions', function () {
let lastCalled = null
const cmd1 = new MockCommand()
const cmd2 = new MockCommand()
const cmd3 = new MockCommand()
cmd1.apply = function () { lastCalled = 'cmd1.apply' }
cmd2.apply = function () { lastCalled = 'cmd2.apply' }
cmd3.apply = function () { lastCalled = 'cmd3.apply' }
cmd1.unapply = function () { lastCalled = 'cmd1.unapply' }
cmd2.unapply = function () { lastCalled = 'cmd2.unapply' }
cmd3.unapply = function () { lastCalled = 'cmd3.unapply' }
undoMgr.addCommandToHistory(cmd1)
undoMgr.addCommandToHistory(cmd2)
undoMgr.addCommandToHistory(cmd3)
assert.ok(!lastCalled)
undoMgr.undo()
assert.equal(lastCalled, 'cmd3.unapply')
undoMgr.redo()
assert.equal(lastCalled, 'cmd3.apply')
undoMgr.undo()
undoMgr.undo()
assert.equal(lastCalled, 'cmd2.unapply')
undoMgr.undo()
assert.equal(lastCalled, 'cmd1.unapply')
lastCalled = null
undoMgr.undo()
assert.ok(!lastCalled)
undoMgr.redo()
assert.equal(lastCalled, 'cmd1.apply')
undoMgr.redo()
assert.equal(lastCalled, 'cmd2.apply')
undoMgr.redo()
assert.equal(lastCalled, 'cmd3.apply')
lastCalled = null
undoMgr.redo()
assert.ok(!lastCalled)
})
it('Test MoveElementCommand', function () {
let move = new history.MoveElementCommand(div3, div1, divparent)
assert.ok(move.unapply)
assert.ok(move.apply)
assert.equal(typeof move.unapply, typeof function () { /* empty fn */ })
assert.equal(typeof move.apply, typeof function () { /* empty fn */ })
move.unapply()
assert.equal(divparent.firstElementChild, div3)
assert.equal(divparent.firstElementChild.nextElementSibling, div1)
assert.equal(divparent.lastElementChild, div2)
move.apply()
assert.equal(divparent.firstElementChild, div1)
assert.equal(divparent.firstElementChild.nextElementSibling, div2)
assert.equal(divparent.lastElementChild, div3)
move = new history.MoveElementCommand(div1, null, divparent)
move.unapply()
assert.equal(divparent.firstElementChild, div2)
assert.equal(divparent.firstElementChild.nextElementSibling, div3)
assert.equal(divparent.lastElementChild, div1)
move.apply()
assert.equal(divparent.firstElementChild, div1)
assert.equal(divparent.firstElementChild.nextElementSibling, div2)
assert.equal(divparent.lastElementChild, div3)
move = new history.MoveElementCommand(div2, div5, div4)
move.unapply()
assert.equal(divparent.firstElementChild, div1)
assert.equal(divparent.firstElementChild.nextElementSibling, div3)
assert.equal(divparent.lastElementChild, div3)
assert.equal(div4.firstElementChild, div2)
assert.equal(div4.firstElementChild.nextElementSibling, div5)
move.apply()
assert.equal(divparent.firstElementChild, div1)
assert.equal(divparent.firstElementChild.nextElementSibling, div2)
assert.equal(divparent.lastElementChild, div3)
assert.equal(div4.firstElementChild, div5)
assert.equal(div4.lastElementChild, div5)
})
it('Test InsertElementCommand', function () {
let insert = new history.InsertElementCommand(div3)
assert.ok(insert.unapply)
assert.ok(insert.apply)
assert.equal(typeof insert.unapply, typeof function () { /* empty fn */ })
assert.equal(typeof insert.apply, typeof function () { /* empty fn */ })
insert.unapply()
assert.equal(divparent.childElementCount, 2)
assert.equal(divparent.firstElementChild, div1)
assert.equal(div1.nextElementSibling, div2)
assert.equal(divparent.lastElementChild, div2)
insert.apply()
assert.equal(divparent.childElementCount, 3)
assert.equal(divparent.firstElementChild, div1)
assert.equal(div1.nextElementSibling, div2)
assert.equal(div2.nextElementSibling, div3)
insert = new history.InsertElementCommand(div2)
insert.unapply()
assert.equal(divparent.childElementCount, 2)
assert.equal(divparent.firstElementChild, div1)
assert.equal(div1.nextElementSibling, div3)
assert.equal(divparent.lastElementChild, div3)
insert.apply()
assert.equal(divparent.childElementCount, 3)
assert.equal(divparent.firstElementChild, div1)
assert.equal(div1.nextElementSibling, div2)
assert.equal(div2.nextElementSibling, div3)
})
it('Test RemoveElementCommand', function () {
const div6 = document.createElement('div')
div6.id = 'div6'
let remove = new history.RemoveElementCommand(div6, null, divparent)
assert.ok(remove.unapply)
assert.ok(remove.apply)
assert.equal(typeof remove.unapply, typeof function () { /* empty fn */ })
assert.equal(typeof remove.apply, typeof function () { /* empty fn */ })
remove.unapply()
assert.equal(divparent.childElementCount, 4)
assert.equal(divparent.firstElementChild, div1)
assert.equal(div1.nextElementSibling, div2)
assert.equal(div2.nextElementSibling, div3)
assert.equal(div3.nextElementSibling, div6)
remove.apply()
assert.equal(divparent.childElementCount, 3)
assert.equal(divparent.firstElementChild, div1)
assert.equal(div1.nextElementSibling, div2)
assert.equal(div2.nextElementSibling, div3)
remove = new history.RemoveElementCommand(div6, div2, divparent)
remove.unapply()
assert.equal(divparent.childElementCount, 4)
assert.equal(divparent.firstElementChild, div1)
assert.equal(div1.nextElementSibling, div6)
assert.equal(div6.nextElementSibling, div2)
assert.equal(div2.nextElementSibling, div3)
remove.apply()
assert.equal(divparent.childElementCount, 3)
assert.equal(divparent.firstElementChild, div1)
assert.equal(div1.nextElementSibling, div2)
assert.equal(div2.nextElementSibling, div3)
})
it('Test ChangeElementCommand', function () {
div1.setAttribute('title', 'new title')
let change = new history.ChangeElementCommand(div1,
{ title: 'old title', class: 'foo' })
assert.ok(change.unapply)
assert.ok(change.apply)
assert.equal(typeof change.unapply, typeof function () { /* empty fn */ })
assert.equal(typeof change.apply, typeof function () { /* empty fn */ })
change.unapply()
assert.equal(div1.getAttribute('title'), 'old title')
assert.equal(div1.getAttribute('class'), 'foo')
change.apply()
assert.equal(div1.getAttribute('title'), 'new title')
assert.ok(!div1.getAttribute('class'))
div1.textContent = 'inner text'
change = new history.ChangeElementCommand(div1,
{ '#text': null })
change.unapply()
assert.ok(!div1.textContent)
change.apply()
assert.equal(div1.textContent, 'inner text')
div1.textContent = ''
change = new history.ChangeElementCommand(div1,
{ '#text': 'old text' })
change.unapply()
assert.equal(div1.textContent, 'old text')
change.apply()
assert.ok(!div1.textContent)
// TODO(codedread): Refactor this #href stuff in history.js and svgcanvas.js
const rect = document.createElementNS(NS.SVG, 'rect')
let justCalled = null
let gethrefvalue = null
let sethrefvalue = null
utilities.mock({
getHref (elem) {
assert.equal(elem, rect)
justCalled = 'getHref'
return gethrefvalue
},
setHref (elem, val) {
assert.equal(elem, rect)
assert.equal(val, sethrefvalue)
justCalled = 'setHref'
},
getRotationAngle () { return 0 }
})
gethrefvalue = '#newhref'
change = new history.ChangeElementCommand(rect,
{ '#href': '#oldhref' })
assert.equal(justCalled, 'getHref')
justCalled = null
sethrefvalue = '#oldhref'
change.unapply()
assert.equal(justCalled, 'setHref')
justCalled = null
sethrefvalue = '#newhref'
change.apply()
assert.equal(justCalled, 'setHref')
// Ensure numeric zero values are not treated like "remove attribute".
const rectZero = document.createElementNS(NS.SVG, 'rect')
rectZero.setAttribute('x', '5')
change = new history.ChangeElementCommand(rectZero, { x: 0 })
change.unapply()
assert.equal(rectZero.getAttribute('x'), '0')
change.apply()
assert.equal(rectZero.getAttribute('x'), '5')
// Ensure "#href" can be removed when the previous value was null.
const rectHref = document.createElementNS(NS.SVG, 'rect')
rectHref.setAttribute('href', '#newhref')
let calls = []
utilities.mock({
getHref (elem) {
assert.equal(elem, rectHref)
calls.push('getHref')
return rectHref.getAttribute('href')
},
setHref (elem, val) {
assert.equal(elem, rectHref)
calls.push('setHref')
rectHref.setAttribute('href', val)
},
getRotationAngle () { return 0 }
})
calls = []
change = new history.ChangeElementCommand(rectHref, { '#href': null })
assert.deepEqual(calls, ['getHref'])
calls = []
change.unapply()
assert.equal(rectHref.hasAttribute('href'), false)
assert.deepEqual(calls, [])
calls = []
change.apply()
assert.equal(rectHref.getAttribute('href'), '#newhref')
assert.deepEqual(calls, ['setHref'])
const line = document.createElementNS(NS.SVG, 'line')
line.setAttribute('class', 'newClass')
change = new history.ChangeElementCommand(line, { class: 'oldClass' })
assert.ok(change.unapply)
assert.ok(change.apply)
assert.equal(typeof change.unapply, typeof function () { /* empty fn */ })
assert.equal(typeof change.apply, typeof function () { /* empty fn */ })
change.unapply()
assert.equal(line.getAttribute('class'), 'oldClass')
change.apply()
assert.equal(line.getAttribute('class'), 'newClass')
})
it('Test BatchCommand', function () {
let concatResult = ''
MockCommand.prototype.apply = function () { concatResult += this.text }
const batch = new history.BatchCommand()
assert.ok(batch.unapply)
assert.ok(batch.apply)
assert.ok(batch.addSubCommand)
assert.ok(batch.isEmpty)
assert.equal(typeof batch.unapply, 'function')
assert.equal(typeof batch.apply, 'function')
assert.equal(typeof batch.addSubCommand, 'function')
assert.equal(typeof batch.isEmpty, 'function')
assert.ok(batch.isEmpty())
batch.addSubCommand(new MockCommand('a'))
assert.ok(!batch.isEmpty())
batch.addSubCommand(new MockCommand('b'))
batch.addSubCommand(new MockCommand('c'))
assert.ok(!concatResult)
batch.apply()
assert.equal(concatResult, 'abc')
MockCommand.prototype.apply = function () { /* empty fn */ }
MockCommand.prototype.unapply = function () { concatResult += this.text }
concatResult = ''
assert.ok(!concatResult)
batch.unapply()
assert.equal(concatResult, 'cba')
MockCommand.prototype.unapply = function () { /* empty fn */ }
})
it('Test BatchCommand with elements() method', function () {
const batch = new history.BatchCommand('test batch with elements')
// Create some mock commands that reference elements
class MockElementCommand {
constructor (elem) { this.elem = elem }
elements () { return [this.elem] }
apply () { /* empty fn */ }
unapply () { /* empty fn */ }
getText () { return 'mock' }
}
const elem1 = document.createElementNS(NS.SVG, 'rect')
const cmd1 = new MockElementCommand(elem1)
batch.addSubCommand(cmd1)
const elems = batch.elements()
assert.ok(Array.isArray(elems))
})
it('Test BatchCommand getText()', function () {
const batch = new history.BatchCommand('my test batch')
assert.equal(batch.getText(), 'my test batch')
})
})