/** * Tests for canvas image dragging functionality * Tests drag interactions, position updates, and multi-drag */ import { describe, it, expect, beforeEach, vi } from 'vitest'; import Konva from 'konva'; import { get } from 'svelte/store'; import { selection } from '$lib/stores/selection'; import { setupImageDrag, moveImageTo, moveImageBy, isDragging, } from '$lib/canvas/interactions/drag'; describe('Image Dragging', () => { let stage: Konva.Stage; let layer: Konva.Layer; let image: Konva.Image; let imageId: string; beforeEach(() => { // Create container const container = document.createElement('div'); container.id = 'test-container'; document.body.appendChild(container); // Create stage and layer stage = new Konva.Stage({ container: 'test-container', width: 800, height: 600, }); layer = new Konva.Layer(); stage.add(layer); // Create test image const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; image = new Konva.Image({ image: imageElement, x: 100, y: 100, width: 200, height: 200, }); layer.add(image); layer.draw(); imageId = 'test-image-1'; // Reset selection selection.clearSelection(); }); afterEach(() => { stage.destroy(); document.body.innerHTML = ''; }); describe('Setup and Initialization', () => { it('sets up drag handlers on image', () => { const cleanup = setupImageDrag(image, imageId); expect(image.draggable()).toBe(true); expect(typeof cleanup).toBe('function'); cleanup(); }); it('cleanup function removes drag handlers', () => { const cleanup = setupImageDrag(image, imageId); cleanup(); expect(image.draggable()).toBe(false); }); it('allows custom drag callbacks', () => { const onDragMove = vi.fn(); const onDragEnd = vi.fn(); setupImageDrag(image, imageId, onDragMove, onDragEnd); // Callbacks should be set up expect(onDragMove).not.toHaveBeenCalled(); expect(onDragEnd).not.toHaveBeenCalled(); }); }); describe('Drag Start', () => { it('selects image on drag start if not selected', () => { setupImageDrag(image, imageId); // Simulate drag start image.fire('dragstart', { evt: { button: 0, ctrlKey: false, metaKey: false }, }); const selectionState = get(selection); expect(selectionState.selectedIds.has(imageId)).toBe(true); }); it('adds to selection with Ctrl key', () => { const otherId = 'other-image'; selection.selectOne(otherId); setupImageDrag(image, imageId); image.fire('dragstart', { evt: { button: 0, ctrlKey: true, metaKey: false }, }); const selectionState = get(selection); expect(selectionState.selectedIds.has(imageId)).toBe(true); expect(selectionState.selectedIds.has(otherId)).toBe(true); expect(selectionState.selectedIds.size).toBe(2); }); it('updates drag state', () => { setupImageDrag(image, imageId); image.fire('dragstart', { evt: { button: 0, ctrlKey: false, metaKey: false }, }); // isDragging should return true during drag // Note: In actual implementation, this would be checked during dragmove }); }); describe('Drag Move', () => { it('calls onDragMove callback with current position', () => { const onDragMove = vi.fn(); setupImageDrag(image, imageId, onDragMove); image.fire('dragstart', { evt: { button: 0, ctrlKey: false, metaKey: false }, }); image.position({ x: 150, y: 150 }); image.fire('dragmove', { evt: {}, }); expect(onDragMove).toHaveBeenCalledWith(imageId, 150, 150); }); it('handles multi-drag when multiple images selected', () => { selection.selectMultiple([imageId, 'other-image']); const multiDragHandler = vi.fn(); stage.on('multiDragMove', multiDragHandler); setupImageDrag(image, imageId); image.fire('dragstart', { evt: { button: 0, ctrlKey: false, metaKey: false }, }); image.position({ x: 150, y: 150 }); image.fire('dragmove', { evt: {}, }); // Should fire multiDragMove event for other selected images expect(multiDragHandler).toHaveBeenCalled(); }); }); describe('Drag End', () => { it('calls onDragEnd callback with final position', () => { const onDragEnd = vi.fn(); setupImageDrag(image, imageId, undefined, onDragEnd); image.fire('dragstart', { evt: { button: 0, ctrlKey: false, metaKey: false }, }); image.position({ x: 200, y: 200 }); image.fire('dragend', { evt: {}, }); expect(onDragEnd).toHaveBeenCalledWith(imageId, 200, 200); }); it('resets drag state after drag ends', () => { setupImageDrag(image, imageId); image.fire('dragstart', { evt: { button: 0, ctrlKey: false, metaKey: false }, }); image.fire('dragend', { evt: {}, }); expect(isDragging()).toBe(false); }); }); describe('Programmatic Movement', () => { it('moveImageTo sets absolute position', () => { moveImageTo(image, 300, 300); expect(image.x()).toBe(300); expect(image.y()).toBe(300); }); it('moveImageBy moves relative to current position', () => { image.position({ x: 100, y: 100 }); moveImageBy(image, 50, 50); expect(image.x()).toBe(150); expect(image.y()).toBe(150); }); it('handles negative delta in moveImageBy', () => { image.position({ x: 100, y: 100 }); moveImageBy(image, -25, -25); expect(image.x()).toBe(75); expect(image.y()).toBe(75); }); it('moveImageTo works with large values', () => { moveImageTo(image, 10000, 10000); expect(image.x()).toBe(10000); expect(image.y()).toBe(10000); }); }); describe('Edge Cases', () => { it('handles dragging to negative coordinates', () => { moveImageTo(image, -100, -100); expect(image.x()).toBe(-100); expect(image.y()).toBe(-100); }); it('handles zero movement', () => { const initialX = image.x(); const initialY = image.y(); moveImageBy(image, 0, 0); expect(image.x()).toBe(initialX); expect(image.y()).toBe(initialY); }); it('handles rapid position changes', () => { for (let i = 0; i < 100; i++) { moveImageBy(image, 1, 1); } expect(image.x()).toBeGreaterThan(100); expect(image.y()).toBeGreaterThan(100); }); }); describe('Integration with Selection', () => { it('maintains selection during drag', () => { selection.selectOne(imageId); setupImageDrag(image, imageId); image.fire('dragstart', { evt: { button: 0, ctrlKey: false, metaKey: false }, }); image.fire('dragend', { evt: {}, }); const selectionState = get(selection); expect(selectionState.selectedIds.has(imageId)).toBe(true); }); it('clears other selections when dragging unselected image', () => { selection.selectMultiple(['other-image-1', 'other-image-2']); setupImageDrag(image, imageId); image.fire('dragstart', { evt: { button: 0, ctrlKey: false, metaKey: false }, }); const selectionState = get(selection); expect(selectionState.selectedIds.has(imageId)).toBe(true); expect(selectionState.selectedIds.has('other-image-1')).toBe(false); expect(selectionState.selectedIds.has('other-image-2')).toBe(false); expect(selectionState.selectedIds.size).toBe(1); }); }); });