All checks were successful
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 8s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 7s
CI/CD Pipeline / VM Test - performance (push) Successful in 8s
CI/CD Pipeline / VM Test - security (push) Successful in 7s
CI/CD Pipeline / Backend Linting (push) Successful in 3s
CI/CD Pipeline / Frontend Linting (push) Successful in 15s
CI/CD Pipeline / Nix Flake Check (push) Successful in 36s
CI/CD Pipeline / CI Summary (push) Successful in 0s
304 lines
7.7 KiB
TypeScript
304 lines
7.7 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|
|
|