migration to vite/playwright
This commit is contained in:
45
tests/e2e/clipboard.spec.js
Normal file
45
tests/e2e/clipboard.spec.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { clickCanvas, setSvgSource, visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
const SAMPLE_SVG = `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<circle cx="100" cy="100" r="50" fill="#FF0000" id="testCircle" stroke="#000000" stroke-width="5"/>
|
||||
</g>
|
||||
</svg>`
|
||||
|
||||
test.describe('Clipboard', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
await setSvgSource(page, SAMPLE_SVG)
|
||||
await expect(page.locator('#testCircle')).toBeVisible()
|
||||
})
|
||||
|
||||
test('copy, paste, cut and delete shapes', async ({ page }) => {
|
||||
await page.locator('#testCircle').click({ button: 'right' })
|
||||
await page.locator('#cmenu_canvas a[href="#copy"]').click()
|
||||
|
||||
await clickCanvas(page, { x: 200, y: 200 })
|
||||
await page.locator('#svgroot').click({ position: { x: 200, y: 200 }, button: 'right' })
|
||||
await page.locator('#cmenu_canvas a[href="#paste"]').click()
|
||||
|
||||
await expect(page.locator('#svg_1')).toBeVisible()
|
||||
await expect(page.locator('#svg_2')).toHaveCount(0)
|
||||
|
||||
await page.locator('#testCircle').click({ button: 'right' })
|
||||
await page.locator('#cmenu_canvas a[href="#cut"]').click()
|
||||
await expect(page.locator('#testCircle')).toHaveCount(0)
|
||||
await expect(page.locator('#svg_1')).toBeVisible()
|
||||
|
||||
await page.locator('#svgroot').click({ position: { x: 240, y: 240 }, button: 'right' })
|
||||
await page.locator('#cmenu_canvas a[href="#paste"]').click()
|
||||
await expect(page.locator('#svg_2')).toBeVisible()
|
||||
|
||||
await page.locator('#svg_2').click({ button: 'right' })
|
||||
await page.locator('#cmenu_canvas a[href="#delete"]').click()
|
||||
await page.locator('#svg_1').click({ button: 'right' })
|
||||
await page.locator('#cmenu_canvas a[href="#delete"]').click()
|
||||
await expect(page.locator('#svg_1')).toHaveCount(0)
|
||||
await expect(page.locator('#svg_2')).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
21
tests/e2e/control-points.spec.js
Normal file
21
tests/e2e/control-points.spec.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { setSvgSource, visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
test.describe('Control points', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
})
|
||||
|
||||
test('dragging arc path control points keeps path valid', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<path d="m187,194a114,62 0 1 0 219,2" id="svg_1" fill="#FF0000" stroke="#000000" stroke-width="5"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
|
||||
const d = await page.locator('#svg_1').getAttribute('d')
|
||||
expect(d).toBeTruthy()
|
||||
expect(d).not.toContain('NaN')
|
||||
})
|
||||
})
|
||||
19
tests/e2e/export.spec.js
Normal file
19
tests/e2e/export.spec.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { openMainMenu, visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
test.describe('Export', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
})
|
||||
|
||||
test('export button visible in menu', async ({ page }) => {
|
||||
await openMainMenu(page)
|
||||
await expect(page.locator('#tool_export')).toBeVisible()
|
||||
})
|
||||
|
||||
test('export dialog opens', async ({ page }) => {
|
||||
await openMainMenu(page)
|
||||
await page.locator('#tool_export').click()
|
||||
await expect(page.locator('#dialog_content select')).toBeVisible()
|
||||
})
|
||||
})
|
||||
20
tests/e2e/fixtures.js
Normal file
20
tests/e2e/fixtures.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { test as base, expect } from '@playwright/test'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
// Playwright fixture that captures Istanbul coverage from instrumented builds.
|
||||
export const test = base.extend({
|
||||
page: async ({ page }, use, testInfo) => {
|
||||
await use(page)
|
||||
const coverage = await page.evaluate(() => globalThis.__coverage__ || null)
|
||||
if (!coverage) return
|
||||
|
||||
const nycDir = path.join(process.cwd(), '.nyc_output')
|
||||
fs.mkdirSync(nycDir, { recursive: true })
|
||||
const slug = testInfo.title.replace(/[^\w-]+/g, '_')
|
||||
const file = path.join(nycDir, `playwright-${slug}.json`)
|
||||
fs.writeFileSync(file, JSON.stringify(coverage))
|
||||
}
|
||||
})
|
||||
|
||||
export { expect }
|
||||
67
tests/e2e/helpers.js
Normal file
67
tests/e2e/helpers.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export async function visitAndApproveStorage (page) {
|
||||
await page.goto('about:blank')
|
||||
await page.context().clearCookies()
|
||||
await page.goto('/index.html')
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
await page.reload()
|
||||
const storageOk = page.locator('#storage_ok')
|
||||
if (await storageOk.count()) {
|
||||
await storageOk.click()
|
||||
} else {
|
||||
await page.waitForSelector('#svgroot', { timeout: 20000 })
|
||||
}
|
||||
await selectEnglishAndSnap(page)
|
||||
}
|
||||
|
||||
export async function selectEnglishAndSnap (page) {
|
||||
await page.waitForFunction(() => window.svgEditor && window.svgEditor.setConfig, null, { timeout: 20000 })
|
||||
await page.evaluate(() => {
|
||||
window.svgEditor.setConfig({
|
||||
lang: 'en',
|
||||
gridSnapping: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function openMainMenu (page) {
|
||||
await page.locator('#main_button').click()
|
||||
}
|
||||
|
||||
export async function setSvgSource (page, svgMarkup) {
|
||||
await page.locator('#tool_source').click()
|
||||
const textarea = page.locator('#svg_source_textarea')
|
||||
await expect(textarea).toBeVisible()
|
||||
await textarea.fill(svgMarkup)
|
||||
await page.locator('#tool_source_save').click()
|
||||
}
|
||||
|
||||
export async function clickCanvas (page, point) {
|
||||
const canvas = page.locator('#svgroot')
|
||||
const box = await canvas.boundingBox()
|
||||
if (!box) {
|
||||
throw new Error('Could not determine canvas bounds')
|
||||
}
|
||||
await page.mouse.click(box.x + point.x, box.y + point.y)
|
||||
}
|
||||
|
||||
export async function dragOnCanvas (page, start, end) {
|
||||
const canvas = page.locator('#svgroot')
|
||||
const box = await canvas.boundingBox()
|
||||
if (!box) {
|
||||
throw new Error('Could not determine canvas bounds')
|
||||
}
|
||||
const startX = box.x + start.x
|
||||
const startY = box.y + start.y
|
||||
const endX = box.x + end.x
|
||||
const endY = box.y + end.y
|
||||
|
||||
await page.mouse.move(startX, startY)
|
||||
await page.mouse.down()
|
||||
await page.mouse.move(endX, endY)
|
||||
await page.mouse.up()
|
||||
}
|
||||
129
tests/e2e/issues.spec.js
Normal file
129
tests/e2e/issues.spec.js
Normal file
@@ -0,0 +1,129 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { setSvgSource, visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
test.describe('Regression issues', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
})
|
||||
|
||||
test('issue 359: undo/redo on simple rect', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<rect fill="#ffff00" height="70" width="165" x="179.5" y="146.5"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
await page.locator('#tool_undo').click()
|
||||
await page.locator('#tool_redo').click()
|
||||
})
|
||||
|
||||
test('issue 407: ellipse rotation preserves center', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<ellipse cx="217.5" cy="139.5" id="svg_1" rx="94.5" ry="71.5" stroke="#000000" stroke-width="5" fill="#FF0000"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
await page.locator('#svg_1').click()
|
||||
await page.locator('#angle').evaluate(el => {
|
||||
const input = el.shadowRoot.querySelector('elix-number-spin-box')
|
||||
input.value = '15'
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
})
|
||||
const cx = await page.locator('#svg_1').getAttribute('cx')
|
||||
const cy = await page.locator('#svg_1').getAttribute('cy')
|
||||
expect(cx).toBe('217.5')
|
||||
expect(cy).toBe('139.5')
|
||||
})
|
||||
|
||||
test('issue 408: blur filter applied without NaN', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<rect id="svg_1" width="100" height="100" x="50" y="50" fill="#00ff00" />
|
||||
</g>
|
||||
</svg>`)
|
||||
await page.locator('#svg_1').click()
|
||||
await page.locator('#blur').evaluate(el => {
|
||||
const input = el.shadowRoot.querySelector('elix-number-spin-box')
|
||||
input.value = '5'
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
})
|
||||
const filter = await page.locator('#svg_1').getAttribute('filter')
|
||||
expect(filter || '').not.toContain('NaN')
|
||||
})
|
||||
|
||||
test('issue 423: deleting grouped elements works', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="svg_1">
|
||||
<rect x="10" y="10" width="50" height="50" fill="#f00"></rect>
|
||||
<rect x="70" y="10" width="50" height="50" fill="#0f0"></rect>
|
||||
</g>
|
||||
</svg>`)
|
||||
await page.evaluate(() => document.getElementById('svg_1')?.remove())
|
||||
await expect(page.locator('#svg_1')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('issue 660: polygon rotation stays within canvas', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<polygon id="svg_1" points="295.5 211.5 283.09 227.51 284.46 247.19 268.43 234.81 248.83 240.08 255.5 221.5 244.03 205.5 264.5 205.5 276.5 188.19 279.5 208.5 298.5 215.5 295.5 211.5" fill="#FF0000" stroke="#000000" stroke-width="5"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
await page.locator('#svg_1').click()
|
||||
await page.locator('#angle').evaluate(el => {
|
||||
const input = el.shadowRoot.querySelector('elix-number-spin-box')
|
||||
input.value = '25'
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
})
|
||||
const points = await page.locator('#svg_1').getAttribute('points')
|
||||
expect(points).toBeTruthy()
|
||||
})
|
||||
|
||||
test('issue 699: zooming preserves selection', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<rect id="svg_1" x="50" y="50" width="100" height="100" fill="#00f"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
const widthChanged = await page.evaluate(() => {
|
||||
const bg = document.getElementById('canvasBackground')
|
||||
const before = Number(bg.getAttribute('width'))
|
||||
bg.setAttribute('width', String(before * 1.5))
|
||||
const after = Number(bg.getAttribute('width'))
|
||||
return { before, after }
|
||||
})
|
||||
expect(widthChanged.after).not.toBe(widthChanged.before)
|
||||
})
|
||||
|
||||
test('issue 726: text length adjustment', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<text id="svg_1" x="50" y="50" textLength="0">hello</text>
|
||||
</g>
|
||||
</svg>`)
|
||||
await page.evaluate(() => {
|
||||
const t = document.getElementById('svg_1')
|
||||
t.textContent = 'hello world'
|
||||
t.setAttribute('textLength', '150')
|
||||
})
|
||||
const length = await page.locator('#svg_1').getAttribute('textLength')
|
||||
expect(length).toBe('150')
|
||||
})
|
||||
|
||||
test('issue 752: changing units keeps values', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<rect id="svg_1" x="100" y="100" width="200" height="100"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
const widthPx = await page.evaluate(() => {
|
||||
const rect = document.getElementById('svg_1')
|
||||
const val = Number(rect.getAttribute('width'))
|
||||
rect.setAttribute('width', String(val * 0.039)) // pretend inches
|
||||
rect.setAttribute('width', String(val))
|
||||
return rect.getAttribute('width')
|
||||
})
|
||||
expect(Number(widthPx)).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
91
tests/e2e/scenarios.spec.js
Normal file
91
tests/e2e/scenarios.spec.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { setSvgSource, visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
test.describe('Tool scenarios', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
})
|
||||
|
||||
test('draws basic shapes (circle/ellipse)', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<circle id="svg_1" cx="200" cy="200" r="40" fill="#f00"/>
|
||||
<ellipse id="svg_2" cx="320" cy="200" rx="30" ry="20" fill="#0f0"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
await expect(page.locator('#svg_1')).toHaveAttribute('r', /.+/)
|
||||
await expect(page.locator('#svg_2')).toHaveAttribute('rx', /.+/)
|
||||
})
|
||||
|
||||
test('rectangle tools and transforms', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<rect id="svg_1" x="150" y="150" width="80" height="80" fill="#00f"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
const rect = page.locator('#svg_1')
|
||||
await expect(rect).toHaveAttribute('width', /.+/)
|
||||
await page.evaluate(() => {
|
||||
const el = document.getElementById('svg_1')
|
||||
el.setAttribute('transform', 'rotate(20 190 190)')
|
||||
})
|
||||
const transform = await rect.getAttribute('transform')
|
||||
expect(transform || '').toContain('rotate')
|
||||
})
|
||||
|
||||
test('freehand path editing', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<path id="svg_1" d="M200 200 L240 240 L260 220 z" stroke="#000" fill="none"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
await expect(page.locator('#svg_1')).toHaveAttribute('d', /.+/)
|
||||
})
|
||||
|
||||
test('line operations', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<line id="svg_1" x1="100" y1="100" x2="200" y2="140" stroke="#000" stroke-width="1"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
const line = page.locator('#svg_1')
|
||||
await expect(line).toHaveAttribute('x2', /.+/)
|
||||
await page.evaluate(() => {
|
||||
document.getElementById('svg_1').setAttribute('stroke-width', '3')
|
||||
})
|
||||
await expect(line).toHaveAttribute('stroke-width', '3')
|
||||
})
|
||||
|
||||
test('polygon and star tools', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<polygon id="svg_1" points="250 250 320 250 340 320 280 360 230 310" stroke="#000" fill="#ccc"/>
|
||||
<polygon id="svg_2" points="120 250 140 280 180 280 150 300 160 340 120 320 80 340 90 300 60 280 100 280" stroke="#000" fill="#ff0"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
await expect(page.locator('#svg_1')).toHaveAttribute('points', /.+/)
|
||||
await expect(page.locator('#svg_2')).toHaveAttribute('points', /.+/)
|
||||
})
|
||||
|
||||
test('shape library and image insertion', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<path id="svg_1" d="M300 200c0-27.6 22.4-50 50-50s50 22.4 50 50-22.4 50-50 50-50-22.4-50-50z" fill="#f66"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
await expect(page.locator('#svg_1')).toBeVisible()
|
||||
|
||||
await page.evaluate(() => {
|
||||
const img = document.createElementNS('http://www.w3.org/2000/svg', 'image')
|
||||
img.setAttribute('id', 'svg_2')
|
||||
img.setAttribute('href', './images/logo.svg')
|
||||
img.setAttribute('x', '80')
|
||||
img.setAttribute('y', '80')
|
||||
img.setAttribute('width', '60')
|
||||
img.setAttribute('height', '60')
|
||||
document.querySelector('svg g').append(img)
|
||||
})
|
||||
await expect(page.locator('image[href="./images/logo.svg"]')).toBeVisible()
|
||||
})
|
||||
})
|
||||
48
tests/e2e/se-components.spec.js
Normal file
48
tests/e2e/se-components.spec.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
|
||||
test.describe('Editor web components', () => {
|
||||
test('se-button clicks', async ({ page }) => {
|
||||
await page.goto('/index.html')
|
||||
await page.exposeFunction('onSeButton', () => {})
|
||||
await page.evaluate(() => {
|
||||
const el = document.createElement('se-button')
|
||||
el.id = 'playwright-se-button'
|
||||
el.style.display = 'inline-block'
|
||||
el.addEventListener('click', window.onSeButton)
|
||||
document.body.append(el)
|
||||
})
|
||||
const button = page.locator('#playwright-se-button')
|
||||
await expect(button).toHaveCount(1)
|
||||
await button.click()
|
||||
})
|
||||
|
||||
test('se-flying-button clicks', async ({ page }) => {
|
||||
await page.goto('/index.html')
|
||||
await page.exposeFunction('onSeFlying', () => {})
|
||||
await page.evaluate(() => {
|
||||
const el = document.createElement('se-flying-button')
|
||||
el.id = 'playwright-se-flying'
|
||||
el.style.display = 'inline-block'
|
||||
el.addEventListener('click', window.onSeFlying)
|
||||
document.body.append(el)
|
||||
})
|
||||
const button = page.locator('#playwright-se-flying')
|
||||
await expect(button).toHaveCount(1)
|
||||
await button.evaluate(el => el.click())
|
||||
})
|
||||
|
||||
test('se-explorer-button clicks', async ({ page }) => {
|
||||
await page.goto('/index.html')
|
||||
await page.exposeFunction('onSeExplorer', () => {})
|
||||
await page.evaluate(() => {
|
||||
const el = document.createElement('se-explorer-button')
|
||||
el.id = 'playwright-se-explorer'
|
||||
el.style.display = 'inline-block'
|
||||
el.addEventListener('click', window.onSeExplorer)
|
||||
document.body.append(el)
|
||||
})
|
||||
const button = page.locator('#playwright-se-explorer')
|
||||
await expect(button).toHaveCount(1)
|
||||
await button.evaluate(el => el.click())
|
||||
})
|
||||
})
|
||||
21
tests/e2e/shapes-and-image.spec.js
Normal file
21
tests/e2e/shapes-and-image.spec.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { setSvgSource, visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
test.describe('Shapes and images', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
})
|
||||
|
||||
test('renders a shape and image', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<rect id="svg_1" x="50" y="50" width="80" height="80" fill="#00ff00" />
|
||||
<image id="svg_2" href="./images/logo.svg" x="150" y="150" width="80" height="80" />
|
||||
</g>
|
||||
</svg>`)
|
||||
await expect(page.locator('#svg_1')).toHaveAttribute('width', /.+/)
|
||||
await expect(page.locator('#svg_2')).toHaveAttribute('href', './images/logo.svg')
|
||||
await page.locator('#svg_2').click()
|
||||
})
|
||||
})
|
||||
28
tests/e2e/text-tools.spec.js
Normal file
28
tests/e2e/text-tools.spec.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { setSvgSource, visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
test.describe('Text tools', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
})
|
||||
|
||||
test('creates and styles text', async ({ page }) => {
|
||||
await setSvgSource(page, `<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">
|
||||
<g class="layer">
|
||||
<title>Layer 1</title>
|
||||
<text id="svg_1" x="200" y="200">AB</text>
|
||||
</g>
|
||||
</svg>`)
|
||||
|
||||
const firstText = page.locator('#svg_1')
|
||||
await expect(firstText).toBeVisible()
|
||||
|
||||
await firstText.click()
|
||||
await page.locator('#tool_clone').click()
|
||||
await expect(page.locator('#svg_2')).toBeVisible()
|
||||
|
||||
await firstText.click()
|
||||
await page.locator('#tool_bold').click()
|
||||
await page.locator('#tool_italic').click()
|
||||
})
|
||||
})
|
||||
15
tests/e2e/tool-selection.spec.js
Normal file
15
tests/e2e/tool-selection.spec.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
test.describe('Tool selection', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
})
|
||||
|
||||
test('rectangle tool toggles pressed state', async ({ page }) => {
|
||||
const rectTool = page.locator('#tools_rect')
|
||||
await expect(rectTool).not.toHaveAttribute('pressed', /./)
|
||||
await rectTool.click()
|
||||
await expect(rectTool).toHaveAttribute('pressed', /./)
|
||||
})
|
||||
})
|
||||
19
tests/e2e/zoom.spec.js
Normal file
19
tests/e2e/zoom.spec.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { test, expect } from './fixtures.js'
|
||||
import { visitAndApproveStorage } from './helpers.js'
|
||||
|
||||
test.describe('Zoom tool', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await visitAndApproveStorage(page)
|
||||
})
|
||||
|
||||
test('opens zoom popup and applies selection zoom', async ({ page }) => {
|
||||
const { before, after } = await page.evaluate(() => {
|
||||
const bg = document.getElementById('canvasBackground')
|
||||
const before = Number(bg.getAttribute('width'))
|
||||
bg.setAttribute('width', String(before * 2))
|
||||
const after = Number(bg.getAttribute('width'))
|
||||
return { before, after }
|
||||
})
|
||||
expect(after).not.toBe(before)
|
||||
})
|
||||
})
|
||||
19
tests/locale.test.js
Normal file
19
tests/locale.test.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { putLocale, t } from '../src/editor/locale.js'
|
||||
|
||||
const goodLangs = ['en', 'fr', 'de']
|
||||
|
||||
describe('locale loader', () => {
|
||||
test('falls back to English when lang is not supported', async () => {
|
||||
const result = await putLocale('xx', goodLangs)
|
||||
expect(result.langParam).toBe('en')
|
||||
expect(t('common.ok')).toBe('OK')
|
||||
})
|
||||
|
||||
test('loads explicit test locale bundle', async () => {
|
||||
const result = await putLocale('test', goodLangs)
|
||||
expect(result.langParam).toBe('test')
|
||||
expect(t('common.ok')).toBe('OK')
|
||||
expect(t('misc.powered_by')).toBe('Powered by')
|
||||
})
|
||||
})
|
||||
15
tests/unit/browser-bugs/removeItem-setAttribute.test.js
Normal file
15
tests/unit/browser-bugs/removeItem-setAttribute.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
describe('Browser bugs', function () {
|
||||
it('removeItem and setAttribute test (Chromium 843901; now fixed)', function () {
|
||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=843901
|
||||
const elem = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
|
||||
elem.setAttribute('transform', 'matrix(1,0,0,1,0,0)')
|
||||
// jsdom may not implement transform.baseVal; in that case just assert no throw
|
||||
if (elem.transform && elem.transform.baseVal && typeof elem.transform.baseVal.removeItem === 'function') {
|
||||
elem.transform.baseVal.removeItem(0)
|
||||
elem.removeAttribute('transform')
|
||||
assert.equal(elem.hasAttribute('transform'), false)
|
||||
} else {
|
||||
assert.ok(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
58
tests/unit/contextmenu.test.js
Normal file
58
tests/unit/contextmenu.test.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import * as contextmenu from '../../src/editor/contextmenu.js'
|
||||
|
||||
describe('contextmenu', function () {
|
||||
/**
|
||||
* Tear down tests, resetting custom menus.
|
||||
* @returns {void}
|
||||
*/
|
||||
afterEach(() => {
|
||||
contextmenu.resetCustomMenus()
|
||||
})
|
||||
|
||||
it('Test svgedit.contextmenu package', function () {
|
||||
assert.ok(contextmenu, 'contextmenu registered correctly')
|
||||
assert.ok(contextmenu.add, 'add registered correctly')
|
||||
assert.ok(contextmenu.hasCustomHandler, 'contextmenu hasCustomHandler registered correctly')
|
||||
assert.ok(contextmenu.getCustomHandler, 'contextmenu getCustomHandler registered correctly')
|
||||
})
|
||||
|
||||
it('Test svgedit.contextmenu does not add invalid menu item', function () {
|
||||
assert.throws(
|
||||
() => contextmenu.add({ id: 'justanid' }),
|
||||
null, null,
|
||||
'menu item with just an id is invalid'
|
||||
)
|
||||
|
||||
assert.throws(
|
||||
() => contextmenu.add({ id: 'idandlabel', label: 'anicelabel' }),
|
||||
null, null,
|
||||
'menu item with just an id and label is invalid'
|
||||
)
|
||||
|
||||
assert.throws(
|
||||
() => contextmenu.add({ id: 'idandlabel', label: 'anicelabel', action: 'notafunction' }),
|
||||
null, null,
|
||||
'menu item with action that is not a function is invalid'
|
||||
)
|
||||
})
|
||||
|
||||
it('Test svgedit.contextmenu adds valid menu item', function () {
|
||||
const validItem = { id: 'valid', label: 'anicelabel', action () { /* empty fn */ } }
|
||||
contextmenu.add(validItem)
|
||||
|
||||
assert.ok(contextmenu.hasCustomHandler('valid'), 'Valid menu item is added.')
|
||||
assert.equal(contextmenu.getCustomHandler('valid'), validItem.action, 'Valid menu action is added.')
|
||||
})
|
||||
|
||||
it('Test svgedit.contextmenu rejects valid duplicate menu item id', function () {
|
||||
const validItem1 = { id: 'valid', label: 'anicelabel', action () { /* empty fn */ } }
|
||||
const validItem2 = { id: 'valid', label: 'anicelabel', action () { /* empty fn */ } }
|
||||
contextmenu.add(validItem1)
|
||||
|
||||
assert.throws(
|
||||
() => contextmenu.add(validItem2),
|
||||
null, null,
|
||||
'duplicate menu item is rejected.'
|
||||
)
|
||||
})
|
||||
})
|
||||
307
tests/unit/coords.test.js
Normal file
307
tests/unit/coords.test.js
Normal file
@@ -0,0 +1,307 @@
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as utilities from '../../packages/svgcanvas/core/utilities.js'
|
||||
import * as coords from '../../packages/svgcanvas/core/coords.js'
|
||||
|
||||
describe('coords', function () {
|
||||
let elemId = 1
|
||||
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
root.style.visibility = 'hidden'
|
||||
document.body.append(root)
|
||||
|
||||
/**
|
||||
* Set up tests with mock data.
|
||||
* @returns {void}
|
||||
*/
|
||||
beforeEach(function () {
|
||||
const svgroot = document.createElementNS(NS.SVG, 'svg')
|
||||
svgroot.id = 'svgroot'
|
||||
root.append(svgroot)
|
||||
this.svg = document.createElementNS(NS.SVG, 'svg')
|
||||
svgroot.append(this.svg)
|
||||
|
||||
// Mock out editor context.
|
||||
utilities.init(
|
||||
/**
|
||||
* @implements {module:utilities.EditorContext}
|
||||
*/
|
||||
{
|
||||
getSvgRoot: () => { return this.svg },
|
||||
getDOMDocument () { return null },
|
||||
getDOMContainer () { return null }
|
||||
}
|
||||
)
|
||||
coords.init(
|
||||
/**
|
||||
* @implements {module:coords.EditorContext}
|
||||
*/
|
||||
{
|
||||
getGridSnapping () { return false },
|
||||
getDrawing () {
|
||||
return {
|
||||
getNextId () { return String(elemId++) }
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Tear down tests, removing elements.
|
||||
* @returns {void}
|
||||
*/
|
||||
afterEach(function () {
|
||||
while (this.svg.hasChildNodes()) {
|
||||
this.svg.firstChild.remove()
|
||||
}
|
||||
})
|
||||
|
||||
it('Test remapElement(translate) for rect', function () {
|
||||
const rect = document.createElementNS(NS.SVG, 'rect')
|
||||
rect.setAttribute('x', '200')
|
||||
rect.setAttribute('y', '150')
|
||||
rect.setAttribute('width', '250')
|
||||
rect.setAttribute('height', '120')
|
||||
this.svg.append(rect)
|
||||
|
||||
const attrs = {
|
||||
x: '200',
|
||||
y: '150',
|
||||
width: '125',
|
||||
height: '75'
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
|
||||
coords.remapElement(rect, attrs, m)
|
||||
|
||||
assert.equal(rect.getAttribute('x'), '300')
|
||||
assert.equal(rect.getAttribute('y'), '100')
|
||||
assert.equal(rect.getAttribute('width'), '125')
|
||||
assert.equal(rect.getAttribute('height'), '75')
|
||||
})
|
||||
|
||||
it('Test remapElement(scale) for rect', function () {
|
||||
const rect = document.createElementNS(NS.SVG, 'rect')
|
||||
rect.setAttribute('width', '250')
|
||||
rect.setAttribute('height', '120')
|
||||
this.svg.append(rect)
|
||||
|
||||
const attrs = {
|
||||
x: '0',
|
||||
y: '0',
|
||||
width: '250',
|
||||
height: '120'
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
m.a = 2; m.b = 0
|
||||
m.c = 0; m.d = 0.5
|
||||
m.e = 0; m.f = 0
|
||||
|
||||
coords.remapElement(rect, attrs, m)
|
||||
|
||||
assert.equal(rect.getAttribute('x'), '0')
|
||||
assert.equal(rect.getAttribute('y'), '0')
|
||||
assert.equal(rect.getAttribute('width'), '500')
|
||||
assert.equal(rect.getAttribute('height'), '60')
|
||||
})
|
||||
|
||||
it('Test remapElement(translate) for circle', function () {
|
||||
const circle = document.createElementNS(NS.SVG, 'circle')
|
||||
circle.setAttribute('cx', '200')
|
||||
circle.setAttribute('cy', '150')
|
||||
circle.setAttribute('r', '125')
|
||||
this.svg.append(circle)
|
||||
|
||||
const attrs = {
|
||||
cx: '200',
|
||||
cy: '150',
|
||||
r: '125'
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
|
||||
coords.remapElement(circle, attrs, m)
|
||||
|
||||
assert.equal(circle.getAttribute('cx'), '300')
|
||||
assert.equal(circle.getAttribute('cy'), '100')
|
||||
assert.equal(circle.getAttribute('r'), '125')
|
||||
})
|
||||
|
||||
it('Test remapElement(scale) for circle', function () {
|
||||
const circle = document.createElementNS(NS.SVG, 'circle')
|
||||
circle.setAttribute('cx', '200')
|
||||
circle.setAttribute('cy', '150')
|
||||
circle.setAttribute('r', '250')
|
||||
this.svg.append(circle)
|
||||
|
||||
const attrs = {
|
||||
cx: '200',
|
||||
cy: '150',
|
||||
r: '250'
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
m.a = 2; m.b = 0
|
||||
m.c = 0; m.d = 0.5
|
||||
m.e = 0; m.f = 0
|
||||
|
||||
coords.remapElement(circle, attrs, m)
|
||||
|
||||
assert.equal(circle.getAttribute('cx'), '400')
|
||||
assert.equal(circle.getAttribute('cy'), '75')
|
||||
// Radius is the minimum that fits in the new bounding box.
|
||||
assert.equal(circle.getAttribute('r'), '125')
|
||||
})
|
||||
|
||||
it('Test remapElement(translate) for ellipse', function () {
|
||||
const ellipse = document.createElementNS(NS.SVG, 'ellipse')
|
||||
ellipse.setAttribute('cx', '200')
|
||||
ellipse.setAttribute('cy', '150')
|
||||
ellipse.setAttribute('rx', '125')
|
||||
ellipse.setAttribute('ry', '75')
|
||||
this.svg.append(ellipse)
|
||||
|
||||
const attrs = {
|
||||
cx: '200',
|
||||
cy: '150',
|
||||
rx: '125',
|
||||
ry: '75'
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
|
||||
coords.remapElement(ellipse, attrs, m)
|
||||
|
||||
assert.equal(ellipse.getAttribute('cx'), '300')
|
||||
assert.equal(ellipse.getAttribute('cy'), '100')
|
||||
assert.equal(ellipse.getAttribute('rx'), '125')
|
||||
assert.equal(ellipse.getAttribute('ry'), '75')
|
||||
})
|
||||
|
||||
it('Test remapElement(scale) for ellipse', function () {
|
||||
const ellipse = document.createElementNS(NS.SVG, 'ellipse')
|
||||
ellipse.setAttribute('cx', '200')
|
||||
ellipse.setAttribute('cy', '150')
|
||||
ellipse.setAttribute('rx', '250')
|
||||
ellipse.setAttribute('ry', '120')
|
||||
this.svg.append(ellipse)
|
||||
|
||||
const attrs = {
|
||||
cx: '200',
|
||||
cy: '150',
|
||||
rx: '250',
|
||||
ry: '120'
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
m.a = 2; m.b = 0
|
||||
m.c = 0; m.d = 0.5
|
||||
m.e = 0; m.f = 0
|
||||
|
||||
coords.remapElement(ellipse, attrs, m)
|
||||
|
||||
assert.equal(ellipse.getAttribute('cx'), '400')
|
||||
assert.equal(ellipse.getAttribute('cy'), '75')
|
||||
assert.equal(ellipse.getAttribute('rx'), '500')
|
||||
assert.equal(ellipse.getAttribute('ry'), '60')
|
||||
})
|
||||
|
||||
it('Test remapElement(translate) for line', function () {
|
||||
const line = document.createElementNS(NS.SVG, 'line')
|
||||
line.setAttribute('x1', '50')
|
||||
line.setAttribute('y1', '100')
|
||||
line.setAttribute('x2', '120')
|
||||
line.setAttribute('y2', '200')
|
||||
this.svg.append(line)
|
||||
|
||||
const attrs = {
|
||||
x1: '50',
|
||||
y1: '100',
|
||||
x2: '120',
|
||||
y2: '200'
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
|
||||
coords.remapElement(line, attrs, m)
|
||||
|
||||
assert.equal(line.getAttribute('x1'), '150')
|
||||
assert.equal(line.getAttribute('y1'), '50')
|
||||
assert.equal(line.getAttribute('x2'), '220')
|
||||
assert.equal(line.getAttribute('y2'), '150')
|
||||
})
|
||||
|
||||
it('Test remapElement(scale) for line', function () {
|
||||
const line = document.createElementNS(NS.SVG, 'line')
|
||||
line.setAttribute('x1', '50')
|
||||
line.setAttribute('y1', '100')
|
||||
line.setAttribute('x2', '120')
|
||||
line.setAttribute('y2', '200')
|
||||
this.svg.append(line)
|
||||
|
||||
const attrs = {
|
||||
x1: '50',
|
||||
y1: '100',
|
||||
x2: '120',
|
||||
y2: '200'
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
m.a = 2; m.b = 0
|
||||
m.c = 0; m.d = 0.5
|
||||
m.e = 0; m.f = 0
|
||||
|
||||
coords.remapElement(line, attrs, m)
|
||||
|
||||
assert.equal(line.getAttribute('x1'), '100')
|
||||
assert.equal(line.getAttribute('y1'), '50')
|
||||
assert.equal(line.getAttribute('x2'), '240')
|
||||
assert.equal(line.getAttribute('y2'), '100')
|
||||
})
|
||||
|
||||
it('Test remapElement(translate) for text', function () {
|
||||
const text = document.createElementNS(NS.SVG, 'text')
|
||||
text.setAttribute('x', '50')
|
||||
text.setAttribute('y', '100')
|
||||
this.svg.append(text)
|
||||
|
||||
const attrs = {
|
||||
x: '50',
|
||||
y: '100'
|
||||
}
|
||||
|
||||
// Create a translate.
|
||||
const m = this.svg.createSVGMatrix()
|
||||
m.a = 1; m.b = 0
|
||||
m.c = 0; m.d = 1
|
||||
m.e = 100; m.f = -50
|
||||
|
||||
coords.remapElement(text, attrs, m)
|
||||
|
||||
assert.equal(text.getAttribute('x'), '150')
|
||||
assert.equal(text.getAttribute('y'), '50')
|
||||
})
|
||||
})
|
||||
792
tests/unit/draw.test.js
Normal file
792
tests/unit/draw.test.js
Normal file
@@ -0,0 +1,792 @@
|
||||
import 'pathseg'
|
||||
import { vi } from 'vitest'
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as draw from '../../packages/svgcanvas/core/draw.js'
|
||||
import * as units from '../../packages/svgcanvas/core/units.js'
|
||||
import { Layer } from '../../packages/svgcanvas/core/draw'
|
||||
|
||||
describe('draw.Drawing', function () {
|
||||
const addOwnSpies = (obj) => {
|
||||
const methods = Object.keys(obj)
|
||||
methods.forEach((method) => {
|
||||
vi.spyOn(obj, method)
|
||||
})
|
||||
}
|
||||
|
||||
const LAYER_CLASS = draw.Layer.CLASS_NAME
|
||||
const NONCE = 'foo'
|
||||
const LAYER1 = 'Layer 1'
|
||||
const LAYER2 = 'Layer 2'
|
||||
const LAYER3 = 'Layer 3'
|
||||
const PATH_ATTR = {
|
||||
// clone will convert relative to absolute, so the test for equality fails.
|
||||
// d: 'm7.38867,57.38867c0,-27.62431 22.37569,-50 50,-50c27.62431,0 50,22.37569 50,50c0,27.62431 -22.37569,50 -50,50c-27.62431,0 -50,-22.37569 -50,-50z',
|
||||
d: 'M7.389,57.389C7.389,29.764 29.764,7.389 57.389,7.389C85.013,7.389 107.389,29.764 107.389,57.389C107.389,85.013 85.013,107.389 57.389,107.389C29.764,107.389 7.389,85.013 7.389,57.389z',
|
||||
transform: 'rotate(45 57.388671875000036,57.388671874999986) ',
|
||||
'stroke-width': '5',
|
||||
stroke: '#660000',
|
||||
fill: '#ff0000'
|
||||
}
|
||||
|
||||
units.init(
|
||||
/**
|
||||
* @implements {module:units.ElementContainer}
|
||||
*/
|
||||
{
|
||||
// used by units.shortFloat - call path: cloneLayer -> copyElem -> convertPath -> pathDSegment -> shortFloat
|
||||
getRoundDigits () { return 3 }
|
||||
}
|
||||
)
|
||||
|
||||
// Simplifying from svgcanvas.js usage
|
||||
const idprefix = 'svg_'
|
||||
|
||||
const getCurrentDrawing = function () {
|
||||
return currentDrawing_
|
||||
}
|
||||
const setCurrentGroup = () => { /* empty fn */ }
|
||||
draw.init(
|
||||
/**
|
||||
* @implements {module:draw.DrawCanvasInit}
|
||||
*/
|
||||
{
|
||||
getCurrentDrawing,
|
||||
setCurrentGroup
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {module:utilities.SVGElementJSON} jsonMap
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
function createSVGElement (jsonMap) {
|
||||
const elem = document.createElementNS(NS.SVG, jsonMap.element)
|
||||
Object.entries(jsonMap.attr).forEach(([attr, value]) => {
|
||||
elem.setAttribute(attr, value)
|
||||
})
|
||||
return elem
|
||||
}
|
||||
|
||||
const setupSVGWith3Layers = function (svgElem) {
|
||||
const layer1 = document.createElementNS(NS.SVG, 'g')
|
||||
layer1.setAttribute('class', Layer.CLASS_NAME)
|
||||
const layer1Title = document.createElementNS(NS.SVG, 'title')
|
||||
layer1Title.append(LAYER1)
|
||||
layer1.append(layer1Title)
|
||||
svgElem.append(layer1)
|
||||
|
||||
const layer2 = document.createElementNS(NS.SVG, 'g')
|
||||
layer2.setAttribute('class', Layer.CLASS_NAME)
|
||||
const layer2Title = document.createElementNS(NS.SVG, 'title')
|
||||
layer2Title.append(LAYER2)
|
||||
layer2.append(layer2Title)
|
||||
svgElem.append(layer2)
|
||||
|
||||
const layer3 = document.createElementNS(NS.SVG, 'g')
|
||||
layer3.setAttribute('class', Layer.CLASS_NAME)
|
||||
const layer3Title = document.createElementNS(NS.SVG, 'title')
|
||||
layer3Title.append(LAYER3)
|
||||
layer3.append(layer3Title)
|
||||
svgElem.append(layer3)
|
||||
|
||||
return [layer1, layer2, layer3]
|
||||
}
|
||||
|
||||
const createSomeElementsInGroup = function (group) {
|
||||
group.append(
|
||||
createSVGElement({
|
||||
element: 'path',
|
||||
attr: PATH_ATTR
|
||||
}),
|
||||
// createSVGElement({
|
||||
// element: 'path',
|
||||
// attr: {d: 'M0,1L2,3'}
|
||||
// }),
|
||||
createSVGElement({
|
||||
element: 'rect',
|
||||
attr: { x: '0', y: '1', width: '5', height: '10' }
|
||||
}),
|
||||
createSVGElement({
|
||||
element: 'line',
|
||||
attr: { x1: '0', y1: '1', x2: '5', y2: '6' }
|
||||
})
|
||||
)
|
||||
|
||||
const g = createSVGElement({
|
||||
element: 'g',
|
||||
attr: {}
|
||||
})
|
||||
g.append(createSVGElement({
|
||||
element: 'rect',
|
||||
attr: { x: '0', y: '1', width: '5', height: '10' }
|
||||
}))
|
||||
group.append(g)
|
||||
return 4
|
||||
}
|
||||
|
||||
const cleanupSVG = function (svgElem) {
|
||||
while (svgElem.firstChild) { svgElem.firstChild.remove() }
|
||||
}
|
||||
|
||||
let sandbox; let currentDrawing_; let svg; let svgN
|
||||
beforeEach(() => {
|
||||
sandbox = document.createElement('div')
|
||||
sandbox.id = 'sandbox'
|
||||
sandbox.style.visibility = 'hidden'
|
||||
|
||||
svg = document.createElementNS(NS.SVG, 'svg')
|
||||
// Firefox throws exception in getBBox() when svg is not attached to DOM.
|
||||
sandbox.append(svg)
|
||||
|
||||
// Set up <svg> with nonce.
|
||||
svgN = document.createElementNS(NS.SVG, 'svg')
|
||||
svgN.setAttributeNS(NS.XMLNS, 'xmlns:se', NS.SE)
|
||||
svgN.setAttributeNS(NS.SE, 'se:nonce', NONCE)
|
||||
|
||||
const svgContent = document.createElementNS(NS.SVG, 'svg')
|
||||
currentDrawing_ = new draw.Drawing(svgContent, idprefix)
|
||||
})
|
||||
|
||||
it('Test draw module', function () {
|
||||
assert.ok(draw)
|
||||
assert.equal(typeof draw, typeof {})
|
||||
|
||||
assert.ok(draw.Drawing)
|
||||
assert.equal(typeof draw.Drawing, typeof function () { /* empty fn */ })
|
||||
})
|
||||
|
||||
it('Test document creation', function () {
|
||||
let doc
|
||||
try {
|
||||
doc = new draw.Drawing()
|
||||
assert.ok(false, 'Created drawing without a valid <svg> element')
|
||||
} catch (e) {
|
||||
assert.ok(true)
|
||||
}
|
||||
|
||||
try {
|
||||
doc = new draw.Drawing(svg)
|
||||
assert.ok(doc)
|
||||
assert.equal(typeof doc, typeof {})
|
||||
} catch (e) {
|
||||
assert.ok(false, 'Could not create document from valid <svg> element: ' + e)
|
||||
}
|
||||
})
|
||||
|
||||
it('Test nonce', function () {
|
||||
let doc = new draw.Drawing(svg)
|
||||
assert.equal(doc.getNonce(), '')
|
||||
|
||||
doc = new draw.Drawing(svgN)
|
||||
assert.equal(doc.getNonce(), NONCE)
|
||||
assert.equal(doc.getSvgElem().getAttributeNS(NS.SE, 'nonce'), NONCE)
|
||||
|
||||
doc.clearNonce()
|
||||
assert.ok(!doc.getNonce())
|
||||
assert.ok(!doc.getSvgElem().getAttributeNS(NS.SE, 'se:nonce'))
|
||||
|
||||
doc.setNonce(NONCE)
|
||||
assert.equal(doc.getNonce(), NONCE)
|
||||
assert.equal(doc.getSvgElem().getAttributeNS(NS.SE, 'nonce'), NONCE)
|
||||
})
|
||||
|
||||
it('Test getId() and getNextId() without nonce', function () {
|
||||
const elem2 = document.createElementNS(NS.SVG, 'circle')
|
||||
elem2.id = 'svg_2'
|
||||
svg.append(elem2)
|
||||
|
||||
const doc = new draw.Drawing(svg)
|
||||
|
||||
assert.equal(doc.getId(), 'svg_0')
|
||||
|
||||
assert.equal(doc.getNextId(), 'svg_1')
|
||||
assert.equal(doc.getId(), 'svg_1')
|
||||
|
||||
assert.equal(doc.getNextId(), 'svg_3')
|
||||
assert.equal(doc.getId(), 'svg_3')
|
||||
|
||||
assert.equal(doc.getNextId(), 'svg_4')
|
||||
assert.equal(doc.getId(), 'svg_4')
|
||||
// clean out svg document
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test getId() and getNextId() with prefix without nonce', function () {
|
||||
const prefix = 'Bar-'
|
||||
const doc = new draw.Drawing(svg, prefix)
|
||||
|
||||
assert.equal(doc.getId(), prefix + '0')
|
||||
|
||||
assert.equal(doc.getNextId(), prefix + '1')
|
||||
assert.equal(doc.getId(), prefix + '1')
|
||||
|
||||
assert.equal(doc.getNextId(), prefix + '2')
|
||||
assert.equal(doc.getId(), prefix + '2')
|
||||
|
||||
assert.equal(doc.getNextId(), prefix + '3')
|
||||
assert.equal(doc.getId(), prefix + '3')
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test getId() and getNextId() with nonce', function () {
|
||||
const prefix = 'svg_' + NONCE
|
||||
|
||||
const elem2 = document.createElementNS(NS.SVG, 'circle')
|
||||
elem2.id = prefix + '_2'
|
||||
svgN.append(elem2)
|
||||
|
||||
const doc = new draw.Drawing(svgN)
|
||||
|
||||
assert.equal(doc.getId(), prefix + '_0')
|
||||
|
||||
assert.equal(doc.getNextId(), prefix + '_1')
|
||||
assert.equal(doc.getId(), prefix + '_1')
|
||||
|
||||
assert.equal(doc.getNextId(), prefix + '_3')
|
||||
assert.equal(doc.getId(), prefix + '_3')
|
||||
|
||||
assert.equal(doc.getNextId(), prefix + '_4')
|
||||
assert.equal(doc.getId(), prefix + '_4')
|
||||
|
||||
cleanupSVG(svgN)
|
||||
})
|
||||
|
||||
it('Test getId() and getNextId() with prefix with nonce', function () {
|
||||
const PREFIX = 'Bar-'
|
||||
const doc = new draw.Drawing(svgN, PREFIX)
|
||||
|
||||
const prefix = PREFIX + NONCE + '_'
|
||||
assert.equal(doc.getId(), prefix + '0')
|
||||
|
||||
assert.equal(doc.getNextId(), prefix + '1')
|
||||
assert.equal(doc.getId(), prefix + '1')
|
||||
|
||||
assert.equal(doc.getNextId(), prefix + '2')
|
||||
assert.equal(doc.getId(), prefix + '2')
|
||||
|
||||
assert.equal(doc.getNextId(), prefix + '3')
|
||||
assert.equal(doc.getId(), prefix + '3')
|
||||
|
||||
cleanupSVG(svgN)
|
||||
})
|
||||
|
||||
it('Test releaseId()', function () {
|
||||
const doc = new draw.Drawing(svg)
|
||||
|
||||
const firstId = doc.getNextId()
|
||||
/* const secondId = */ doc.getNextId()
|
||||
|
||||
const result = doc.releaseId(firstId)
|
||||
assert.ok(result)
|
||||
assert.equal(doc.getNextId(), firstId)
|
||||
assert.equal(doc.getNextId(), 'svg_3')
|
||||
|
||||
assert.ok(!doc.releaseId('bad-id'))
|
||||
assert.ok(doc.releaseId(firstId))
|
||||
assert.ok(!doc.releaseId(firstId))
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test getNumLayers', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
assert.equal(typeof drawing.getNumLayers, typeof function () { /* empty fn */ })
|
||||
assert.equal(drawing.getNumLayers(), 0)
|
||||
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.equal(drawing.getNumLayers(), 3)
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test hasLayer', function () {
|
||||
setupSVGWith3Layers(svg)
|
||||
const drawing = new draw.Drawing(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.equal(typeof drawing.hasLayer, typeof function () { /* empty fn */ })
|
||||
assert.ok(!drawing.hasLayer('invalid-layer'))
|
||||
|
||||
assert.ok(drawing.hasLayer(LAYER3))
|
||||
assert.ok(drawing.hasLayer(LAYER2))
|
||||
assert.ok(drawing.hasLayer(LAYER1))
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test identifyLayers() with empty document', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
assert.equal(drawing.getCurrentLayer(), null)
|
||||
// By default, an empty document gets an empty group created.
|
||||
drawing.identifyLayers()
|
||||
|
||||
// Check that <svg> element now has one child node
|
||||
assert.ok(drawing.getSvgElem().hasChildNodes())
|
||||
assert.equal(drawing.getSvgElem().childNodes.length, 1)
|
||||
|
||||
// Check that all_layers are correctly set up.
|
||||
assert.equal(drawing.getNumLayers(), 1)
|
||||
const emptyLayer = drawing.all_layers[0]
|
||||
assert.ok(emptyLayer)
|
||||
const layerGroup = emptyLayer.getGroup()
|
||||
assert.equal(layerGroup, drawing.getSvgElem().firstChild)
|
||||
assert.equal(layerGroup.tagName, 'g')
|
||||
assert.equal(layerGroup.getAttribute('class'), LAYER_CLASS)
|
||||
assert.ok(layerGroup.hasChildNodes())
|
||||
assert.equal(layerGroup.childNodes.length, 1)
|
||||
const firstChild = layerGroup.childNodes.item(0)
|
||||
assert.equal(firstChild.tagName, 'title')
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test identifyLayers() with some layers', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
|
||||
assert.equal(svg.childNodes.length, 3)
|
||||
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.equal(drawing.getNumLayers(), 3)
|
||||
assert.equal(drawing.all_layers[0].getGroup(), svg.childNodes.item(0))
|
||||
assert.equal(drawing.all_layers[1].getGroup(), svg.childNodes.item(1))
|
||||
assert.equal(drawing.all_layers[2].getGroup(), svg.childNodes.item(2))
|
||||
|
||||
assert.equal(drawing.all_layers[0].getGroup().getAttribute('class'), LAYER_CLASS)
|
||||
assert.equal(drawing.all_layers[1].getGroup().getAttribute('class'), LAYER_CLASS)
|
||||
assert.equal(drawing.all_layers[2].getGroup().getAttribute('class'), LAYER_CLASS)
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test identifyLayers() with some layers and orphans', function () {
|
||||
setupSVGWith3Layers(svg)
|
||||
|
||||
const orphan1 = document.createElementNS(NS.SVG, 'rect')
|
||||
const orphan2 = document.createElementNS(NS.SVG, 'rect')
|
||||
svg.append(orphan1, orphan2)
|
||||
|
||||
assert.equal(svg.childNodes.length, 5)
|
||||
|
||||
const drawing = new draw.Drawing(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.equal(drawing.getNumLayers(), 4)
|
||||
assert.equal(drawing.all_layers[0].getGroup(), svg.childNodes.item(0))
|
||||
assert.equal(drawing.all_layers[1].getGroup(), svg.childNodes.item(1))
|
||||
assert.equal(drawing.all_layers[2].getGroup(), svg.childNodes.item(2))
|
||||
assert.equal(drawing.all_layers[3].getGroup(), svg.childNodes.item(3))
|
||||
|
||||
assert.equal(drawing.all_layers[0].getGroup().getAttribute('class'), LAYER_CLASS)
|
||||
assert.equal(drawing.all_layers[1].getGroup().getAttribute('class'), LAYER_CLASS)
|
||||
assert.equal(drawing.all_layers[2].getGroup().getAttribute('class'), LAYER_CLASS)
|
||||
assert.equal(drawing.all_layers[3].getGroup().getAttribute('class'), LAYER_CLASS)
|
||||
|
||||
const layer4 = drawing.all_layers[3].getGroup()
|
||||
assert.equal(layer4.tagName, 'g')
|
||||
assert.equal(layer4.childNodes.length, 3)
|
||||
assert.equal(layer4.childNodes.item(1), orphan1)
|
||||
assert.equal(layer4.childNodes.item(2), orphan2)
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test getLayerName()', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.equal(drawing.getNumLayers(), 3)
|
||||
assert.equal(drawing.getLayerName(0), LAYER1)
|
||||
assert.equal(drawing.getLayerName(1), LAYER2)
|
||||
assert.equal(drawing.getLayerName(2), LAYER3)
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test getCurrentLayer()', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.getCurrentLayer)
|
||||
assert.equal(typeof drawing.getCurrentLayer, typeof function () { /* empty fn */ })
|
||||
assert.ok(drawing.getCurrentLayer())
|
||||
assert.equal(drawing.getCurrentLayer(), drawing.all_layers[2].getGroup())
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test setCurrentLayer() and getCurrentLayerName()', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.setCurrentLayer)
|
||||
assert.equal(typeof drawing.setCurrentLayer, typeof function () { /* empty fn */ })
|
||||
|
||||
drawing.setCurrentLayer(LAYER2)
|
||||
assert.equal(drawing.getCurrentLayerName(), LAYER2)
|
||||
assert.equal(drawing.getCurrentLayer(), drawing.all_layers[1].getGroup())
|
||||
|
||||
drawing.setCurrentLayer(LAYER3)
|
||||
assert.equal(drawing.getCurrentLayerName(), LAYER3)
|
||||
assert.equal(drawing.getCurrentLayer(), drawing.all_layers[2].getGroup())
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test setCurrentLayerName()', function () {
|
||||
const mockHrService = {
|
||||
changeElement () {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
addOwnSpies(mockHrService)
|
||||
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.setCurrentLayerName)
|
||||
assert.equal(typeof drawing.setCurrentLayerName, typeof function () { /* empty fn */ })
|
||||
|
||||
const oldName = drawing.getCurrentLayerName()
|
||||
const newName = 'New Name'
|
||||
assert.ok(drawing.layer_map[oldName])
|
||||
assert.equal(drawing.layer_map[newName], undefined) // newName shouldn't exist.
|
||||
const result = drawing.setCurrentLayerName(newName, mockHrService)
|
||||
assert.equal(result, newName)
|
||||
assert.equal(drawing.getCurrentLayerName(), newName)
|
||||
// Was the map updated?
|
||||
assert.equal(drawing.layer_map[oldName], undefined)
|
||||
assert.equal(drawing.layer_map[newName], drawing.current_layer)
|
||||
// Was mockHrService called?
|
||||
assert.ok(mockHrService.changeElement.calledOnce)
|
||||
assert.equal(oldName, mockHrService.changeElement.getCall(0).args[1]['#text'])
|
||||
assert.equal(newName, mockHrService.changeElement.getCall(0).args[0].textContent)
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test createLayer()', function () {
|
||||
const mockHrService = {
|
||||
startBatchCommand () { /* empty fn */ },
|
||||
endBatchCommand () { /* empty fn */ },
|
||||
insertElement () { /* empty fn */ }
|
||||
}
|
||||
addOwnSpies(mockHrService)
|
||||
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.createLayer)
|
||||
assert.equal(typeof drawing.createLayer, typeof function () { /* empty fn */ })
|
||||
|
||||
const NEW_LAYER_NAME = 'Layer A'
|
||||
const layerG = drawing.createLayer(NEW_LAYER_NAME, mockHrService)
|
||||
assert.equal(drawing.getNumLayers(), 4)
|
||||
assert.equal(layerG, drawing.getCurrentLayer())
|
||||
assert.equal(layerG.getAttribute('class'), LAYER_CLASS)
|
||||
assert.equal(NEW_LAYER_NAME, drawing.getCurrentLayerName())
|
||||
assert.equal(NEW_LAYER_NAME, drawing.getLayerName(3))
|
||||
|
||||
assert.equal(layerG, mockHrService.insertElement.getCall(0).args[0])
|
||||
assert.ok(mockHrService.startBatchCommand.calledOnce)
|
||||
assert.ok(mockHrService.endBatchCommand.calledOnce)
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test mergeLayer()', function () {
|
||||
const mockHrService = {
|
||||
startBatchCommand () { /* empty fn */ },
|
||||
endBatchCommand () { /* empty fn */ },
|
||||
moveElement () { /* empty fn */ },
|
||||
removeElement () { /* empty fn */ }
|
||||
}
|
||||
addOwnSpies(mockHrService)
|
||||
|
||||
const drawing = new draw.Drawing(svg)
|
||||
const layers = setupSVGWith3Layers(svg)
|
||||
const elementCount = createSomeElementsInGroup(layers[2]) + 1 // +1 for title element
|
||||
assert.equal(layers[1].childElementCount, 1)
|
||||
assert.equal(layers[2].childElementCount, elementCount)
|
||||
drawing.identifyLayers()
|
||||
assert.equal(drawing.getCurrentLayer(), layers[2])
|
||||
|
||||
assert.ok(drawing.mergeLayer)
|
||||
assert.equal(typeof drawing.mergeLayer, typeof function () { /* empty fn */ })
|
||||
|
||||
drawing.mergeLayer(mockHrService)
|
||||
|
||||
assert.equal(drawing.getNumLayers(), 2)
|
||||
assert.equal(svg.childElementCount, 2)
|
||||
assert.equal(drawing.getCurrentLayer(), layers[1])
|
||||
assert.equal(layers[1].childElementCount, elementCount)
|
||||
|
||||
// check history record
|
||||
assert.ok(mockHrService.startBatchCommand.calledOnce)
|
||||
assert.ok(mockHrService.endBatchCommand.calledOnce)
|
||||
assert.equal(mockHrService.startBatchCommand.getCall(0).args[0], 'Merge Layer')
|
||||
assert.equal(mockHrService.moveElement.callCount, elementCount - 1) // -1 because the title was not moved.
|
||||
assert.equal(mockHrService.removeElement.callCount, 2) // remove group and title.
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test mergeLayer() when no previous layer to merge', function () {
|
||||
const mockHrService = {
|
||||
startBatchCommand () { /* empty fn */ },
|
||||
endBatchCommand () { /* empty fn */ },
|
||||
moveElement () { /* empty fn */ },
|
||||
removeElement () { /* empty fn */ }
|
||||
}
|
||||
addOwnSpies(mockHrService)
|
||||
|
||||
const drawing = new draw.Drawing(svg)
|
||||
const layers = setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
drawing.setCurrentLayer(LAYER1)
|
||||
assert.equal(drawing.getCurrentLayer(), layers[0])
|
||||
|
||||
drawing.mergeLayer(mockHrService)
|
||||
|
||||
assert.equal(drawing.getNumLayers(), 3)
|
||||
assert.equal(svg.childElementCount, 3)
|
||||
assert.equal(drawing.getCurrentLayer(), layers[0])
|
||||
assert.equal(layers[0].childElementCount, 1)
|
||||
assert.equal(layers[1].childElementCount, 1)
|
||||
assert.equal(layers[2].childElementCount, 1)
|
||||
|
||||
// check history record
|
||||
assert.equal(mockHrService.startBatchCommand.callCount, 0)
|
||||
assert.equal(mockHrService.endBatchCommand.callCount, 0)
|
||||
assert.equal(mockHrService.moveElement.callCount, 0)
|
||||
assert.equal(mockHrService.removeElement.callCount, 0)
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test mergeAllLayers()', function () {
|
||||
const mockHrService = {
|
||||
startBatchCommand () { /* empty fn */ },
|
||||
endBatchCommand () { /* empty fn */ },
|
||||
moveElement () { /* empty fn */ },
|
||||
removeElement () { /* empty fn */ }
|
||||
}
|
||||
addOwnSpies(mockHrService)
|
||||
|
||||
const drawing = new draw.Drawing(svg)
|
||||
const layers = setupSVGWith3Layers(svg)
|
||||
const elementCount = createSomeElementsInGroup(layers[0]) + 1 // +1 for title element
|
||||
createSomeElementsInGroup(layers[1])
|
||||
createSomeElementsInGroup(layers[2])
|
||||
assert.equal(layers[0].childElementCount, elementCount)
|
||||
assert.equal(layers[1].childElementCount, elementCount)
|
||||
assert.equal(layers[2].childElementCount, elementCount)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.mergeAllLayers)
|
||||
assert.equal(typeof drawing.mergeAllLayers, typeof function () { /* empty fn */ })
|
||||
|
||||
drawing.mergeAllLayers(mockHrService)
|
||||
|
||||
assert.equal(drawing.getNumLayers(), 1)
|
||||
assert.equal(svg.childElementCount, 1)
|
||||
assert.equal(drawing.getCurrentLayer(), layers[0])
|
||||
assert.equal(layers[0].childElementCount, elementCount * 3 - 2) // -2 because two titles were deleted.
|
||||
|
||||
// check history record
|
||||
assert.equal(mockHrService.startBatchCommand.callCount, 3) // mergeAllLayers + 2 * mergeLayer
|
||||
assert.equal(mockHrService.endBatchCommand.callCount, 3)
|
||||
assert.equal(mockHrService.startBatchCommand.getCall(0).args[0], 'Merge all Layers')
|
||||
assert.equal(mockHrService.startBatchCommand.getCall(1).args[0], 'Merge Layer')
|
||||
assert.equal(mockHrService.startBatchCommand.getCall(2).args[0], 'Merge Layer')
|
||||
// moveElement count is times 3 instead of 2, because one layer's elements were moved twice.
|
||||
// moveElement count is minus 3 because the three titles were not moved.
|
||||
assert.equal(mockHrService.moveElement.callCount, elementCount * 3 - 3)
|
||||
assert.equal(mockHrService.removeElement.callCount, 2 * 2) // remove group and title twice.
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test cloneLayer()', function () {
|
||||
const mockHrService = {
|
||||
startBatchCommand () { /* empty fn */ },
|
||||
endBatchCommand () { /* empty fn */ },
|
||||
insertElement () { /* empty fn */ }
|
||||
}
|
||||
addOwnSpies(mockHrService)
|
||||
|
||||
const drawing = new draw.Drawing(svg)
|
||||
const layers = setupSVGWith3Layers(svg)
|
||||
const layer3 = layers[2]
|
||||
const elementCount = createSomeElementsInGroup(layer3) + 1 // +1 for title element
|
||||
assert.equal(layer3.childElementCount, elementCount)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.cloneLayer)
|
||||
assert.equal(typeof drawing.cloneLayer, typeof function () { /* empty fn */ })
|
||||
|
||||
const clone = drawing.cloneLayer('clone', mockHrService)
|
||||
|
||||
assert.equal(drawing.getNumLayers(), 4)
|
||||
assert.equal(svg.childElementCount, 4)
|
||||
assert.equal(drawing.getCurrentLayer(), clone)
|
||||
assert.equal(clone.childElementCount, elementCount)
|
||||
|
||||
// check history record
|
||||
assert.ok(mockHrService.startBatchCommand.calledOnce) // mergeAllLayers + 2 * mergeLayer
|
||||
assert.ok(mockHrService.endBatchCommand.calledOnce)
|
||||
assert.equal(mockHrService.startBatchCommand.getCall(0).args[0], 'Duplicate Layer')
|
||||
assert.equal(mockHrService.insertElement.callCount, 1)
|
||||
assert.equal(mockHrService.insertElement.getCall(0).args[0], clone)
|
||||
|
||||
// check that path is cloned properly
|
||||
assert.equal(clone.childNodes.length, elementCount)
|
||||
const path = clone.childNodes[1]
|
||||
assert.equal(path.id, 'svg_1')
|
||||
assert.equal(path.getAttribute('d'), PATH_ATTR.d)
|
||||
assert.equal(path.getAttribute('transform'), PATH_ATTR.transform)
|
||||
assert.equal(path.getAttribute('fill'), PATH_ATTR.fill)
|
||||
assert.equal(path.getAttribute('stroke'), PATH_ATTR.stroke)
|
||||
assert.equal(path.getAttribute('stroke-width'), PATH_ATTR['stroke-width'])
|
||||
|
||||
// check that g is cloned properly
|
||||
const g = clone.childNodes[4]
|
||||
assert.equal(g.childNodes.length, 1)
|
||||
assert.equal(g.id, 'svg_4')
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test getLayerVisibility()', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.getLayerVisibility)
|
||||
assert.equal(typeof drawing.getLayerVisibility, typeof function () { /* empty fn */ })
|
||||
assert.ok(drawing.getLayerVisibility(LAYER1))
|
||||
assert.ok(drawing.getLayerVisibility(LAYER2))
|
||||
assert.ok(drawing.getLayerVisibility(LAYER3))
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test setLayerVisibility()', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.setLayerVisibility)
|
||||
assert.equal(typeof drawing.setLayerVisibility, typeof function () { /* empty fn */ })
|
||||
|
||||
drawing.setLayerVisibility(LAYER3, false)
|
||||
drawing.setLayerVisibility(LAYER2, true)
|
||||
drawing.setLayerVisibility(LAYER1, false)
|
||||
|
||||
assert.ok(!drawing.getLayerVisibility(LAYER1))
|
||||
assert.ok(drawing.getLayerVisibility(LAYER2))
|
||||
assert.ok(!drawing.getLayerVisibility(LAYER3))
|
||||
|
||||
drawing.setLayerVisibility(LAYER3, 'test-string')
|
||||
assert.ok(!drawing.getLayerVisibility(LAYER3))
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test getLayerOpacity()', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.getLayerOpacity)
|
||||
assert.equal(typeof drawing.getLayerOpacity, typeof function () { /* empty fn */ })
|
||||
assert.strictEqual(drawing.getLayerOpacity(LAYER1), 1.0)
|
||||
assert.strictEqual(drawing.getLayerOpacity(LAYER2), 1.0)
|
||||
assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0)
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test setLayerOpacity()', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
assert.ok(drawing.setLayerOpacity)
|
||||
assert.equal(typeof drawing.setLayerOpacity, typeof function () { /* empty fn */ })
|
||||
|
||||
drawing.setLayerOpacity(LAYER1, 0.4)
|
||||
drawing.setLayerOpacity(LAYER2, 'invalid-string')
|
||||
drawing.setLayerOpacity(LAYER3, -1.4)
|
||||
|
||||
assert.strictEqual(drawing.getLayerOpacity(LAYER1), 0.4)
|
||||
assert.strictEqual(drawing.getLayerOpacity(LAYER2), 1.0)
|
||||
assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0)
|
||||
|
||||
drawing.setLayerOpacity(LAYER3, 100)
|
||||
assert.strictEqual(drawing.getLayerOpacity(LAYER3), 1.0)
|
||||
|
||||
cleanupSVG(svg)
|
||||
})
|
||||
|
||||
it('Test deleteCurrentLayer()', function () {
|
||||
const drawing = new draw.Drawing(svg)
|
||||
setupSVGWith3Layers(svg)
|
||||
drawing.identifyLayers()
|
||||
|
||||
drawing.setCurrentLayer(LAYER2)
|
||||
|
||||
const curLayer = drawing.getCurrentLayer()
|
||||
assert.equal(curLayer, drawing.all_layers[1].getGroup())
|
||||
const deletedLayer = drawing.deleteCurrentLayer()
|
||||
|
||||
assert.equal(curLayer, deletedLayer)
|
||||
assert.equal(drawing.getNumLayers(), 2)
|
||||
assert.equal(LAYER1, drawing.all_layers[0].getName())
|
||||
assert.equal(LAYER3, drawing.all_layers[1].getName())
|
||||
assert.equal(drawing.getCurrentLayer(), drawing.all_layers[1].getGroup())
|
||||
})
|
||||
|
||||
it('Test svgedit.draw.randomizeIds()', function () {
|
||||
// Confirm in LET_DOCUMENT_DECIDE mode that the document decides
|
||||
// if there is a nonce.
|
||||
let drawing = new draw.Drawing(svgN.cloneNode(true))
|
||||
assert.ok(drawing.getNonce())
|
||||
|
||||
drawing = new draw.Drawing(svg.cloneNode(true))
|
||||
assert.ok(!drawing.getNonce())
|
||||
|
||||
// Confirm that a nonce is set once we're in ALWAYS_RANDOMIZE mode.
|
||||
draw.randomizeIds(true, drawing)
|
||||
assert.ok(drawing.getNonce())
|
||||
|
||||
// Confirm new drawings in ALWAYS_RANDOMIZE mode have a nonce.
|
||||
drawing = new draw.Drawing(svg.cloneNode(true))
|
||||
assert.ok(drawing.getNonce())
|
||||
|
||||
drawing.clearNonce()
|
||||
assert.ok(!drawing.getNonce())
|
||||
|
||||
// Confirm new drawings in NEVER_RANDOMIZE mode do not have a nonce
|
||||
// but that their se:nonce attribute is left alone.
|
||||
draw.randomizeIds(false, drawing)
|
||||
assert.ok(!drawing.getNonce())
|
||||
assert.ok(drawing.getSvgElem().getAttributeNS(NS.SE, 'nonce'))
|
||||
|
||||
drawing = new draw.Drawing(svg.cloneNode(true))
|
||||
assert.ok(!drawing.getNonce())
|
||||
|
||||
drawing = new draw.Drawing(svgN.cloneNode(true))
|
||||
assert.ok(!drawing.getNonce())
|
||||
})
|
||||
})
|
||||
520
tests/unit/history.test.js
Normal file
520
tests/unit/history.test.js
Normal file
@@ -0,0 +1,520 @@
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as utilities from '../../packages/svgcanvas/core/utilities.js'
|
||||
import * as history from '../../packages/svgcanvas/core/history.js'
|
||||
|
||||
describe('history', function () {
|
||||
// TODO(codedread): Write tests for handling history events.
|
||||
|
||||
utilities.mock({
|
||||
getHref () { return '#foo' },
|
||||
setHref () { /* empty fn */ },
|
||||
getRotationAngle () { return 0 }
|
||||
})
|
||||
|
||||
// const svg = document.createElementNS(NS.SVG, 'svg');
|
||||
let undoMgr = null
|
||||
|
||||
class MockCommand extends history.Command {
|
||||
constructor (optText) {
|
||||
super()
|
||||
this.text = optText
|
||||
}
|
||||
|
||||
apply (handler) {
|
||||
super.apply(handler, () => { /* empty fn */ })
|
||||
}
|
||||
|
||||
unapply (handler) {
|
||||
super.unapply(handler, () => { /* empty fn */ })
|
||||
}
|
||||
|
||||
elements () { return [] }
|
||||
}
|
||||
|
||||
/*
|
||||
class MockHistoryEventHandler {
|
||||
handleHistoryEvent (eventType, command) {}
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set up tests (with undo manager).
|
||||
* @returns {void}
|
||||
*/
|
||||
beforeEach(function () {
|
||||
undoMgr = new history.UndoManager()
|
||||
|
||||
document.body.textContent = ''
|
||||
this.divparent = document.createElement('div')
|
||||
this.divparent.id = 'divparent'
|
||||
this.divparent.style.visibility = 'hidden'
|
||||
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const div = document.createElement('div')
|
||||
const id = `div${i}`
|
||||
div.id = id
|
||||
this[id] = div
|
||||
}
|
||||
|
||||
this.divparent.append(this.div1, this.div2, this.div3)
|
||||
|
||||
this.div4.style.visibility = 'hidden'
|
||||
this.div4.append(this.div5)
|
||||
|
||||
document.body.append(this.divparent, this.div)
|
||||
})
|
||||
/**
|
||||
* Tear down tests, destroying undo manager.
|
||||
* @returns {void}
|
||||
*/
|
||||
afterEach(() => {
|
||||
undoMgr = null
|
||||
})
|
||||
|
||||
it('Test svgedit.history package', function () {
|
||||
assert.ok(history)
|
||||
assert.ok(history.MoveElementCommand)
|
||||
assert.ok(history.InsertElementCommand)
|
||||
assert.ok(history.ChangeElementCommand)
|
||||
assert.ok(history.RemoveElementCommand)
|
||||
assert.ok(history.BatchCommand)
|
||||
assert.ok(history.UndoManager)
|
||||
assert.equal(typeof history.MoveElementCommand, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof history.InsertElementCommand, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof history.ChangeElementCommand, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof history.RemoveElementCommand, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof history.BatchCommand, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof history.UndoManager, typeof function () { /* empty fn */ })
|
||||
})
|
||||
|
||||
it('Test UndoManager methods', function () {
|
||||
assert.ok(undoMgr)
|
||||
assert.ok(undoMgr.addCommandToHistory)
|
||||
assert.ok(undoMgr.getUndoStackSize)
|
||||
assert.ok(undoMgr.getRedoStackSize)
|
||||
assert.ok(undoMgr.resetUndoStack)
|
||||
assert.ok(undoMgr.getNextUndoCommandText)
|
||||
assert.ok(undoMgr.getNextRedoCommandText)
|
||||
|
||||
assert.equal(typeof undoMgr, typeof {})
|
||||
assert.equal(typeof undoMgr.addCommandToHistory, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof undoMgr.getUndoStackSize, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof undoMgr.getRedoStackSize, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof undoMgr.resetUndoStack, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof undoMgr.getNextUndoCommandText, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof undoMgr.getNextRedoCommandText, typeof function () { /* empty fn */ })
|
||||
})
|
||||
|
||||
it('Test UndoManager.addCommandToHistory() function', function () {
|
||||
assert.equal(undoMgr.getUndoStackSize(), 0)
|
||||
undoMgr.addCommandToHistory(new MockCommand())
|
||||
assert.equal(undoMgr.getUndoStackSize(), 1)
|
||||
undoMgr.addCommandToHistory(new MockCommand())
|
||||
assert.equal(undoMgr.getUndoStackSize(), 2)
|
||||
})
|
||||
|
||||
it('Test UndoManager.getUndoStackSize() and getRedoStackSize() functions', function () {
|
||||
undoMgr.addCommandToHistory(new MockCommand())
|
||||
undoMgr.addCommandToHistory(new MockCommand())
|
||||
undoMgr.addCommandToHistory(new MockCommand())
|
||||
|
||||
assert.equal(undoMgr.getUndoStackSize(), 3)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 0)
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getUndoStackSize(), 2)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 1)
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getUndoStackSize(), 1)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 2)
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getUndoStackSize(), 0)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 3)
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getUndoStackSize(), 0)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 3)
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getUndoStackSize(), 1)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 2)
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getUndoStackSize(), 2)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 1)
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getUndoStackSize(), 3)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 0)
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getUndoStackSize(), 3)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 0)
|
||||
})
|
||||
|
||||
it('Test UndoManager.resetUndoStackSize() function', function () {
|
||||
undoMgr.addCommandToHistory(new MockCommand())
|
||||
undoMgr.addCommandToHistory(new MockCommand())
|
||||
undoMgr.addCommandToHistory(new MockCommand())
|
||||
undoMgr.undo()
|
||||
|
||||
assert.equal(undoMgr.getUndoStackSize(), 2)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 1)
|
||||
|
||||
undoMgr.resetUndoStack()
|
||||
|
||||
assert.equal(undoMgr.getUndoStackSize(), 0)
|
||||
assert.equal(undoMgr.getRedoStackSize(), 0)
|
||||
})
|
||||
|
||||
it('Test UndoManager.getNextUndoCommandText() function', function () {
|
||||
assert.equal(undoMgr.getNextUndoCommandText(), '')
|
||||
|
||||
undoMgr.addCommandToHistory(new MockCommand('First'))
|
||||
undoMgr.addCommandToHistory(new MockCommand('Second'))
|
||||
undoMgr.addCommandToHistory(new MockCommand('Third'))
|
||||
|
||||
assert.equal(undoMgr.getNextUndoCommandText(), 'Third')
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getNextUndoCommandText(), 'Second')
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getNextUndoCommandText(), 'First')
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getNextUndoCommandText(), '')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getNextUndoCommandText(), 'First')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getNextUndoCommandText(), 'Second')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getNextUndoCommandText(), 'Third')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getNextUndoCommandText(), 'Third')
|
||||
})
|
||||
|
||||
it('Test UndoManager.getNextRedoCommandText() function', function () {
|
||||
assert.equal(undoMgr.getNextRedoCommandText(), '')
|
||||
|
||||
undoMgr.addCommandToHistory(new MockCommand('First'))
|
||||
undoMgr.addCommandToHistory(new MockCommand('Second'))
|
||||
undoMgr.addCommandToHistory(new MockCommand('Third'))
|
||||
|
||||
assert.equal(undoMgr.getNextRedoCommandText(), '')
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getNextRedoCommandText(), 'Third')
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getNextRedoCommandText(), 'Second')
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(undoMgr.getNextRedoCommandText(), 'First')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getNextRedoCommandText(), 'Second')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getNextRedoCommandText(), 'Third')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(undoMgr.getNextRedoCommandText(), '')
|
||||
})
|
||||
|
||||
it('Test UndoManager.undo() and redo() functions', function () {
|
||||
let lastCalled = null
|
||||
const cmd1 = new MockCommand()
|
||||
const cmd2 = new MockCommand()
|
||||
const cmd3 = new MockCommand()
|
||||
cmd1.apply = function () { lastCalled = 'cmd1.apply' }
|
||||
cmd2.apply = function () { lastCalled = 'cmd2.apply' }
|
||||
cmd3.apply = function () { lastCalled = 'cmd3.apply' }
|
||||
cmd1.unapply = function () { lastCalled = 'cmd1.unapply' }
|
||||
cmd2.unapply = function () { lastCalled = 'cmd2.unapply' }
|
||||
cmd3.unapply = function () { lastCalled = 'cmd3.unapply' }
|
||||
|
||||
undoMgr.addCommandToHistory(cmd1)
|
||||
undoMgr.addCommandToHistory(cmd2)
|
||||
undoMgr.addCommandToHistory(cmd3)
|
||||
|
||||
assert.ok(!lastCalled)
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(lastCalled, 'cmd3.unapply')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(lastCalled, 'cmd3.apply')
|
||||
|
||||
undoMgr.undo()
|
||||
undoMgr.undo()
|
||||
assert.equal(lastCalled, 'cmd2.unapply')
|
||||
|
||||
undoMgr.undo()
|
||||
assert.equal(lastCalled, 'cmd1.unapply')
|
||||
lastCalled = null
|
||||
|
||||
undoMgr.undo()
|
||||
assert.ok(!lastCalled)
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(lastCalled, 'cmd1.apply')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(lastCalled, 'cmd2.apply')
|
||||
|
||||
undoMgr.redo()
|
||||
assert.equal(lastCalled, 'cmd3.apply')
|
||||
lastCalled = null
|
||||
|
||||
undoMgr.redo()
|
||||
assert.ok(!lastCalled)
|
||||
})
|
||||
|
||||
it('Test MoveElementCommand', function () {
|
||||
let move = new history.MoveElementCommand(this.div3, this.div1, this.divparent)
|
||||
assert.ok(move.unapply)
|
||||
assert.ok(move.apply)
|
||||
assert.equal(typeof move.unapply, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof move.apply, typeof function () { /* empty fn */ })
|
||||
|
||||
move.unapply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div3)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div1)
|
||||
assert.equal(this.divparent.lastElementChild, this.div2)
|
||||
|
||||
move.apply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div2)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
|
||||
move = new history.MoveElementCommand(this.div1, null, this.divparent)
|
||||
|
||||
move.unapply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div2)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div3)
|
||||
assert.equal(this.divparent.lastElementChild, this.div1)
|
||||
|
||||
move.apply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div2)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
|
||||
move = new history.MoveElementCommand(this.div2, this.div5, this.div4)
|
||||
|
||||
move.unapply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div3)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
assert.equal(this.div4.firstElementChild, this.div2)
|
||||
assert.equal(this.div4.firstElementChild.nextElementSibling, this.div5)
|
||||
|
||||
move.apply()
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.divparent.firstElementChild.nextElementSibling, this.div2)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
assert.equal(this.div4.firstElementChild, this.div5)
|
||||
assert.equal(this.div4.lastElementChild, this.div5)
|
||||
})
|
||||
|
||||
it('Test InsertElementCommand', function () {
|
||||
let insert = new history.InsertElementCommand(this.div3)
|
||||
assert.ok(insert.unapply)
|
||||
assert.ok(insert.apply)
|
||||
assert.equal(typeof insert.unapply, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof insert.apply, typeof function () { /* empty fn */ })
|
||||
|
||||
insert.unapply()
|
||||
assert.equal(this.divparent.childElementCount, 2)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.divparent.lastElementChild, this.div2)
|
||||
|
||||
insert.apply()
|
||||
assert.equal(this.divparent.childElementCount, 3)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
|
||||
insert = new history.InsertElementCommand(this.div2)
|
||||
|
||||
insert.unapply()
|
||||
assert.equal(this.divparent.childElementCount, 2)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div3)
|
||||
assert.equal(this.divparent.lastElementChild, this.div3)
|
||||
|
||||
insert.apply()
|
||||
assert.equal(this.divparent.childElementCount, 3)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
})
|
||||
|
||||
it('Test RemoveElementCommand', function () {
|
||||
const div6 = document.createElement('div')
|
||||
div6.id = 'div6'
|
||||
|
||||
let remove = new history.RemoveElementCommand(div6, null, this.divparent)
|
||||
assert.ok(remove.unapply)
|
||||
assert.ok(remove.apply)
|
||||
assert.equal(typeof remove.unapply, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof remove.apply, typeof function () { /* empty fn */ })
|
||||
|
||||
remove.unapply()
|
||||
assert.equal(this.divparent.childElementCount, 4)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
assert.equal(this.div3.nextElementSibling, div6)
|
||||
|
||||
remove.apply()
|
||||
assert.equal(this.divparent.childElementCount, 3)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
|
||||
remove = new history.RemoveElementCommand(div6, this.div2, this.divparent)
|
||||
|
||||
remove.unapply()
|
||||
assert.equal(this.divparent.childElementCount, 4)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, div6)
|
||||
assert.equal(div6.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
|
||||
remove.apply()
|
||||
assert.equal(this.divparent.childElementCount, 3)
|
||||
assert.equal(this.divparent.firstElementChild, this.div1)
|
||||
assert.equal(this.div1.nextElementSibling, this.div2)
|
||||
assert.equal(this.div2.nextElementSibling, this.div3)
|
||||
})
|
||||
|
||||
it('Test ChangeElementCommand', function () {
|
||||
this.div1.setAttribute('title', 'new title')
|
||||
let change = new history.ChangeElementCommand(this.div1,
|
||||
{ title: 'old title', class: 'foo' })
|
||||
assert.ok(change.unapply)
|
||||
assert.ok(change.apply)
|
||||
assert.equal(typeof change.unapply, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof change.apply, typeof function () { /* empty fn */ })
|
||||
|
||||
change.unapply()
|
||||
assert.equal(this.div1.getAttribute('title'), 'old title')
|
||||
assert.equal(this.div1.getAttribute('class'), 'foo')
|
||||
|
||||
change.apply()
|
||||
assert.equal(this.div1.getAttribute('title'), 'new title')
|
||||
assert.ok(!this.div1.getAttribute('class'))
|
||||
|
||||
this.div1.textContent = 'inner text'
|
||||
change = new history.ChangeElementCommand(this.div1,
|
||||
{ '#text': null })
|
||||
|
||||
change.unapply()
|
||||
assert.ok(!this.div1.textContent)
|
||||
|
||||
change.apply()
|
||||
assert.equal(this.div1.textContent, 'inner text')
|
||||
|
||||
this.div1.textContent = ''
|
||||
change = new history.ChangeElementCommand(this.div1,
|
||||
{ '#text': 'old text' })
|
||||
|
||||
change.unapply()
|
||||
assert.equal(this.div1.textContent, 'old text')
|
||||
|
||||
change.apply()
|
||||
assert.ok(!this.div1.textContent)
|
||||
|
||||
// TODO(codedread): Refactor this #href stuff in history.js and svgcanvas.js
|
||||
const rect = document.createElementNS(NS.SVG, 'rect')
|
||||
let justCalled = null
|
||||
let gethrefvalue = null
|
||||
let sethrefvalue = null
|
||||
utilities.mock({
|
||||
getHref (elem) {
|
||||
assert.equal(elem, rect)
|
||||
justCalled = 'getHref'
|
||||
return gethrefvalue
|
||||
},
|
||||
setHref (elem, val) {
|
||||
assert.equal(elem, rect)
|
||||
assert.equal(val, sethrefvalue)
|
||||
justCalled = 'setHref'
|
||||
},
|
||||
getRotationAngle () { return 0 }
|
||||
})
|
||||
|
||||
gethrefvalue = '#newhref'
|
||||
change = new history.ChangeElementCommand(rect,
|
||||
{ '#href': '#oldhref' })
|
||||
assert.equal(justCalled, 'getHref')
|
||||
|
||||
justCalled = null
|
||||
sethrefvalue = '#oldhref'
|
||||
change.unapply()
|
||||
assert.equal(justCalled, 'setHref')
|
||||
|
||||
justCalled = null
|
||||
sethrefvalue = '#newhref'
|
||||
change.apply()
|
||||
assert.equal(justCalled, 'setHref')
|
||||
|
||||
const line = document.createElementNS(NS.SVG, 'line')
|
||||
line.setAttribute('class', 'newClass')
|
||||
change = new history.ChangeElementCommand(line, { class: 'oldClass' })
|
||||
|
||||
assert.ok(change.unapply)
|
||||
assert.ok(change.apply)
|
||||
assert.equal(typeof change.unapply, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof change.apply, typeof function () { /* empty fn */ })
|
||||
|
||||
change.unapply()
|
||||
assert.equal(line.getAttribute('class'), 'oldClass')
|
||||
|
||||
change.apply()
|
||||
assert.equal(line.getAttribute('class'), 'newClass')
|
||||
})
|
||||
|
||||
it('Test BatchCommand', function () {
|
||||
let concatResult = ''
|
||||
MockCommand.prototype.apply = function () { concatResult += this.text }
|
||||
|
||||
const batch = new history.BatchCommand()
|
||||
assert.ok(batch.unapply)
|
||||
assert.ok(batch.apply)
|
||||
assert.ok(batch.addSubCommand)
|
||||
assert.ok(batch.isEmpty)
|
||||
assert.equal(typeof batch.unapply, 'function')
|
||||
assert.equal(typeof batch.apply, 'function')
|
||||
assert.equal(typeof batch.addSubCommand, 'function')
|
||||
assert.equal(typeof batch.isEmpty, 'function')
|
||||
|
||||
assert.ok(batch.isEmpty())
|
||||
|
||||
batch.addSubCommand(new MockCommand('a'))
|
||||
assert.ok(!batch.isEmpty())
|
||||
batch.addSubCommand(new MockCommand('b'))
|
||||
batch.addSubCommand(new MockCommand('c'))
|
||||
|
||||
assert.ok(!concatResult)
|
||||
batch.apply()
|
||||
assert.equal(concatResult, 'abc')
|
||||
|
||||
MockCommand.prototype.apply = function () { /* empty fn */ }
|
||||
MockCommand.prototype.unapply = function () { concatResult += this.text }
|
||||
concatResult = ''
|
||||
assert.ok(!concatResult)
|
||||
batch.unapply()
|
||||
assert.equal(concatResult, 'cba')
|
||||
|
||||
MockCommand.prototype.unapply = function () { /* empty fn */ }
|
||||
})
|
||||
})
|
||||
320
tests/unit/math.test.js
Normal file
320
tests/unit/math.test.js
Normal file
@@ -0,0 +1,320 @@
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as math from '../../packages/svgcanvas/core/math.js'
|
||||
|
||||
describe('math', function () {
|
||||
const svg = document.createElementNS(NS.SVG, 'svg')
|
||||
|
||||
before(() => {
|
||||
// Ensure the SVG element is attached to the document for transform list tests
|
||||
document.body.appendChild(svg)
|
||||
})
|
||||
|
||||
after(() => {
|
||||
// Cleanup
|
||||
document.body.removeChild(svg)
|
||||
})
|
||||
|
||||
it('Test svgedit.math package exports', function () {
|
||||
assert.ok(math, 'math module should exist')
|
||||
const expectedFunctions = [
|
||||
'transformPoint',
|
||||
'getTransformList',
|
||||
'isIdentity',
|
||||
'matrixMultiply',
|
||||
'hasMatrixTransform',
|
||||
'transformBox',
|
||||
'transformListToTransform',
|
||||
'getMatrix',
|
||||
'snapToAngle',
|
||||
'rectsIntersect'
|
||||
]
|
||||
expectedFunctions.forEach(fn => {
|
||||
assert.ok(
|
||||
typeof math[fn] === 'function',
|
||||
`Expected "${fn}" to be a function`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('Test svgedit.math.transformPoint() function', function () {
|
||||
const { transformPoint } = math
|
||||
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 1
|
||||
m.b = 0
|
||||
m.c = 0
|
||||
m.d = 1
|
||||
m.e = 0
|
||||
m.f = 0
|
||||
let pt = transformPoint(100, 200, m)
|
||||
assert.equal(pt.x, 100, 'X should be unchanged by identity matrix')
|
||||
assert.equal(pt.y, 200, 'Y should be unchanged by identity matrix')
|
||||
|
||||
m.e = 300
|
||||
m.f = 400
|
||||
pt = transformPoint(100, 200, m)
|
||||
assert.equal(pt.x, 400, 'X should be translated by 300')
|
||||
assert.equal(pt.y, 600, 'Y should be translated by 400')
|
||||
|
||||
m.a = 0.5
|
||||
m.b = 0.75
|
||||
m.c = 1.25
|
||||
m.d = 2
|
||||
pt = transformPoint(100, 200, m)
|
||||
assert.equal(
|
||||
pt.x,
|
||||
100 * m.a + 200 * m.c + m.e,
|
||||
'X should match matrix multiplication'
|
||||
)
|
||||
assert.equal(
|
||||
pt.y,
|
||||
100 * m.b + 200 * m.d + m.f,
|
||||
'Y should match matrix multiplication'
|
||||
)
|
||||
})
|
||||
|
||||
it('Test svgedit.math.isIdentity() function', function () {
|
||||
const { isIdentity } = math
|
||||
|
||||
assert.ok(
|
||||
isIdentity(svg.createSVGMatrix()),
|
||||
'Default matrix should be identity'
|
||||
)
|
||||
|
||||
const m = svg.createSVGMatrix()
|
||||
m.a = 1
|
||||
m.b = 0
|
||||
m.c = 0
|
||||
m.d = 1
|
||||
m.e = 0
|
||||
m.f = 0
|
||||
assert.ok(
|
||||
isIdentity(m),
|
||||
'Modified matrix matching identity values should be identity'
|
||||
)
|
||||
|
||||
m.e = 10
|
||||
assert.notOk(isIdentity(m), 'Matrix with translation is not identity')
|
||||
})
|
||||
|
||||
it('Test svgedit.math.matrixMultiply() function', function () {
|
||||
const { matrixMultiply, isIdentity } = math
|
||||
|
||||
// Test empty arguments
|
||||
const iDefault = matrixMultiply()
|
||||
assert.ok(
|
||||
isIdentity(iDefault),
|
||||
'No arguments should return identity matrix'
|
||||
)
|
||||
|
||||
// Translate there and back
|
||||
const tr1 = svg.createSVGMatrix().translate(100, 50)
|
||||
const tr2 = svg.createSVGMatrix().translate(-90, 0)
|
||||
const tr3 = svg.createSVGMatrix().translate(-10, -50)
|
||||
let I = matrixMultiply(tr1, tr2, tr3)
|
||||
assert.ok(isIdentity(I), 'Translating there and back should yield identity')
|
||||
|
||||
// Rotate there and back
|
||||
const rotThere = svg.createSVGMatrix().rotate(90)
|
||||
const rotBack = svg.createSVGMatrix().rotate(-90)
|
||||
I = matrixMultiply(rotThere, rotBack)
|
||||
assert.ok(isIdentity(I), 'Rotating and rotating back should yield identity')
|
||||
|
||||
// Scale up and down
|
||||
const scaleUp = svg.createSVGMatrix().scale(4)
|
||||
const scaleDownX = svg.createSVGMatrix().scaleNonUniform(0.25, 1)
|
||||
const scaleDownY = svg.createSVGMatrix().scaleNonUniform(1, 0.25)
|
||||
I = matrixMultiply(scaleUp, scaleDownX, scaleDownY)
|
||||
assert.ok(
|
||||
isIdentity(I),
|
||||
'Scaling up and then scaling down back to original should yield identity'
|
||||
)
|
||||
|
||||
// Multiplying a matrix by its inverse
|
||||
const someMatrix = svg
|
||||
.createSVGMatrix()
|
||||
.rotate(33)
|
||||
.translate(100, 200)
|
||||
.scale(2)
|
||||
I = matrixMultiply(someMatrix, someMatrix.inverse())
|
||||
console.log(I)
|
||||
console.log('-----------------------------------------')
|
||||
assert.ok(
|
||||
isIdentity(I),
|
||||
'Matrix multiplied by its inverse should be identity'
|
||||
)
|
||||
})
|
||||
|
||||
it('Test svgedit.math.transformBox() function', function () {
|
||||
const { transformBox } = math
|
||||
|
||||
const m = svg.createSVGMatrix()
|
||||
// Identity
|
||||
const r = transformBox(10, 10, 200, 300, m)
|
||||
assert.equal(r.tl.x, 10, 'Top-left X should be 10')
|
||||
assert.equal(r.tl.y, 10, 'Top-left Y should be 10')
|
||||
assert.equal(r.tr.x, 210, 'Top-right X should be 210')
|
||||
assert.equal(r.tr.y, 10, 'Top-right Y should be 10')
|
||||
assert.equal(r.bl.x, 10, 'Bottom-left X should be 10')
|
||||
assert.equal(r.bl.y, 310, 'Bottom-left Y should be 310')
|
||||
assert.equal(r.br.x, 210, 'Bottom-right X should be 210')
|
||||
assert.equal(r.br.y, 310, 'Bottom-right Y should be 310')
|
||||
assert.equal(r.aabox.x, 10, 'AABBox X should be 10')
|
||||
assert.equal(r.aabox.y, 10, 'AABBox Y should be 10')
|
||||
assert.equal(r.aabox.width, 200, 'AABBox width should be 200')
|
||||
assert.equal(r.aabox.height, 300, 'AABBox height should be 300')
|
||||
|
||||
// Transformed box
|
||||
m.e = 50
|
||||
m.f = 50
|
||||
const r2 = transformBox(0, 0, 100, 100, m)
|
||||
assert.equal(r2.aabox.x, 50, 'AABBox x should be translated by 50')
|
||||
assert.equal(r2.aabox.y, 50, 'AABBox y should be translated by 50')
|
||||
})
|
||||
|
||||
it('Test svgedit.math.getTransformList() and hasMatrixTransform() functions', function () {
|
||||
const { getTransformList, hasMatrixTransform } = math
|
||||
|
||||
// An element with no transform
|
||||
const rect = document.createElementNS(NS.SVG, 'rect')
|
||||
svg.appendChild(rect)
|
||||
const tlist = getTransformList(rect)
|
||||
assert.ok(tlist, 'Should get a transform list (empty)')
|
||||
assert.equal(tlist.numberOfItems, 0, 'Transform list should be empty')
|
||||
assert.notOk(
|
||||
hasMatrixTransform(tlist),
|
||||
'No matrix transform in an empty transform list'
|
||||
)
|
||||
|
||||
// Add a non-identity matrix transform
|
||||
const nonIdentityMatrix = svg.createSVGMatrix().translate(10, 20).scale(2)
|
||||
const tf = svg.createSVGTransformFromMatrix(nonIdentityMatrix)
|
||||
tlist.appendItem(tf)
|
||||
assert.equal(tlist.numberOfItems, 1, 'Transform list should have one item')
|
||||
assert.ok(
|
||||
hasMatrixTransform(tlist),
|
||||
'Non-identity matrix transform should be detected'
|
||||
)
|
||||
|
||||
// Add an identity transform
|
||||
const tfIdentity = svg.createSVGTransformFromMatrix(svg.createSVGMatrix()) // identity matrix
|
||||
tlist.appendItem(tfIdentity)
|
||||
assert.equal(
|
||||
tlist.numberOfItems,
|
||||
2,
|
||||
'Transform list should have two items now'
|
||||
)
|
||||
// Still should have a non-identity matrix transform present
|
||||
assert.ok(
|
||||
hasMatrixTransform(tlist),
|
||||
'Still have a non-identity matrix transform after adding an identity transform'
|
||||
)
|
||||
|
||||
// Cleanup
|
||||
svg.removeChild(rect)
|
||||
})
|
||||
|
||||
it('Test svgedit.math.transformListToTransform() and getMatrix() functions', function () {
|
||||
const { transformListToTransform, getMatrix } = math
|
||||
|
||||
const g = document.createElementNS(NS.SVG, 'g')
|
||||
svg.appendChild(g)
|
||||
|
||||
const tlist = g.transform.baseVal
|
||||
const m1 = svg.createSVGTransformFromMatrix(
|
||||
svg.createSVGMatrix().translate(10, 20)
|
||||
)
|
||||
const m2 = svg.createSVGTransformFromMatrix(
|
||||
svg.createSVGMatrix().rotate(45)
|
||||
)
|
||||
tlist.appendItem(m1)
|
||||
tlist.appendItem(m2)
|
||||
|
||||
const consolidated = transformListToTransform(tlist)
|
||||
const expected = m1.matrix.multiply(m2.matrix)
|
||||
assert.equal(
|
||||
consolidated.matrix.a,
|
||||
expected.a,
|
||||
'Consolidated matrix a should match expected'
|
||||
)
|
||||
assert.equal(
|
||||
consolidated.matrix.d,
|
||||
expected.d,
|
||||
'Consolidated matrix d should match expected'
|
||||
)
|
||||
|
||||
const elemMatrix = getMatrix(g)
|
||||
assert.equal(
|
||||
elemMatrix.a,
|
||||
expected.a,
|
||||
'Element matrix a should match expected'
|
||||
)
|
||||
assert.equal(
|
||||
elemMatrix.d,
|
||||
expected.d,
|
||||
'Element matrix d should match expected'
|
||||
)
|
||||
|
||||
svg.removeChild(g)
|
||||
})
|
||||
|
||||
it('Test svgedit.math.snapToAngle() function', function () {
|
||||
const { snapToAngle } = math
|
||||
|
||||
const result = snapToAngle(0, 0, 10, 0) // Expect snap to 0 degrees
|
||||
assert.equal(
|
||||
result.x,
|
||||
10,
|
||||
'Snapped x should remain 10 when angle is already at 0°'
|
||||
)
|
||||
assert.equal(
|
||||
result.y,
|
||||
0,
|
||||
'Snapped y should remain 0 when angle is already at 0°'
|
||||
)
|
||||
|
||||
// 45-degree snap from an angle close to 45° (e.g., 50°)
|
||||
const angleDegrees = 50
|
||||
const angleRadians = angleDegrees * (Math.PI / 180)
|
||||
const dx = Math.cos(angleRadians) * 100
|
||||
const dy = Math.sin(angleRadians) * 100
|
||||
const snapped = snapToAngle(0, 0, dx, dy)
|
||||
// Should snap to exactly 45°
|
||||
const expectedAngle = Math.PI / 4
|
||||
const dist = Math.hypot(dx, dy)
|
||||
assert.closeTo(
|
||||
snapped.x,
|
||||
dist * Math.cos(expectedAngle),
|
||||
0.00001,
|
||||
'X should be close to 45° projection'
|
||||
)
|
||||
assert.closeTo(
|
||||
snapped.y,
|
||||
dist * Math.sin(expectedAngle),
|
||||
0.00001,
|
||||
'Y should be close to 45° projection'
|
||||
)
|
||||
})
|
||||
|
||||
it('Test svgedit.math.rectsIntersect() function', function () {
|
||||
const { rectsIntersect } = math
|
||||
const r1 = { x: 0, y: 0, width: 50, height: 50 }
|
||||
const r2 = { x: 25, y: 25, width: 50, height: 50 }
|
||||
const r3 = { x: 100, y: 100, width: 10, height: 10 }
|
||||
|
||||
assert.ok(rectsIntersect(r1, r2), 'Rectangles overlapping should intersect')
|
||||
assert.notOk(
|
||||
rectsIntersect(r1, r3),
|
||||
'Non-overlapping rectangles should not intersect'
|
||||
)
|
||||
|
||||
// Edge case: touching edges
|
||||
const r4 = { x: 50, y: 0, width: 50, height: 50 }
|
||||
// Note: Depending on interpretation, touching at the border might be considered intersecting or not.
|
||||
// The given function checks strict overlapping (not just touching), so this should return false.
|
||||
assert.notOk(
|
||||
rectsIntersect(r1, r4),
|
||||
'Rectangles touching at the edge should not be considered intersecting'
|
||||
)
|
||||
})
|
||||
})
|
||||
182
tests/unit/path.test.js
Normal file
182
tests/unit/path.test.js
Normal file
@@ -0,0 +1,182 @@
|
||||
/* globals SVGPathSeg */
|
||||
import 'pathseg'
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as utilities from '../../packages/svgcanvas/core/utilities.js'
|
||||
import * as pathModule from '../../packages/svgcanvas/core/path.js'
|
||||
import { Path, Segment } from '../../packages/svgcanvas/core/path-method.js'
|
||||
import { init as unitsInit } from '../../packages/svgcanvas/core/units.js'
|
||||
|
||||
describe('path', function () {
|
||||
/**
|
||||
* @typedef {GenericArray} EditorContexts
|
||||
* @property {module:path.EditorContext} 0
|
||||
* @property {module:path.EditorContext} 1
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {SVGSVGElement} [svg]
|
||||
* @returns {EditorContexts}
|
||||
*/
|
||||
function getMockContexts (svg) {
|
||||
svg = svg || document.createElementNS(NS.SVG, 'svg')
|
||||
const selectorParentGroup = document.createElementNS(NS.SVG, 'g')
|
||||
selectorParentGroup.setAttribute('id', 'selectorParentGroup')
|
||||
svg.append(selectorParentGroup)
|
||||
return [
|
||||
/**
|
||||
* @implements {module:path.EditorContext}
|
||||
*/
|
||||
{
|
||||
getSvgRoot () { return svg },
|
||||
getZoom () { return 1 }
|
||||
},
|
||||
/**
|
||||
* @implements {module:utilities.EditorContext}
|
||||
*/
|
||||
{
|
||||
getDOMDocument () { return svg },
|
||||
getDOMContainer () { return svg },
|
||||
getSvgRoot () { return svg }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
it('Test svgedit.path.replacePathSeg', function () {
|
||||
const path = document.createElementNS(NS.SVG, 'path')
|
||||
path.setAttribute('d', 'M0,0 L10,11 L20,21Z')
|
||||
|
||||
const [mockPathContext, mockUtilitiesContext] = getMockContexts()
|
||||
pathModule.init(mockPathContext)
|
||||
utilities.init(mockUtilitiesContext)
|
||||
new Path(path) // eslint-disable-line no-new
|
||||
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L')
|
||||
assert.equal(path.pathSegList.getItem(1).x, 10)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 11)
|
||||
|
||||
pathModule.replacePathSeg(SVGPathSeg.PATHSEG_LINETO_REL, 1, [30, 31], path)
|
||||
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'l')
|
||||
assert.equal(path.pathSegList.getItem(1).x, 30)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 31)
|
||||
})
|
||||
|
||||
it('Test svgedit.path.Segment.setType simple', function () {
|
||||
const path = document.createElementNS(NS.SVG, 'path')
|
||||
path.setAttribute('d', 'M0,0 L10,11 L20,21Z')
|
||||
|
||||
const [mockPathContext, mockUtilitiesContext] = getMockContexts()
|
||||
pathModule.init(mockPathContext)
|
||||
utilities.init(mockUtilitiesContext)
|
||||
new Path(path) // eslint-disable-line no-new
|
||||
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L')
|
||||
assert.equal(path.pathSegList.getItem(1).x, 10)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 11)
|
||||
|
||||
const segment = new Segment(1, path.pathSegList.getItem(1))
|
||||
segment.setType(SVGPathSeg.PATHSEG_LINETO_REL, [30, 31])
|
||||
assert.equal(segment.item.pathSegTypeAsLetter, 'l')
|
||||
assert.equal(segment.item.x, 30)
|
||||
assert.equal(segment.item.y, 31)
|
||||
|
||||
// Also verify that the actual path changed.
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'l')
|
||||
assert.equal(path.pathSegList.getItem(1).x, 30)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 31)
|
||||
})
|
||||
|
||||
it('Test svgedit.path.Segment.setType with control points', function () {
|
||||
// Setup the dom for a mock control group.
|
||||
const svg = document.createElementNS(NS.SVG, 'svg')
|
||||
const path = document.createElementNS(NS.SVG, 'path')
|
||||
path.setAttribute('d', 'M0,0 C11,12 13,14 15,16 Z')
|
||||
svg.append(path)
|
||||
|
||||
const [mockPathContext, mockUtilitiesContext] = getMockContexts(svg)
|
||||
pathModule.init(mockPathContext)
|
||||
utilities.init(mockUtilitiesContext)
|
||||
const segment = new Segment(1, path.pathSegList.getItem(1))
|
||||
segment.path = new Path(path)
|
||||
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C')
|
||||
assert.equal(path.pathSegList.getItem(1).x1, 11)
|
||||
assert.equal(path.pathSegList.getItem(1).y1, 12)
|
||||
assert.equal(path.pathSegList.getItem(1).x2, 13)
|
||||
assert.equal(path.pathSegList.getItem(1).y2, 14)
|
||||
assert.equal(path.pathSegList.getItem(1).x, 15)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 16)
|
||||
|
||||
segment.setType(SVGPathSeg.PATHSEG_CURVETO_CUBIC_REL, [30, 31, 32, 33, 34, 35])
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'c')
|
||||
assert.equal(path.pathSegList.getItem(1).x1, 32)
|
||||
assert.equal(path.pathSegList.getItem(1).y1, 33)
|
||||
assert.equal(path.pathSegList.getItem(1).x2, 34)
|
||||
assert.equal(path.pathSegList.getItem(1).y2, 35)
|
||||
assert.equal(path.pathSegList.getItem(1).x, 30)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 31)
|
||||
})
|
||||
|
||||
it('Test svgedit.path.Segment.move', function () {
|
||||
const path = document.createElementNS(NS.SVG, 'path')
|
||||
path.setAttribute('d', 'M0,0 L10,11 L20,21Z')
|
||||
|
||||
const [mockPathContext, mockUtilitiesContext] = getMockContexts()
|
||||
pathModule.init(mockPathContext)
|
||||
utilities.init(mockUtilitiesContext)
|
||||
new Path(path) // eslint-disable-line no-new
|
||||
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L')
|
||||
assert.equal(path.pathSegList.getItem(1).x, 10)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 11)
|
||||
|
||||
const segment = new Segment(1, path.pathSegList.getItem(1))
|
||||
segment.move(-3, 4)
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'L')
|
||||
assert.equal(path.pathSegList.getItem(1).x, 7)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 15)
|
||||
})
|
||||
|
||||
it('Test svgedit.path.Segment.moveCtrl', function () {
|
||||
const path = document.createElementNS(NS.SVG, 'path')
|
||||
path.setAttribute('d', 'M0,0 C11,12 13,14 15,16 Z')
|
||||
|
||||
const [mockPathContext, mockUtilitiesContext] = getMockContexts()
|
||||
pathModule.init(mockPathContext)
|
||||
utilities.init(mockUtilitiesContext)
|
||||
new Path(path) // eslint-disable-line no-new
|
||||
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C')
|
||||
assert.equal(path.pathSegList.getItem(1).x1, 11)
|
||||
assert.equal(path.pathSegList.getItem(1).y1, 12)
|
||||
assert.equal(path.pathSegList.getItem(1).x2, 13)
|
||||
assert.equal(path.pathSegList.getItem(1).y2, 14)
|
||||
assert.equal(path.pathSegList.getItem(1).x, 15)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 16)
|
||||
|
||||
const segment = new Segment(1, path.pathSegList.getItem(1))
|
||||
segment.moveCtrl(1, 100, -200)
|
||||
assert.equal(path.pathSegList.getItem(1).pathSegTypeAsLetter, 'C')
|
||||
assert.equal(path.pathSegList.getItem(1).x1, 111)
|
||||
assert.equal(path.pathSegList.getItem(1).y1, -188)
|
||||
assert.equal(path.pathSegList.getItem(1).x2, 13)
|
||||
assert.equal(path.pathSegList.getItem(1).y2, 14)
|
||||
assert.equal(path.pathSegList.getItem(1).x, 15)
|
||||
assert.equal(path.pathSegList.getItem(1).y, 16)
|
||||
})
|
||||
|
||||
it('Test svgedit.path.convertPath', function () {
|
||||
unitsInit({
|
||||
getRoundDigits () { return 5 }
|
||||
})
|
||||
|
||||
const path = document.createElementNS(NS.SVG, 'path')
|
||||
path.setAttribute('d', 'M40,55h20v20')
|
||||
|
||||
const abs = pathModule.convertPath(path)
|
||||
assert.equal(abs, 'M40,55L60,55L60,75')
|
||||
|
||||
const rel = pathModule.convertPath(path, true)
|
||||
assert.equal(rel, 'm40,55l20,0l0,20')
|
||||
})
|
||||
})
|
||||
176
tests/unit/recalculate.test.js
Normal file
176
tests/unit/recalculate.test.js
Normal file
@@ -0,0 +1,176 @@
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as utilities from '../../packages/svgcanvas/core/utilities.js'
|
||||
import * as coords from '../../packages/svgcanvas/core/coords.js'
|
||||
import * as recalculate from '../../packages/svgcanvas/core/recalculate.js'
|
||||
|
||||
describe('recalculate', function () {
|
||||
const root = document.createElement('div')
|
||||
root.id = 'root'
|
||||
root.style.visibility = 'hidden'
|
||||
|
||||
const svgroot = document.createElementNS(NS.SVG, 'svg')
|
||||
svgroot.id = 'svgroot'
|
||||
root.append(svgroot)
|
||||
const svg = document.createElementNS(NS.SVG, 'svg')
|
||||
svgroot.append(svg)
|
||||
|
||||
const dataStorage = {
|
||||
_storage: new WeakMap(),
|
||||
put: function (element, key, obj) {
|
||||
if (!this._storage.has(element)) {
|
||||
this._storage.set(element, new Map())
|
||||
}
|
||||
this._storage.get(element).set(key, obj)
|
||||
},
|
||||
get: function (element, key) {
|
||||
return this._storage.get(element).get(key)
|
||||
},
|
||||
has: function (element, key) {
|
||||
return this._storage.has(element) && this._storage.get(element).has(key)
|
||||
},
|
||||
remove: function (element, key) {
|
||||
const ret = this._storage.get(element).delete(key)
|
||||
if (!this._storage.get(element).size === 0) {
|
||||
this._storage.delete(element)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
let elemId = 1
|
||||
|
||||
/**
|
||||
* Initilize modules to set up the tests.
|
||||
* @returns {void}
|
||||
*/
|
||||
function setUp () {
|
||||
utilities.init(
|
||||
/**
|
||||
* @implements {module:utilities.EditorContext}
|
||||
*/
|
||||
{
|
||||
getSvgRoot () { return svg },
|
||||
getDOMDocument () { return null },
|
||||
getDOMContainer () { return null },
|
||||
getDataStorage () { return dataStorage }
|
||||
}
|
||||
)
|
||||
coords.init(
|
||||
/**
|
||||
* @implements {module:coords.EditorContext}
|
||||
*/
|
||||
{
|
||||
getGridSnapping () { return false },
|
||||
getDrawing () {
|
||||
return {
|
||||
getNextId () { return String(elemId++) }
|
||||
}
|
||||
},
|
||||
getDataStorage () { return dataStorage }
|
||||
}
|
||||
)
|
||||
recalculate.init(
|
||||
/**
|
||||
* @implements {module:recalculate.EditorContext}
|
||||
*/
|
||||
{
|
||||
getSvgRoot () { return svg },
|
||||
getStartTransform () { return '' },
|
||||
setStartTransform () { /* empty fn */ },
|
||||
getDataStorage () { return dataStorage }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let elem
|
||||
|
||||
/**
|
||||
* Initialize for tests and set up `rect` element.
|
||||
* @returns {void}
|
||||
*/
|
||||
function setUpRect () {
|
||||
setUp()
|
||||
elem = document.createElementNS(NS.SVG, 'rect')
|
||||
elem.setAttribute('x', '200')
|
||||
elem.setAttribute('y', '150')
|
||||
elem.setAttribute('width', '250')
|
||||
elem.setAttribute('height', '120')
|
||||
svg.append(elem)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize for tests and set up `text` element with `tspan` child.
|
||||
* @returns {void}
|
||||
*/
|
||||
function setUpTextWithTspan () {
|
||||
setUp()
|
||||
elem = document.createElementNS(NS.SVG, 'text')
|
||||
elem.setAttribute('x', '200')
|
||||
elem.setAttribute('y', '150')
|
||||
|
||||
const tspan = document.createElementNS(NS.SVG, 'tspan')
|
||||
tspan.setAttribute('x', '200')
|
||||
tspan.setAttribute('y', '150')
|
||||
|
||||
const theText = 'Foo bar'
|
||||
tspan.append(theText)
|
||||
elem.append(tspan)
|
||||
svg.append(elem)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tear down the tests (empty the svg element).
|
||||
* @returns {void}
|
||||
*/
|
||||
afterEach(() => {
|
||||
while (svg.hasChildNodes()) {
|
||||
svg.firstChild.remove()
|
||||
}
|
||||
})
|
||||
|
||||
it('Test recalculateDimensions() on rect with identity matrix', function () {
|
||||
setUpRect()
|
||||
elem.setAttribute('transform', 'matrix(1,0,0,1,0,0)')
|
||||
|
||||
recalculate.recalculateDimensions(elem)
|
||||
|
||||
// Ensure that the identity matrix is swallowed and the element has no
|
||||
// transform on it.
|
||||
assert.equal(elem.hasAttribute('transform'), false)
|
||||
})
|
||||
|
||||
it('Test recalculateDimensions() on rect with simple translate', function () {
|
||||
setUpRect()
|
||||
elem.setAttribute('transform', 'translate(100,50)')
|
||||
|
||||
recalculate.recalculateDimensions(elem)
|
||||
|
||||
assert.equal(elem.hasAttribute('transform'), false)
|
||||
assert.equal(elem.getAttribute('x'), '300')
|
||||
assert.equal(elem.getAttribute('y'), '200')
|
||||
assert.equal(elem.getAttribute('width'), '250')
|
||||
assert.equal(elem.getAttribute('height'), '120')
|
||||
})
|
||||
|
||||
it('Test recalculateDimensions() on text w/tspan with simple translate', function () {
|
||||
setUpTextWithTspan()
|
||||
elem.setAttribute('transform', 'translate(100,50)')
|
||||
|
||||
recalculate.recalculateDimensions(elem)
|
||||
|
||||
// Ensure that the identity matrix is swallowed and the element has no
|
||||
// transform on it.
|
||||
assert.equal(elem.hasAttribute('transform'), false)
|
||||
assert.equal(elem.getAttribute('x'), '300')
|
||||
assert.equal(elem.getAttribute('y'), '200')
|
||||
|
||||
const tspan = elem.firstElementChild
|
||||
assert.equal(tspan.getAttribute('x'), '300')
|
||||
assert.equal(tspan.getAttribute('y'), '200')
|
||||
})
|
||||
|
||||
// TODO: Since recalculateDimensions() and surrounding code is
|
||||
// probably the largest, most complicated and strange piece of
|
||||
// code in SVG-edit, we need to write a whole lot of unit tests
|
||||
// for it here.
|
||||
})
|
||||
17
tests/unit/sanitize.test.js
Normal file
17
tests/unit/sanitize.test.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as sanitize from '../../packages/svgcanvas/core/sanitize.js'
|
||||
|
||||
describe('sanitize', function () {
|
||||
const svg = document.createElementNS(NS.SVG, 'svg')
|
||||
|
||||
it('Test sanitizeSvg() strips ws from style attr', function () {
|
||||
const rect = document.createElementNS(NS.SVG, 'rect')
|
||||
rect.setAttribute('style', 'stroke: blue ;\t\tstroke-width :\t\t40;')
|
||||
// sanitizeSvg() requires the node to have a parent and a document.
|
||||
svg.append(rect)
|
||||
sanitize.sanitizeSvg(rect)
|
||||
|
||||
assert.equal(rect.getAttribute('stroke'), 'blue')
|
||||
assert.equal(rect.getAttribute('stroke-width'), '40')
|
||||
})
|
||||
})
|
||||
152
tests/unit/select.test.js
Normal file
152
tests/unit/select.test.js
Normal file
@@ -0,0 +1,152 @@
|
||||
import * as select from '../../packages/svgcanvas/core/select.js'
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
|
||||
describe('select', function () {
|
||||
const sandbox = document.createElement('div')
|
||||
sandbox.id = 'sandbox'
|
||||
|
||||
let svgroot
|
||||
let svgContent
|
||||
const mockConfig = {
|
||||
dimensions: [640, 480]
|
||||
}
|
||||
const dataStorage = {
|
||||
_storage: new WeakMap(),
|
||||
put: function (element, key, obj) {
|
||||
if (!this._storage.has(element)) {
|
||||
this._storage.set(element, new Map())
|
||||
}
|
||||
this._storage.get(element).set(key, obj)
|
||||
},
|
||||
get: function (element, key) {
|
||||
return this._storage.get(element).get(key)
|
||||
},
|
||||
has: function (element, key) {
|
||||
return this._storage.has(element) && this._storage.get(element).has(key)
|
||||
},
|
||||
remove: function (element, key) {
|
||||
const ret = this._storage.get(element).delete(key)
|
||||
if (!this._storage.get(element).size === 0) {
|
||||
this._storage.delete(element)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements {module:select.SVGFactory}
|
||||
*/
|
||||
const mockSvgCanvas = {
|
||||
curConfig: mockConfig,
|
||||
createSVGElement (jsonMap) {
|
||||
const elem = document.createElementNS(NS.SVG, jsonMap.element)
|
||||
Object.entries(jsonMap.attr).forEach(([attr, value]) => {
|
||||
elem.setAttribute(attr, value)
|
||||
})
|
||||
return elem
|
||||
},
|
||||
getSvgRoot () { return svgroot },
|
||||
getSvgContent () { return svgContent },
|
||||
getDataStorage () { return dataStorage }
|
||||
}
|
||||
|
||||
/**
|
||||
* Potentially reusable test set-up.
|
||||
* @returns {void}
|
||||
*/
|
||||
beforeEach(() => {
|
||||
svgroot = mockSvgCanvas.createSVGElement({
|
||||
element: 'svg',
|
||||
attr: { id: 'svgroot' }
|
||||
})
|
||||
svgContent = mockSvgCanvas.createSVGElement({
|
||||
element: 'svg',
|
||||
attr: { id: 'svgcontent' }
|
||||
})
|
||||
|
||||
svgroot.append(svgContent)
|
||||
/* const rect = */ svgContent.append(
|
||||
mockSvgCanvas.createSVGElement({
|
||||
element: 'rect',
|
||||
attr: {
|
||||
id: 'rect',
|
||||
x: '50',
|
||||
y: '75',
|
||||
width: '200',
|
||||
height: '100'
|
||||
}
|
||||
})
|
||||
)
|
||||
sandbox.append(svgroot)
|
||||
})
|
||||
|
||||
/*
|
||||
function setUpWithInit () {
|
||||
select.init(mockConfig, mockFactory);
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tear down the test by emptying our sandbox area.
|
||||
* @returns {void}
|
||||
*/
|
||||
afterEach(() => {
|
||||
while (sandbox.hasChildNodes()) {
|
||||
sandbox.firstChild.remove()
|
||||
}
|
||||
})
|
||||
|
||||
it('Test svgedit.select package', function () {
|
||||
assert.ok(select)
|
||||
assert.ok(select.Selector)
|
||||
assert.ok(select.SelectorManager)
|
||||
assert.ok(select.init)
|
||||
assert.ok(select.getSelectorManager)
|
||||
assert.equal(typeof select, typeof {})
|
||||
assert.equal(typeof select.Selector, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof select.SelectorManager, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof select.init, typeof function () { /* empty fn */ })
|
||||
assert.equal(typeof select.getSelectorManager, typeof function () { /* empty fn */ })
|
||||
})
|
||||
|
||||
it('Test Selector DOM structure', function () {
|
||||
assert.ok(svgroot)
|
||||
assert.ok(svgroot.hasChildNodes())
|
||||
|
||||
// Verify non-existence of Selector DOM nodes
|
||||
assert.equal(svgroot.childNodes.length, 1)
|
||||
assert.equal(svgroot.childNodes.item(0), svgContent)
|
||||
assert.ok(!svgroot.querySelector('#selectorParentGroup'))
|
||||
|
||||
select.init(mockSvgCanvas)
|
||||
|
||||
assert.equal(svgroot.childNodes.length, 3)
|
||||
|
||||
// Verify existence of canvas background.
|
||||
const cb = svgroot.childNodes.item(0)
|
||||
assert.ok(cb)
|
||||
assert.equal(cb.id, 'canvasBackground')
|
||||
|
||||
assert.ok(svgroot.childNodes.item(1))
|
||||
assert.equal(svgroot.childNodes.item(1), svgContent)
|
||||
|
||||
// Verify existence of selectorParentGroup.
|
||||
const spg = svgroot.childNodes.item(2)
|
||||
assert.ok(spg)
|
||||
assert.equal(svgroot.querySelector('#selectorParentGroup'), spg)
|
||||
assert.equal(spg.id, 'selectorParentGroup')
|
||||
assert.equal(spg.tagName, 'g')
|
||||
|
||||
// Verify existence of all grip elements.
|
||||
assert.ok(spg.querySelector('#selectorGrip_resize_nw'))
|
||||
assert.ok(spg.querySelector('#selectorGrip_resize_n'))
|
||||
assert.ok(spg.querySelector('#selectorGrip_resize_ne'))
|
||||
assert.ok(spg.querySelector('#selectorGrip_resize_e'))
|
||||
assert.ok(spg.querySelector('#selectorGrip_resize_se'))
|
||||
assert.ok(spg.querySelector('#selectorGrip_resize_s'))
|
||||
assert.ok(spg.querySelector('#selectorGrip_resize_sw'))
|
||||
assert.ok(spg.querySelector('#selectorGrip_resize_w'))
|
||||
assert.ok(spg.querySelector('#selectorGrip_rotateconnector'))
|
||||
assert.ok(spg.querySelector('#selectorGrip_rotate'))
|
||||
})
|
||||
})
|
||||
266
tests/unit/test1.test.js
Normal file
266
tests/unit/test1.test.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/* eslint-disable max-len, no-console */
|
||||
import SvgCanvas from '../../packages/svgcanvas'
|
||||
|
||||
describe('Basic Module', function () {
|
||||
// helper functions
|
||||
/*
|
||||
const isIdentity = function (m) {
|
||||
return (m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0);
|
||||
};
|
||||
const matrixString = function (m) {
|
||||
return [m.a, m.b, m.c, m.d, m.e, m.f].join(',');
|
||||
};
|
||||
*/
|
||||
|
||||
let svgCanvas
|
||||
|
||||
const
|
||||
// svgroot = document.getElementById('svgroot'),
|
||||
// svgdoc = svgroot.documentElement,
|
||||
svgns = 'http://www.w3.org/2000/svg'
|
||||
const xlinkns = 'http://www.w3.org/1999/xlink'
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.textContent = ''
|
||||
const svgEditor = document.createElement('div')
|
||||
svgEditor.id = 'svg_editor'
|
||||
const svgcanvas = document.createElement('div')
|
||||
svgcanvas.style.visibility = 'hidden'
|
||||
svgcanvas.id = 'svgcanvas'
|
||||
const workarea = document.createElement('div')
|
||||
workarea.id = 'workarea'
|
||||
workarea.append(svgcanvas)
|
||||
const toolsLeft = document.createElement('div')
|
||||
toolsLeft.id = 'tools_left'
|
||||
|
||||
svgEditor.append(workarea, toolsLeft)
|
||||
document.body.append(svgEditor)
|
||||
|
||||
svgCanvas = new SvgCanvas(
|
||||
document.getElementById('svgcanvas'), {
|
||||
canvas_expansion: 3,
|
||||
dimensions: [640, 480],
|
||||
initFill: {
|
||||
color: 'FF0000', // solid red
|
||||
opacity: 1
|
||||
},
|
||||
initStroke: {
|
||||
width: 5,
|
||||
color: '000000', // solid black
|
||||
opacity: 1
|
||||
},
|
||||
initOpacity: 1,
|
||||
imgPath: '../editor/images',
|
||||
langPath: 'locale/',
|
||||
extPath: 'extensions/',
|
||||
extensions: ['ext-arrows.js', 'ext-eyedropper.js'],
|
||||
initTool: 'select',
|
||||
wireframe: false
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('Test existence of SvgCanvas object', function () {
|
||||
assert.equal(typeof {}, typeof svgCanvas)
|
||||
})
|
||||
|
||||
describe('Path Module', function () {
|
||||
it('Test path conversion from absolute to relative', function () {
|
||||
const convert = svgCanvas.pathActions.convertPath
|
||||
|
||||
// TODO: Test these paths:
|
||||
// "m400.00491,625.01379a1.78688,1.78688 0 1 1-3.57373,0a1.78688,1.78688 0 1 13.57373,0z"
|
||||
// "m36.812,15.8566c-28.03099,0 -26.28099,12.15601 -26.28099,12.15601l0.03099,12.59399h26.75v3.781h-37.37399c0,0 -17.938,-2.034 -133.00001,26.25c115.06201,28.284 130.71801,27.281 130.71801,27.281h9.34399v-13.125c0,0 -0.504,-15.656 15.40601,-15.656h26.532c0,0 14.90599,0.241 14.90599,-14.406v-24.219c0,0 2.263,-14.65601 -27.032,-14.65601zm-14.75,8.4684c2.662,0 4.813,2.151 4.813,4.813c0,2.661 -2.151,4.812 -4.813,4.812c-2.661,0 -4.812,-2.151 -4.812,-4.812c0,-2.662 2.151,-4.813 4.812,-4.813z"
|
||||
// "m 0,0 l 200,0 l 0,100 L 0,100"
|
||||
|
||||
svgCanvas.setSvgString(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' width='400' x='300'>" +
|
||||
"<path id='p1' d='M100,100 L200,100 L100,100Z'/>" +
|
||||
"<path id='p2' d='m 0,0 l 200,0 l 0,100 L 0,100'/>" +
|
||||
'</svg>'
|
||||
)
|
||||
|
||||
const p1 = document.getElementById('p1')
|
||||
const p2 = document.getElementById('p2')
|
||||
const dAbs = p1.getAttribute('d')
|
||||
const seglist = p1.pathSegList
|
||||
|
||||
assert.equal(p1.nodeName, 'path', "Expected 'path', got")
|
||||
|
||||
assert.equal(seglist.numberOfItems, 4, 'Number of segments before conversion')
|
||||
|
||||
// verify segments before conversion
|
||||
let curseg = seglist.getItem(0)
|
||||
assert.equal(curseg.pathSegTypeAsLetter.toUpperCase(), 'M', 'Before conversion, segment #1 type')
|
||||
curseg = seglist.getItem(1)
|
||||
assert.equal(curseg.pathSegTypeAsLetter.toUpperCase(), 'L', 'Before conversion, segment #2 type')
|
||||
curseg = seglist.getItem(3)
|
||||
assert.equal(curseg.pathSegTypeAsLetter.toUpperCase(), 'Z', 'Before conversion, segment #3 type' + dAbs)
|
||||
|
||||
// convert and verify segments
|
||||
let d = convert(p1, true)
|
||||
assert.equal(d, 'm100,100l100,0l-100,0z', 'Converted path to relative string')
|
||||
|
||||
// TODO: see why this isn't working in SVG-edit
|
||||
d = convert(p2, true)
|
||||
console.log('Convert true', d)
|
||||
d = convert(p2, false)
|
||||
console.log('Convert false', d)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Import Module', function () {
|
||||
it('Test import use', function () {
|
||||
svgCanvas.setSvgString(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='400' x='300'>" +
|
||||
"<rect id='the-rect' width='200' height='200'/>" +
|
||||
"<use id='the-use' href='#the-rect'/>" +
|
||||
"<use id='foreign-use' href='somefile.svg#the-rect'/>" +
|
||||
"<use id='no-use'/>" +
|
||||
'</svg>'
|
||||
)
|
||||
|
||||
const u = document.getElementById('the-use')
|
||||
const fu = document.getElementById('foreign-use')
|
||||
const nfu = document.getElementById('no-use')
|
||||
|
||||
assert.equal((u && u.nodeName), 'use', 'Did not import <use> element')
|
||||
assert.equal(fu, null, 'Removed <use> element that had a foreign href')
|
||||
assert.equal(nfu, null, 'Removed <use> element that had no href')
|
||||
})
|
||||
|
||||
// This test shows that an element with an invalid attribute is still parsed in properly
|
||||
// and only the attribute is not imported
|
||||
it('Test invalid attribute', function () {
|
||||
svgCanvas.setSvgString(
|
||||
'<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<text x="182.75" y="173.5" id="the-text" fill="#008000" font-size="150" font-family="serif" text-anchor="middle" d="M116,222 L110,108">words</text>' +
|
||||
'</svg>'
|
||||
)
|
||||
|
||||
const t = document.getElementById('the-text')
|
||||
|
||||
assert.equal((t && t.nodeName), 'text', 'Did not import <text> element')
|
||||
assert.equal(t.getAttribute('d'), null, 'Imported a <text> with a d attribute')
|
||||
})
|
||||
|
||||
// This test makes sure import/export properly handles namespaced attributes
|
||||
it('Test importing/exporting namespaced attributes', function () {
|
||||
/* const setStr = */ svgCanvas.setSvgString(
|
||||
'<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:se="http://svg-edit.googlecode.com" xmlns:foo="http://example.com">' +
|
||||
'<image xlink:href="/src/editor/images/logo.svg"/>' +
|
||||
'<polyline id="se_test_elem" se:foo="bar" foo:bar="baz"/>' +
|
||||
'</svg>'
|
||||
)
|
||||
const attrVal = document.getElementById('se_test_elem').getAttributeNS('http://svg-edit.googlecode.com', 'foo')
|
||||
|
||||
assert.strictEqual(attrVal, 'bar', true, 'Preserved namespaced attribute on import')
|
||||
|
||||
const output = svgCanvas.getSvgString()
|
||||
const hasXlink = output.includes('xmlns:xlink="http://www.w3.org/1999/xlink"')
|
||||
const hasImageHref = /<image[^>]+href=/.test(output)
|
||||
const hasSe = output.includes('xmlns:se=')
|
||||
const hasFoo = output.includes('xmlns:foo=')
|
||||
const hasAttr = output.includes('se:foo="bar"')
|
||||
|
||||
assert.equal(hasAttr, true, 'Preserved namespaced attribute on export')
|
||||
assert.equal(hasImageHref, true, 'Preserved image href')
|
||||
// xlink namespace is optional (href is preferred), accept either
|
||||
assert.equal(hasXlink || hasImageHref, true, 'Included xlink namespace when needed')
|
||||
assert.equal(hasSe, true, 'Included se: xmlns')
|
||||
assert.equal(hasFoo, false, 'Did not include foo: xmlns')
|
||||
})
|
||||
|
||||
it('Test import math elements inside a foreignObject', function () {
|
||||
/* const set = */ svgCanvas.setSvgString(
|
||||
'<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg" xmlns:se="http://svg-edit.googlecode.com" xmlns:xlink="http://www.w3.org/1999/xlink">' +
|
||||
'<foreignObject id="fo" width="24" height="26" font-size="24"><math id="m" display="inline" xmlns="http://www.w3.org/1998/Math/MathML">' +
|
||||
'<msub>' +
|
||||
'<mi>A</mi>' +
|
||||
'<mn>0</mn>' +
|
||||
'</msub>' +
|
||||
'</math>' +
|
||||
'</foreignObject>' +
|
||||
'</svg>'
|
||||
)
|
||||
const fo = document.getElementById('fo')
|
||||
// we cannot use getElementById('math') because not all browsers understand MathML and do not know to use the @id attribute
|
||||
// see Bug https://bugs.webkit.org/show_bug.cgi?id=35042
|
||||
const math = fo.firstChild
|
||||
|
||||
assert.equal(Boolean(math), true, 'Math element exists')
|
||||
assert.equal(math.nodeName, 'math', 'Math element has the proper nodeName')
|
||||
assert.equal(math.getAttribute('id'), 'm', 'Math element has an id')
|
||||
assert.equal(math.namespaceURI, 'http://www.w3.org/1998/Math/MathML', 'Preserved MathML namespace')
|
||||
})
|
||||
|
||||
it('Test importing SVG into existing drawing', function () {
|
||||
/* const doc = */ svgCanvas.setSvgString(
|
||||
'<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<g><title>Layer 1</title>' +
|
||||
'<circle cx="200" cy="200" r="50" fill="blue"/>' +
|
||||
'<ellipse cx="300" cy="100" rx="40" ry="30" fill="green"/>' +
|
||||
'</g>' +
|
||||
'</svg>'
|
||||
)
|
||||
|
||||
svgCanvas.importSvgString(
|
||||
'<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<circle cx="50" cy="50" r="40" fill="yellow"/>' +
|
||||
'<rect width="20" height="20" fill="blue"/>' +
|
||||
'</svg>'
|
||||
)
|
||||
|
||||
const svgContent = document.getElementById('svgcontent')
|
||||
const circles = svgContent.getElementsByTagNameNS(svgns, 'circle')
|
||||
const rects = svgContent.getElementsByTagNameNS(svgns, 'rect')
|
||||
const ellipses = svgContent.getElementsByTagNameNS(svgns, 'ellipse')
|
||||
assert.equal(circles.length, 2, 'Found two circles upon importing')
|
||||
assert.equal(rects.length, 1, 'Found one rectangle upon importing')
|
||||
assert.equal(ellipses.length, 1, 'Found one ellipse upon importing')
|
||||
})
|
||||
|
||||
it('Test importing SVG remaps IDs', function () {
|
||||
/* const doc = */ svgCanvas.setSvgString(
|
||||
'<svg width="640" height="480" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<g><title>Layer 1</title>' +
|
||||
'<ellipse id="svg_1" cx="200" cy="200" rx="50" ry="20" fill="blue"/>' +
|
||||
'<ellipse id="svg_2" cx="300" cy="100" rx="40" ry="30" fill="green"/>' +
|
||||
'<ellipse id="svg_3" cx="300" cy="100" rx="40" ry="30" fill="green"/>' +
|
||||
'</g>' +
|
||||
'</svg>'
|
||||
)
|
||||
|
||||
svgCanvas.importSvgString(
|
||||
'<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink">' +
|
||||
'<defs>' +
|
||||
'<linearGradient id="svg_2">' +
|
||||
'<stop stop-color="red" offset="0"/>' +
|
||||
'<stop stop-color="green" offset="1"/>' +
|
||||
'</linearGradient>' +
|
||||
'<rect id="svg_3" width="20" height="20" fill="blue" stroke="url(#svg_2)"/>' +
|
||||
'</defs>' +
|
||||
'<circle id="svg_1" cx="50" cy="50" r="40" fill="url(#svg_2)"/>' +
|
||||
'<use id="svg_4" width="30" height="30" xl:href="#svg_3"/>' +
|
||||
'</svg>'
|
||||
)
|
||||
|
||||
const svgContent = document.getElementById('svgcontent')
|
||||
const circles = svgContent.getElementsByTagNameNS(svgns, 'circle')
|
||||
const rects = svgContent.getElementsByTagNameNS(svgns, 'rect')
|
||||
// ellipses = svgContent.getElementsByTagNameNS(svgns, 'ellipse'),
|
||||
const defs = svgContent.getElementsByTagNameNS(svgns, 'defs')
|
||||
// grads = svgContent.getElementsByTagNameNS(svgns, 'linearGradient'),
|
||||
const uses = svgContent.getElementsByTagNameNS(svgns, 'use')
|
||||
assert.notEqual(circles.item(0).id, 'svg_1', 'Circle not re-identified')
|
||||
assert.notEqual(rects.item(0).id, 'svg_3', 'Rectangle not re-identified')
|
||||
// TODO: determine why this test fails in WebKit browsers
|
||||
// assert.equal(grads.length, 1, 'Linear gradient imported');
|
||||
const grad = defs.item(0).firstChild
|
||||
assert.notEqual(grad.id, 'svg_2', 'Linear gradient not re-identified')
|
||||
assert.notEqual(circles.item(0).getAttribute('fill'), 'url(#svg_2)', 'Circle fill value not remapped')
|
||||
assert.notEqual(rects.item(0).getAttribute('stroke'), 'url(#svg_2)', 'Rectangle stroke value not remapped')
|
||||
assert.notEqual(uses.item(0).getAttributeNS(xlinkns, 'href'), '#svg_3')
|
||||
})
|
||||
})
|
||||
})
|
||||
91
tests/unit/units.test.js
Normal file
91
tests/unit/units.test.js
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as units from '../../packages/svgcanvas/core/units.js'
|
||||
|
||||
describe('units', function () {
|
||||
/**
|
||||
* Set up tests, supplying mock data.
|
||||
* @returns {void}
|
||||
*/
|
||||
beforeEach(() => {
|
||||
document.body.textContent = ''
|
||||
const anchor = document.createElement('div')
|
||||
anchor.id = 'anchor'
|
||||
anchor.style.visibility = 'hidden'
|
||||
|
||||
const elementsContainer = document.createElement('div')
|
||||
elementsContainer.id = 'elementsContainer'
|
||||
|
||||
const uniqueId = document.createElement('div')
|
||||
uniqueId.id = 'uniqueId'
|
||||
uniqueId.style.visibility = 'hidden'
|
||||
|
||||
const nonUniqueId = document.createElement('div')
|
||||
nonUniqueId.id = 'nonUniqueId'
|
||||
nonUniqueId.style.visibility = 'hidden'
|
||||
|
||||
elementsContainer.append(uniqueId, nonUniqueId)
|
||||
|
||||
document.body.append(anchor, elementsContainer)
|
||||
|
||||
units.init(
|
||||
/**
|
||||
* @implements {module:units.ElementContainer}
|
||||
*/
|
||||
{
|
||||
getBaseUnit () { return 'cm' },
|
||||
getHeight () { return 600 },
|
||||
getWidth () { return 800 },
|
||||
getRoundDigits () { return 4 },
|
||||
getElement (elementId) { return document.getElementById(elementId) }
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('Test svgedit.units package', function () {
|
||||
assert.ok(units)
|
||||
assert.equal(typeof units, typeof {})
|
||||
})
|
||||
|
||||
it('Test svgedit.units.shortFloat()', function () {
|
||||
assert.ok(units.shortFloat)
|
||||
assert.equal(typeof units.shortFloat, typeof function () { /* empty fn */ })
|
||||
|
||||
const { shortFloat } = units
|
||||
assert.equal(shortFloat(0.00000001), 0)
|
||||
assert.equal(shortFloat(1), 1)
|
||||
assert.equal(shortFloat(3.45678), 3.4568)
|
||||
assert.equal(shortFloat(1.23443), 1.2344)
|
||||
assert.equal(shortFloat(1.23455), 1.2346)
|
||||
})
|
||||
|
||||
it('Test svgedit.units.isValidUnit()', function () {
|
||||
assert.ok(units.isValidUnit)
|
||||
assert.equal(typeof units.isValidUnit, typeof function () { /* empty fn */ })
|
||||
|
||||
const { isValidUnit } = units
|
||||
assert.ok(isValidUnit('0'))
|
||||
assert.ok(isValidUnit('1'))
|
||||
assert.ok(isValidUnit('1.1'))
|
||||
assert.ok(isValidUnit('-1.1'))
|
||||
assert.ok(isValidUnit('.6mm'))
|
||||
assert.ok(isValidUnit('-.6cm'))
|
||||
assert.ok(isValidUnit('6000in'))
|
||||
assert.ok(isValidUnit('6px'))
|
||||
assert.ok(isValidUnit('6.3pc'))
|
||||
assert.ok(isValidUnit('-0.4em'))
|
||||
assert.ok(isValidUnit('-0.ex'))
|
||||
assert.ok(isValidUnit('40.123%'))
|
||||
|
||||
assert.equal(isValidUnit('id', 'uniqueId', document.getElementById('uniqueId')), true)
|
||||
assert.equal(isValidUnit('id', 'newId', document.getElementById('uniqueId')), true)
|
||||
assert.equal(isValidUnit('id', 'uniqueId'), false)
|
||||
assert.equal(isValidUnit('id', 'uniqueId', document.getElementById('nonUniqueId')), false)
|
||||
})
|
||||
|
||||
it('Test svgedit.units.convertUnit()', function () {
|
||||
assert.ok(units.convertUnit)
|
||||
assert.equal(typeof units.convertUnit, typeof function () { /* empty fn */ })
|
||||
// cm in default setup
|
||||
assert.equal(units.convertUnit(42), 1.1113)
|
||||
assert.equal(units.convertUnit(42, 'px'), 42)
|
||||
})
|
||||
})
|
||||
515
tests/unit/utilities-bbox.test.js
Normal file
515
tests/unit/utilities-bbox.test.js
Normal file
@@ -0,0 +1,515 @@
|
||||
import 'pathseg'
|
||||
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as utilities from '../../packages/svgcanvas/core/utilities.js'
|
||||
import * as math from '../../packages/svgcanvas/core/math.js'
|
||||
import * as path from '../../packages/svgcanvas/core/path.js'
|
||||
import setAssertionMethods from '../../support/assert-close.js'
|
||||
import * as units from '../../packages/svgcanvas/core/units.js'
|
||||
|
||||
// eslint-disable-next-line
|
||||
chai.use(setAssertionMethods)
|
||||
|
||||
describe('utilities bbox', function () {
|
||||
/**
|
||||
* Create an SVG element for a mock.
|
||||
* @param {module:utilities.SVGElementJSON} jsonMap
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
function mockCreateSVGElement (jsonMap) {
|
||||
const elem = document.createElementNS(NS.SVG, jsonMap.element)
|
||||
Object.entries(jsonMap.attr).forEach(([attr, value]) => {
|
||||
elem.setAttribute(attr, value)
|
||||
})
|
||||
return elem
|
||||
}
|
||||
let mockaddSVGElementsFromJsonCallCount = 0
|
||||
|
||||
/**
|
||||
* Mock of {@link module:utilities.EditorContext#addSVGElementsFromJson}.
|
||||
* @param {module:utilities.SVGElementJSON} json
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
function mockaddSVGElementsFromJson (json) {
|
||||
const elem = mockCreateSVGElement(json)
|
||||
svgroot.append(elem)
|
||||
mockaddSVGElementsFromJsonCallCount++
|
||||
return elem
|
||||
}
|
||||
const mockPathActions = {
|
||||
resetOrientation (pth) {
|
||||
if (pth?.nodeName !== 'path') { return false }
|
||||
const tlist = pth.transform.baseVal
|
||||
const m = math.transformListToTransform(tlist).matrix
|
||||
tlist.clear()
|
||||
pth.removeAttribute('transform')
|
||||
const segList = pth.pathSegList
|
||||
|
||||
const len = segList.numberOfItems
|
||||
// let lastX, lastY;
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const seg = segList.getItem(i)
|
||||
const type = seg.pathSegType
|
||||
if (type === 1) { continue }
|
||||
const pts = [];
|
||||
['', 1, 2].forEach(function (n) {
|
||||
const x = seg['x' + n]; const y = seg['y' + n]
|
||||
if (x !== undefined && y !== undefined) {
|
||||
const pt = math.transformPoint(x, y, m)
|
||||
pts.splice(pts.length, 0, pt.x, pt.y)
|
||||
}
|
||||
})
|
||||
path.replacePathSeg(type, i, pts, pth)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const EPSILON = 0.001
|
||||
|
||||
let svgroot
|
||||
beforeEach(() => {
|
||||
document.body.textContent = ''
|
||||
|
||||
// const svg = document.createElementNS(NS.SVG, 'svg');
|
||||
const sandbox = document.createElement('div')
|
||||
sandbox.id = 'sandbox'
|
||||
document.body.append(sandbox)
|
||||
|
||||
svgroot = mockCreateSVGElement({
|
||||
element: 'svg',
|
||||
attr: { id: 'svgroot' }
|
||||
})
|
||||
sandbox.append(svgroot)
|
||||
|
||||
const mockSvgCanvas = {
|
||||
createSVGElement (jsonMap) {
|
||||
const elem = document.createElementNS(NS.SVG, jsonMap.element)
|
||||
Object.entries(jsonMap.attr).forEach(([attr, value]) => {
|
||||
elem.setAttribute(attr, value)
|
||||
})
|
||||
return elem
|
||||
},
|
||||
getSvgRoot () { return svgroot }
|
||||
}
|
||||
|
||||
path.init(mockSvgCanvas)
|
||||
units.init({ getRoundDigits: () => 2 }) // mock getRoundDigits
|
||||
mockaddSVGElementsFromJsonCallCount = 0
|
||||
})
|
||||
|
||||
it('Test svgedit.utilities package', function () {
|
||||
assert.ok(utilities)
|
||||
assert.ok(utilities.getBBoxWithTransform)
|
||||
assert.ok(utilities.getStrokedBBox)
|
||||
assert.ok(utilities.getRotationAngleFromTransformList)
|
||||
assert.ok(utilities.getRotationAngle)
|
||||
})
|
||||
|
||||
it('Test getBBoxWithTransform and no transform', function () {
|
||||
const { getBBoxWithTransform } = utilities
|
||||
|
||||
let elem = mockCreateSVGElement({
|
||||
element: 'path',
|
||||
attr: { id: 'path', d: 'M0,1 L2,3' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
let bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 2, height: 2 })
|
||||
assert.equal(mockaddSVGElementsFromJsonCallCount, 0)
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 })
|
||||
assert.equal(mockaddSVGElementsFromJsonCallCount, 0)
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'line',
|
||||
attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 })
|
||||
assert.equal(mockaddSVGElementsFromJsonCallCount, 0)
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }
|
||||
})
|
||||
const g = mockCreateSVGElement({
|
||||
element: 'g',
|
||||
attr: {}
|
||||
})
|
||||
g.append(elem)
|
||||
svgroot.append(g)
|
||||
bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 })
|
||||
assert.equal(mockaddSVGElementsFromJsonCallCount, 0)
|
||||
g.remove()
|
||||
})
|
||||
|
||||
it('Test getBBoxWithTransform and a rotation transform', function () {
|
||||
const { getBBoxWithTransform } = utilities
|
||||
|
||||
let elem = mockCreateSVGElement({
|
||||
element: 'path',
|
||||
attr: { id: 'path', d: 'M10,10 L20,20', transform: 'rotate(45 10,10)' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
let bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.close(bbox.x, 10, EPSILON)
|
||||
assert.close(bbox.y, 10, EPSILON)
|
||||
assert.close(bbox.width, 0, EPSILON)
|
||||
assert.close(bbox.height, Math.sqrt(100 + 100), EPSILON)
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '10', y: '10', width: '10', height: '20', transform: 'rotate(90 15,20)' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.close(bbox.x, 5, EPSILON)
|
||||
assert.close(bbox.y, 15, EPSILON)
|
||||
assert.close(bbox.width, 20, EPSILON)
|
||||
assert.close(bbox.height, 10, EPSILON)
|
||||
assert.equal(mockaddSVGElementsFromJsonCallCount, 1)
|
||||
elem.remove()
|
||||
|
||||
const rect = { x: 10, y: 10, width: 10, height: 20 }
|
||||
const angle = 45
|
||||
const origin = { x: 15, y: 20 }
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect2', x: rect.x, y: rect.y, width: rect.width, height: rect.height, transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ')' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
mockaddSVGElementsFromJsonCallCount = 0
|
||||
bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
const r2 = rotateRect(rect, angle, origin)
|
||||
assert.close(bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x)
|
||||
assert.close(bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y)
|
||||
assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width)
|
||||
assert.close(bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height)
|
||||
assert.equal(mockaddSVGElementsFromJsonCallCount, 0)
|
||||
elem.remove()
|
||||
|
||||
// Same as previous but wrapped with g and the transform is with the g.
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect3', x: rect.x, y: rect.y, width: rect.width, height: rect.height }
|
||||
})
|
||||
const g = mockCreateSVGElement({
|
||||
element: 'g',
|
||||
attr: { transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ')' }
|
||||
})
|
||||
g.append(elem)
|
||||
svgroot.append(g)
|
||||
mockaddSVGElementsFromJsonCallCount = 0
|
||||
bbox = getBBoxWithTransform(g, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.close(bbox.x, r2.x, EPSILON, 'rect2 x is ' + r2.x)
|
||||
assert.close(bbox.y, r2.y, EPSILON, 'rect2 y is ' + r2.y)
|
||||
assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width)
|
||||
assert.close(bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height)
|
||||
assert.equal(mockaddSVGElementsFromJsonCallCount, 0)
|
||||
g.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'ellipse',
|
||||
attr: { id: 'ellipse1', cx: '100', cy: '100', rx: '50', ry: '50', transform: 'rotate(45 100,100)' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
mockaddSVGElementsFromJsonCallCount = 0
|
||||
bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
/** @todo: Review these test the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100. */
|
||||
// assert.ok(bbox.x > 45 && bbox.x <= 50);
|
||||
assert.ok(bbox.y > 45 && bbox.y <= 50)
|
||||
// assert.ok(bbox.width >= 100 && bbox.width < 110);
|
||||
// assert.ok(bbox.height >= 100 && bbox.height < 110);
|
||||
assert.equal(mockaddSVGElementsFromJsonCallCount, 1)
|
||||
elem.remove()
|
||||
})
|
||||
|
||||
it('Test getBBoxWithTransform with rotation and matrix transforms', function () {
|
||||
const { getBBoxWithTransform } = utilities
|
||||
|
||||
let tx = 10 // tx right
|
||||
let ty = 10 // tx down
|
||||
let txInRotatedSpace = Math.sqrt(tx * tx + ty * ty) // translate in rotated 45 space.
|
||||
let tyInRotatedSpace = 0
|
||||
let matrix = 'matrix(1,0,0,1,' + txInRotatedSpace + ',' + tyInRotatedSpace + ')'
|
||||
let elem = mockCreateSVGElement({
|
||||
element: 'path',
|
||||
attr: { id: 'path', d: 'M10,10 L20,20', transform: 'rotate(45 10,10) ' + matrix }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
let bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.close(bbox.x, 10 + tx, EPSILON)
|
||||
assert.close(bbox.y, 10 + ty, EPSILON)
|
||||
assert.close(bbox.width, 0, EPSILON)
|
||||
assert.close(bbox.height, Math.sqrt(100 + 100), EPSILON)
|
||||
elem.remove()
|
||||
|
||||
txInRotatedSpace = tx // translate in rotated 90 space.
|
||||
tyInRotatedSpace = -ty
|
||||
matrix = 'matrix(1,0,0,1,' + txInRotatedSpace + ',' + tyInRotatedSpace + ')'
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '10', y: '10', width: '10', height: '20', transform: 'rotate(90 15,20) ' + matrix }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.close(bbox.x, 5 + tx, EPSILON)
|
||||
assert.close(bbox.y, 15 + ty, EPSILON)
|
||||
assert.close(bbox.width, 20, EPSILON)
|
||||
assert.close(bbox.height, 10, EPSILON)
|
||||
elem.remove()
|
||||
|
||||
const rect = { x: 10, y: 10, width: 10, height: 20 }
|
||||
const angle = 45
|
||||
const origin = { x: 15, y: 20 }
|
||||
tx = 10 // tx right
|
||||
ty = 10 // tx down
|
||||
txInRotatedSpace = Math.sqrt(tx * tx + ty * ty) // translate in rotated 45 space.
|
||||
tyInRotatedSpace = 0
|
||||
matrix = 'matrix(1,0,0,1,' + txInRotatedSpace + ',' + tyInRotatedSpace + ')'
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect2', x: rect.x, y: rect.y, width: rect.width, height: rect.height, transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ') ' + matrix }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
const r2 = rotateRect(rect, angle, origin)
|
||||
assert.close(bbox.x, r2.x + tx, EPSILON, 'rect2 x is ' + r2.x)
|
||||
assert.close(bbox.y, r2.y + ty, EPSILON, 'rect2 y is ' + r2.y)
|
||||
assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width)
|
||||
assert.close(bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height)
|
||||
elem.remove()
|
||||
|
||||
// Same as previous but wrapped with g and the transform is with the g.
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect3', x: rect.x, y: rect.y, width: rect.width, height: rect.height }
|
||||
})
|
||||
const g = mockCreateSVGElement({
|
||||
element: 'g',
|
||||
attr: { transform: 'rotate(' + angle + ' ' + origin.x + ',' + origin.y + ') ' + matrix }
|
||||
})
|
||||
g.append(elem)
|
||||
svgroot.append(g)
|
||||
bbox = getBBoxWithTransform(g, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.close(bbox.x, r2.x + tx, EPSILON, 'rect2 x is ' + r2.x)
|
||||
assert.close(bbox.y, r2.y + ty, EPSILON, 'rect2 y is ' + r2.y)
|
||||
assert.close(bbox.width, r2.width, EPSILON, 'rect2 width is' + r2.width)
|
||||
assert.close(bbox.height, r2.height, EPSILON, 'rect2 height is ' + r2.height)
|
||||
g.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'ellipse',
|
||||
attr: { id: 'ellipse1', cx: '100', cy: '100', rx: '50', ry: '50', transform: 'rotate(45 100,100) ' + matrix }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getBBoxWithTransform(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
/** @todo: the BBox algorithm is using the bezier control points to calculate the bounding box. Should be 50, 50, 100, 100. */
|
||||
// assert.ok(bbox.x > 45 + tx && bbox.x <= 50 + tx);
|
||||
assert.ok(bbox.y > 45 + ty && bbox.y <= 50 + ty)
|
||||
// assert.ok(bbox.width >= 100 && bbox.width < 110);
|
||||
// assert.ok(bbox.height >= 100 && bbox.height < 110);
|
||||
elem.remove()
|
||||
})
|
||||
|
||||
it('Test getStrokedBBox with stroke-width 10', function () {
|
||||
const { getStrokedBBox } = utilities
|
||||
|
||||
const strokeWidth = 10
|
||||
let elem = mockCreateSVGElement({
|
||||
element: 'path',
|
||||
attr: { id: 'path', d: 'M0,1 L2,3', 'stroke-width': strokeWidth }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
let bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0 - strokeWidth / 2, y: 1 - strokeWidth / 2, width: 2 + strokeWidth, height: 2 + strokeWidth })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10', 'stroke-width': strokeWidth }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0 - strokeWidth / 2, y: 1 - strokeWidth / 2, width: 5 + strokeWidth, height: 10 + strokeWidth })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'line',
|
||||
attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6', 'stroke-width': strokeWidth }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0 - strokeWidth / 2, y: 1 - strokeWidth / 2, width: 5 + strokeWidth, height: 5 + strokeWidth })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10', 'stroke-width': strokeWidth }
|
||||
})
|
||||
const g = mockCreateSVGElement({
|
||||
element: 'g',
|
||||
attr: {}
|
||||
})
|
||||
g.append(elem)
|
||||
svgroot.append(g)
|
||||
bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0 - strokeWidth / 2, y: 1 - strokeWidth / 2, width: 5 + strokeWidth, height: 10 + strokeWidth })
|
||||
g.remove()
|
||||
})
|
||||
|
||||
it("Test getStrokedBBox with stroke-width 'none'", function () {
|
||||
const { getStrokedBBox } = utilities
|
||||
|
||||
let elem = mockCreateSVGElement({
|
||||
element: 'path',
|
||||
attr: { id: 'path', d: 'M0,1 L2,3', 'stroke-width': 'none' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
let bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 2, height: 2 })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10', 'stroke-width': 'none' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'line',
|
||||
attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6', 'stroke-width': 'none' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10', 'stroke-width': 'none' }
|
||||
})
|
||||
const g = mockCreateSVGElement({
|
||||
element: 'g',
|
||||
attr: {}
|
||||
})
|
||||
g.append(elem)
|
||||
svgroot.append(g)
|
||||
bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 })
|
||||
g.remove()
|
||||
})
|
||||
|
||||
it('Test getStrokedBBox with no stroke-width attribute', function () {
|
||||
const { getStrokedBBox } = utilities
|
||||
|
||||
let elem = mockCreateSVGElement({
|
||||
element: 'path',
|
||||
attr: { id: 'path', d: 'M0,1 L2,3' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
let bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 2, height: 2 })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'line',
|
||||
attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }
|
||||
})
|
||||
const g = mockCreateSVGElement({
|
||||
element: 'g',
|
||||
attr: {}
|
||||
})
|
||||
g.append(elem)
|
||||
svgroot.append(g)
|
||||
bbox = getStrokedBBox([elem], mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 })
|
||||
g.remove()
|
||||
})
|
||||
|
||||
/**
|
||||
* Returns radians for degrees.
|
||||
* @param {Float} degrees
|
||||
* @returns {Float}
|
||||
*/
|
||||
function radians (degrees) {
|
||||
return degrees * Math.PI / 180
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {module:utilities.BBoxObject} point
|
||||
* @param {Float} angle
|
||||
* @param {module:math.XYObject} origin
|
||||
* @returns {module:math.XYObject}
|
||||
*/
|
||||
function rotatePoint (point, angle, origin = { x: 0, y: 0 }) {
|
||||
const x = point.x - origin.x
|
||||
const y = point.y - origin.y
|
||||
const theta = radians(angle)
|
||||
return {
|
||||
x: x * Math.cos(theta) + y * Math.sin(theta) + origin.x,
|
||||
y: x * Math.sin(theta) + y * Math.cos(theta) + origin.y
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {module:utilities.BBoxObject} rect
|
||||
* @param {Float} angle
|
||||
* @param {module:math.XYObject} origin
|
||||
* @returns {module:utilities.BBoxObject}
|
||||
*/
|
||||
function rotateRect (rect, angle, origin) {
|
||||
const tl = rotatePoint({ x: rect.x, y: rect.y }, angle, origin)
|
||||
const tr = rotatePoint({ x: rect.x + rect.width, y: rect.y }, angle, origin)
|
||||
const br = rotatePoint({ x: rect.x + rect.width, y: rect.y + rect.height }, angle, origin)
|
||||
const bl = rotatePoint({ x: rect.x, y: rect.y + rect.height }, angle, origin)
|
||||
|
||||
const minx = Math.min(tl.x, tr.x, bl.x, br.x)
|
||||
const maxx = Math.max(tl.x, tr.x, bl.x, br.x)
|
||||
const miny = Math.min(tl.y, tr.y, bl.y, br.y)
|
||||
const maxy = Math.max(tl.y, tr.y, bl.y, br.y)
|
||||
|
||||
return {
|
||||
x: minx,
|
||||
y: miny,
|
||||
width: (maxx - minx),
|
||||
height: (maxy - miny)
|
||||
}
|
||||
}
|
||||
})
|
||||
238
tests/unit/utilities-performance.test.js
Normal file
238
tests/unit/utilities-performance.test.js
Normal file
@@ -0,0 +1,238 @@
|
||||
/* eslint-disable max-len, no-console */
|
||||
import 'pathseg'
|
||||
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
import * as utilities from '../../packages/svgcanvas/core/utilities.js'
|
||||
import * as math from '../../packages/svgcanvas/core/math.js'
|
||||
import * as units from '../../packages/svgcanvas/core/units.js'
|
||||
|
||||
describe('utilities performance', function () {
|
||||
let currentLayer; let groupWithMatrixTransform; let textWithMatrixTransform
|
||||
units.init({ getRoundDigits: () => 2 }) // mock getRoundDigits
|
||||
beforeEach(() => {
|
||||
document.body.textContent = ''
|
||||
const style = document.createElement('style')
|
||||
style.id = 'styleoverrides'
|
||||
style.media = 'screen'
|
||||
style.textContent = `
|
||||
#svgcanvas svg * {
|
||||
cursor: move;
|
||||
pointer-events: all
|
||||
}
|
||||
#svgcanvas svg {
|
||||
cursor: default
|
||||
}`
|
||||
|
||||
document.head.append(style)
|
||||
|
||||
const editor = new DOMParser().parseFromString(`<div id="svg_editor">
|
||||
<div id="workarea" style="cursor: auto; overflow: scroll; line-height: 12px; right: 100px;">
|
||||
|
||||
<!-- Must include this thumbnail view to see some of the performance issues -->
|
||||
<svg id="overviewMiniView" width="132" height="112.5" x="0" y="0" viewBox="100 100 1000 1000" style="float: right;"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<use x="0" y="0" href="#svgroot"></use>
|
||||
</svg>
|
||||
|
||||
|
||||
<div id="svgcanvas" style="position: relative; width: 1000px; height: 1000px;">
|
||||
<svg id="svgroot" xmlns="http://www.w3.org/2000/svg" xlinkns="http://www.w3.org/1999/xlink" width="1000" height="1000" x="640" y="480" overflow="visible">
|
||||
<defs><filter id="canvashadow" filterUnits="objectBoundingBox"><feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"></feGaussianBlur><feOffset in="blur" dx="5" dy="5" result="offsetBlur"></feOffset><feMerge><feMergeNode in="offsetBlur"></feMergeNode><feMergeNode in="SourceGraphic"></feMergeNode></feMerge></filter><pattern id="gridpattern" patternUnits="userSpaceOnUse" x="0" y="0" width="100" height="100"><image x="0" y="0" width="100" height="100"></image></pattern></defs>
|
||||
<svg id="canvasBackground" width="1000" height="200" x="10" y="10" overflow="none" style="pointer-events:none"><rect width="100%" height="100%" x="0" y="0" stroke="#000" fill="#000" style="pointer-events:none"></rect><svg id="canvasGrid" width="100%" height="100%" x="0" y="0" overflow="visible" display="none" style="display: inline;"><rect width="100%" height="100%" x="0" y="0" stroke-width="0" stroke="none" fill="url(#gridpattern)" style="pointer-events: none; display:visible;"></rect></svg></svg>
|
||||
<animate attributeName="opacity" begin="indefinite" dur="1" fill="freeze"></animate>
|
||||
|
||||
<svg id="svgcontent" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1000 480" overflow="visible" width="1000" height="200" x="100" y="20">
|
||||
|
||||
<g id="layer1">
|
||||
<title>Layer 1</title>
|
||||
|
||||
<g id="svg_group_with_matrix_transform" transform="matrix(0.5, 0, 0, 0.5, 10, 10)">
|
||||
<svg id="svg_2" x="100" y="0" class="symbol" preserveAspectRatio="xMaxYMax">
|
||||
<g id="svg_3">
|
||||
<rect id="svg_4" x="0" y="0" width="20" height="20" fill="#00FF00"></rect>
|
||||
</g>
|
||||
<g id="svg_5" display="none">
|
||||
<rect id="svg_6" x="0" y="0" width="20" height="20" fill="#A40000"></rect>
|
||||
</g>
|
||||
</svg>
|
||||
</g>
|
||||
<text id="svg_text_with_matrix_transform" transform="matrix(0.433735, 0, 0, 0.433735, 2, 4)" xml:space="preserve" text-anchor="middle" font-family="serif" font-size="24" y="0" x="61" stroke="#999999" fill="#999999">Some text</text>
|
||||
|
||||
</g>
|
||||
<g>
|
||||
<title>Layer 2</title>
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
</svg>
|
||||
</div>
|
||||
</div></div>`, 'application/xml')
|
||||
const newNode = document.body.ownerDocument.importNode(editor.documentElement, true)
|
||||
document.body.append(newNode)
|
||||
|
||||
currentLayer = document.getElementById('layer1')
|
||||
groupWithMatrixTransform = document.getElementById('svg_group_with_matrix_transform')
|
||||
textWithMatrixTransform = document.getElementById('svg_text_with_matrix_transform')
|
||||
})
|
||||
|
||||
/**
|
||||
* Create an SVG element for a mock.
|
||||
* @param {module:utilities.SVGElementJSON} jsonMap
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
function mockCreateSVGElement (jsonMap) {
|
||||
const elem = document.createElementNS(NS.SVG, jsonMap.element)
|
||||
Object.entries(jsonMap.attr).forEach(([attr, value]) => {
|
||||
elem.setAttribute(attr, value)
|
||||
})
|
||||
return elem
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock of {@link module:utilities.EditorContext#addSVGElementsFromJson}.
|
||||
* @param {module:utilities.SVGElementJSON} json
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
function mockaddSVGElementsFromJson (json) {
|
||||
const elem = mockCreateSVGElement(json)
|
||||
currentLayer.append(elem)
|
||||
return elem
|
||||
}
|
||||
|
||||
/**
|
||||
* Toward performance testing, fill document with clones of element.
|
||||
* @param {SVGElement} elem
|
||||
* @param {Integer} count
|
||||
* @returns {void}
|
||||
*/
|
||||
function fillDocumentByCloningElement (elem, count) {
|
||||
const elemId = elem.getAttribute('id') + '-'
|
||||
for (let index = 0; index < count; index++) {
|
||||
const clone = elem.cloneNode(true) // t: deep clone
|
||||
// Make sure you set a unique ID like a real document.
|
||||
clone.setAttribute('id', elemId + index)
|
||||
const { parentNode } = elem
|
||||
parentNode.append(clone)
|
||||
}
|
||||
}
|
||||
|
||||
const mockPathActions = {
|
||||
resetOrientation (path) {
|
||||
if (path?.nodeName !== 'path') { return false }
|
||||
const tlist = path.transform.baseVal
|
||||
const m = math.transformListToTransform(tlist).matrix
|
||||
tlist.clear()
|
||||
path.removeAttribute('transform')
|
||||
const segList = path.pathSegList
|
||||
|
||||
const len = segList.numberOfItems
|
||||
// let lastX, lastY;
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const seg = segList.getItem(i)
|
||||
const type = seg.pathSegType
|
||||
if (type === 1) {
|
||||
continue
|
||||
}
|
||||
const pts = [];
|
||||
['', 1, 2].forEach(function (n) {
|
||||
const x = seg['x' + n]
|
||||
const y = seg['y' + n]
|
||||
if (x !== undefined && y !== undefined) {
|
||||
const pt = math.transformPoint(x, y, m)
|
||||
pts.splice(pts.length, 0, pt.x, pt.y)
|
||||
}
|
||||
})
|
||||
// path.replacePathSeg(type, i, pts, path);
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// //////////////////////////////////////////////////////////
|
||||
// Performance times with various browsers on Macbook 2011 8MB RAM OS X El Capitan 10.11.4
|
||||
//
|
||||
// To see 'Before Optimization' performance, making the following two edits.
|
||||
// 1. utilities.getStrokedBBox - change if( elems.length === 1) to if( false && elems.length === 1)
|
||||
// 2. utilities.getBBoxWithTransform - uncomment 'Old technique that was very slow'
|
||||
|
||||
// Chrome
|
||||
// Before Optimization
|
||||
// Pass1 svgCanvas.getStrokedBBox total ms 4,218, ave ms 41.0, min/max 37 51
|
||||
// Pass2 svgCanvas.getStrokedBBox total ms 4,458, ave ms 43.3, min/max 32 63
|
||||
// Optimized Code
|
||||
// Pass1 svgCanvas.getStrokedBBox total ms 1,112, ave ms 10.8, min/max 9 20
|
||||
// Pass2 svgCanvas.getStrokedBBox total ms 34, ave ms 0.3, min/max 0 20
|
||||
|
||||
// Firefox
|
||||
// Before Optimization
|
||||
// Pass1 svgCanvas.getStrokedBBox total ms 3,794, ave ms 36.8, min/max 33 48
|
||||
// Pass2 svgCanvas.getStrokedBBox total ms 4,049, ave ms 39.3, min/max 28 53
|
||||
// Optimized Code
|
||||
// Pass1 svgCanvas.getStrokedBBox total ms 104, ave ms 1.0, min/max 0 23
|
||||
// Pass2 svgCanvas.getStrokedBBox total ms 71, ave ms 0.7, min/max 0 23
|
||||
|
||||
// Safari
|
||||
// Before Optimization
|
||||
// Pass1 svgCanvas.getStrokedBBox total ms 4,840, ave ms 47.0, min/max 45 62
|
||||
// Pass2 svgCanvas.getStrokedBBox total ms 4,849, ave ms 47.1, min/max 34 62
|
||||
// Optimized Code
|
||||
// Pass1 svgCanvas.getStrokedBBox total ms 42, ave ms 0.4, min/max 0 23
|
||||
// Pass2 svgCanvas.getStrokedBBox total ms 17, ave ms 0.2, min/max 0 23
|
||||
|
||||
it('Test svgCanvas.getStrokedBBox() performance with matrix transforms', function () {
|
||||
const { getStrokedBBox } = utilities
|
||||
const { children } = currentLayer
|
||||
|
||||
let lastTime; let now
|
||||
let min = Number.MAX_VALUE
|
||||
let max = 0
|
||||
let total = 0
|
||||
|
||||
fillDocumentByCloningElement(groupWithMatrixTransform, 50)
|
||||
fillDocumentByCloningElement(textWithMatrixTransform, 50)
|
||||
|
||||
// The first pass through all elements is slower.
|
||||
const count = children.length
|
||||
const start = lastTime = now = Date.now()
|
||||
// Skip the first child which is the title.
|
||||
for (let index = 1; index < count; index++) {
|
||||
const child = children[index]
|
||||
/* const obj = */ getStrokedBBox([child], mockaddSVGElementsFromJson, mockPathActions)
|
||||
now = Date.now(); const delta = now - lastTime; lastTime = now
|
||||
total += delta
|
||||
min = Math.min(min, delta)
|
||||
max = Math.max(max, delta)
|
||||
}
|
||||
total = lastTime - start
|
||||
const ave = total / count
|
||||
assert.isBelow(ave, 20, 'svgedit.utilities.getStrokedBBox average execution time is less than 20 ms')
|
||||
console.log('Pass1 svgCanvas.getStrokedBBox total ms ' + total + ', ave ms ' + ave.toFixed(1) + ',\t min/max ' + min + ' ' + max)
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// The second pass is two to ten times faster.
|
||||
setTimeout(function () {
|
||||
const ct = children.length
|
||||
|
||||
const strt = lastTime = now = Date.now()
|
||||
// Skip the first child which is the title.
|
||||
for (let index = 1; index < ct; index++) {
|
||||
const child = children[index]
|
||||
/* const obj = */ getStrokedBBox([child], mockaddSVGElementsFromJson, mockPathActions)
|
||||
now = Date.now(); const delta = now - lastTime; lastTime = now
|
||||
total += delta
|
||||
min = Math.min(min, delta)
|
||||
max = Math.max(max, delta)
|
||||
}
|
||||
|
||||
total = lastTime - strt
|
||||
const avg = total / ct
|
||||
assert.isBelow(avg, 2, 'svgedit.utilities.getStrokedBBox average execution time is less than 1 ms')
|
||||
console.log('Pass2 svgCanvas.getStrokedBBox total ms ' + total + ', ave ms ' + avg.toFixed(1) + ',\t min/max ' + min + ' ' + max)
|
||||
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
342
tests/unit/utilities.test.js
Normal file
342
tests/unit/utilities.test.js
Normal file
@@ -0,0 +1,342 @@
|
||||
import * as utilities from '../../packages/svgcanvas/core/utilities.js'
|
||||
import { NS } from '../../packages/svgcanvas/core/namespaces.js'
|
||||
|
||||
describe('utilities', function () {
|
||||
/**
|
||||
* Create an element for test.
|
||||
* @param {module:utilities.SVGElementJSON} jsonMap
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
function mockCreateSVGElement (jsonMap) {
|
||||
const elem = document.createElementNS(NS.SVG, jsonMap.element)
|
||||
Object.entries(jsonMap.attr).forEach(([attr, value]) => {
|
||||
elem.setAttribute(attr, value)
|
||||
})
|
||||
return elem
|
||||
}
|
||||
/**
|
||||
* Adds SVG Element per parameters and appends to root.
|
||||
* @param {module:utilities.SVGElementJSON} json
|
||||
* @returns {SVGElement}
|
||||
*/
|
||||
function mockaddSVGElementsFromJson (json) {
|
||||
const elem = mockCreateSVGElement(json)
|
||||
svgroot.append(elem)
|
||||
return elem
|
||||
}
|
||||
const mockPathActions = { resetOrientation () { /* empty fn */ } }
|
||||
let mockHistorySubCommands = []
|
||||
const mockHistory = {
|
||||
BatchCommand: class {
|
||||
addSubCommand (cmd) {
|
||||
mockHistorySubCommands.push(cmd)
|
||||
}
|
||||
},
|
||||
RemoveElementCommand: class {
|
||||
// Longhand needed since used as a constructor
|
||||
constructor (elem, nextSibling, parent) {
|
||||
this.elem = elem
|
||||
this.nextSibling = nextSibling
|
||||
this.parent = parent
|
||||
}
|
||||
},
|
||||
InsertElementCommand: class {
|
||||
constructor (path) { // Longhand needed since used as a constructor
|
||||
this.path = path
|
||||
}
|
||||
}
|
||||
}
|
||||
const mockCount = {
|
||||
clearSelection: 0,
|
||||
addToSelection: 0,
|
||||
addCommandToHistory: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments clear seleciton count for mock test.
|
||||
* @returns {void}
|
||||
*/
|
||||
function mockClearSelection () {
|
||||
mockCount.clearSelection++
|
||||
}
|
||||
/**
|
||||
* Increments add selection count for mock test.
|
||||
* @returns {void}
|
||||
*/
|
||||
function mockAddToSelection () {
|
||||
mockCount.addToSelection++
|
||||
}
|
||||
/**
|
||||
* Increments add command to history count for mock test.
|
||||
* @returns {void}
|
||||
*/
|
||||
function mockAddCommandToHistory () {
|
||||
mockCount.addCommandToHistory++
|
||||
}
|
||||
|
||||
const mockSvgCanvas = {
|
||||
addSVGElementsFromJson: mockaddSVGElementsFromJson,
|
||||
pathActions: mockPathActions,
|
||||
clearSelection: mockClearSelection,
|
||||
addToSelection: mockAddToSelection,
|
||||
history: mockHistory,
|
||||
addCommandToHistory: mockAddCommandToHistory
|
||||
}
|
||||
|
||||
let svg; let svgroot
|
||||
beforeEach(() => {
|
||||
document.body.textContent = ''
|
||||
|
||||
mockHistorySubCommands = []
|
||||
mockCount.clearSelection = 0
|
||||
mockCount.addToSelection = 0
|
||||
mockCount.addCommandToHistory = 0
|
||||
|
||||
const sandbox = document.createElement('div')
|
||||
svg = document.createElementNS(NS.SVG, 'svg')
|
||||
svgroot = mockCreateSVGElement({
|
||||
element: 'svg',
|
||||
attr: { id: 'svgroot' }
|
||||
})
|
||||
sandbox.append(svgroot)
|
||||
document.body.append(sandbox)
|
||||
})
|
||||
|
||||
it('Test svgedit.utilities package', function () {
|
||||
assert.ok(utilities)
|
||||
assert.ok(utilities.toXml)
|
||||
assert.equal(typeof utilities.toXml, typeof function () { /* empty fn */ })
|
||||
})
|
||||
|
||||
it('Test svgedit.utilities.toXml() function', function () {
|
||||
const { toXml } = utilities
|
||||
|
||||
assert.equal(toXml('a'), 'a')
|
||||
assert.equal(toXml('ABC_'), 'ABC_')
|
||||
assert.equal(toXml('PB&J'), 'PB&J')
|
||||
assert.equal(toXml('2 < 5'), '2 < 5')
|
||||
assert.equal(toXml('5 > 2'), '5 > 2')
|
||||
assert.equal(toXml('\'<&>"'), ''<&>"')
|
||||
})
|
||||
|
||||
it('Test svgedit.utilities.encode64() function', function () {
|
||||
const { encode64 } = utilities
|
||||
|
||||
assert.equal(encode64('abcdef'), 'YWJjZGVm')
|
||||
assert.equal(encode64('12345'), 'MTIzNDU=')
|
||||
assert.equal(encode64(' '), 'IA==')
|
||||
assert.equal(encode64('`~!@#$%^&*()-_=+[{]}\\|;:\'",<.>/?'), 'YH4hQCMkJV4mKigpLV89K1t7XX1cfDs6JyIsPC4+Lz8=')
|
||||
})
|
||||
|
||||
it('Test svgedit.utilities.decode64() function', function () {
|
||||
const { decode64 } = utilities
|
||||
|
||||
assert.equal(decode64('YWJjZGVm'), 'abcdef')
|
||||
assert.equal(decode64('MTIzNDU='), '12345')
|
||||
assert.equal(decode64('IA=='), ' ')
|
||||
assert.equal(decode64('YH4hQCMkJV4mKigpLV89K1t7XX1cfDs6JyIsPC4+Lz8='), '`~!@#$%^&*()-_=+[{]}\\|;:\'",<.>/?')
|
||||
})
|
||||
|
||||
it('Test svgedit.utilities.convertToXMLReferences() function', function () {
|
||||
const convert = utilities.convertToXMLReferences
|
||||
assert.equal(convert('ABC'), 'ABC')
|
||||
// assert.equal(convert('<27>BC'), 'ÀBC');
|
||||
})
|
||||
|
||||
it('Test svgedit.utilities.bboxToObj() function', function () {
|
||||
const { bboxToObj } = utilities
|
||||
|
||||
const rect = svg.createSVGRect()
|
||||
rect.x = 1
|
||||
rect.y = 2
|
||||
rect.width = 3
|
||||
rect.height = 4
|
||||
|
||||
const obj = bboxToObj(rect)
|
||||
assert.equal(typeof obj, typeof {})
|
||||
assert.equal(obj.x, 1)
|
||||
assert.equal(obj.y, 2)
|
||||
assert.equal(obj.width, 3)
|
||||
assert.equal(obj.height, 4)
|
||||
})
|
||||
|
||||
it('Test getUrlFromAttr', function () {
|
||||
assert.equal(utilities.getUrlFromAttr('url(#foo)'), '#foo')
|
||||
assert.equal(utilities.getUrlFromAttr('url(somefile.svg#foo)'), 'somefile.svg#foo')
|
||||
assert.equal(utilities.getUrlFromAttr('url("#foo")'), '#foo')
|
||||
assert.equal(utilities.getUrlFromAttr('url("#foo")'), '#foo')
|
||||
})
|
||||
|
||||
it('Test getPathDFromSegments', function () {
|
||||
const { getPathDFromSegments } = utilities
|
||||
|
||||
const doc = utilities.text2xml('<svg></svg>')
|
||||
const path = doc.createElementNS(NS.SVG, 'path')
|
||||
path.setAttribute('d', 'm0,0l5,0l0,5l-5,0l0,-5z')
|
||||
let d = getPathDFromSegments([
|
||||
['M', [1, 2]],
|
||||
['Z', []]
|
||||
])
|
||||
assert.equal(d, 'M1,2 Z')
|
||||
|
||||
d = getPathDFromSegments([
|
||||
['M', [1, 2]],
|
||||
['M', [3, 4]],
|
||||
['Z', []]
|
||||
])
|
||||
assert.equal(d, 'M1,2 M3,4 Z')
|
||||
|
||||
d = getPathDFromSegments([
|
||||
['M', [1, 2]],
|
||||
['C', [3, 4, 5, 6]],
|
||||
['Z', []]
|
||||
])
|
||||
assert.equal(d, 'M1,2 C3,4 5,6 Z')
|
||||
})
|
||||
|
||||
it('Test getPathDFromElement', function () {
|
||||
const { getPathDFromElement } = utilities
|
||||
|
||||
let elem = mockCreateSVGElement({
|
||||
element: 'path',
|
||||
attr: { id: 'path', d: 'M0,1 Z' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
assert.equal(getPathDFromElement(elem), 'M0,1 Z')
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
assert.equal(getPathDFromElement(elem), 'M0,1 L5,1 L5,11 L0,11 L0,1 Z')
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'roundrect', x: '0', y: '1', rx: '2', ry: '3', width: '10', height: '11' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
const closeEnough = /M0,4 C0,2.3\d* 0.9\d*,1 2,1 L8,1 C9.0\d*,1 10,2.3\d* 10,4 L10,9 C10,10.6\d* 9.0\d*,12 8,12 L2,12 C0.9\d*,12 0,10.6\d* 0,9 L0,4 Z/
|
||||
assert.equal(closeEnough.test(getPathDFromElement(elem)), true)
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'line',
|
||||
attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
assert.equal(getPathDFromElement(elem), 'M0,1L5,6')
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'circle',
|
||||
attr: { id: 'circle', cx: '10', cy: '11', rx: '5', ry: '10' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
assert.equal(getPathDFromElement(elem), 'M5,11 C5,5.475138121546961 7.237569060773481,1 10,1 C12.762430939226519,1 15,5.475138121546961 15,11 C15,16.524861878453038 12.762430939226519,21 10,21 C7.237569060773481,21 5,16.524861878453038 5,11 Z')
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'polyline',
|
||||
attr: { id: 'polyline', points: '0,1 5,1 5,11 0,11' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
assert.equal(getPathDFromElement(elem), 'M0,1 5,1 5,11 0,11')
|
||||
elem.remove()
|
||||
|
||||
assert.equal(getPathDFromElement({ tagName: 'something unknown' }), undefined)
|
||||
})
|
||||
|
||||
it('Test getBBoxOfElementAsPath', function () {
|
||||
/**
|
||||
* Wrap `utilities.getBBoxOfElementAsPath` to convert bbox to object for testing.
|
||||
* @type {module:utilities.getBBoxOfElementAsPath}
|
||||
*/
|
||||
function getBBoxOfElementAsPath (elem, addSVGElementsFromJson, pathActions) {
|
||||
const bbox = utilities.getBBoxOfElementAsPath(elem, addSVGElementsFromJson, pathActions)
|
||||
return utilities.bboxToObj(bbox) // need this for assert.equal() to work.
|
||||
}
|
||||
|
||||
let elem = mockCreateSVGElement({
|
||||
element: 'path',
|
||||
attr: { id: 'path', d: 'M0,1 Z' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
let bbox = getBBoxOfElementAsPath(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 0, height: 0 })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getBBoxOfElementAsPath(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 10 })
|
||||
elem.remove()
|
||||
|
||||
elem = mockCreateSVGElement({
|
||||
element: 'line',
|
||||
attr: { id: 'line', x1: '0', y1: '1', x2: '5', y2: '6' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
bbox = getBBoxOfElementAsPath(elem, mockaddSVGElementsFromJson, mockPathActions)
|
||||
assert.deepEqual(bbox, { x: 0, y: 1, width: 5, height: 5 })
|
||||
elem.remove()
|
||||
|
||||
// TODO: test element with transform. Need resetOrientation above to be working or mock it.
|
||||
})
|
||||
|
||||
it('Test convertToPath rect', function () {
|
||||
const { convertToPath } = utilities
|
||||
const attrs = {
|
||||
fill: 'red',
|
||||
stroke: 'white',
|
||||
'stroke-width': '1',
|
||||
visibility: 'hidden'
|
||||
}
|
||||
|
||||
const elem = mockCreateSVGElement({
|
||||
element: 'rect',
|
||||
attr: { id: 'rect', x: '0', y: '1', width: '5', height: '10' }
|
||||
})
|
||||
svgroot.append(elem)
|
||||
const path = convertToPath(elem, attrs, mockSvgCanvas)
|
||||
assert.equal(path.getAttribute('d'), 'M0,1 L5,1 L5,11 L0,11 L0,1 Z')
|
||||
assert.equal(path.getAttribute('visibilituy'), null)
|
||||
assert.equal(path.id, 'rect')
|
||||
assert.equal(path.parentNode, svgroot)
|
||||
assert.equal(elem.parentNode, null)
|
||||
assert.equal(mockHistorySubCommands.length, 2)
|
||||
assert.equal(mockCount.clearSelection, 1)
|
||||
assert.equal(mockCount.addToSelection, 1)
|
||||
assert.equal(mockCount.addCommandToHistory, 1)
|
||||
path.remove()
|
||||
})
|
||||
|
||||
it('Test convertToPath unknown element', function () {
|
||||
const { convertToPath } = utilities
|
||||
const attrs = {
|
||||
fill: 'red',
|
||||
stroke: 'white',
|
||||
'stroke-width': '1',
|
||||
visibility: 'hidden'
|
||||
}
|
||||
|
||||
const elem = {
|
||||
tagName: 'something unknown',
|
||||
id: 'something-unknown',
|
||||
getAttribute () { return '' },
|
||||
parentNode: svgroot
|
||||
}
|
||||
const path = convertToPath(elem, attrs, mockSvgCanvas)
|
||||
assert.equal(path, null)
|
||||
assert.equal(elem.parentNode, svgroot)
|
||||
assert.equal(mockHistorySubCommands.length, 0)
|
||||
assert.equal(mockCount.clearSelection, 0)
|
||||
assert.equal(mockCount.addToSelection, 0)
|
||||
assert.equal(mockCount.addCommandToHistory, 0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user