migration to vite/playwright

This commit is contained in:
JFH
2025-11-29 11:31:34 +01:00
parent babd3490c9
commit a37fbac749
186 changed files with 2699 additions and 13224 deletions

View 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)
})
})

View 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
View 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
View 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
View 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
View 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)
})
})

View 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()
})
})

View 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())
})
})

View 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()
})
})

View 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()
})
})

View 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
View 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)
})
})