increase test coverage
extend test coverage
This commit is contained in:
61
tests/e2e/unit/svgcore-clear.spec.js
Normal file
61
tests/e2e/unit/svgcore-clear.spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
92
tests/e2e/unit/svgcore-draw-extra.spec.js
Normal file
92
tests/e2e/unit/svgcore-draw-extra.spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
147
tests/e2e/unit/svgcore-drawing.spec.js
Normal file
147
tests/e2e/unit/svgcore-drawing.spec.js
Normal 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('')
|
||||
})
|
||||
})
|
||||
122
tests/e2e/unit/svgcore-geometry.spec.js
Normal file
122
tests/e2e/unit/svgcore-geometry.spec.js
Normal 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' })
|
||||
})
|
||||
})
|
||||
207
tests/e2e/unit/svgcore-history-draw.spec.js
Normal file
207
tests/e2e/unit/svgcore-history-draw.spec.js
Normal 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('')
|
||||
})
|
||||
})
|
||||
46
tests/e2e/unit/svgcore-history.spec.js
Normal file
46
tests/e2e/unit/svgcore-history.spec.js
Normal 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))
|
||||
})
|
||||
})
|
||||
28
tests/e2e/unit/svgcore-namespaces.spec.js
Normal file
28
tests/e2e/unit/svgcore-namespaces.spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
37
tests/e2e/unit/svgcore-path-extra.spec.js
Normal file
37
tests/e2e/unit/svgcore-path-extra.spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
307
tests/e2e/unit/svgcore-recalculate-extra.spec.js
Normal file
307
tests/e2e/unit/svgcore-recalculate-extra.spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
115
tests/e2e/unit/svgcore-recalculate.spec.js
Normal file
115
tests/e2e/unit/svgcore-recalculate.spec.js
Normal 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' })
|
||||
})
|
||||
})
|
||||
140
tests/e2e/unit/svgcore-remap-extra.spec.js
Normal file
140
tests/e2e/unit/svgcore-remap-extra.spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
86
tests/e2e/unit/svgcore-smoke.spec.js
Normal file
86
tests/e2e/unit/svgcore-smoke.spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
83
tests/e2e/unit/svgcore-touch.spec.js
Normal file
83
tests/e2e/unit/svgcore-touch.spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
98
tests/e2e/unit/svgcore-util.spec.js
Normal file
98
tests/e2e/unit/svgcore-util.spec.js
Normal 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'])
|
||||
})
|
||||
})
|
||||
115
tests/e2e/unit/svgcore-utilities-extra.spec.js
Normal file
115
tests/e2e/unit/svgcore-utilities-extra.spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
149
tests/e2e/unit/svgcore-utilities.spec.js
Normal file
149
tests/e2e/unit/svgcore-utilities.spec.js
Normal 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&J '"<>')
|
||||
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 })
|
||||
})
|
||||
})
|
||||
158
tests/e2e/unit/svgcore.spec.js
Normal file
158
tests/e2e/unit/svgcore.spec.js
Normal 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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user