/** * Tests for canvas image transformations * Tests rotate, scale, flip, crop, opacity, greyscale, and reset */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import Konva from 'konva'; import { rotateImageTo, rotateImageBy, rotateImage90CW, rotateImage90CCW, rotateImage180, resetImageRotation, getImageRotation, } from '$lib/canvas/transforms/rotate'; import { scaleImageTo, scaleImageBy, resetImageScale, doubleImageSize, halfImageSize, getImageScale, isAtMinScale, isAtMaxScale, } from '$lib/canvas/transforms/scale'; import { flipImageHorizontal, flipImageVertical, isFlippedHorizontal, isFlippedVertical, resetFlipHorizontal, resetFlipVertical, resetAllFlips, setFlipState, } from '$lib/canvas/transforms/flip'; import { setImageOpacity, setImageOpacityPercent, increaseOpacity, decreaseOpacity, resetImageOpacity, getImageOpacity, getImageOpacityPercent, isFullyOpaque, isFullyTransparent, } from '$lib/canvas/transforms/opacity'; import { applyGreyscale, removeGreyscale, toggleGreyscale, isGreyscaleApplied, setGreyscale, } from '$lib/canvas/transforms/greyscale'; import { cropImage, removeCrop, getCropRegion, isCropped, cropToSquare, } from '$lib/canvas/transforms/crop'; import { resetAllTransformations, resetGeometricTransformations, resetVisualTransformations, hasTransformations, getTransformationSummary, } from '$lib/canvas/transforms/reset'; describe('Image Rotation', () => { let image: Konva.Image; beforeEach(() => { const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; image = new Konva.Image({ image: imageElement, width: 200, height: 200, }); }); it('rotates to specific angle', () => { rotateImageTo(image, 45); expect(getImageRotation(image)).toBe(45); }); it('normalizes rotation to 0-360', () => { rotateImageTo(image, 450); expect(getImageRotation(image)).toBe(90); rotateImageTo(image, -90); expect(getImageRotation(image)).toBe(270); }); it('rotates by delta', () => { rotateImageTo(image, 30); rotateImageBy(image, 15); expect(getImageRotation(image)).toBe(45); }); it('rotates 90° clockwise', () => { rotateImage90CW(image); expect(getImageRotation(image)).toBe(90); }); it('rotates 90° counter-clockwise', () => { rotateImageTo(image, 90); rotateImage90CCW(image); expect(getImageRotation(image)).toBe(0); }); it('rotates to 180°', () => { rotateImage180(image); expect(getImageRotation(image)).toBe(180); }); it('resets rotation to 0', () => { rotateImageTo(image, 135); resetImageRotation(image); expect(getImageRotation(image)).toBe(0); }); }); describe('Image Scaling', () => { let image: Konva.Image; beforeEach(() => { const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; image = new Konva.Image({ image: imageElement, width: 200, height: 200, }); }); it('scales to specific factor', () => { scaleImageTo(image, 2.0); expect(getImageScale(image)).toBe(2.0); }); it('clamps scale to minimum', () => { scaleImageTo(image, 0.001); expect(getImageScale(image)).toBe(0.01); }); it('clamps scale to maximum', () => { scaleImageTo(image, 15.0); expect(getImageScale(image)).toBe(10.0); }); it('scales by factor', () => { scaleImageTo(image, 2.0); scaleImageBy(image, 1.5); expect(getImageScale(image)).toBe(3.0); }); it('doubles image size', () => { scaleImageTo(image, 1.0); doubleImageSize(image); expect(getImageScale(image)).toBe(2.0); }); it('halves image size', () => { scaleImageTo(image, 2.0); halfImageSize(image); expect(getImageScale(image)).toBe(1.0); }); it('resets scale to 1.0', () => { scaleImageTo(image, 3.5); resetImageScale(image); expect(getImageScale(image)).toBe(1.0); }); it('detects minimum scale', () => { scaleImageTo(image, 0.01); expect(isAtMinScale(image)).toBe(true); scaleImageTo(image, 1.0); expect(isAtMinScale(image)).toBe(false); }); it('detects maximum scale', () => { scaleImageTo(image, 10.0); expect(isAtMaxScale(image)).toBe(true); scaleImageTo(image, 1.0); expect(isAtMaxScale(image)).toBe(false); }); }); describe('Image Flipping', () => { let image: Konva.Image; beforeEach(() => { const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; image = new Konva.Image({ image: imageElement, width: 200, height: 200, }); }); it('flips horizontally', () => { flipImageHorizontal(image); expect(isFlippedHorizontal(image)).toBe(true); expect(image.scaleX()).toBeLessThan(0); }); it('flips vertically', () => { flipImageVertical(image); expect(isFlippedVertical(image)).toBe(true); expect(image.scaleY()).toBeLessThan(0); }); it('can flip both directions', () => { flipImageHorizontal(image); flipImageVertical(image); expect(isFlippedHorizontal(image)).toBe(true); expect(isFlippedVertical(image)).toBe(true); }); it('resets horizontal flip', () => { flipImageHorizontal(image); resetFlipHorizontal(image); expect(isFlippedHorizontal(image)).toBe(false); expect(image.scaleX()).toBeGreaterThan(0); }); it('resets vertical flip', () => { flipImageVertical(image); resetFlipVertical(image); expect(isFlippedVertical(image)).toBe(false); expect(image.scaleY()).toBeGreaterThan(0); }); it('resets all flips', () => { flipImageHorizontal(image); flipImageVertical(image); resetAllFlips(image); expect(isFlippedHorizontal(image)).toBe(false); expect(isFlippedVertical(image)).toBe(false); }); it('sets flip state explicitly', () => { setFlipState(image, true, false); expect(isFlippedHorizontal(image)).toBe(true); expect(isFlippedVertical(image)).toBe(false); setFlipState(image, false, true); expect(isFlippedHorizontal(image)).toBe(false); expect(isFlippedVertical(image)).toBe(true); }); it('preserves scale when flipping', () => { image.scale({ x: 2.0, y: 2.0 }); flipImageHorizontal(image); expect(Math.abs(image.scaleX())).toBe(2.0); expect(image.scaleY()).toBe(2.0); }); }); describe('Image Opacity', () => { let image: Konva.Image; beforeEach(() => { const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; image = new Konva.Image({ image: imageElement, width: 200, height: 200, }); }); it('sets opacity to specific value', () => { setImageOpacity(image, 0.5); expect(getImageOpacity(image)).toBe(0.5); }); it('clamps opacity to 0-1 range', () => { setImageOpacity(image, 1.5); expect(getImageOpacity(image)).toBe(1.0); setImageOpacity(image, -0.5); expect(getImageOpacity(image)).toBe(0.0); }); it('sets opacity by percentage', () => { setImageOpacityPercent(image, 75); expect(getImageOpacity(image)).toBe(0.75); expect(getImageOpacityPercent(image)).toBe(75); }); it('increases opacity', () => { setImageOpacity(image, 0.5); increaseOpacity(image, 0.2); expect(getImageOpacity(image)).toBeCloseTo(0.7); }); it('decreases opacity', () => { setImageOpacity(image, 0.8); decreaseOpacity(image, 0.3); expect(getImageOpacity(image)).toBeCloseTo(0.5); }); it('resets opacity to 1.0', () => { setImageOpacity(image, 0.3); resetImageOpacity(image); expect(getImageOpacity(image)).toBe(1.0); }); it('detects fully opaque', () => { setImageOpacity(image, 1.0); expect(isFullyOpaque(image)).toBe(true); setImageOpacity(image, 0.99); expect(isFullyOpaque(image)).toBe(false); }); it('detects fully transparent', () => { setImageOpacity(image, 0.0); expect(isFullyTransparent(image)).toBe(true); setImageOpacity(image, 0.01); expect(isFullyTransparent(image)).toBe(false); }); }); describe('Image Greyscale', () => { let image: Konva.Image; let layer: Konva.Layer; beforeEach(() => { const container = document.createElement('div'); document.body.appendChild(container); const stage = new Konva.Stage({ container, width: 800, height: 600, }); layer = new Konva.Layer(); stage.add(layer); const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; image = new Konva.Image({ image: imageElement, width: 200, height: 200, }); layer.add(image); }); afterEach(() => { document.body.innerHTML = ''; }); it('applies greyscale filter', () => { applyGreyscale(image); expect(isGreyscaleApplied(image)).toBe(true); }); it('removes greyscale filter', () => { applyGreyscale(image); removeGreyscale(image); expect(isGreyscaleApplied(image)).toBe(false); }); it('toggles greyscale on and off', () => { toggleGreyscale(image); expect(isGreyscaleApplied(image)).toBe(true); toggleGreyscale(image); expect(isGreyscaleApplied(image)).toBe(false); }); it('sets greyscale state explicitly', () => { setGreyscale(image, true); expect(isGreyscaleApplied(image)).toBe(true); setGreyscale(image, false); expect(isGreyscaleApplied(image)).toBe(false); }); it('does not apply duplicate filter', () => { applyGreyscale(image); const filters1 = image.filters() || []; applyGreyscale(image); const filters2 = image.filters() || []; expect(filters1.length).toBe(filters2.length); }); }); describe('Image Cropping', () => { let image: Konva.Image; beforeEach(() => { const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; image = new Konva.Image({ image: imageElement, width: 200, height: 200, }); }); it('applies crop region', () => { const cropRegion = { x: 10, y: 10, width: 100, height: 100 }; cropImage(image, cropRegion); expect(isCropped(image)).toBe(true); const crop = getCropRegion(image); expect(crop).toEqual(cropRegion); }); it('removes crop', () => { const cropRegion = { x: 10, y: 10, width: 100, height: 100 }; cropImage(image, cropRegion); removeCrop(image); expect(isCropped(image)).toBe(false); expect(getCropRegion(image)).toBeNull(); }); it('validates crop region bounds', () => { // Try to crop outside image bounds const invalidCrop = { x: 150, y: 150, width: 200, height: 200 }; cropImage(image, invalidCrop); const crop = getCropRegion(image); expect(crop).not.toBeNull(); // Crop should be adjusted to fit within image if (crop) { expect(crop.x).toBeLessThanOrEqual(200); expect(crop.y).toBeLessThanOrEqual(200); } }); it('crops to square', () => { cropToSquare(image); const crop = getCropRegion(image); expect(crop).not.toBeNull(); if (crop) { expect(crop.width).toBe(crop.height); } }); }); describe('Reset Transformations', () => { let image: Konva.Image; let layer: Konva.Layer; beforeEach(() => { const container = document.createElement('div'); document.body.appendChild(container); const stage = new Konva.Stage({ container, width: 800, height: 600, }); layer = new Konva.Layer(); stage.add(layer); const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; image = new Konva.Image({ image: imageElement, width: 200, height: 200, }); layer.add(image); }); afterEach(() => { document.body.innerHTML = ''; }); it('detects when transformations are applied', () => { expect(hasTransformations(image)).toBe(false); rotateImageTo(image, 45); expect(hasTransformations(image)).toBe(true); }); it('resets all transformations', () => { rotateImageTo(image, 90); scaleImageTo(image, 2.0); setImageOpacity(image, 0.5); flipImageHorizontal(image); applyGreyscale(image); resetAllTransformations(image); expect(getImageRotation(image)).toBe(0); expect(getImageScale(image)).toBe(1.0); expect(getImageOpacity(image)).toBe(1.0); expect(isFlippedHorizontal(image)).toBe(false); expect(isGreyscaleApplied(image)).toBe(false); }); it('resets only geometric transformations', () => { rotateImageTo(image, 90); scaleImageTo(image, 2.0); setImageOpacity(image, 0.5); resetGeometricTransformations(image); expect(getImageRotation(image)).toBe(0); expect(getImageScale(image)).toBe(1.0); expect(getImageOpacity(image)).toBe(0.5); // Unchanged }); it('resets only visual transformations', () => { rotateImageTo(image, 90); setImageOpacity(image, 0.5); applyGreyscale(image); resetVisualTransformations(image); expect(getImageRotation(image)).toBe(90); // Unchanged expect(getImageOpacity(image)).toBe(1.0); expect(isGreyscaleApplied(image)).toBe(false); }); it('gets transformation summary', () => { rotateImageTo(image, 45); scaleImageTo(image, 1.5); setImageOpacity(image, 0.8); flipImageHorizontal(image); applyGreyscale(image); const summary = getTransformationSummary(image); expect(summary.rotation).toBe(45); expect(summary.scale).toBe(1.5); expect(summary.opacity).toBe(0.8); expect(summary.flippedH).toBe(true); expect(summary.flippedV).toBe(false); expect(summary.greyscale).toBe(true); }); }); describe('Combined Transformations', () => { let image: Konva.Image; beforeEach(() => { const imageElement = new Image(); imageElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; image = new Konva.Image({ image: imageElement, width: 200, height: 200, }); }); it('applies multiple transformations independently', () => { rotateImageTo(image, 45); scaleImageTo(image, 2.0); setImageOpacity(image, 0.7); flipImageHorizontal(image); expect(getImageRotation(image)).toBe(45); expect(Math.abs(getImageScale(image))).toBe(2.0); expect(getImageOpacity(image)).toBe(0.7); expect(isFlippedHorizontal(image)).toBe(true); }); it('transformations do not interfere with each other', () => { scaleImageTo(image, 3.0); const scale1 = getImageScale(image); rotateImageTo(image, 90); const scale2 = getImageScale(image); expect(scale1).toBe(scale2); // Rotation doesn't affect scale }); it('can undo transformations individually', () => { rotateImageTo(image, 90); scaleImageTo(image, 2.0); setImageOpacity(image, 0.5); resetImageRotation(image); expect(getImageRotation(image)).toBe(0); expect(getImageScale(image)).toBe(2.0); // Unchanged expect(getImageOpacity(image)).toBe(0.5); // Unchanged }); });