import { test, expect } from './fixtures.js' import { setSvgSource, visitAndApproveStorage } from './helpers.js' test.describe('Group transform preservation', () => { test.beforeEach(async ({ page }) => { await visitAndApproveStorage(page) }) test('preserve group translate transform on click, move, and rotate', async ({ page }) => { // Load SVG with group containing translate transform await setSvgSource(page, ` `) // Wait for SVG to be loaded await page.waitForSelector('#svgroot', { timeout: 5000 }) // Click on one of the paths inside the group // This should select the parent group const firstPath = page.locator('#svg_2') await firstPath.click() // Verify the group was selected (not the individual path) const selectedGroup = page.locator('#svg_1') await expect(selectedGroup).toBeVisible() // Test 1: Verify group transform is preserved after click let groupTransform = await selectedGroup.getAttribute('transform') expect(groupTransform).toContain('translate(91.56') expect(groupTransform).toContain('99.67') // Test 2: Move 100 pixels to the left using arrow keys // Press Left arrow 10 times (each press moves 10 pixels with grid snapping) for (let i = 0; i < 10; i++) { await page.keyboard.press('ArrowLeft') } // Verify group transform still contains the original translate groupTransform = await selectedGroup.getAttribute('transform') expect(groupTransform).toContain('translate(91.56') expect(groupTransform).toContain('99.67') // And now also has a translate for the movement expect(groupTransform).toMatch(/translate\([^)]+\).*translate\([^)]+\)/) // Test 3: Rotate the group await page.locator('#angle').evaluate(el => { const input = el.shadowRoot.querySelector('elix-number-spin-box') input.value = '5' input.dispatchEvent(new Event('change', { bubbles: true })) }) // Verify group transform has both rotate and original translate groupTransform = await selectedGroup.getAttribute('transform') expect(groupTransform).toContain('rotate(5') expect(groupTransform).toContain('translate(91.56') expect(groupTransform).toContain('99.67') // Verify child paths still have their own transforms const path1Transform = await page.locator('#svg_2').getAttribute('transform') const path2Transform = await page.locator('#svg_3').getAttribute('transform') const path3Transform = await page.locator('#svg_4').getAttribute('transform') expect(path1Transform).toContain('matrix') expect(path2Transform).toContain('rotate(-90') expect(path3Transform).toContain('rotate(-90') }) test('multiple arrow key movements preserve group transform', async ({ page }) => { await setSvgSource(page, ` `) await page.waitForSelector('#svgroot', { timeout: 5000 }) // Select the group by clicking the rect await page.locator('#testRect').click() // Move right 5 times for (let i = 0; i < 5; i++) { await page.keyboard.press('ArrowRight') } // Move down 3 times for (let i = 0; i < 3; i++) { await page.keyboard.press('ArrowDown') } // Verify original transform is still there const groupTransform = await page.locator('#testGroup').getAttribute('transform') expect(groupTransform).toContain('translate(100') expect(groupTransform).toContain('100)') }) test('rotation followed by movement preserves both transforms', async ({ page }) => { await setSvgSource(page, ` `) await page.waitForSelector('#svgroot', { timeout: 5000 }) // Select the group await page.locator('#testCircle').click() // Rotate first await page.locator('#angle').evaluate(el => { const input = el.shadowRoot.querySelector('elix-number-spin-box') input.value = '45' input.dispatchEvent(new Event('change', { bubbles: true })) }) // Then move await page.keyboard.press('ArrowLeft') await page.keyboard.press('ArrowLeft') // Verify both rotate and translate are present const groupTransform = await page.locator('#testGroup').getAttribute('transform') expect(groupTransform).toContain('rotate(45') expect(groupTransform).toContain('translate(200') expect(groupTransform).toContain('150)') }) test('multiple movements preserve group structure without flattening', async ({ page }) => { await setSvgSource(page, ` `) await page.waitForSelector('#svgroot', { timeout: 5000 }) // Click to select the group const rect = page.locator('#testRect') await rect.click() // Store original transform const originalTransform = await page.locator('#testGroup').getAttribute('transform') expect(originalTransform).toContain('translate(100') // First movement: move right and down using keyboard for (let i = 0; i < 5; i++) { await page.keyboard.press('ArrowRight') } for (let i = 0; i < 5; i++) { await page.keyboard.press('ArrowDown') } // Verify group still has transform attribute (not flattened to children) let groupTransform = await page.locator('#testGroup').getAttribute('transform') expect(groupTransform).toContain('translate') // Verify original transform is preserved expect(groupTransform).toContain('100') // Most importantly: verify child has no transform (not flattened) let rectTransform = await rect.getAttribute('transform') expect(rectTransform).toBeNull() // Second movement: move left and up for (let i = 0; i < 3; i++) { await page.keyboard.press('ArrowLeft') } for (let i = 0; i < 3; i++) { await page.keyboard.press('ArrowUp') } // Verify group still has transform groupTransform = await page.locator('#testGroup').getAttribute('transform') expect(groupTransform).toContain('translate') // Critical: child should STILL have no transform rectTransform = await rect.getAttribute('transform') expect(rectTransform).toBeNull() // Third movement: ensure consistency for (let i = 0; i < 2; i++) { await page.keyboard.press('ArrowRight') } // Final verification: group has transforms, child does not groupTransform = await page.locator('#testGroup').getAttribute('transform') expect(groupTransform).toContain('translate') rectTransform = await rect.getAttribute('transform') expect(rectTransform).toBeNull() }) test('ungroup preserves element positions without jumping', async ({ page }) => { // Test the real bug case: group with translate containing paths with complex transforms await setSvgSource(page, ` `) await page.waitForSelector('#svgroot', { timeout: 5000 }) // Get initial bounding boxes before ungrouping const path1Box = await page.locator('#path1').boundingBox() const path2Box = await page.locator('#path2').boundingBox() const path3Box = await page.locator('#path3').boundingBox() // Click to select the group await page.locator('#testGroup').click() // Ungroup via keyboard shortcut or UI await page.keyboard.press('Control+Shift+G') // Wait for ungroup to complete await page.waitForTimeout(100) // Verify paths still exist and have transforms const path1Transform = await page.locator('#path1').getAttribute('transform') const path2Transform = await page.locator('#path2').getAttribute('transform') const path3Transform = await page.locator('#path3').getAttribute('transform') // All paths should have transforms (group's translate prepended to original) expect(path1Transform).toBeTruthy() expect(path2Transform).toBeTruthy() expect(path3Transform).toBeTruthy() // Critical: bounding boxes should not change (no visual jump) const path1BoxAfter = await page.locator('#path1').boundingBox() const path2BoxAfter = await page.locator('#path2').boundingBox() const path3BoxAfter = await page.locator('#path3').boundingBox() // Allow 1px tolerance for rounding expect(Math.abs(path1BoxAfter.x - path1Box.x)).toBeLessThan(2) expect(Math.abs(path1BoxAfter.y - path1Box.y)).toBeLessThan(2) expect(Math.abs(path2BoxAfter.x - path2Box.x)).toBeLessThan(2) expect(Math.abs(path2BoxAfter.y - path2Box.y)).toBeLessThan(2) expect(Math.abs(path3BoxAfter.x - path3Box.x)).toBeLessThan(2) expect(Math.abs(path3BoxAfter.y - path3Box.y)).toBeLessThan(2) }) test('drag after ungroup works correctly without jumps', async ({ page }) => { await setSvgSource(page, ` `) await page.waitForSelector('#svgroot', { timeout: 5000 }) // Select the group by clicking one of its children await page.locator('#rect1').click() // Ungroup await page.keyboard.press('Control+Shift+G') await page.waitForTimeout(100) // All elements should still be selected after ungroup // Get their positions before drag const rect1Before = await page.locator('#rect1').boundingBox() const rect2Before = await page.locator('#rect2').boundingBox() const rect3Before = await page.locator('#rect3').boundingBox() // Drag all selected elements using arrow keys for (let i = 0; i < 5; i++) { await page.keyboard.press('ArrowRight') } for (let i = 0; i < 5; i++) { await page.keyboard.press('ArrowDown') } // Get positions after drag const rect1After = await page.locator('#rect1').boundingBox() const rect2After = await page.locator('#rect2').boundingBox() const rect3After = await page.locator('#rect3').boundingBox() // All elements should have moved by approximately the same amount const rect1Delta = { x: rect1After.x - rect1Before.x, y: rect1After.y - rect1Before.y } const rect2Delta = { x: rect2After.x - rect2Before.x, y: rect2After.y - rect2Before.y } const rect3Delta = { x: rect3After.x - rect3Before.x, y: rect3After.y - rect3Before.y } // All should have moved approximately 50px right and 50px down (with grid snapping) expect(rect1Delta.x).toBeGreaterThan(40) expect(rect1Delta.y).toBeGreaterThan(40) // Deltas should be similar for all elements (moved together) expect(Math.abs(rect1Delta.x - rect2Delta.x)).toBeLessThan(5) expect(Math.abs(rect1Delta.y - rect2Delta.y)).toBeLessThan(5) expect(Math.abs(rect1Delta.x - rect3Delta.x)).toBeLessThan(5) expect(Math.abs(rect1Delta.y - rect3Delta.y)).toBeLessThan(5) // Verify transforms are consolidated (not accumulating) const rect1Transform = await page.locator('#rect1').getAttribute('transform') // Should have single consolidated transforms, not multiple stacked // Transform can be null (no transform) or contain at most one translate if (rect1Transform) { expect((rect1Transform.match(/translate/g) || []).length).toBeLessThanOrEqual(1) } }) })