Files
svgedit/tests/unit/select-module.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

480 lines
16 KiB
JavaScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { init as selectInit, getSelectorManager, Selector, SelectorManager } from '../../packages/svgcanvas/core/select.js'
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
describe('Select Module', () => {
let svgRoot
let svgContent
let svgCanvas
let rectElement
let circleElement
beforeEach(() => {
// Create mock SVG elements
svgRoot = document.createElementNS(NS.SVG, 'svg')
svgRoot.setAttribute('width', '640')
svgRoot.setAttribute('height', '480')
document.body.append(svgRoot)
svgContent = document.createElementNS(NS.SVG, 'g')
svgContent.setAttribute('id', 'svgcontent')
svgRoot.append(svgContent)
rectElement = document.createElementNS(NS.SVG, 'rect')
rectElement.setAttribute('id', 'rect1')
rectElement.setAttribute('x', '10')
rectElement.setAttribute('y', '10')
rectElement.setAttribute('width', '100')
rectElement.setAttribute('height', '50')
svgContent.append(rectElement)
circleElement = document.createElementNS(NS.SVG, 'circle')
circleElement.setAttribute('id', 'circle1')
circleElement.setAttribute('cx', '200')
circleElement.setAttribute('cy', '200')
circleElement.setAttribute('r', '50')
svgContent.append(circleElement)
// Mock data storage
const mockDataStorage = {
_storage: new Map(),
put: function (element, key, value) {
if (!this._storage.has(element)) {
this._storage.set(element, new Map())
}
this._storage.get(element).set(key, value)
},
get: function (element, key) {
return this._storage.has(element) ? this._storage.get(element).get(key) : undefined
},
has: function (element, key) {
return this._storage.has(element) && this._storage.get(element).has(key)
}
}
// Mock svgCanvas
svgCanvas = {
getSvgRoot: () => svgRoot,
getSvgContent: () => svgContent,
getZoom: () => 1,
getDataStorage: () => mockDataStorage,
curConfig: {
imgPath: 'images',
dimensions: [640, 480]
},
createSVGElement: vi.fn((config) => {
const elem = document.createElementNS(NS.SVG, config.element)
if (config.attr) {
Object.entries(config.attr).forEach(([key, value]) => {
elem.setAttribute(key, String(value))
})
}
return elem
})
}
// Initialize select module
selectInit(svgCanvas)
})
afterEach(() => {
document.body.textContent = ''
})
describe('Module initialization', () => {
it('should initialize and return SelectorManager singleton', () => {
const manager = getSelectorManager()
expect(manager).toBeDefined()
expect(manager).toBeInstanceOf(SelectorManager)
})
it('should return the same SelectorManager instance', () => {
const manager1 = getSelectorManager()
const manager2 = getSelectorManager()
expect(manager1).toBe(manager2)
})
it('should not expose private selectorManager field', () => {
const manager = getSelectorManager()
expect(manager.selectorManager).toBeUndefined()
expect(manager.selectorManager_).toBeUndefined()
})
})
describe('SelectorManager class', () => {
let manager
beforeEach(() => {
manager = getSelectorManager()
})
it('should have initialized all required properties', () => {
expect(manager.selectorParentGroup).toBeDefined()
expect(manager.rubberBandBox).toBeNull()
expect(manager.selectors).toEqual([])
expect(manager.selectorMap).toEqual({})
expect(manager.selectorGrips).toBeDefined()
expect(manager.selectorGripsGroup).toBeDefined()
expect(manager.rotateGripConnector).toBeDefined()
expect(manager.rotateGrip).toBeDefined()
})
it('should have all 8 selector grips', () => {
const directions = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w']
directions.forEach(dir => {
expect(manager.selectorGrips[dir]).toBeDefined()
expect(manager.selectorGrips[dir].tagName).toBe('circle')
})
})
it('should create selectorParentGroup in DOM', () => {
const parentGroup = svgRoot.querySelector('#selectorParentGroup')
expect(parentGroup).toBeDefined()
expect(parentGroup).toBe(manager.selectorParentGroup)
})
describe('requestSelector', () => {
it('should create a new selector for an element', () => {
const selector = manager.requestSelector(rectElement)
expect(selector).toBeInstanceOf(Selector)
expect(selector.selectedElement).toBe(rectElement)
expect(selector.locked).toBe(true)
})
it('should return existing selector for same element', () => {
const selector1 = manager.requestSelector(rectElement)
const selector2 = manager.requestSelector(rectElement)
expect(selector1).toBe(selector2)
})
it('should reuse unlocked selectors', () => {
const selector1 = manager.requestSelector(rectElement)
manager.releaseSelector(rectElement)
const selector2 = manager.requestSelector(circleElement)
expect(selector1).toBe(selector2)
expect(selector2.selectedElement).toBe(circleElement)
})
it('should create multiple selectors when needed', () => {
const selector1 = manager.requestSelector(rectElement)
const selector2 = manager.requestSelector(circleElement)
expect(selector1).not.toBe(selector2)
expect(manager.selectors.length).toBe(2)
})
it('should return null for null element', () => {
const selector = manager.requestSelector(null)
expect(selector).toBeNull()
})
it('should add selector to selectorMap', () => {
const selector = manager.requestSelector(rectElement)
expect(manager.selectorMap[rectElement.id]).toBe(selector)
})
})
describe('releaseSelector', () => {
it('should unlock selector', () => {
const selector = manager.requestSelector(rectElement)
expect(selector.locked).toBe(true)
manager.releaseSelector(rectElement)
expect(selector.locked).toBe(false)
})
it('should remove selector from selectorMap', () => {
manager.requestSelector(rectElement)
expect(manager.selectorMap[rectElement.id]).toBeDefined()
manager.releaseSelector(rectElement)
expect(manager.selectorMap[rectElement.id]).toBeUndefined()
})
it('should clear selectedElement', () => {
const selector = manager.requestSelector(rectElement)
manager.releaseSelector(rectElement)
expect(selector.selectedElement).toBeNull()
})
it('should hide selector group', () => {
const selector = manager.requestSelector(rectElement)
manager.releaseSelector(rectElement)
expect(selector.selectorGroup.getAttribute('display')).toBe('none')
})
it('should handle null element gracefully', () => {
expect(() => manager.releaseSelector(null)).not.toThrow()
})
})
describe('getRubberBandBox', () => {
it('should create rubber band box on first call', () => {
const rubberBand = manager.getRubberBandBox()
expect(rubberBand).toBeDefined()
expect(rubberBand.tagName).toBe('rect')
expect(rubberBand.id).toBe('selectorRubberBand')
})
it('should return same rubber band box on subsequent calls', () => {
const rubberBand1 = manager.getRubberBandBox()
const rubberBand2 = manager.getRubberBandBox()
expect(rubberBand1).toBe(rubberBand2)
})
it('should have correct initial display state', () => {
const rubberBand = manager.getRubberBandBox()
expect(rubberBand.getAttribute('display')).toBe('none')
})
})
describe('initGroup', () => {
it('should reset selectors and selectorMap', () => {
manager.requestSelector(rectElement)
manager.initGroup()
expect(manager.selectors).toEqual([])
expect(manager.selectorMap).toEqual({})
})
it('should recreate selectorParentGroup', () => {
const oldGroup = manager.selectorParentGroup
manager.initGroup()
const newGroup = manager.selectorParentGroup
expect(newGroup).not.toBe(oldGroup)
expect(newGroup.id).toBe('selectorParentGroup')
})
it('should create canvasBackground if not exists', () => {
// Remove any existing background
const existing = document.getElementById('canvasBackground')
if (existing) existing.remove()
manager.initGroup()
const background = document.getElementById('canvasBackground')
expect(background).toBeDefined()
expect(background.tagName).toBe('svg')
})
})
})
describe('Selector class', () => {
let manager
let selector
beforeEach(() => {
manager = getSelectorManager()
selector = manager.requestSelector(rectElement)
})
it('should have correct initial properties', () => {
expect(selector.id).toBeDefined()
expect(selector.selectedElement).toBe(rectElement)
expect(selector.locked).toBe(true)
expect(selector.selectorGroup).toBeDefined()
expect(selector.selectorRect).toBeDefined()
})
it('should have all grip coordinates initialized', () => {
const expectedGrips = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w']
expectedGrips.forEach(grip => {
expect(selector.gripCoords[grip]).toBeDefined()
})
})
describe('reset', () => {
it('should update selectedElement', () => {
selector.reset(circleElement)
expect(selector.selectedElement).toBe(circleElement)
})
it('should lock the selector', () => {
selector.locked = false
selector.reset(circleElement)
expect(selector.locked).toBe(true)
})
it('should show selectorGroup', () => {
selector.selectorGroup.setAttribute('display', 'none')
selector.reset(circleElement)
expect(selector.selectorGroup.getAttribute('display')).toBe('inline')
})
})
describe('resize', () => {
it('should update selectorRect d attribute', () => {
selector.resize()
const d = selector.selectorRect.getAttribute('d')
expect(d).toBeTruthy()
expect(d).toMatch(/^M/)
})
it('should update grip coordinates', () => {
selector.resize()
expect(selector.gripCoords.nw).toBeDefined()
expect(Array.isArray(selector.gripCoords.nw)).toBe(true)
expect(selector.gripCoords.nw.length).toBe(2)
})
it('should use provided bbox when given', () => {
const customBbox = { x: 50, y: 50, width: 200, height: 100 }
selector.resize(customBbox)
const d = selector.selectorRect.getAttribute('d')
expect(d).toBeTruthy()
})
})
describe('showGrips', () => {
it('should show grips when true', () => {
selector.showGrips(true)
expect(selector.hasGrips).toBe(true)
expect(manager.selectorGripsGroup.getAttribute('display')).toBe('inline')
})
it('should hide grips when false', () => {
selector.showGrips(true)
selector.showGrips(false)
expect(selector.hasGrips).toBe(false)
expect(manager.selectorGripsGroup.getAttribute('display')).toBe('none')
})
it('should append gripsGroup to selectorGroup when showing', () => {
selector.showGrips(true)
expect(selector.selectorGroup.contains(manager.selectorGripsGroup)).toBe(true)
})
})
describe('updateGripCursors (static)', () => {
it('should update cursor styles for rotated elements', () => {
Selector.updateGripCursors(45)
const updatedCursor = manager.selectorGrips.nw.getAttribute('style')
// After 45-degree rotation, cursors should shift
expect(updatedCursor).toBeTruthy()
expect(updatedCursor).toMatch(/cursor:/)
})
it('should handle negative angles', () => {
expect(() => Selector.updateGripCursors(-45)).not.toThrow()
})
it('should handle zero angle', () => {
Selector.updateGripCursors(0)
expect(manager.selectorGrips.nw.getAttribute('style')).toMatch(/nw-resize/)
})
it('should handle 360-degree rotation', () => {
Selector.updateGripCursors(360)
expect(manager.selectorGrips.nw.getAttribute('style')).toMatch(/nw-resize/)
})
})
})
describe('Integration scenarios', () => {
let manager
beforeEach(() => {
manager = getSelectorManager()
})
it('should handle multiple element selection workflow', () => {
const selector1 = manager.requestSelector(rectElement)
const selector2 = manager.requestSelector(circleElement)
expect(selector1.selectedElement).toBe(rectElement)
expect(selector2.selectedElement).toBe(circleElement)
expect(manager.selectors.length).toBe(2)
selector1.showGrips(true)
expect(selector1.hasGrips).toBe(true)
manager.releaseSelector(rectElement)
expect(selector1.locked).toBe(false)
})
it('should handle selector reuse efficiently', () => {
// Create and release multiple selectors
const s1 = manager.requestSelector(rectElement)
manager.releaseSelector(rectElement)
const s2 = manager.requestSelector(circleElement)
manager.releaseSelector(circleElement)
const s3 = manager.requestSelector(rectElement)
// Should reuse the same selector object
expect(s1).toBe(s2)
expect(s2).toBe(s3)
expect(manager.selectors.length).toBe(1)
})
it('should handle element with transforms', () => {
rectElement.setAttribute('transform', 'rotate(45 60 35)')
const selector = manager.requestSelector(rectElement)
expect(() => selector.resize()).not.toThrow()
expect(selector.selectorRect.getAttribute('d')).toBeTruthy()
})
it('should handle group elements', () => {
const group = document.createElementNS(NS.SVG, 'g')
group.setAttribute('id', 'testgroup')
group.append(rectElement.cloneNode())
svgContent.append(group)
const selector = manager.requestSelector(group)
expect(() => selector.resize()).not.toThrow()
})
it('should handle rubber band box for multi-select', () => {
const rubberBand = manager.getRubberBandBox()
rubberBand.setAttribute('x', '10')
rubberBand.setAttribute('y', '10')
rubberBand.setAttribute('width', '100')
rubberBand.setAttribute('height', '100')
rubberBand.setAttribute('display', 'inline')
expect(rubberBand.getAttribute('display')).toBe('inline')
})
})
describe('Edge cases', () => {
let manager
beforeEach(() => {
manager = getSelectorManager()
})
it('should handle elements with zero dimensions', () => {
const zeroRect = document.createElementNS(NS.SVG, 'rect')
zeroRect.setAttribute('id', 'zerorect')
zeroRect.setAttribute('width', '0')
zeroRect.setAttribute('height', '0')
svgContent.append(zeroRect)
const selector = manager.requestSelector(zeroRect)
expect(() => selector.resize()).not.toThrow()
})
it('should handle elements without id', () => {
const noIdRect = document.createElementNS(NS.SVG, 'rect')
svgContent.append(noIdRect)
const selector = manager.requestSelector(noIdRect)
expect(selector).toBeDefined()
})
it('should handle requesting same element twice without release', () => {
const selector1 = manager.requestSelector(rectElement)
const selector2 = manager.requestSelector(rectElement)
expect(selector1).toBe(selector2)
expect(selector1.locked).toBe(true)
})
})
describe('Private field encapsulation', () => {
it('should not expose SelectModule private field', () => {
const manager = getSelectorManager()
expect(manager.selectorManager).toBeUndefined()
expect(manager['#selectorManager']).toBeUndefined()
})
})
})