phase 8
This commit is contained in:
626
frontend/tests/canvas/transforms.test.ts
Normal file
626
frontend/tests/canvas/transforms.test.ts
Normal file
@@ -0,0 +1,626 @@
|
||||
/**
|
||||
* 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
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user