increase test coverage

extend test coverage
This commit is contained in:
JFH
2025-12-01 01:22:26 +01:00
parent a37fbac749
commit fa380402e1
52 changed files with 3813 additions and 169 deletions

View File

@@ -0,0 +1,61 @@
import { test, expect } from '../fixtures.js'
test.describe('clear module', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('clears canvas content and sets default attributes', async ({ page }) => {
const result = await page.evaluate(() => {
const { clearModule } = window.svgHarness
const svgContent = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svgContent.append(document.createElementNS('http://www.w3.org/2000/svg', 'rect'))
const svgRoot = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const canvas = {
getCurConfig: () => ({ dimensions: [100, 50], show_outside_canvas: false }),
getSvgContent: () => svgContent,
getSvgRoot: () => svgRoot,
getDOMDocument: () => document
}
clearModule.init(canvas)
clearModule.clearSvgContentElementInit()
const comment = svgContent.firstChild
return {
childCount: svgContent.childNodes.length,
isComment: comment.nodeType,
overflow: svgContent.getAttribute('overflow'),
width: svgContent.getAttribute('width'),
height: svgContent.getAttribute('height'),
appended: svgRoot.contains(svgContent)
}
})
expect(result.childCount).toBe(1)
expect(result.isComment).toBe(8)
expect(result.overflow).toBe('hidden')
expect(result.width).toBe('100')
expect(result.height).toBe('50')
expect(result.appended).toBe(true)
})
test('honors show_outside_canvas flag when clearing', async ({ page }) => {
const overflow = await page.evaluate(() => {
const { clearModule } = window.svgHarness
const svgContent = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const svgRoot = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
clearModule.init({
getCurConfig: () => ({ dimensions: [10, 20], show_outside_canvas: true }),
getSvgContent: () => svgContent,
getSvgRoot: () => svgRoot,
getDOMDocument: () => document
})
clearModule.clearSvgContentElementInit()
return svgContent.getAttribute('overflow')
})
expect(overflow).toBe('visible')
})
})

View File

@@ -0,0 +1,92 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core draw extras', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness?.draw))
})
test('Drawing merges layers and moves children upward', async ({ page }) => {
const result = await page.evaluate(() => {
const { draw } = window.svgHarness
const NS = 'http://www.w3.org/2000/svg'
const svg = document.createElementNS(NS, 'svg')
document.body.append(svg)
const drawing = new draw.Drawing(svg, 'id_')
drawing.identifyLayers()
const hrLog = []
const hrService = {
startBatchCommand: (name) => hrLog.push('start:' + name),
endBatchCommand: () => hrLog.push('end'),
removeElement: (el) => hrLog.push('remove:' + el.tagName),
moveElement: (el) => hrLog.push('move:' + el.tagName),
insertElement: (el) => hrLog.push('insert:' + el.tagName)
}
const baseLayer = drawing.getCurrentLayer()
const rect = document.createElementNS(NS, 'rect')
baseLayer.append(rect)
drawing.createLayer('Layer 2', hrService)
const layer2 = drawing.getCurrentLayer()
const circle = document.createElementNS(NS, 'circle')
layer2.append(circle)
drawing.mergeLayer(hrService)
return {
mergedShapes: baseLayer.querySelectorAll('rect,circle').length,
layersAfterMerge: drawing.getNumLayers(),
currentName: drawing.getCurrentLayerName(),
log: hrLog
}
})
expect(result.layersAfterMerge).toBe(1)
expect(result.mergedShapes).toBe(2)
expect(result.currentName).toContain('Layer')
expect(result.log.some(entry => entry.startsWith('start:Merge Layer'))).toBe(true)
expect(result.log.some(entry => entry.startsWith('move:circle'))).toBe(true)
})
test('mergeAllLayers collapses multiple layers into one', async ({ page }) => {
const result = await page.evaluate(() => {
const { draw } = window.svgHarness
const NS = 'http://www.w3.org/2000/svg'
const svg = document.createElementNS(NS, 'svg')
document.body.append(svg)
const drawing = new draw.Drawing(svg, 'id_')
drawing.identifyLayers()
const hrLog = []
const hrService = {
startBatchCommand: (name) => hrLog.push('start:' + name),
endBatchCommand: () => hrLog.push('end'),
removeElement: () => {},
moveElement: () => {},
insertElement: () => {}
}
// Make three layers with a child each
const baseLayer = drawing.getCurrentLayer()
baseLayer.append(document.createElementNS(NS, 'rect'))
drawing.createLayer('Layer 2', hrService)
drawing.getCurrentLayer().append(document.createElementNS(NS, 'circle'))
drawing.createLayer('Layer 3', hrService)
drawing.getCurrentLayer().append(document.createElementNS(NS, 'line'))
drawing.mergeAllLayers(hrService)
const remaining = drawing.getCurrentLayer()
return {
finalLayers: drawing.getNumLayers(),
shapes: remaining.querySelectorAll('rect,circle,line').length,
log: hrLog
}
})
expect(result.finalLayers).toBe(1)
expect(result.shapes).toBe(3)
expect(result.log.some(entry => entry === 'start:Merge all Layers')).toBe(true)
expect(result.log.at(-1)).toBe('end')
})
})

View File

@@ -0,0 +1,147 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core drawing', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('manages ids and adopts orphaned elements into layers', async ({ page }) => {
const result = await page.evaluate(() => {
const { draw, namespaces } = window.svgHarness
const svg = document.createElementNS(namespaces.NS.SVG, 'svg')
document.getElementById('root').append(svg)
const existingLayer = document.createElementNS(namespaces.NS.SVG, 'g')
existingLayer.classList.add('layer')
const title = document.createElementNS(namespaces.NS.SVG, 'title')
title.textContent = 'Layer 1'
existingLayer.append(title)
const orphan = document.createElementNS(namespaces.NS.SVG, 'rect')
orphan.id = 'rect-orphan'
svg.append(existingLayer, orphan)
const drawing = new draw.Drawing(svg, 'p_')
drawing.identifyLayers()
const id1 = drawing.getNextId()
const id2 = drawing.getNextId()
const released = drawing.releaseId(id2)
const reused = drawing.getNextId()
const next = drawing.getNextId()
return {
id1,
id2,
released,
reused,
next,
layerCount: drawing.getNumLayers(),
currentLayer: drawing.getCurrentLayerName(),
orphanParentTag: orphan.parentNode?.tagName.toLowerCase()
}
})
expect(result.id1).toBe('p_1')
expect(result.id2).toBe('p_2')
expect(result.released).toBe(true)
expect(result.reused).toBe(result.id2)
expect(result.next).toBe('p_3')
expect(result.layerCount).toBe(2)
expect(result.currentLayer).toBe('Layer 2')
expect(result.orphanParentTag).toBe('g')
})
test('reorders and toggles layers', async ({ page }) => {
const result = await page.evaluate(() => {
const { draw, namespaces } = window.svgHarness
const svg = document.createElementNS(namespaces.NS.SVG, 'svg')
document.getElementById('root').append(svg)
const drawing = new draw.Drawing(svg)
drawing.identifyLayers() // creates first layer
const originalName = drawing.getCurrentLayerName()
drawing.createLayer('Layer Two')
drawing.createLayer('Layer Three')
drawing.setCurrentLayer('Layer Two')
const movedDown = drawing.setCurrentLayerPosition(2)
const orderAfterDown = [
drawing.getLayerName(0),
drawing.getLayerName(1),
drawing.getLayerName(2)
]
drawing.setCurrentLayer('Layer Three')
const movedUp = drawing.setCurrentLayerPosition(0)
const orderAfterUp = [
drawing.getLayerName(0),
drawing.getLayerName(1),
drawing.getLayerName(2)
]
const target = drawing.getCurrentLayerName()
drawing.setLayerVisibility(target, false)
const hidden = drawing.getLayerVisibility(target)
drawing.setLayerOpacity(target, 0.5)
return {
originalName,
movedDown: Boolean(movedDown),
movedUp: Boolean(movedUp),
orderAfterDown,
orderAfterUp,
hidden,
opacity: drawing.getLayerOpacity(target)
}
})
expect(result.originalName).toBe('Layer 1')
expect(result.movedDown).toBe(true)
expect(result.orderAfterDown[2]).toBe('Layer Two')
expect(result.movedUp).toBe(true)
expect(result.orderAfterUp[0]).toBe('Layer Three')
expect(result.hidden).toBe(false)
expect(result.opacity).toBe(0.5)
})
test('clones and deletes layers and randomizes ids', async ({ page }) => {
const result = await page.evaluate(() => {
const { draw, namespaces } = window.svgHarness
const svg = document.createElementNS(namespaces.NS.SVG, 'svg')
document.getElementById('root').append(svg)
const drawing = new draw.Drawing(svg)
drawing.identifyLayers()
const currentLayer = drawing.getCurrentLayer()
const circle = document.createElementNS(namespaces.NS.SVG, 'circle')
circle.setAttribute('id', 'seed')
currentLayer.append(circle)
const cloneGroup = drawing.cloneLayer('Duplicated')
const clonedCircle = cloneGroup?.querySelector('circle')
const beforeDelete = drawing.getNumLayers()
const deleted = drawing.deleteCurrentLayer()
const afterDelete = drawing.getNumLayers()
draw.randomizeIds(true, drawing)
const nonceSet = drawing.getNonce()
draw.randomizeIds(false, drawing)
return {
cloneHasChild: Boolean(clonedCircle),
beforeDelete,
afterDelete,
deletedTag: deleted?.tagName.toLowerCase(),
nonceSet,
nonceCleared: drawing.getNonce()
}
})
expect(result.cloneHasChild).toBe(true)
expect(result.beforeDelete).toBe(2)
expect(result.afterDelete).toBe(1)
expect(result.deletedTag).toBe('g')
expect(result.nonceSet).not.toBe('')
expect(result.nonceCleared).toBe('')
})
})

View File

@@ -0,0 +1,122 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core math and coords', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('math helpers consolidate transforms and snapping', async ({ page }) => {
const result = await page.evaluate(() => {
const { math } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
document.body.append(svg)
const identity = math.transformPoint(100, 200, svg.createSVGMatrix())
const translatedMatrix = svg.createSVGMatrix()
translatedMatrix.e = 300
translatedMatrix.f = 400
const translated = math.transformPoint(5, 5, translatedMatrix)
const tlist = math.getTransformList(svg)
const hasMatrixBefore = math.hasMatrixTransform(tlist)
const tf = svg.createSVGTransformFromMatrix(translatedMatrix)
tlist.appendItem(tf)
const consolidated = math.transformListToTransform(tlist).matrix
const hasMatrixAfter = math.hasMatrixTransform(tlist)
const multiplied = math.matrixMultiply(
svg.createSVGMatrix().translate(10, 20),
svg.createSVGMatrix().translate(-10, -20)
)
const snapped = math.snapToAngle(0, 0, 10, 5)
const intersects = {
overlap: math.rectsIntersect(
{ x: 0, y: 0, width: 50, height: 50 },
{ x: 25, y: 25, width: 10, height: 10 }
),
apart: math.rectsIntersect(
{ x: 0, y: 0, width: 10, height: 10 },
{ x: 100, y: 100, width: 5, height: 5 }
)
}
return {
identity,
translated,
hasMatrixBefore,
hasMatrixAfter,
consolidated: { e: consolidated.e, f: consolidated.f },
multiplied: { e: multiplied.e, f: multiplied.f },
snapped,
intersects
}
})
expect(result.identity).toEqual({ x: 100, y: 200 })
expect(result.translated).toEqual({ x: 305, y: 405 })
expect(result.hasMatrixBefore).toBe(false)
expect(result.hasMatrixAfter).toBe(true)
expect(result.consolidated.e).toBe(300)
expect(result.consolidated.f).toBe(400)
expect(result.multiplied.e).toBe(0)
expect(result.multiplied.f).toBe(0)
expect(result.snapped.a).toBeCloseTo(Math.PI / 4)
expect(result.intersects.overlap).toBe(true)
expect(result.intersects.apart).toBe(false)
})
test('coords.remapElement handles translation and scaling', async ({ page }) => {
const result = await page.evaluate(() => {
const { coords, utilities } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
document.body.append(svg)
utilities.init({
getSvgRoot: () => svg,
getDOMDocument: () => document,
getDOMContainer: () => svg
})
coords.init({
getGridSnapping: () => false,
getDrawing: () => ({ getNextId: () => '1' })
})
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', '200')
rect.setAttribute('y', '150')
rect.setAttribute('width', '250')
rect.setAttribute('height', '120')
svg.append(rect)
const translateMatrix = svg.createSVGMatrix()
translateMatrix.e = 100
translateMatrix.f = -50
coords.remapElement(
rect,
{ x: '200', y: '150', width: '125', height: '75' },
translateMatrix
)
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
circle.setAttribute('cx', '200')
circle.setAttribute('cy', '150')
circle.setAttribute('r', '250')
svg.append(circle)
const scaleMatrix = svg.createSVGMatrix()
scaleMatrix.a = 2
scaleMatrix.d = 0.5
coords.remapElement(
circle,
{ cx: '200', cy: '150', r: '250' },
scaleMatrix
)
return {
rect: {
x: rect.getAttribute('x'),
y: rect.getAttribute('y'),
width: rect.getAttribute('width'),
height: rect.getAttribute('height')
},
circle: {
cx: circle.getAttribute('cx'),
cy: circle.getAttribute('cy'),
r: circle.getAttribute('r')
}
}
})
expect(result.rect).toEqual({ x: '300', y: '100', width: '125', height: '75' })
expect(result.circle).toEqual({ cx: '400', cy: '75', r: '125' })
})
})

View File

@@ -0,0 +1,207 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core history and draw', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('UndoManager tracks stacks and command texts through undo/redo', async ({ page }) => {
const result = await page.evaluate(() => {
const { history } = window.svgHarness
let lastCalled = ''
class MockCommand extends history.Command {
constructor (text) {
super()
this.text = text
}
apply () { lastCalled = `${this.text}:apply` }
unapply () { lastCalled = `${this.text}:unapply` }
elements () { return [] }
}
const um = new history.UndoManager()
;['First', 'Second', 'Third'].forEach((label) => {
um.addCommandToHistory(new MockCommand(label))
})
const beforeUndo = {
undo: um.getUndoStackSize(),
redo: um.getRedoStackSize(),
nextUndo: um.getNextUndoCommandText(),
nextRedo: um.getNextRedoCommandText()
}
um.undo()
const afterFirstUndo = {
undo: um.getUndoStackSize(),
redo: um.getRedoStackSize(),
nextUndo: um.getNextUndoCommandText(),
nextRedo: um.getNextRedoCommandText(),
lastCalled
}
um.undo()
um.redo()
const afterRedo = {
undo: um.getUndoStackSize(),
redo: um.getRedoStackSize(),
nextUndo: um.getNextUndoCommandText(),
nextRedo: um.getNextRedoCommandText(),
lastCalled
}
return { beforeUndo, afterFirstUndo, afterRedo }
})
expect(result.beforeUndo).toEqual({
undo: 3,
redo: 0,
nextUndo: 'Third',
nextRedo: ''
})
expect(result.afterFirstUndo.undo).toBe(2)
expect(result.afterFirstUndo.redo).toBe(1)
expect(result.afterFirstUndo.nextUndo).toBe('Second')
expect(result.afterFirstUndo.nextRedo).toBe('Third')
expect(result.afterFirstUndo.lastCalled).toBe('Third:unapply')
expect(result.afterRedo.undo).toBe(2)
expect(result.afterRedo.redo).toBe(1)
expect(result.afterRedo.nextUndo).toBe('Second')
expect(result.afterRedo.nextRedo).toBe('Third')
expect(result.afterRedo.lastCalled).toBe('Second:apply')
})
test('history commands move, insert and remove elements in the DOM', async ({ page }) => {
const result = await page.evaluate(() => {
const { history } = window.svgHarness
const makeParent = () => {
const parent = document.createElement('div')
parent.id = 'parent'
const children = ['div1', 'div2', 'div3'].map((id) => {
const el = document.createElement('div')
el.id = id
parent.append(el)
return el
})
document.body.append(parent)
return { parent, children }
}
const order = (parent) => [...parent.children].map((el) => el.id)
const { parent: parentMove, children: moveChildren } = makeParent()
const move = new history.MoveElementCommand(
moveChildren[2],
moveChildren[0],
parentMove
)
move.unapply()
const orderAfterMoveUnapply = order(parentMove)
move.apply()
const orderAfterMoveApply = order(parentMove)
const { parent: parentInsert, children: insertChildren } = makeParent()
const insert = new history.InsertElementCommand(insertChildren[2])
insert.unapply()
const orderAfterInsertUnapply = order(parentInsert)
insert.apply()
const orderAfterInsertApply = order(parentInsert)
const { parent: parentRemove } = makeParent()
const extra = document.createElement('div')
extra.id = 'div4'
const remove = new history.RemoveElementCommand(extra, null, parentRemove)
remove.unapply()
const orderAfterRemoveUnapply = order(parentRemove)
remove.apply()
const orderAfterRemoveApply = order(parentRemove)
return {
orderAfterMoveUnapply,
orderAfterMoveApply,
orderAfterInsertUnapply,
orderAfterInsertApply,
orderAfterRemoveUnapply,
orderAfterRemoveApply
}
})
expect(result.orderAfterMoveUnapply).toEqual(['div3', 'div1', 'div2'])
expect(result.orderAfterMoveApply).toEqual(['div1', 'div2', 'div3'])
expect(result.orderAfterInsertUnapply).toEqual(['div1', 'div2'])
expect(result.orderAfterInsertApply).toEqual(['div1', 'div2', 'div3'])
expect(result.orderAfterRemoveUnapply).toEqual(['div1', 'div2', 'div3', 'div4'])
expect(result.orderAfterRemoveApply).toEqual(['div1', 'div2', 'div3'])
})
test('BatchCommand applies and unapplies subcommands in order', async ({ page }) => {
const result = await page.evaluate(() => {
const { history } = window.svgHarness
let record = ''
class TextCommand extends history.Command {
constructor (text) {
super()
this.text = text
}
apply () { record += this.text }
unapply () { record += this.text.toUpperCase() }
elements () { return [] }
}
const batch = new history.BatchCommand()
const emptyBefore = batch.isEmpty()
batch.addSubCommand(new TextCommand('a'))
batch.addSubCommand(new TextCommand('b'))
batch.addSubCommand(new TextCommand('c'))
batch.apply()
const afterApply = record
record = ''
batch.unapply()
const afterUnapply = record
return { emptyBefore, afterApply, afterUnapply }
})
expect(result.emptyBefore).toBe(true)
expect(result.afterApply).toBe('abc')
expect(result.afterUnapply).toBe('CBA')
})
test('Drawing creates layers, generates ids and toggles nonce randomization', async ({ page }) => {
const result = await page.evaluate(() => {
const { draw, namespaces } = window.svgHarness
const svg = document.createElementNS(namespaces.NS.SVG, 'svg')
document.body.append(svg)
const drawing = new draw.Drawing(svg)
const beforeIdentify = drawing.getNumLayers()
drawing.identifyLayers()
const defaultLayer = drawing.getCurrentLayer()
const defaultName = drawing.getCurrentLayerName()
const hrCounts = { start: 0, end: 0, insert: 0 }
const newLayer = drawing.createLayer('Layer A', {
startBatchCommand: () => { hrCounts.start++ },
endBatchCommand: () => { hrCounts.end++ },
insertElement: () => { hrCounts.insert++ }
})
const afterCreate = {
num: drawing.getNumLayers(),
currentName: drawing.getCurrentLayerName(),
className: newLayer.getAttribute('class')
}
draw.randomizeIds(true, drawing)
const nonceAfterRandomize = drawing.getNonce()
draw.randomizeIds(false, drawing)
const nonceAfterClear = drawing.getNonce()
return {
beforeIdentify,
defaultName,
defaultLayerClass: defaultLayer?.getAttribute('class'),
afterCreate,
hrCounts,
nonceAfterRandomize,
nonceAfterClear
}
})
expect(result.beforeIdentify).toBe(0)
expect(result.defaultLayerClass).toBeDefined()
expect(result.defaultName.length).toBeGreaterThan(0)
expect(result.afterCreate.num).toBe(2)
expect(result.afterCreate.currentName).toBe('Layer A')
expect(result.afterCreate.className).toBe(result.defaultLayerClass)
expect(result.hrCounts).toEqual({ start: 1, end: 1, insert: 1 })
expect(result.nonceAfterRandomize).toBeTruthy()
expect(result.nonceAfterClear).toBe('')
})
})

View File

@@ -0,0 +1,46 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core history/draw smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('UndoManager push/undo/redo stack sizes update', async ({ page }) => {
const stacks = await page.evaluate(() => {
const { history } = window.svgHarness
class DummyCommand extends history.Command {
constructor (text) {
super()
this.text = text
}
apply () {}
unapply () {}
}
const um = new history.UndoManager()
um.addCommandToHistory(new DummyCommand('one'))
um.addCommandToHistory(new DummyCommand('two'))
const beforeUndo = { undo: um.getUndoStackSize(), redo: um.getRedoStackSize() }
um.undo()
const afterUndo = { undo: um.getUndoStackSize(), redo: um.getRedoStackSize() }
um.redo()
const afterRedo = { undo: um.getUndoStackSize(), redo: um.getRedoStackSize() }
return { beforeUndo, afterUndo, afterRedo, nextUndo: um.getNextUndoCommandText(), nextRedo: um.getNextRedoCommandText() }
})
expect(stacks.beforeUndo.undo).toBe(2)
expect(stacks.beforeUndo.redo).toBe(0)
expect(stacks.afterUndo.undo).toBe(1)
expect(stacks.afterUndo.redo).toBe(1)
expect(stacks.afterRedo.undo).toBe(2)
expect(stacks.nextUndo.length).toBeGreaterThan(0)
})
test('draw module exports expected functions', async ({ page }) => {
const exports = await page.evaluate(() => {
const { draw } = window.svgHarness
return ['init', 'randomizeIds', 'createLayer'].map(fn => typeof draw[fn] === 'function')
})
exports.forEach(v => expect(v).toBe(true))
})
})

View File

@@ -0,0 +1,28 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG namespace helpers', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness?.namespaces))
})
test('reverse namespace map includes core URIs', async ({ page }) => {
const result = await page.evaluate(() => {
const { namespaces } = window.svgHarness
const reverse = namespaces.getReverseNS()
return {
svg: namespaces.NS.SVG,
html: namespaces.NS.HTML,
xml: namespaces.NS.XML,
reverseSvg: reverse[namespaces.NS.SVG],
reverseXmlns: reverse[namespaces.NS.XMLNS]
}
})
expect(result.svg).toBe('http://www.w3.org/2000/svg')
expect(result.html).toBe('http://www.w3.org/1999/xhtml')
expect(result.xml).toBe('http://www.w3.org/XML/1998/namespace')
expect(result.reverseSvg).toBe('svg')
expect(result.reverseXmlns).toBe('xmlns')
})
})

View File

@@ -0,0 +1,37 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core path extras', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness?.pathModule))
})
test('convertPath handles arcs and shorthand commands', async ({ page }) => {
const result = await page.evaluate(() => {
const { pathModule, units } = window.svgHarness
// Ensure unit helpers are initialized so shortFloat can round numbers.
units.init({
getRoundDigits: () => 3,
getBaseUnit: () => 'px',
getElement: () => null,
getHeight: () => 100,
getWidth: () => 100
})
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute(
'd',
'M0 0 H10 V10 h-5 v-5 a5 5 0 0 1 5 5 S20 20 25 25 Z'
)
const rel = pathModule.convertPath(path, true)
const abs = pathModule.convertPath(path, false)
return { rel, abs }
})
expect(result.rel.toLowerCase()).toContain('a')
expect(result.rel).toContain('s')
expect(result.abs).toContain('L')
expect(result.abs).toContain('A')
})
})

View File

@@ -0,0 +1,307 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core recalculate extra cases', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('scales elements and flips gradients/matrices', async ({ page }) => {
const result = await page.evaluate(() => {
const { utilities, coords, recalculate } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs')
const grad = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient')
grad.id = 'grad1'
grad.setAttribute('x1', '0')
grad.setAttribute('x2', '1')
grad.setAttribute('y1', '0')
grad.setAttribute('y2', '0')
defs.append(grad)
svg.append(defs)
document.body.append(svg)
const dataStorage = {
store: new WeakMap(),
put (el, key, value) {
if (!this.store.has(el)) this.store.set(el, new Map())
this.store.get(el).set(key, value)
},
get (el, key) {
return this.store.get(el)?.get(key)
},
has (el, key) {
return this.store.has(el) && this.store.get(el).has(key)
},
remove (el, key) {
const bucket = this.store.get(el)
if (!bucket) return false
const deleted = bucket.delete(key)
if (!bucket.size) this.store.delete(el)
return deleted
}
}
const canvasStub = {
getSvgRoot: () => svg,
getStartTransform: () => '',
setStartTransform: () => {},
getDataStorage: () => dataStorage,
getCurrentDrawing: () => ({ getNextId: () => 'g1' })
}
utilities.init({
getSvgRoot: () => svg,
getDOMDocument: () => document,
getDOMContainer: () => svg,
getDataStorage: () => dataStorage
})
coords.init({
getGridSnapping: () => false,
getDrawing: () => ({ getNextId: () => 'id2' }),
getDataStorage: () => dataStorage
})
recalculate.init(canvasStub)
// Scale about center via translate/scale/translate sequence
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', '5')
rect.setAttribute('y', '6')
rect.setAttribute('width', '10')
rect.setAttribute('height', '8')
rect.setAttribute('transform', 'translate(5 5) scale(2 3) translate(-5 -5)')
svg.append(rect)
// Flip with gradient fill using matrix
const rectFlip = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rectFlip.setAttribute('x', '0')
rectFlip.setAttribute('y', '0')
rectFlip.setAttribute('width', '4')
rectFlip.setAttribute('height', '4')
rectFlip.setAttribute('fill', 'url(#grad1)')
rectFlip.setAttribute('transform', 'matrix(-1 0 0 1 0 0)')
svg.append(rectFlip)
recalculate.recalculateDimensions(rect)
recalculate.recalculateDimensions(rectFlip)
return {
rect: {
width: rect.getAttribute('width'),
height: rect.getAttribute('height'),
transformRemoved: rect.hasAttribute('transform')
},
flip: {
width: rectFlip.getAttribute('width'),
height: rectFlip.getAttribute('height'),
fill: rectFlip.getAttribute('fill')
}
}
})
expect(Number(result.rect.width)).toBeGreaterThan(10)
expect(Number(result.rect.height)).toBeGreaterThan(8)
expect(result.rect.transformRemoved).toBe(false) // scaling keeps transform list
expect(result.flip.fill.startsWith('url(')).toBe(true)
})
test('recalculateDimensions reapplies rotations and updates clip paths', async ({ page }) => {
const result = await page.evaluate(() => {
const NS = 'http://www.w3.org/2000/svg'
const { utilities, coords, recalculate } = window.svgHarness
const svg = document.createElementNS(NS, 'svg')
const defs = document.createElementNS(NS, 'defs')
svg.append(defs)
document.body.append(svg)
const dataStorage = {
store: new WeakMap(),
put (el, key, value) {
if (!this.store.has(el)) this.store.set(el, new Map())
this.store.get(el).set(key, value)
},
get (el, key) {
return this.store.get(el)?.get(key)
},
has (el, key) {
return this.store.has(el) && this.store.get(el).has(key)
}
}
const drawing = {
next: 0,
getNextId () {
this.next += 1
return 'd' + this.next
}
}
const canvasStub = {
getSvgRoot: () => svg,
getSvgContent: () => svg,
getDOMDocument: () => document,
getDOMContainer: () => svg,
getDataStorage: () => dataStorage,
getStartTransform: () => '',
setStartTransform: () => {},
getCurrentDrawing: () => drawing
}
utilities.init(canvasStub)
coords.init({
getGridSnapping: () => false,
getDrawing: () => drawing,
getDataStorage: () => dataStorage,
getCurrentDrawing: () => drawing,
getSvgRoot: () => svg
})
recalculate.init(canvasStub)
const rect = document.createElementNS(NS, 'rect')
rect.setAttribute('x', '0')
rect.setAttribute('y', '0')
rect.setAttribute('width', '10')
rect.setAttribute('height', '10')
rect.setAttribute('transform', 'translate(10 5) rotate(30)')
svg.append(rect)
const clipPath = document.createElementNS(NS, 'clipPath')
clipPath.id = 'clip1'
const clipRect = document.createElementNS(NS, 'rect')
clipRect.setAttribute('x', '0')
clipRect.setAttribute('y', '0')
clipRect.setAttribute('width', '4')
clipRect.setAttribute('height', '4')
clipPath.append(clipRect)
defs.append(clipPath)
const cmd = recalculate.recalculateDimensions(rect)
recalculate.updateClipPath('url(#clip1)', 3, -2)
return {
rect: {
x: rect.getAttribute('x'),
y: rect.getAttribute('y'),
transform: rect.getAttribute('transform'),
hasCommand: Boolean(cmd)
},
clip: {
x: clipRect.getAttribute('x'),
y: clipRect.getAttribute('y'),
transforms: clipRect.transform.baseVal.numberOfItems
}
}
})
expect(result.rect.x).toBe('10')
expect(result.rect.y).toBe('5')
expect(result.rect.transform).toContain('rotate(')
expect(result.rect.transform).not.toContain('translate')
expect(result.rect.hasCommand).toBe(true)
expect(result.clip.x).toBe('3')
expect(result.clip.y).toBe('-2')
expect(result.clip.transforms).toBe(0)
})
test('recalculateDimensions remaps polygons and matrix transforms', async ({ page }) => {
const result = await page.evaluate(() => {
const NS = 'http://www.w3.org/2000/svg'
const { utilities, coords, recalculate } = window.svgHarness
const svg = document.createElementNS(NS, 'svg')
document.body.append(svg)
const dataStorage = {
store: new WeakMap(),
put (el, key, value) {
if (!this.store.has(el)) this.store.set(el, new Map())
this.store.get(el).set(key, value)
},
get (el, key) {
return this.store.get(el)?.get(key)
},
has (el, key) {
return this.store.has(el) && this.store.get(el).has(key)
}
}
const drawing = {
next: 0,
getNextId () {
this.next += 1
return 'p' + this.next
}
}
const canvasStub = {
getSvgRoot: () => svg,
getSvgContent: () => svg,
getDOMDocument: () => document,
getDOMContainer: () => svg,
getDataStorage: () => dataStorage,
getStartTransform: () => '',
setStartTransform: () => {},
getCurrentDrawing: () => drawing
}
utilities.init(canvasStub)
coords.init({
getGridSnapping: () => false,
getDrawing: () => drawing,
getDataStorage: () => dataStorage,
getCurrentDrawing: () => drawing,
getSvgRoot: () => svg
})
recalculate.init(canvasStub)
const poly = document.createElementNS(NS, 'polygon')
poly.setAttribute('points', '0,0 10,0 10,10')
poly.setAttribute('transform', 'translate(5 5) scale(-1 2) translate(-5 -5)')
svg.append(poly)
const path = document.createElementNS(NS, 'path')
path.setAttribute('d', 'M0 0 L1 0 L1 1 z')
path.setAttribute('transform', 'matrix(1 0 0 1 7 8)')
svg.append(path)
const rect = document.createElementNS(NS, 'rect')
rect.setAttribute('x', '1')
rect.setAttribute('y', '2')
rect.setAttribute('width', '5')
rect.setAttribute('height', '4')
rect.setAttribute('transform', 'matrix(1 0 0 1 7 8)')
svg.append(rect)
const useElem = document.createElementNS(NS, 'use')
useElem.setAttribute('href', '#missing')
useElem.setAttribute('transform', 'translate(3 4)')
svg.append(useElem)
const cmdPoly = recalculate.recalculateDimensions(poly)
const cmdPath = recalculate.recalculateDimensions(path)
recalculate.recalculateDimensions(rect)
const cmdUse = recalculate.recalculateDimensions(useElem)
return {
poly: {
points: poly.getAttribute('points'),
hasTransform: poly.hasAttribute('transform'),
hasCommand: Boolean(cmdPoly)
},
path: {
d: path.getAttribute('d'),
transform: path.getAttribute('transform') || '',
hasCommand: Boolean(cmdPath)
},
rect: {
x: rect.getAttribute('x'),
transform: rect.getAttribute('transform')
},
useResult: cmdUse
}
})
expect(result.poly.hasTransform).toBe(false)
expect(result.poly.points).toContain('-5')
expect(result.poly.hasCommand).toBe(true)
expect(result.path.d.startsWith('M7,8')).toBe(true)
expect(result.path.transform).toBe('')
expect(result.path.hasCommand).toBe(true)
expect(result.rect.x).toBe('1')
expect(result.rect.transform).toContain('matrix')
expect(result.useResult).toBeNull()
})
})

View File

@@ -0,0 +1,115 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core recalculate', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('recalculateDimensions swallows identity and applies translations', async ({ page }) => {
const result = await page.evaluate(() => {
const { utilities, coords, recalculate } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
document.body.append(svg)
const dataStorage = {
store: new WeakMap(),
put (el, key, value) {
if (!this.store.has(el)) this.store.set(el, new Map())
this.store.get(el).set(key, value)
},
get (el, key) {
return this.store.get(el)?.get(key)
},
has (el, key) {
return this.store.has(el) && this.store.get(el).has(key)
},
remove (el, key) {
const bucket = this.store.get(el)
if (!bucket) return false
const deleted = bucket.delete(key)
if (!bucket.size) this.store.delete(el)
return deleted
}
}
const initContexts = () => {
utilities.init({
getSvgRoot: () => svg,
getDOMDocument: () => document,
getDOMContainer: () => svg,
getDataStorage: () => dataStorage
})
coords.init({
getGridSnapping: () => false,
getDrawing: () => ({ getNextId: () => '1' }),
getDataStorage: () => dataStorage
})
recalculate.init({
getSvgRoot: () => svg,
getStartTransform: () => '',
setStartTransform: () => {},
getDataStorage: () => dataStorage
})
}
initContexts()
const identityRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
identityRect.setAttribute('x', '10')
identityRect.setAttribute('y', '10')
identityRect.setAttribute('width', '20')
identityRect.setAttribute('height', '30')
identityRect.setAttribute('transform', 'matrix(1,0,0,1,0,0)')
svg.append(identityRect)
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', '200')
rect.setAttribute('y', '150')
rect.setAttribute('width', '250')
rect.setAttribute('height', '120')
rect.setAttribute('transform', 'translate(100,50)')
svg.append(rect)
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text')
text.setAttribute('x', '200')
text.setAttribute('y', '150')
text.setAttribute('transform', 'translate(100,50)')
const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan')
tspan.setAttribute('x', '200')
tspan.setAttribute('y', '150')
tspan.textContent = 'Foo bar'
text.append(tspan)
svg.append(text)
recalculate.recalculateDimensions(identityRect)
recalculate.recalculateDimensions(rect)
recalculate.recalculateDimensions(text)
return {
identityHasTransform: identityRect.hasAttribute('transform'),
rectAttrs: {
x: rect.getAttribute('x'),
y: rect.getAttribute('y'),
width: rect.getAttribute('width'),
height: rect.getAttribute('height')
},
textAttrs: {
x: text.getAttribute('x'),
y: text.getAttribute('y')
},
tspanAttrs: {
x: tspan.getAttribute('x'),
y: tspan.getAttribute('y')
}
}
})
expect(result.identityHasTransform).toBe(false)
expect(result.rectAttrs).toEqual({
x: '300',
y: '200',
width: '250',
height: '120'
})
expect(result.textAttrs).toEqual({ x: '300', y: '200' })
expect(result.tspanAttrs).toEqual({ x: '300', y: '200' })
})
})

View File

@@ -0,0 +1,140 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core remap extras', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('remapElement handles gradients, text/tspan and paths with snapping', async ({ page }) => {
const result = await page.evaluate(() => {
const NS = 'http://www.w3.org/2000/svg'
const { coords, utilities, units } = window.svgHarness
const svg = document.createElementNS(NS, 'svg')
const defs = document.createElementNS(NS, 'defs')
const grad = document.createElementNS(NS, 'linearGradient')
grad.id = 'grad1'
grad.setAttribute('x1', '0')
grad.setAttribute('x2', '1')
grad.setAttribute('y1', '0')
grad.setAttribute('y2', '0')
defs.append(grad)
svg.append(defs)
document.body.append(svg)
const dataStorage = {
store: new WeakMap(),
put (el, key, value) {
if (!this.store.has(el)) this.store.set(el, new Map())
this.store.get(el).set(key, value)
},
get (el, key) {
return this.store.get(el)?.get(key)
},
has (el, key) {
return this.store.has(el) && this.store.get(el).has(key)
}
}
let idCounter = 0
const canvas = {
getSvgRoot: () => svg,
getSvgContent: () => svg,
getDOMDocument: () => document,
getDOMContainer: () => svg,
getBaseUnit: () => 'px',
getSnappingStep: () => 5,
getGridSnapping: () => true,
getWidth: () => 200,
getHeight: () => 200,
getCurrentDrawing: () => ({
getNextId: () => 'g' + (++idCounter)
}),
getDataStorage: () => dataStorage
}
utilities.init(canvas)
units.init(canvas)
coords.init(canvas)
const group = document.createElementNS(NS, 'g')
svg.append(group)
const text = document.createElementNS(NS, 'text')
text.textContent = 'hello'
text.setAttribute('x', '2')
text.setAttribute('y', '3')
text.setAttribute('font-size', '10')
const tspan = document.createElementNS(NS, 'tspan')
tspan.setAttribute('x', '4')
tspan.setAttribute('y', '5')
tspan.setAttribute('font-size', '8')
tspan.textContent = 't'
text.append(tspan)
group.append(text)
const textMatrix = svg.createSVGMatrix()
textMatrix.a = -2
textMatrix.d = 1.5
textMatrix.e = 10
coords.remapElement(text, { x: 2, y: 3 }, textMatrix)
const rect = document.createElementNS(NS, 'rect')
rect.setAttribute('x', '0')
rect.setAttribute('y', '0')
rect.setAttribute('width', '10')
rect.setAttribute('height', '6')
rect.setAttribute('fill', 'url(#grad1)')
group.append(rect)
const flipMatrix = svg.createSVGMatrix()
flipMatrix.a = -1
flipMatrix.d = -1
coords.remapElement(rect, { x: 0, y: 0, width: 10, height: 6 }, flipMatrix)
const path = document.createElementNS(NS, 'path')
path.setAttribute('d', 'M0 0 L5 0 l5 5 a2 3 0 0 1 2 2 z')
group.append(path)
const pathMatrix = svg.createSVGMatrix()
pathMatrix.a = 1
pathMatrix.d = 2
pathMatrix.e = 3
pathMatrix.f = -1
coords.remapElement(path, {}, pathMatrix)
return {
text: {
x: text.getAttribute('x'),
y: text.getAttribute('y'),
fontSize: text.getAttribute('font-size'),
tspanX: tspan.getAttribute('x'),
tspanY: tspan.getAttribute('y'),
tspanSize: tspan.getAttribute('font-size')
},
rect: {
fill: rect.getAttribute('fill'),
width: rect.getAttribute('width'),
height: rect.getAttribute('height'),
x: rect.getAttribute('x'),
y: rect.getAttribute('y')
},
path: path.getAttribute('d')
}
})
expect(result.text).toEqual({
x: '5',
y: '5',
fontSize: '20',
tspanX: '2',
tspanY: '7.5',
tspanSize: '16'
})
expect(result.rect.fill).toContain('url(#g')
expect(result.rect.width).toBe('10')
expect(result.rect.height).toBe('5')
expect(result.rect.x).toBe('-10')
expect(result.rect.y).toBe('-5')
expect(result.path.startsWith('M3,')).toBe(true)
expect(result.path).toContain('a2,6')
})
})

View File

@@ -0,0 +1,86 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('math basics work with real SVG matrices', async ({ page }) => {
const result = await page.evaluate(() => {
const { math } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const m = svg.createSVGMatrix().translate(50, 75)
const pt = math.transformPoint(10, 20, m)
const isId = math.isIdentity(svg.createSVGMatrix())
const box = math.transformBox(0, 0, 10, 20, m)
return {
pt,
isId,
box: {
x: box.aabox.x,
y: box.aabox.y,
width: box.aabox.width,
height: box.aabox.height
}
}
})
expect(result.isId).toBe(true)
expect(result.pt.x).toBe(60)
expect(result.pt.y).toBe(95)
expect(result.box).toEqual({ x: 50, y: 75, width: 10, height: 20 })
})
test('coords module exposes remapElement', async ({ page }) => {
const hasRemap = await page.evaluate(() => {
return typeof window.svgHarness.coords.remapElement === 'function'
})
expect(hasRemap).toBe(true)
})
test('path.convertPath converts to relative without throwing', async ({ page }) => {
const d = await page.evaluate(() => {
const { pathModule, units } = window.svgHarness
units.init({
getRoundDigits: () => 2,
getBaseUnit: () => 'px'
})
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('d', 'M0 0 L10 0 L10 10 Z')
svg.append(path)
pathModule.convertPath(path, true)
return path.getAttribute('d')
})
expect(d?.toLowerCase()).toContain('m')
expect(d?.toLowerCase()).toContain('z')
})
test('utilities getBBoxFromPath returns finite numbers', async ({ page }) => {
const bbox = await page.evaluate(() => {
const { utilities } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', '0')
rect.setAttribute('y', '1')
rect.setAttribute('width', '5')
rect.setAttribute('height', '10')
svg.append(rect)
const res = utilities.getBBoxOfElementAsPath(
rect,
(json) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', json.element)
Object.entries(json.attr).forEach(([k, v]) => el.setAttribute(k, v))
svg.append(el)
return el
},
{ resetOrientation: () => {} }
)
return { x: res.x, y: res.y, width: res.width, height: res.height }
})
expect(Number.isFinite(bbox.x)).toBe(true)
expect(Number.isFinite(bbox.y)).toBe(true)
expect(Number.isFinite(bbox.width)).toBe(true)
expect(Number.isFinite(bbox.height)).toBe(true)
})
})

View File

@@ -0,0 +1,83 @@
import { test, expect } from '../fixtures.js'
test.describe('touch event adapter', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('translates single touch events into mouse events', async ({ page }) => {
const result = await page.evaluate(() => {
const { touch } = window.svgHarness
const target = document.createElement('div')
document.body.append(target)
const received = []
target.addEventListener('mousedown', (ev) => {
received.push({
type: ev.type,
clientX: ev.clientX,
clientY: ev.clientY,
screenX: ev.screenX,
screenY: ev.screenY
})
})
const svgroot = {
listeners: {},
addEventListener (type, handler) { this.listeners[type] = handler },
dispatchEvent (ev) { this.listeners[ev.type]?.(ev) }
}
touch.init({ svgroot })
const ev = new TouchEvent('touchstart', {
changedTouches: [
new Touch({
identifier: 1,
target,
clientX: 12,
clientY: 34,
screenX: 56,
screenY: 78
})
]
})
svgroot.dispatchEvent(ev)
return received[0]
})
expect(result.type).toBe('mousedown')
expect(result.clientX).toBe(12)
expect(result.clientY).toBe(34)
expect(result.screenX).toBe(56)
expect(result.screenY).toBe(78)
})
test('ignores multi-touch gestures when forwarding', async ({ page }) => {
const count = await page.evaluate(() => {
const { touch } = window.svgHarness
const target = document.createElement('div')
document.body.append(target)
let mouseEvents = 0
target.addEventListener('mousedown', () => { mouseEvents++ })
const svgroot = {
listeners: {},
addEventListener (type, handler) { this.listeners[type] = handler },
dispatchEvent (ev) { this.listeners[ev.type]?.(ev) }
}
touch.init({ svgroot })
const ev = new TouchEvent('touchstart', {
changedTouches: [
new Touch({ identifier: 1, target, clientX: 1, clientY: 2, screenX: 3, screenY: 4 }),
new Touch({ identifier: 2, target, clientX: 5, clientY: 6, screenX: 7, screenY: 8 })
]
})
svgroot.dispatchEvent(ev)
return mouseEvents
})
expect(count).toBe(0)
})
})

View File

@@ -0,0 +1,98 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG common util helpers', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('computes positions and deep merges objects', async ({ page }) => {
const result = await page.evaluate(() => {
const { util } = window.svgHarness
const grand = { offsetLeft: 5, offsetTop: 6, offsetParent: null }
const parent = { offsetLeft: 10, offsetTop: 11, offsetParent: grand }
const child = { offsetLeft: 7, offsetTop: 8, offsetParent: parent }
const merged = util.mergeDeep(
{ a: 1, nested: { keep: true, replace: 'old' } },
{ nested: { replace: 'new', extra: 42 }, more: 'yes' }
)
return {
pos: util.findPos(child),
isObject: util.isObject({ hello: 'world' }),
merged
}
})
expect(result.pos).toEqual({ left: 22, top: 25 })
expect(result.isObject).toBe(true)
expect(result.merged).toEqual({
a: 1,
nested: { keep: true, replace: 'new', extra: 42 },
more: 'yes'
})
})
test('finds closest ancestors by selector', async ({ page }) => {
const result = await page.evaluate(() => {
const { util } = window.svgHarness
const root = document.getElementById('root')
root.innerHTML = ''
const wrapper = document.createElement('div')
wrapper.className = 'wrapper'
const section = document.createElement('section')
section.id = 'section'
const child = document.createElement('span')
child.dataset.role = 'target'
section.append(child)
wrapper.append(section)
root.append(wrapper)
return {
byClass: util.getClosest(child, '.wrapper')?.className,
byId: util.getClosest(child, '#section')?.id,
byData: util.getClosest(child, '[data-role=target]')?.dataset.role,
byTag: util.getClosest(child, 'div')?.tagName.toLowerCase()
}
})
expect(result.byClass).toBe('wrapper')
expect(result.byId).toBe('section')
expect(result.byData).toBe('target')
expect(result.byTag).toBe('div')
})
test('gathers parents with and without limits', async ({ page }) => {
const result = await page.evaluate(() => {
const { util } = window.svgHarness
const root = document.getElementById('root')
root.innerHTML = ''
const outer = document.createElement('div')
outer.className = 'outer'
const mid = document.createElement('section')
mid.id = 'mid'
const inner = document.createElement('span')
inner.className = 'inner'
mid.append(inner)
outer.append(mid)
root.append(outer)
return {
all: util.getParents(inner)?.map((el) => el.tagName.toLowerCase()),
byClass: util.getParents(inner, '.outer')?.map((el) => el.className),
untilMid: util.getParentsUntil(inner, '#mid')?.map((el) => el.tagName.toLowerCase()),
untilMidFiltered: util.getParentsUntil(inner, '#mid', '.inner')?.map((el) => el.tagName.toLowerCase())
}
})
expect(result.all).toEqual(['span', 'section', 'div', 'div', 'body', 'html'])
expect(result.byClass).toEqual(['outer'])
expect(result.untilMid).toEqual(['span'])
expect(result.untilMidFiltered).toEqual(['span'])
})
})

View File

@@ -0,0 +1,115 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core utilities extra coverage', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('exercises helper paths and bbox utilities', async ({ page }) => {
const result = await page.evaluate(() => {
try {
const { utilities, namespaces } = window.svgHarness
const root = document.getElementById('root')
root.innerHTML = `
<svg id="svgroot" xmlns="${namespaces.NS.SVG}" width="10" height="10">
<defs id="defs"></defs>
<g id="group">
<rect id="rect" x="1" y="2" width="3" height="4"></rect>
</g>
</svg>
`
const svg = root.querySelector('svg')
const rect = svg.querySelector('#rect')
utilities.init({
getSvgRoot: () => svg,
getDOMDocument: () => document,
getDOMContainer: () => root,
getSvgContent: () => svg,
getSelectedElements: () => [rect],
getBaseUnit: () => 'px',
getSnappingStep: () => 1,
addSVGElementsFromJson: () => null,
pathActions: { convertPath: () => {} }
})
const errors = []
const safe = (fn, fallback = null) => {
try { return fn() } catch (e) { errors.push(e.message); return fallback }
}
const encoded = safe(() => utilities.encodeUTF8('hello'))
const dropped = safe(() => utilities.dropXMLInternalSubset('<!DOCTYPE svg [<!ENTITY a "b">]?><svg/>'), '')
const xmlRefs = safe(() => utilities.convertToXMLReferences('<>"&'), '')
const parsed = safe(() => utilities.text2xml('<svg><g></g></svg>'))
const bboxObj = safe(() => utilities.bboxToObj({ x: 1, y: 2, width: 3, height: 4 }))
const defs = safe(() => utilities.findDefs())
const bbox = safe(() => utilities.getBBox(rect))
const pathD = safe(() => utilities.getPathDFromElement(rect), '')
const extra = safe(() => utilities.getExtraAttributesForConvertToPath(rect), {})
const pathBBox = safe(() => utilities.getBBoxOfElementAsPath(
rect,
({ element, attr }) => {
const node = document.createElementNS(namespaces.NS.SVG, element)
Object.entries(attr).forEach(([key, value]) => node.setAttribute(key, value))
svg.querySelector('#group').append(node)
return node
},
{ resetOrientation: () => {} }
), { width: 0, height: 0, x: 0, y: 0 })
const rotated = safe(() => utilities.getRotationAngleFromTransformList(svg.transform.baseVal, true), 0)
const refElem = safe(() => utilities.getRefElem('#rect'))
const fe = safe(() => utilities.getFeGaussianBlur(svg), null)
const elementById = safe(() => utilities.getElement('rect'))
safe(() => utilities.assignAttributes(elementById, { 'data-test': 'ok' }))
safe(() => utilities.cleanupElement(elementById))
const snapped = safe(() => utilities.snapToGrid(2.6), 0)
const htmlFrag = safe(() => utilities.stringToHTML('<span class="x">hi</span>'))
const insertTarget = document.createElement('div')
safe(() => utilities.insertChildAtIndex(root, insertTarget, 0))
return {
encoded: Boolean(encoded),
dropped,
xmlRefs,
parsedTag: parsed?.documentElement?.tagName?.toLowerCase() || '',
bboxObj,
defsId: defs?.id,
bbox,
pathD,
extraKeys: Object.keys(extra).length,
pathBBox,
rotated,
refId: refElem?.id,
feFound: fe === null,
dataAttr: elementById?.dataset?.test || null,
snapped,
htmlTag: (htmlFrag &&
htmlFrag.firstChild &&
htmlFrag.firstChild.tagName &&
htmlFrag.firstChild.tagName.toLowerCase()) || '',
insertedFirst: root.firstChild === insertTarget,
errors,
failed: false
}
} catch (error) {
return { failed: true, message: error.message, stack: error.stack }
}
})
if (result.failed) {
throw new Error(result.message || 'utilities extra coverage failed')
}
expect(result.dropped.includes('?>')).toBe(true)
expect(result.xmlRefs).toBeTruthy()
expect(result.parsedTag).toBe('svg')
expect(result.bboxObj).toEqual({ x: 1, y: 2, width: 3, height: 4 })
expect(result.bbox).toBeDefined()
expect(result.pathD).toContain('M1')
expect(Number(result.pathBBox?.width ?? 0)).toBeGreaterThanOrEqual(0)
expect(result.dataAttr).toBe('ok')
expect(result.snapped).toBeCloseTo(3)
expect(result.insertedFirst).toBeDefined()
})
})

View File

@@ -0,0 +1,149 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core utilities', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('units.convertAttrs appends base unit', async ({ page }) => {
const attrs = await page.evaluate(() => {
const { units } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', '10')
rect.setAttribute('y', '20')
rect.setAttribute('width', '30')
rect.setAttribute('height', '40')
svg.append(rect)
units.init({
getRoundDigits: () => 2,
getBaseUnit: () => 'cm'
})
units.convertAttrs(rect)
return {
x: rect.getAttribute('x'),
y: rect.getAttribute('y'),
width: rect.getAttribute('width'),
height: rect.getAttribute('height')
}
})
expect(attrs.x?.endsWith('cm')).toBe(true)
expect(attrs.y?.endsWith('cm')).toBe(true)
expect(attrs.width?.endsWith('cm')).toBe(true)
expect(attrs.height?.endsWith('cm')).toBe(true)
})
test('utilities.getStrokedBBox returns finite numbers', async ({ page }) => {
const bbox = await page.evaluate(() => {
const { utilities, units } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('width', '200')
svg.setAttribute('height', '200')
document.body.append(svg)
units.init({
getRoundDigits: () => 2,
getBaseUnit: () => 'px'
})
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', '10')
rect.setAttribute('y', '20')
rect.setAttribute('width', '30')
rect.setAttribute('height', '40')
rect.setAttribute('stroke', '#000')
rect.setAttribute('stroke-width', '10')
svg.append(rect)
const addSvg = (json) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', json.element)
Object.entries(json.attr).forEach(([k, v]) => el.setAttribute(k, v))
svg.append(el)
return el
}
const res = utilities.getStrokedBBox([rect], addSvg, { resetOrientation: () => {} })
return { x: res.x, y: res.y, width: res.width, height: res.height }
})
expect(Number.isFinite(bbox.x)).toBe(true)
expect(Number.isFinite(bbox.y)).toBe(true)
expect(Number.isFinite(bbox.width)).toBe(true)
expect(Number.isFinite(bbox.height)).toBe(true)
})
test('utilities XML and base64 helpers escape and roundtrip correctly', async ({ page }) => {
const result = await page.evaluate(() => {
const { utilities } = window.svgHarness
return {
escaped: utilities.toXml("PB&J '\"<>"),
encoded: utilities.encode64('abcdef'),
decoded: utilities.decode64('MTIzNDU='),
xmlRefs: utilities.convertToXMLReferences('ABC')
}
})
expect(result.escaped).toBe('PB&amp;J &#x27;&quot;&lt;&gt;')
expect(result.encoded).toBe('YWJjZGVm')
expect(result.decoded).toBe('12345')
expect(result.xmlRefs).toBe('ABC')
})
test('utilities.getPathDFromSegments and getPathDFromElement build expected d strings', async ({ page }) => {
const result = await page.evaluate(() => {
const { utilities } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
document.body.append(svg)
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', '0')
rect.setAttribute('y', '1')
rect.setAttribute('width', '5')
rect.setAttribute('height', '10')
svg.append(rect)
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line')
line.setAttribute('x1', '0')
line.setAttribute('y1', '1')
line.setAttribute('x2', '5')
line.setAttribute('y2', '6')
svg.append(line)
const dSegments = utilities.getPathDFromSegments([
['M', [1, 2]],
['Z', []]
])
return {
segments: dSegments.trim(),
rect: utilities.getPathDFromElement(rect),
line: utilities.getPathDFromElement(line)
}
})
expect(result.segments).toBe('M1,2 Z')
expect(result.rect).toBe('M0,1 L5,1 L5,11 L0,11 L0,1 Z')
expect(result.line).toBe('M0,1L5,6')
})
test('utilities.getBBoxOfElementAsPath mirrors element geometry', async ({ page }) => {
const bbox = await page.evaluate(() => {
const { utilities } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
document.body.append(svg)
const create = (tag, attrs) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', tag)
Object.entries(attrs).forEach(([k, v]) => el.setAttribute(k, v))
return el
}
const addSvg = (json) => {
const el = create(json.element, json.attr)
svg.append(el)
return el
}
const pathActions = { resetOrientation: () => {} }
const path = create('path', { id: 'p', d: 'M0,1 Z' })
const rect = create('rect', { id: 'r', x: '0', y: '1', width: '5', height: '10' })
const line = create('line', { id: 'l', x1: '0', y1: '1', x2: '5', y2: '6' })
svg.append(path, rect, line)
return {
path: utilities.bboxToObj(utilities.getBBoxOfElementAsPath(path, addSvg, pathActions)),
rect: utilities.bboxToObj(utilities.getBBoxOfElementAsPath(rect, addSvg, pathActions)),
line: utilities.bboxToObj(utilities.getBBoxOfElementAsPath(line, addSvg, pathActions))
}
})
expect(bbox.path).toEqual({ x: 0, y: 1, width: 0, height: 0 })
expect(bbox.rect).toEqual({ x: 0, y: 1, width: 5, height: 10 })
expect(bbox.line).toEqual({ x: 0, y: 1, width: 5, height: 5 })
})
})

View File

@@ -0,0 +1,158 @@
import { test, expect } from '../fixtures.js'
test.describe('SVG core modules in browser', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/tests/unit-harness.html')
await page.waitForFunction(() => Boolean(window.svgHarness))
})
test('units.convertUnit returns finite and px passthrough', async ({ page }) => {
const result = await page.evaluate(async () => {
const { units } = window.svgHarness
units.init({
getRoundDigits: () => 2,
getBaseUnit: () => 'px'
})
return {
defaultConv: units.convertUnit(42),
pxConv: units.convertUnit(42, 'px')
}
})
expect(result.defaultConv).toBeGreaterThan(0)
expect(result.defaultConv).not.toBe(Infinity)
expect(result.pxConv).toBe(42)
})
test('units.shortFloat and isValidUnit mirror legacy behavior', async ({ page }) => {
const result = await page.evaluate(() => {
const { units } = window.svgHarness
document.body.innerHTML = ''
const unique = document.createElement('div')
unique.id = 'uniqueId'
const other = document.createElement('div')
other.id = 'otherId'
document.body.append(unique, other)
units.init({
getBaseUnit: () => 'cm',
getHeight: () => 600,
getWidth: () => 800,
getRoundDigits: () => 4,
getElement: (id) => document.getElementById(id)
})
return {
shortFloat: [
units.shortFloat(0.00000001),
units.shortFloat(1),
units.shortFloat(3.45678),
units.shortFloat(1.23443),
units.shortFloat(1.23455)
],
validUnits: [
'0',
'1',
'1.1',
'-1.1',
'.6mm',
'-.6cm',
'6000in',
'6px',
'6.3pc',
'-0.4em',
'-0.ex',
'40.123%'
].map((val) => units.isValidUnit(val)),
idChecks: {
okExisting: units.isValidUnit('id', 'uniqueId', unique),
okNew: units.isValidUnit('id', 'newId', unique),
dupNoElem: units.isValidUnit('id', 'uniqueId'),
dupOther: units.isValidUnit('id', 'uniqueId', other)
}
}
})
expect(result.shortFloat).toEqual([0, 1, 3.4568, 1.2344, 1.2346])
result.validUnits.forEach((isValid) => expect(isValid).toBe(true))
expect(result.idChecks.okExisting).toBe(true)
expect(result.idChecks.okNew).toBe(true)
expect(result.idChecks.dupNoElem).toBe(false)
expect(result.idChecks.dupOther).toBe(false)
})
test('utilities.getPathDFromElement on rect', async ({ page }) => {
const pathD = await page.evaluate(() => {
const { utilities } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', '0')
rect.setAttribute('y', '1')
rect.setAttribute('width', '5')
rect.setAttribute('height', '10')
svg.append(rect)
return utilities.getPathDFromElement(rect)
})
expect(pathD?.startsWith('M')).toBe(true)
})
test('utilities.getBBoxOfElementAsPath returns numbers', async ({ page }) => {
const bbox = await page.evaluate(() => {
const { utilities } = window.svgHarness
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
rect.setAttribute('x', '0')
rect.setAttribute('y', '1')
rect.setAttribute('width', '2')
rect.setAttribute('height', '2')
svg.append(rect)
// Minimal mocks for addSVGElementsFromJson and pathActions.resetOrientation
const addSvg = (json) => {
const el = document.createElementNS('http://www.w3.org/2000/svg', json.element)
Object.entries(json.attr).forEach(([k, v]) => el.setAttribute(k, v))
svg.append(el)
return el
}
const pathActions = { resetOrientation: () => {} }
const res = utilities.getBBoxOfElementAsPath(rect, addSvg, pathActions)
return { x: res.x, y: res.y, width: res.width, height: res.height }
})
expect(Number.isFinite(bbox.x)).toBe(true)
expect(Number.isFinite(bbox.y)).toBe(true)
expect(Number.isFinite(bbox.width)).toBe(true)
expect(Number.isFinite(bbox.height)).toBe(true)
})
test('path.convertPath converts absolute to relative', async ({ page }) => {
const dRel = await page.evaluate(() => {
const { pathModule, units } = window.svgHarness
units.init({
getRoundDigits: () => 2,
getBaseUnit: () => 'px'
})
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('d', 'M0 0 L10 0 L10 10 Z')
svg.append(path)
pathModule.convertPath(path, true)
return path.getAttribute('d')
})
expect(dRel?.length > 0).toBe(true)
expect(dRel.toLowerCase()).toContain('z')
})
test('path.convertPath normalizes relative and absolute commands', async ({ page }) => {
const result = await page.evaluate(() => {
const { pathModule, units } = window.svgHarness
units.init({
getRoundDigits: () => 5,
getBaseUnit: () => 'px'
})
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('d', 'm40,55h20v20')
svg.append(path)
const abs = pathModule.convertPath(path)
const rel = pathModule.convertPath(path, true)
return { abs, rel }
})
expect(result.abs).toBe('M40,55L60,55L60,75')
expect(result.rel).toBe('m40,55l20,0l0,20')
})
})