/** * Tests for alignment and distribution operations * Tests align (top/bottom/left/right/center) and distribute */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import Konva from 'konva'; import { alignTop, alignBottom, alignLeft, alignRight, centerHorizontal, centerVertical, centerBoth, } from '$lib/canvas/operations/align'; import { distributeHorizontal, distributeVertical } from '$lib/canvas/operations/distribute'; import { grid, snapToGrid, drawGrid, removeGrid, updateGrid } from '$lib/canvas/grid'; import { get } from 'svelte/store'; describe('Alignment Operations', () => { let stage: Konva.Stage; let layer: Konva.Layer; let images: Map; beforeEach(() => { const container = document.createElement('div'); container.id = 'test-container'; document.body.appendChild(container); stage = new Konva.Stage({ container: 'test-container', width: 800, height: 600, }); layer = new Konva.Layer(); stage.add(layer); images = new Map(); const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; // Create 3 images at different positions const positions = [ { x: 100, y: 100 }, { x: 300, y: 150 }, { x: 200, y: 250 }, ]; positions.forEach((pos, index) => { const img = new Konva.Image({ image: imageElement, x: pos.x, y: pos.y, width: 100, height: 100, }); layer.add(img); images.set(`img${index + 1}`, img); }); layer.draw(); }); afterEach(() => { stage.destroy(); document.body.innerHTML = ''; }); describe('Align Top', () => { it('aligns images to topmost edge', () => { alignTop(images, ['img1', 'img2', 'img3']); const img1Y = images.get('img1')!.getClientRect().y; const img2Y = images.get('img2')!.getClientRect().y; const img3Y = images.get('img3')!.getClientRect().y; expect(img1Y).toBeCloseTo(img2Y); expect(img1Y).toBeCloseTo(img3Y); expect(img1Y).toBe(100); // Should align to img1 (topmost) }); it('calls callback on completion', () => { const callback = vi.fn(); alignTop(images, ['img1', 'img2'], { onAlignComplete: callback }); expect(callback).toHaveBeenCalledWith(['img1', 'img2']); }); }); describe('Align Bottom', () => { it('aligns images to bottommost edge', () => { alignBottom(images, ['img1', 'img2', 'img3']); const img1Bottom = images.get('img1')!.getClientRect().y + 100; const img2Bottom = images.get('img2')!.getClientRect().y + 100; const img3Bottom = images.get('img3')!.getClientRect().y + 100; expect(img1Bottom).toBeCloseTo(img2Bottom); expect(img1Bottom).toBeCloseTo(img3Bottom); }); }); describe('Align Left', () => { it('aligns images to leftmost edge', () => { alignLeft(images, ['img1', 'img2', 'img3']); const img1X = images.get('img1')!.getClientRect().x; const img2X = images.get('img2')!.getClientRect().x; const img3X = images.get('img3')!.getClientRect().x; expect(img1X).toBeCloseTo(img2X); expect(img1X).toBeCloseTo(img3X); }); }); describe('Align Right', () => { it('aligns images to rightmost edge', () => { alignRight(images, ['img1', 'img2', 'img3']); const img1Right = images.get('img1')!.getClientRect().x + 100; const img2Right = images.get('img2')!.getClientRect().x + 100; const img3Right = images.get('img3')!.getClientRect().x + 100; expect(img1Right).toBeCloseTo(img2Right); expect(img1Right).toBeCloseTo(img3Right); }); }); describe('Center Horizontal', () => { it('centers images horizontally within selection bounds', () => { centerHorizontal(images, ['img1', 'img2', 'img3']); const img1CenterX = images.get('img1')!.getClientRect().x + 50; const img2CenterX = images.get('img2')!.getClientRect().x + 50; const img3CenterX = images.get('img3')!.getClientRect().x + 50; expect(img1CenterX).toBeCloseTo(img2CenterX); expect(img1CenterX).toBeCloseTo(img3CenterX); }); }); describe('Center Vertical', () => { it('centers images vertically within selection bounds', () => { centerVertical(images, ['img1', 'img2', 'img3']); const img1CenterY = images.get('img1')!.getClientRect().y + 50; const img2CenterY = images.get('img2')!.getClientRect().y + 50; const img3CenterY = images.get('img3')!.getClientRect().y + 50; expect(img1CenterY).toBeCloseTo(img2CenterY); expect(img1CenterY).toBeCloseTo(img3CenterY); }); }); describe('Center Both', () => { it('centers images both horizontally and vertically', () => { centerBoth(images, ['img1', 'img2', 'img3']); const img1Center = { x: images.get('img1')!.getClientRect().x + 50, y: images.get('img1')!.getClientRect().y + 50, }; const img2Center = { x: images.get('img2')!.getClientRect().x + 50, y: images.get('img2')!.getClientRect().y + 50, }; expect(img1Center.x).toBeCloseTo(img2Center.x); expect(img1Center.y).toBeCloseTo(img2Center.y); }); }); describe('Edge Cases', () => { it('handles single image gracefully', () => { alignTop(images, ['img1']); // Should not throw, position may change expect(images.get('img1')!.y()).toBeDefined(); }); it('handles empty selection', () => { alignTop(images, []); alignLeft(images, []); // Should not throw }); }); }); describe('Distribution Operations', () => { let stage: Konva.Stage; let layer: Konva.Layer; let images: Map; beforeEach(() => { const container = document.createElement('div'); container.id = 'test-container'; document.body.appendChild(container); stage = new Konva.Stage({ container: 'test-container', width: 800, height: 600, }); layer = new Konva.Layer(); stage.add(layer); images = new Map(); const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; // Create 4 images for distribution [100, 200, 500, 600].forEach((x, index) => { const img = new Konva.Image({ image: imageElement, x, y: 100, width: 50, height: 50, }); layer.add(img); images.set(`img${index + 1}`, img); }); layer.draw(); }); afterEach(() => { stage.destroy(); document.body.innerHTML = ''; }); describe('Distribute Horizontal', () => { it('distributes images with equal horizontal spacing', () => { distributeHorizontal(images, ['img1', 'img2', 'img3', 'img4']); // Get positions after distribution const positions = ['img1', 'img2', 'img3', 'img4'].map( (id) => images.get(id)!.getClientRect().x ); // Check that spacing between consecutive images is equal const spacing1 = positions[1] - (positions[0] + 50); const spacing2 = positions[2] - (positions[1] + 50); expect(spacing1).toBeCloseTo(spacing2, 1); }); it('preserves first and last image positions', () => { const firstX = images.get('img1')!.x(); const lastX = images.get('img4')!.x(); distributeHorizontal(images, ['img1', 'img2', 'img3', 'img4']); expect(images.get('img1')!.x()).toBe(firstX); expect(images.get('img4')!.x()).toBe(lastX); }); it('does nothing with less than 3 images', () => { const img1X = images.get('img1')!.x(); const img2X = images.get('img2')!.x(); distributeHorizontal(images, ['img1', 'img2']); expect(images.get('img1')!.x()).toBe(img1X); expect(images.get('img2')!.x()).toBe(img2X); }); }); describe('Distribute Vertical', () => { it('distributes images with equal vertical spacing', () => { // Reposition images vertically images.get('img1')!.y(100); images.get('img2')!.y(200); images.get('img3')!.y(400); images.get('img4')!.y(500); distributeVertical(images, ['img1', 'img2', 'img3', 'img4']); // Get positions after distribution const positions = ['img1', 'img2', 'img3', 'img4'].map( (id) => images.get(id)!.getClientRect().y ); // Check spacing const spacing1 = positions[1] - (positions[0] + 50); const spacing2 = positions[2] - (positions[1] + 50); expect(spacing1).toBeCloseTo(spacing2, 1); }); }); }); describe('Grid Functionality', () => { let stage: Konva.Stage; let layer: Konva.Layer; beforeEach(() => { const container = document.createElement('div'); container.id = 'test-container'; document.body.appendChild(container); stage = new Konva.Stage({ container: 'test-container', width: 800, height: 600, }); layer = new Konva.Layer(); stage.add(layer); layer.draw(); grid.reset(); }); afterEach(() => { stage.destroy(); document.body.innerHTML = ''; }); describe('Grid Store', () => { it('starts with default settings', () => { const settings = get(grid); expect(settings.enabled).toBe(true); expect(settings.size).toBe(20); expect(settings.visible).toBe(false); expect(settings.snapEnabled).toBe(false); }); it('toggles visibility', () => { grid.toggleVisible(); expect(get(grid).visible).toBe(true); grid.toggleVisible(); expect(get(grid).visible).toBe(false); }); it('toggles snap', () => { grid.toggleSnap(); expect(get(grid).snapEnabled).toBe(true); grid.toggleSnap(); expect(get(grid).snapEnabled).toBe(false); }); it('sets grid size with bounds', () => { grid.setSize(50); expect(get(grid).size).toBe(50); grid.setSize(1); // Below min expect(get(grid).size).toBe(5); grid.setSize(300); // Above max expect(get(grid).size).toBe(200); }); it('resets to defaults', () => { grid.setSize(100); grid.toggleVisible(); grid.toggleSnap(); grid.reset(); const settings = get(grid); expect(settings.size).toBe(20); expect(settings.visible).toBe(false); expect(settings.snapEnabled).toBe(false); }); }); describe('Snap to Grid', () => { it('snaps position to grid', () => { const snapped = snapToGrid(123, 456, 20); expect(snapped.x).toBe(120); expect(snapped.y).toBe(460); }); it('handles exact grid positions', () => { const snapped = snapToGrid(100, 200, 20); expect(snapped.x).toBe(100); expect(snapped.y).toBe(200); }); it('rounds to nearest grid point', () => { const snapped1 = snapToGrid(19, 19, 20); expect(snapped1.x).toBe(20); const snapped2 = snapToGrid(11, 11, 20); expect(snapped2.x).toBe(20); const snapped3 = snapToGrid(9, 9, 20); expect(snapped3.x).toBe(0); }); it('works with different grid sizes', () => { const snapped50 = snapToGrid(123, 456, 50); expect(snapped50.x).toBe(100); expect(snapped50.y).toBe(450); const snapped10 = snapToGrid(123, 456, 10); expect(snapped10.x).toBe(120); expect(snapped10.y).toBe(460); }); }); describe('Visual Grid', () => { it('draws grid on layer', () => { const gridGroup = drawGrid(layer, 800, 600, 20); expect(gridGroup).toBeDefined(); expect(gridGroup.children.length).toBeGreaterThan(0); }); it('removes grid from layer', () => { drawGrid(layer, 800, 600, 20); removeGrid(layer); const grids = layer.find('.grid'); expect(grids.length).toBe(0); }); it('updates grid when settings change', () => { updateGrid(layer, get(grid), 800, 600); // Grid should not be visible by default const grids = layer.find('.grid'); expect(grids.length).toBe(0); // Enable visibility grid.toggleVisible(); updateGrid(layer, get(grid), 800, 600); const gridsAfter = layer.find('.grid'); expect(gridsAfter.length).toBeGreaterThan(0); }); it('grid lines respect opacity', () => { const gridGroup = drawGrid(layer, 800, 600, 20, '#000000', 0.3); // Check first child (a line) const firstLine = gridGroup.children[0] as Konva.Line; expect(firstLine.opacity()).toBe(0.3); }); }); });