phase 10
All checks were successful
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 7s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 7s
CI/CD Pipeline / VM Test - performance (push) Successful in 7s
CI/CD Pipeline / VM Test - security (push) Successful in 7s
CI/CD Pipeline / Backend Linting (push) Successful in 2s
CI/CD Pipeline / Frontend Linting (push) Successful in 15s
CI/CD Pipeline / Nix Flake Check (push) Successful in 41s
CI/CD Pipeline / CI Summary (push) Successful in 1s
All checks were successful
CI/CD Pipeline / VM Test - backend-integration (push) Successful in 7s
CI/CD Pipeline / VM Test - full-stack (push) Successful in 7s
CI/CD Pipeline / VM Test - performance (push) Successful in 7s
CI/CD Pipeline / VM Test - security (push) Successful in 7s
CI/CD Pipeline / Backend Linting (push) Successful in 2s
CI/CD Pipeline / Frontend Linting (push) Successful in 15s
CI/CD Pipeline / Nix Flake Check (push) Successful in 41s
CI/CD Pipeline / CI Summary (push) Successful in 1s
This commit is contained in:
443
frontend/tests/canvas/clipboard.test.ts
Normal file
443
frontend/tests/canvas/clipboard.test.ts
Normal file
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Tests for clipboard operations (copy, cut, paste)
|
||||
* Tests clipboard store and operations
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { get } from 'svelte/store';
|
||||
import {
|
||||
clipboard,
|
||||
hasClipboardContent,
|
||||
clipboardCount,
|
||||
isCutOperation,
|
||||
} from '$lib/stores/clipboard';
|
||||
import type { ClipboardImageData } from '$lib/stores/clipboard';
|
||||
import {
|
||||
copySelectedImages,
|
||||
copyImages,
|
||||
copySingleImage,
|
||||
hasClipboardContent as hasContent,
|
||||
getClipboardCount,
|
||||
} from '$lib/canvas/clipboard/copy';
|
||||
import { cutSelectedImages, cutImages, cutSingleImage } from '$lib/canvas/clipboard/cut';
|
||||
import {
|
||||
pasteFromClipboard,
|
||||
pasteAtPosition,
|
||||
canPaste,
|
||||
getPastePreview,
|
||||
} from '$lib/canvas/clipboard/paste';
|
||||
import { selection } from '$lib/stores/selection';
|
||||
import { viewport } from '$lib/stores/viewport';
|
||||
|
||||
describe('Clipboard Store', () => {
|
||||
beforeEach(() => {
|
||||
clipboard.clear();
|
||||
selection.clearSelection();
|
||||
});
|
||||
|
||||
it('starts empty', () => {
|
||||
const state = get(clipboard);
|
||||
expect(state.images).toEqual([]);
|
||||
expect(state.operation).toBeNull();
|
||||
});
|
||||
|
||||
it('stores copied images', () => {
|
||||
const images: ClipboardImageData[] = [
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
];
|
||||
|
||||
clipboard.copy(images);
|
||||
|
||||
const state = get(clipboard);
|
||||
expect(state.images).toHaveLength(1);
|
||||
expect(state.operation).toBe('copy');
|
||||
});
|
||||
|
||||
it('stores cut images', () => {
|
||||
const images: ClipboardImageData[] = [
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
];
|
||||
|
||||
clipboard.cut(images);
|
||||
|
||||
const state = get(clipboard);
|
||||
expect(state.images).toHaveLength(1);
|
||||
expect(state.operation).toBe('cut');
|
||||
});
|
||||
|
||||
it('clears clipboard', () => {
|
||||
const images: ClipboardImageData[] = [
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
];
|
||||
|
||||
clipboard.copy(images);
|
||||
clipboard.clear();
|
||||
|
||||
const state = get(clipboard);
|
||||
expect(state.images).toEqual([]);
|
||||
expect(state.operation).toBeNull();
|
||||
});
|
||||
|
||||
it('hasClipboardContent reflects state', () => {
|
||||
expect(get(hasClipboardContent)).toBe(false);
|
||||
|
||||
clipboard.copy([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(get(hasClipboardContent)).toBe(true);
|
||||
});
|
||||
|
||||
it('clipboardCount reflects count', () => {
|
||||
expect(get(clipboardCount)).toBe(0);
|
||||
|
||||
clipboard.copy([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
{
|
||||
boardImageId: 'bi2',
|
||||
imageId: 'img2',
|
||||
position: { x: 200, y: 200 },
|
||||
transformations: {},
|
||||
zOrder: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(get(clipboardCount)).toBe(2);
|
||||
});
|
||||
|
||||
it('isCutOperation reflects operation type', () => {
|
||||
clipboard.copy([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
]);
|
||||
expect(get(isCutOperation)).toBe(false);
|
||||
|
||||
clipboard.cut([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
]);
|
||||
expect(get(isCutOperation)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Copy Operations', () => {
|
||||
beforeEach(() => {
|
||||
clipboard.clear();
|
||||
selection.clearSelection();
|
||||
});
|
||||
|
||||
it('copies selected images', () => {
|
||||
selection.selectMultiple(['img1', 'img2']);
|
||||
|
||||
const getImageData = (id: string): ClipboardImageData | null => ({
|
||||
boardImageId: `bi-${id}`,
|
||||
imageId: id,
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
});
|
||||
|
||||
const count = copySelectedImages(getImageData);
|
||||
|
||||
expect(count).toBe(2);
|
||||
expect(get(clipboardCount)).toBe(2);
|
||||
expect(get(isCutOperation)).toBe(false);
|
||||
});
|
||||
|
||||
it('copies specific images', () => {
|
||||
const getImageData = (id: string): ClipboardImageData | null => ({
|
||||
boardImageId: `bi-${id}`,
|
||||
imageId: id,
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
});
|
||||
|
||||
const count = copyImages(['img1', 'img2', 'img3'], getImageData);
|
||||
|
||||
expect(count).toBe(3);
|
||||
expect(get(clipboardCount)).toBe(3);
|
||||
});
|
||||
|
||||
it('copies single image', () => {
|
||||
const getImageData = (id: string): ClipboardImageData | null => ({
|
||||
boardImageId: `bi-${id}`,
|
||||
imageId: id,
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
});
|
||||
|
||||
const success = copySingleImage(getImageData, 'img1');
|
||||
|
||||
expect(success).toBe(true);
|
||||
expect(get(clipboardCount)).toBe(1);
|
||||
});
|
||||
|
||||
it('returns 0 when copying empty selection', () => {
|
||||
const getImageData = (): ClipboardImageData | null => null;
|
||||
|
||||
const count = copySelectedImages(getImageData);
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('hasClipboardContent returns true after copy', () => {
|
||||
const getImageData = (id: string): ClipboardImageData | null => ({
|
||||
boardImageId: `bi-${id}`,
|
||||
imageId: id,
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
});
|
||||
|
||||
copyImages(['img1'], getImageData);
|
||||
|
||||
expect(hasContent()).toBe(true);
|
||||
expect(getClipboardCount()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cut Operations', () => {
|
||||
beforeEach(() => {
|
||||
clipboard.clear();
|
||||
selection.clearSelection();
|
||||
});
|
||||
|
||||
it('cuts selected images', () => {
|
||||
selection.selectMultiple(['img1', 'img2']);
|
||||
|
||||
const getImageData = (id: string): ClipboardImageData | null => ({
|
||||
boardImageId: `bi-${id}`,
|
||||
imageId: id,
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
});
|
||||
|
||||
const count = cutSelectedImages(getImageData);
|
||||
|
||||
expect(count).toBe(2);
|
||||
expect(get(clipboardCount)).toBe(2);
|
||||
expect(get(isCutOperation)).toBe(true);
|
||||
});
|
||||
|
||||
it('cuts specific images', () => {
|
||||
const getImageData = (id: string): ClipboardImageData | null => ({
|
||||
boardImageId: `bi-${id}`,
|
||||
imageId: id,
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
});
|
||||
|
||||
const count = cutImages(['img1', 'img2'], getImageData);
|
||||
|
||||
expect(count).toBe(2);
|
||||
expect(get(isCutOperation)).toBe(true);
|
||||
});
|
||||
|
||||
it('cuts single image', () => {
|
||||
const getImageData = (id: string): ClipboardImageData | null => ({
|
||||
boardImageId: `bi-${id}`,
|
||||
imageId: id,
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
});
|
||||
|
||||
const success = cutSingleImage(getImageData, 'img1');
|
||||
|
||||
expect(success).toBe(true);
|
||||
expect(get(clipboardCount)).toBe(1);
|
||||
expect(get(isCutOperation)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Paste Operations', () => {
|
||||
beforeEach(() => {
|
||||
clipboard.clear();
|
||||
viewport.reset();
|
||||
});
|
||||
|
||||
it('pastes images at viewport center', () => {
|
||||
clipboard.copy([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
{
|
||||
boardImageId: 'bi2',
|
||||
imageId: 'img2',
|
||||
position: { x: 200, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const pasted = pasteFromClipboard(800, 600);
|
||||
|
||||
expect(pasted).toHaveLength(2);
|
||||
expect(pasted[0].newPosition).toBeDefined();
|
||||
expect(pasted[1].newPosition).toBeDefined();
|
||||
});
|
||||
|
||||
it('pastes at specific position', () => {
|
||||
clipboard.copy([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const pasted = pasteAtPosition(500, 500);
|
||||
|
||||
expect(pasted).toHaveLength(1);
|
||||
expect(pasted[0].newPosition.x).toBe(500);
|
||||
expect(pasted[0].newPosition.y).toBe(500);
|
||||
});
|
||||
|
||||
it('preserves relative positions when pasting', () => {
|
||||
clipboard.copy([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
{
|
||||
boardImageId: 'bi2',
|
||||
imageId: 'img2',
|
||||
position: { x: 200, y: 150 },
|
||||
transformations: {},
|
||||
zOrder: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const pasted = pasteAtPosition(0, 0);
|
||||
|
||||
// Relative distance should be preserved
|
||||
const deltaX1 = pasted[0].newPosition.x;
|
||||
const deltaX2 = pasted[1].newPosition.x;
|
||||
expect(deltaX2 - deltaX1).toBe(100); // Original was 200 - 100 = 100
|
||||
});
|
||||
|
||||
it('clears clipboard after cut paste', () => {
|
||||
clipboard.cut([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
pasteAtPosition(200, 200);
|
||||
|
||||
expect(get(hasClipboardContent)).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves clipboard after copy paste', () => {
|
||||
clipboard.copy([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
pasteAtPosition(200, 200);
|
||||
|
||||
expect(get(hasClipboardContent)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns empty array when pasting empty clipboard', () => {
|
||||
const pasted = pasteFromClipboard(800, 600);
|
||||
|
||||
expect(pasted).toEqual([]);
|
||||
});
|
||||
|
||||
it('canPaste reflects clipboard state', () => {
|
||||
expect(canPaste()).toBe(false);
|
||||
|
||||
clipboard.copy([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(canPaste()).toBe(true);
|
||||
});
|
||||
|
||||
it('getPastePreview shows where images will be pasted', () => {
|
||||
clipboard.copy([
|
||||
{
|
||||
boardImageId: 'bi1',
|
||||
imageId: 'img1',
|
||||
position: { x: 100, y: 100 },
|
||||
transformations: {},
|
||||
zOrder: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const preview = getPastePreview(800, 600);
|
||||
|
||||
expect(preview).toHaveLength(1);
|
||||
expect(preview[0]).toHaveProperty('x');
|
||||
expect(preview[0]).toHaveProperty('y');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user