Files
svgedit/tests/unit/elem-get-set.test.js
shanyue f6fbc8d9ff
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
check each push / build (push) Has been cancelled
feat(svgcanvas): Trigger 'changed' event on text attribute modifications (#1085)
* feat(svgcanvas): Trigger 'changed' event on text attribute modifications

This update adds a call to the 'changed' event for various text attribute methods, ensuring that changes to text elements are properly tracked and reflected in the SVG canvas. The methods updated include setBold, setItalic, setTextAnchor, setLetterSpacing, setWordSpacing, setTextLength, setLengthAdjust, setFontFamily, setFontColor, and setFontSize.

* refactor(svgcanvas): Simplify text attribute methods and optimize 'changed' event emission

This update introduces helper functions to streamline the handling of selected text elements and their attribute changes. The methods setBold, setItalic, setTextAnchor, setLetterSpacing, setWordSpacing, and setTextLength now utilize these helpers to filter and notify only modified text elements, improving performance and clarity in event emissions.
2026-03-20 00:23:35 +01:00

280 lines
8.3 KiB
JavaScript

import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest'
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
import * as history from '../../packages/svgcanvas/core/history.js'
import dataStorage from '../../packages/svgcanvas/core/dataStorage.js'
import { init as initElemGetSet } from '../../packages/svgcanvas/core/elem-get-set.js'
import * as undo from '../../packages/svgcanvas/core/undo.js'
const createSvgElement = (name) => {
return document.createElementNS(NS.SVG, name)
}
describe('elem-get-set', () => {
/** @type {any} */
let canvas
/** @type {any[]} */
let historyStack
/** @type {SVGSVGElement} */
let svgContent
beforeEach(() => {
historyStack = []
svgContent = /** @type {SVGSVGElement} */ (createSvgElement('svg'))
canvas = {
history,
zoom: 1,
contentW: 100,
contentH: 100,
selectorManager: {
requestSelector () {
return { resize () {} }
}
},
pathActions: {
zoomChange () {},
clear () {}
},
runExtensions () {},
call: vi.fn(),
changeSelectedAttribute: vi.fn(),
getDOMDocument () { return document },
getSvgContent () { return svgContent },
getSelectedElements () { return this.selectedElements || [] },
getDataStorage () { return dataStorage },
getZoom () { return this.zoom },
setZoom (value) { this.zoom = value },
getResolution () {
return {
w: Number(svgContent.getAttribute('width')) / this.zoom,
h: Number(svgContent.getAttribute('height')) / this.zoom,
zoom: this.zoom
}
},
addCommandToHistory (cmd) {
historyStack.push(cmd)
},
setCurText () {},
textActions: {
setCursor () {}
}
}
svgContent.setAttribute('width', '100')
svgContent.setAttribute('height', '100')
initElemGetSet(canvas)
})
afterEach(() => {
while (svgContent.firstChild) {
svgContent.firstChild.remove()
}
})
it('setGroupTitle() inserts title and undo removes it', () => {
const g = createSvgElement('g')
svgContent.append(g)
canvas.selectedElements = [g]
canvas.setGroupTitle('Hello')
expect(g.firstChild?.nodeName).toBe('title')
expect(g.firstChild?.textContent).toBe('Hello')
expect(historyStack).toHaveLength(1)
historyStack[0].unapply(null)
expect(g.querySelector('title')).toBeNull()
historyStack[0].apply(null)
expect(g.querySelector('title')?.textContent).toBe('Hello')
})
it('setGroupTitle() updates title text with undo/redo', () => {
const g = createSvgElement('g')
const title = createSvgElement('title')
title.textContent = 'Old'
g.append(title)
svgContent.append(g)
canvas.selectedElements = [g]
canvas.setGroupTitle('New')
expect(g.querySelector('title')?.textContent).toBe('New')
expect(historyStack).toHaveLength(1)
historyStack[0].unapply(null)
expect(g.querySelector('title')?.textContent).toBe('Old')
historyStack[0].apply(null)
expect(g.querySelector('title')?.textContent).toBe('New')
})
it('setGroupTitle() removes title and undo restores it', () => {
const g = createSvgElement('g')
const title = createSvgElement('title')
title.textContent = 'Label'
g.append(title)
svgContent.append(g)
canvas.selectedElements = [g]
canvas.setGroupTitle('')
expect(g.querySelector('title')).toBeNull()
expect(historyStack).toHaveLength(1)
historyStack[0].unapply(null)
expect(g.querySelector('title')?.textContent).toBe('Label')
historyStack[0].apply(null)
expect(g.querySelector('title')).toBeNull()
})
it('setDocumentTitle() inserts and removes title with undo/redo', () => {
canvas.setDocumentTitle('Doc')
const docTitle = svgContent.querySelector(':scope > title')
expect(docTitle?.textContent).toBe('Doc')
expect(historyStack).toHaveLength(1)
historyStack[0].unapply(null)
expect(svgContent.querySelector(':scope > title')).toBeNull()
historyStack[0].apply(null)
expect(svgContent.querySelector(':scope > title')?.textContent).toBe('Doc')
})
it('setDocumentTitle() does nothing when empty and no title exists', () => {
canvas.setDocumentTitle('')
expect(svgContent.querySelector(':scope > title')).toBeNull()
expect(historyStack).toHaveLength(0)
})
it('setBBoxZoom() returns the computed zoom for zero-size bbox', () => {
canvas.zoom = 1
canvas.selectedElements = [createSvgElement('rect')]
const bbox = { width: 0, height: 0, x: 0, y: 0, factor: 2 }
const result = canvas.setBBoxZoom(bbox, 100, 100)
expect(result?.zoom).toBe(2)
expect(canvas.getZoom()).toBe(2)
})
it('setImageURL() records undo even when image fails to load', () => {
const originalImage = globalThis.Image
try {
globalThis.Image = class FakeImage {
constructor () {
this.width = 10
this.height = 10
this.onload = null
this.onerror = null
}
get src () {
return this._src
}
set src (value) {
this._src = value
this.onerror && this.onerror(new Error('load failed'))
}
}
const image = createSvgElement('image')
image.setAttribute('href', 'old.png')
svgContent.append(image)
canvas.selectedElements = [image]
canvas.setImageURL('bad.png')
expect(image.getAttribute('href')).toBe('bad.png')
expect(historyStack).toHaveLength(1)
historyStack[0].unapply(null)
expect(image.getAttribute('href')).toBe('old.png')
historyStack[0].apply(null)
expect(image.getAttribute('href')).toBe('bad.png')
} finally {
globalThis.Image = originalImage
}
})
it('setRectRadius() preserves attribute absence on undo', () => {
const rect = createSvgElement('rect')
svgContent.append(rect)
canvas.selectedElements = [rect]
canvas.setRectRadius('5')
expect(rect.getAttribute('rx')).toBe('5')
expect(rect.getAttribute('ry')).toBe('5')
expect(historyStack).toHaveLength(1)
historyStack[0].unapply(null)
expect(rect.hasAttribute('rx')).toBe(false)
expect(rect.hasAttribute('ry')).toBe(false)
})
it('undo updates contentW/contentH for svgContent size changes', () => {
const svg = createSvgElement('svg')
const localCanvas = {
contentW: 100,
contentH: 100,
getSvgContent () { return svg },
clearSelection () {},
pathActions: { clear () {} },
call () {}
}
undo.init(localCanvas)
svg.setAttribute('width', '200')
svg.setAttribute('height', '150')
localCanvas.contentW = 200
localCanvas.contentH = 150
const cmd = new history.ChangeElementCommand(svg, { width: 100, height: 100 })
localCanvas.undoMgr.addCommandToHistory(cmd)
localCanvas.undoMgr.undo()
expect(localCanvas.contentW).toBe(100)
expect(localCanvas.contentH).toBe(100)
localCanvas.undoMgr.redo()
expect(localCanvas.contentW).toBe(200)
expect(localCanvas.contentH).toBe(150)
})
it('setBold() emits changed only for modified text elements', () => {
const text = createSvgElement('text')
text.textContent = 'Hello'
const rect = createSvgElement('rect')
svgContent.append(text, rect)
canvas.selectedElements = [text, rect]
canvas.setBold(true)
expect(canvas.changeSelectedAttribute).toHaveBeenCalledWith('font-weight', 'bold', [text])
expect(canvas.call).toHaveBeenCalledWith('changed', [text])
})
it('setBold() skips changed event for no-op text updates', () => {
const text = createSvgElement('text')
text.textContent = 'Hello'
text.setAttribute('font-weight', 'bold')
svgContent.append(text)
canvas.selectedElements = [text]
canvas.setBold(true)
expect(canvas.changeSelectedAttribute).not.toHaveBeenCalled()
expect(canvas.call).not.toHaveBeenCalled()
})
it('setFontColor() ignores non-text selections when emitting changed', () => {
const text = createSvgElement('text')
text.textContent = 'Hello'
const rect = createSvgElement('rect')
svgContent.append(text, rect)
canvas.selectedElements = [text, rect]
canvas.setFontColor('#f00')
expect(canvas.changeSelectedAttribute).toHaveBeenCalledWith('fill', '#f00', [text])
expect(canvas.call).toHaveBeenCalledWith('changed', [text])
})
})