import type { Transform } from 'fabric'; import { FabricImage, Canvas, Control, Point } from 'fabric'; import { createImageCroppingControls } from './croppingControls'; import { changeImageWidth, changeImageHeight, changeImageCropX, changeImageCropY, cropPanMoveHandler, ghostScalePositionHandler, scaleEquallyCropGenerator, renderGhostImage, changeImageHeightWithAutoCover, changeImageWidthWithAutoCover, } from './croppingHandlers'; import { describe, expect, test, beforeEach, afterEach, vi } from 'vitest'; describe('croppingHandlers', () => { let canvas: Canvas; let image: FabricImage; let transform: Transform; let eventData: any; function prepareTransform(target: FabricImage, corner: string): Transform { const origin = canvas._getOriginFromCorner(target, corner); return { target, corner, originX: origin.x, originY: origin.y, } as unknown as Transform; } function createMockImage( options: Partial<{ width: number; height: number; cropX: number; cropY: number; elementWidth: number; elementHeight: number; flipX: boolean; flipY: boolean; }> = {}, ): FabricImage { const { width = 100, height = 100, cropX = 0, cropY = 0, elementWidth = 200, elementHeight = 200, flipX = false, flipY = false, } = options; const imgElement = new Image(elementWidth, elementHeight); const img = new FabricImage(imgElement, { left: 50, top: 50, width, height, cropX, cropY, flipX, flipY, }); img.controls = createImageCroppingControls(); return img; } beforeEach(() => { canvas = new Canvas(); image = createMockImage(); canvas.add(image); eventData = {}; transform = prepareTransform(image, 'mrc'); }); afterEach(() => { canvas.off(); canvas.clear(); }); describe('changeImageWidth', () => { test('changes width normally when within bounds', () => { expect(image.width).toBe(100); const changed = changeImageWidth(eventData, transform, 180, 50); expect(changed).toBe(true); expect(image.width).toBe(180); }); test('constrains width to available width (upper limit)', () => { // Image element is 200px wide, cropX is 0, so max available is 200 image = createMockImage({ width: 100, cropX: 50, elementWidth: 200 }); canvas.add(image); transform = prepareTransform(image, 'mrc'); // Try to set width beyond available (200 - 50 = 150 available) changeImageWidth(eventData, transform, 500, 50); expect(image.width).toBe(150); }); test('constrains width to minimum of 1 (lower limit)', () => { image = createMockImage({ width: 100, cropX: 50, elementWidth: 200 }); transform = prepareTransform(image, 'mrc'); changeImageWidth(eventData, transform, 0.1, 50); expect(image.width).toBe(1); }); test('returns false when no modification occurred', () => { image = createMockImage({ width: 100, cropX: 50, elementWidth: 200, }); transform = prepareTransform(image, 'mrc'); const changed = changeImageWidth(eventData, transform, 200, 50); expect(changed).toBe(true); const changed2 = changeImageWidth(eventData, transform, 200, 50); expect(changed2).toBe(false); }); }); describe('changeImageHeight', () => { beforeEach(() => { image = createMockImage({ height: 100, cropY: 50, elementHeight: 200, }); transform = prepareTransform(image, 'mbc'); }); test('changes height normally when within bounds', () => { expect(image.height).toBe(100); const changed = changeImageHeight(eventData, transform, 50, 130); expect(changed).toBe(true); expect(image.height).toBe(130); }); test('constrains height to available height (upper limit)', () => { // Try to set height beyond available (200 - 50 = 150 available changeImageHeight(eventData, transform, 50, 500); expect(image.height).toBeLessThanOrEqual(150); }); test('constrains height to minimum of 1 (lower limit)', () => { // Mock to simulate setting negative height changeImageHeight(eventData, transform, 50, 0.1); expect(image.height).toBe(1); }); test('returns false when no modification occurred', () => { const changed = changeImageHeight(eventData, transform, 50, 200); expect(changed).toBe(true); const changed2 = changeImageHeight(eventData, transform, 50, 200); expect(changed2).toBe(false); }); }); describe('changeImageCropX', () => { beforeEach(() => { image = createMockImage({ width: 100, cropX: 50, elementWidth: 200, }); // Use 'ml' corner for cropX - changing left side moves cropX transform = prepareTransform(image, 'mlc'); }); test('changes cropX and width together', () => { const changed = changeImageCropX(eventData, transform, 20, 50); expect(image.cropX).toBe(70); expect(image.width).toBe(80); expect(changed).toBe(true); }); test('constrains cropX to minimum of 0 and adjusts width accordingly', () => { image = createMockImage({ width: 100, cropX: 10, elementWidth: 200 }); transform = prepareTransform(image, 'mlc'); changeImageCropX(eventData, transform, -10, 50); // newCropX is clamped to 0 (was -10) expect(image.cropX).toBe(0); // width = 100 + 10 - 0 = 110 expect(image.width).toBe(110); }); test('constrains cropX so image stays within element bounds and adjusts width accordingly', () => { changeImageCropX(eventData, transform, 50, 50); // newCropX = 100, but clamped to elementWidth - width = 200 - 100 = 100 (stays 100) expect(image.cropX).toBe(100); // width = 100 + 50 - 100 = 50 expect(image.width).toBe(50); // cropX + width should not exceed element width (200) expect(image.cropX + image.width).toBeLessThanOrEqual(200); }); test('returns false when no modification occurred', () => { const changed = changeImageCropX(eventData, transform, 0, 50); expect(changed).toBe(false); }); }); describe('changeImageCropY', () => { beforeEach(() => { image = createMockImage({ height: 100, cropY: 50, elementHeight: 200, }); // Use 'mt' corner for cropY - changing top side moves cropY transform = prepareTransform(image, 'mtc'); }); test('changes cropY and height together', () => { const changed = changeImageCropY(eventData, transform, 50, 20); // newCropY = 50 + 100 - 80 = 70 // height = 100 + 50 - 70 = 80 expect(image.cropY).toBe(70); expect(image.height).toBe(80); expect(changed).toBe(true); }); test('constrains cropY to minimum of 0 and adjusts height accordingly', () => { image = createMockImage({ height: 100, cropY: 10, elementHeight: 200 }); canvas.add(image); transform = prepareTransform(image, 'mtc'); changeImageCropY(eventData, transform, 50, -30); // newCropY is clamped to 0 (was -10) expect(image.cropY).toBe(0); // height = 100 + 10 - 0 = 110 expect(image.height).toBe(110); }); test('returns false when no modification occurred', () => { const changed = changeImageCropY(eventData, transform, 50, 0); expect(changed).toBe(false); }); }); describe('cropPanMoveHandler', () => { beforeEach(() => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 50, elementWidth: 300, elementHeight: 300, }); canvas.add(image); }); test('pans the image by adjusting cropX and cropY', () => { const original = { left: image.left, top: image.top, cropX: image.cropX, cropY: image.cropY, }; // Simulate moving the image 10px to the right and 10px down image.left = original.left + 10; image.top = original.top + 10; const moveEvent = { transform: { target: image, original, } as unknown as Transform, }; cropPanMoveHandler(moveEvent as any); // cropX should decrease (panning right means showing more of the left side) expect(image.cropX).toBeLessThan(original.cropX); // cropY should decrease (panning down means showing more of the top) expect(image.cropY).toBeLessThan(original.cropY); // Position should be restored to original expect(image.left).toBe(original.left); expect(image.top).toBe(original.top); }); test('constrains cropX to minimum of 0', () => { const original = { left: image.left, top: image.top, cropX: 10, cropY: 50, }; image.cropX = 10; // Move far right to try to get negative cropX image.left = original.left + 100; image.top = original.top; const moveEvent = { transform: { target: image, original, } as unknown as Transform, }; cropPanMoveHandler(moveEvent as any); expect(image.cropX).toBeGreaterThanOrEqual(0); }); test('constrains cropY to minimum of 0', () => { const original = { left: image.left, top: image.top, cropX: 50, cropY: 10, }; image.cropY = 10; // Move far down to try to get negative cropY image.left = original.left; image.top = original.top + 100; const moveEvent = { transform: { target: image, original, } as unknown as Transform, }; cropPanMoveHandler(moveEvent as any); expect(image.cropY).toBeGreaterThanOrEqual(0); }); test('constrains cropX so crop area stays within element bounds', () => { const original = { left: image.left, top: image.top, cropX: 150, // Near the right edge (element is 300px wide) cropY: 50, }; image.cropX = 150; // Move far left to try to exceed element width image.left = original.left - 200; image.top = original.top; const moveEvent = { transform: { target: image, original, } as unknown as Transform, }; cropPanMoveHandler(moveEvent as any); // cropX + width should not exceed element width expect(image.cropX + image.width).toBeLessThanOrEqual(300); }); test('constrains cropY so crop area stays within element bounds', () => { const original = { left: image.left, top: image.top, cropX: 50, cropY: 150, // Near the bottom edge (element is 300px tall) }; image.cropY = 150; // Move far up to try to exceed element height image.left = original.left; image.top = original.top - 200; const moveEvent = { transform: { target: image, original, } as unknown as Transform, }; cropPanMoveHandler(moveEvent as any); // cropY + height should not exceed element height expect(image.cropY + image.height).toBeLessThanOrEqual(300); }); test('pans correctly when flipX is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 100, cropY: 50, elementWidth: 300, elementHeight: 300, flipX: true, }); canvas.add(image); const original = { left: image.left, top: image.top, cropX: image.cropX, cropY: image.cropY, }; // Move the image 10px to the right image.left = original.left + 10; image.top = original.top; const moveEvent = { transform: { target: image, original, } as unknown as Transform, }; cropPanMoveHandler(moveEvent as any); // With flipX, moving right should increase cropX (opposite of normal) expect(image.cropX).toBeGreaterThan(original.cropX); }); test('pans correctly when flipY is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 100, elementWidth: 300, elementHeight: 300, flipY: true, }); canvas.add(image); const original = { left: image.left, top: image.top, cropX: image.cropX, cropY: image.cropY, }; // Move the image 10px down image.left = original.left; image.top = original.top + 10; const moveEvent = { transform: { target: image, original, } as unknown as Transform, }; cropPanMoveHandler(moveEvent as any); // With flipY, moving down should increase cropY (opposite of normal) expect(image.cropY).toBeGreaterThan(original.cropY); }); }); describe('flip-aware crop controls', () => { test('mlc control changes width when flipX is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 0, elementWidth: 200, elementHeight: 200, flipX: true, }); canvas.add(image); transform = prepareTransform(image, 'mlc'); const initialCropX = image.cropX; const initialWidth = image.width; // Call the mlc action handler image.controls.mlc.actionHandler(eventData, transform, 30, 50); // When flipX is true, mlc should change width, not cropX expect(image.cropX).toBe(initialCropX); expect(image.width).not.toBe(initialWidth); }); test('mrc control changes cropX when flipX is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 0, elementWidth: 200, elementHeight: 200, flipX: true, }); canvas.add(image); transform = prepareTransform(image, 'mrc'); const initialCropX = image.cropX; // Call the mrc action handler image.controls.mrc.actionHandler(eventData, transform, 180, 50); // When flipX is true, mrc should behave like mlc (change cropX) expect(image.cropX).not.toBe(initialCropX); }); test('mtc control changes height when flipY is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 0, cropY: 50, elementWidth: 200, elementHeight: 200, flipY: true, }); canvas.add(image); transform = prepareTransform(image, 'mtc'); const initialCropY = image.cropY; const initialHeight = image.height; // Call the mtc action handler image.controls.mtc.actionHandler(eventData, transform, 50, 30); // When flipY is true, mtc should change height, not cropY expect(image.cropY).toBe(initialCropY); expect(image.height).not.toBe(initialHeight); }); test('mbc control changes cropY when flipY is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 0, cropY: 50, elementWidth: 200, elementHeight: 200, flipY: true, }); canvas.add(image); transform = prepareTransform(image, 'mbc'); const initialCropY = image.cropY; // Call the mbc action handler image.controls.mbc.actionHandler(eventData, transform, 50, 180); // When flipY is true, mbc should behave like mtc (change cropY) expect(image.cropY).not.toBe(initialCropY); }); }); describe('ghostScalePositionHandler', () => { beforeEach(() => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 50, elementWidth: 300, elementHeight: 300, }); canvas.add(image); }); test('positions top-left corner control correctly', () => { const control = new Control({ x: -0.5, y: -0.5 }); const result = ghostScalePositionHandler.call( control, new Point(100, 100), [1, 2, 3, 4, 5, 6], // this matrix is not used image, ); expect(result).toEqual({ x: -50, y: -50 }); }); test('positions bottom-right corner control correctly', () => { const control = new Control({ x: 0.5, y: 0.5 }); const result = ghostScalePositionHandler.call( control, new Point(100, 100), [1, 2, 3, 4, 5, 6], // this matrix is not used image, ); expect(result).toEqual({ x: 250, y: 250 }); }); test('positions top-right corner control correctly', () => { const control = new Control({ x: 0.5, y: -0.5 }); const result = ghostScalePositionHandler.call( control, new Point(100, 100), [1, 2, 3, 4, 5, 6], // this matrix is not used image, ); expect(result).toEqual({ x: 250, y: -50 }); }); test('positions bottom-left corner control correctly', () => { const control = new Control({ x: -0.5, y: 0.5 }); const result = ghostScalePositionHandler.call( control, new Point(100, 100), [1, 2, 3, 4, 5, 6], // this matrix is not used image, ); expect(result).toEqual({ x: -50, y: 250 }); }); }); describe('scaleEquallyCropGenerator', () => { beforeEach(() => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 50, elementWidth: 300, elementHeight: 300, }); canvas.add(image); }); test('returns a TransformActionHandler function', () => { const handler = scaleEquallyCropGenerator(-0.5, -0.5); expect(typeof handler).toBe('function'); }); test('scales image uniformly from top-left corner', () => { const handler = scaleEquallyCropGenerator(-0.5, -0.5); transform = prepareTransform(image, 'tls'); expect(image.scaleX).toBe(1); // Simulate dragging to scale up const result = handler(eventData, transform, -400, -400); // The handler should return a boolean expect(result).toBe(true); expect(image.scaleX.toFixed(2)).toBe('2.17'); expect(image.scaleX).toBe(image.scaleY); }); test('scales image uniformly from bottom-right corner', () => { const handler = scaleEquallyCropGenerator(0.5, 0.5); transform = prepareTransform(image, 'brs'); expect(image.scaleX).toBe(1); const result = handler(eventData, transform, 400, 400); expect(result).toBe(true); expect(image.scaleX).toBe(1.5); expect(image.scaleX).toBe(image.scaleY); }); test('returns false when scaling would exceed element bounds', () => { // Set up image near the edge of element image = createMockImage({ width: 250, height: 250, cropX: 25, cropY: 25, elementWidth: 300, elementHeight: 300, }); canvas.add(image); const handler = scaleEquallyCropGenerator(-0.5, -0.5); transform = prepareTransform(image, 'tls'); // Try to scale down significantly which might push bounds const result = handler(eventData, transform, 10, 10); expect(result).toBe(false); }); test('adjusts cropX and cropY when scaling from negative corner', () => { image = createMockImage({ width: 90, height: 90, cropX: 25, cropY: 25, elementWidth: 300, elementHeight: 300, }); canvas.add(image); const handler = scaleEquallyCropGenerator(-0.5, -0.5); transform = prepareTransform(image, 'tls'); expect(image.cropX).toBe(25); expect(image.cropY).toBe(25); const result = handler(eventData, transform, 5, 5); expect(result).toBe(true); // When scaling from top-left, cropX and cropY should be recalculated expect(image.cropX).toBe(0); expect(image.cropY).toBe(0); }); test.each([ { controlName: 'tls', oppositeControlName: 'brs', flipX: true, flipY: false, }, { controlName: 'tls', oppositeControlName: 'brs', flipX: false, flipY: true, }, { controlName: 'tls', oppositeControlName: 'brs', flipX: true, flipY: true, }, { controlName: 'trs', oppositeControlName: 'bls', flipX: true, flipY: false, }, { controlName: 'trs', oppositeControlName: 'bls', flipX: false, flipY: true, }, { controlName: 'trs', oppositeControlName: 'bls', flipX: true, flipY: true, }, { controlName: 'brs', oppositeControlName: 'tls', flipX: true, flipY: false, }, { controlName: 'brs', oppositeControlName: 'tls', flipX: false, flipY: true, }, { controlName: 'brs', oppositeControlName: 'tls', flipX: true, flipY: true, }, { controlName: 'bls', oppositeControlName: 'trs', flipX: true, flipY: false, }, { controlName: 'bls', oppositeControlName: 'trs', flipX: false, flipY: true, }, { controlName: 'bls', oppositeControlName: 'trs', flipX: true, flipY: true, }, ])( 'keeps the opposite ghost corner fixed for $controlName when flipX=$flipX flipY=$flipY', ({ controlName, oppositeControlName, flipX, flipY }) => { image = createMockImage({ width: 120, height: 100, cropX: 40, cropY: 50, elementWidth: 320, elementHeight: 260, flipX, flipY, }); canvas.add(image); const getControlPoint = (name: string) => ghostScalePositionHandler.call( image.controls[name], new Point(image.width, image.height), [1, 0, 0, 1, 0, 0], image, ); const pointBefore = getControlPoint(controlName); const oppositePointBefore = getControlPoint(oppositeControlName); const center = image.getCenterPoint(); const dx = pointBefore.x < center.x ? 40 : -40; const dy = pointBefore.y < center.y ? 30 : -30; const handler = image.controls[controlName].actionHandler; const localTransform = prepareTransform(image, controlName); const changed = handler( eventData, localTransform, pointBefore.x + dx, pointBefore.y + dy, ); expect(changed).toBe(true); const oppositePointAfter = getControlPoint(oppositeControlName); expect(oppositePointAfter.x).toBeCloseTo(oppositePointBefore.x, 6); expect(oppositePointAfter.y).toBeCloseTo(oppositePointBefore.y, 6); }, ); }); describe('renderGhostImage', () => { beforeEach(() => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 50, elementWidth: 300, elementHeight: 300, }); }); test('draws image at correct position based on crop values', () => { const mockCtx = { globalAlpha: 1, strokeStyle: '', lineWidth: 0, drawImage: vi.fn(), strokeRect: vi.fn(), } as unknown as CanvasRenderingContext2D; renderGhostImage.call(image, { ctx: mockCtx }); // Should draw at (-width/2 - cropX, -height/2 - cropY) // = (-50 - 50, -50 - 50) = (-100, -100) expect(mockCtx.drawImage).toHaveBeenCalledWith( image._element, -100, -100, ); }); test('temporarily reduces globalAlpha by 50%', () => { let alphaWhenDrawing: number | undefined; const mockCtx = { globalAlpha: 0.8, strokeStyle: '', lineWidth: 0, drawImage: vi.fn(() => { alphaWhenDrawing = mockCtx.globalAlpha; }), strokeRect: vi.fn(), } as unknown as CanvasRenderingContext2D; renderGhostImage.call(image, { ctx: mockCtx }); // During draw, alpha should be 0.8 * 0.5 = 0.4 expect(alphaWhenDrawing).toBe(0.4); // After render, alpha should be restored expect(mockCtx.globalAlpha).toBe(0.8); }); test('draws border using borderColor', () => { image.borderColor = 'blue'; const mockCtx = { globalAlpha: 1, strokeStyle: '', lineWidth: 0, drawImage: vi.fn(), strokeRect: vi.fn(), } as unknown as CanvasRenderingContext2D; renderGhostImage.call(image, { ctx: mockCtx }); expect(mockCtx.strokeStyle).toBe('blue'); expect(mockCtx.strokeRect).toHaveBeenCalledWith(-100, -100, 300, 300); }); }); describe('changeImageEdgeWidth', () => { function prepareEdgeTransform( target: FabricImage, originX: 'left' | 'center' | 'right', originY: 'top' | 'center' | 'bottom', corner = 'mr', ): Transform { target.controls[corner] = new Control({ x: originX === 'left' ? 0.5 : originX === 'right' ? -0.5 : 0, y: originY === 'top' ? 0.5 : originY === 'bottom' ? -0.5 : 0, }); return { target, corner, originX, originY, width: target.width, height: target.height, original: { cropX: target.cropX, cropY: target.cropY, scaleX: target.scaleX, scaleY: target.scaleY, }, } as unknown as Transform; } test('increases width within available space (right edge)', () => { // 100px wide, cropX=50, element=300 -> 150px available on right image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 50, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'left', 'center'); const changed = changeImageWidthWithAutoCover( eventData, transform, 180, 50, ); expect(changed).toBe(true); expect(image.width).toBeGreaterThan(100); expect(image.scaleX).toBe(1); }); test('constrains width to element boundary (right edge)', () => { image = createMockImage({ width: 100, cropX: 50, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'left', 'center'); changeImageWidthWithAutoCover(eventData, transform, 500, 50); expect(image.width).toBeLessThanOrEqual(250); // 300 - 50 }); test('triggers cover scale when beyond element bounds', () => { // Already at max width, no crop space left image = createMockImage({ width: 300, height: 200, cropX: 0, cropY: 50, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'left', 'center'); changeImageWidthWithAutoCover(eventData, transform, 500, 100); expect(image.scaleX).toBeGreaterThan(1); expect(image.scaleX).toBe(image.scaleY); // uniform expect(image.width).toBe(300); }); test('expands into cropX space (left edge)', () => { image = createMockImage({ width: 100, cropX: 100, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'right', 'center'); changeImageWidthWithAutoCover(eventData, transform, -250, 50); expect(image.cropX).toBe(0); expect(image.width).toBe(200); // original 100 + cropX 100 }); test('triggers cover scale from left edge when cropX exhausted', () => { image = createMockImage({ width: 200, height: 200, cropX: 0, cropY: 50, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'right', 'center'); changeImageWidthWithAutoCover(eventData, transform, -400, 100); expect(image.scaleX).toBeGreaterThan(1); expect(image.cropX).toBe(0); }); }); describe('changeImageEdgeHeight', () => { function prepareEdgeTransform( target: FabricImage, originX: 'left' | 'center' | 'right', originY: 'top' | 'center' | 'bottom', corner = 'mb', ): Transform { target.controls[corner] = new Control({ x: originX === 'left' ? 0.5 : originX === 'right' ? -0.5 : 0, y: originY === 'top' ? 0.5 : originY === 'bottom' ? -0.5 : 0, }); return { target, corner, originX, originY, width: target.width, height: target.height, original: { cropX: target.cropX, cropY: target.cropY, scaleX: target.scaleX, scaleY: target.scaleY, }, } as unknown as Transform; } test('increases height within available space (bottom edge)', () => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 50, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'center', 'top'); changeImageHeightWithAutoCover(eventData, transform, 50, 180); expect(image.height).toBeGreaterThan(100); expect(image.scaleY).toBe(1); }); test('constrains height to element boundary (bottom edge)', () => { image = createMockImage({ height: 100, cropY: 50, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'center', 'top'); changeImageHeightWithAutoCover(eventData, transform, 50, 500); expect(image.height).toBeLessThanOrEqual(250); // 300 - 50 }); test('triggers cover scale when beyond element bounds', () => { image = createMockImage({ width: 200, height: 300, cropX: 50, cropY: 0, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'center', 'top'); changeImageHeightWithAutoCover(eventData, transform, 100, 500); expect(image.scaleY).toBeGreaterThan(1); expect(image.scaleX).toBe(image.scaleY); // uniform expect(image.height).toBe(300); }); test('expands into cropY space (top edge)', () => { image = createMockImage({ height: 100, cropY: 100, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'center', 'bottom'); changeImageHeightWithAutoCover(eventData, transform, 50, -250); expect(image.cropY).toBe(0); expect(image.height).toBe(200); // original 100 + cropY 100 }); test('triggers cover scale from top edge when cropY exhausted', () => { image = createMockImage({ width: 200, height: 200, cropX: 50, cropY: 0, elementWidth: 300, elementHeight: 300, }); canvas.add(image); transform = prepareEdgeTransform(image, 'center', 'bottom'); changeImageHeightWithAutoCover(eventData, transform, 100, -400); expect(image.scaleY).toBeGreaterThan(1); expect(image.cropY).toBe(0); }); }); describe('edge resize with flipped images', () => { function prepareEdgeTransform( target: FabricImage, originX: 'left' | 'center' | 'right', originY: 'top' | 'center' | 'bottom', corner: string, ): Transform { target.controls[corner] = new Control({ x: originX === 'left' ? 0.5 : originX === 'right' ? -0.5 : 0, y: originY === 'top' ? 0.5 : originY === 'bottom' ? -0.5 : 0, }); return { target, corner, originX, originY, width: target.width, height: target.height, original: { cropX: target.cropX, cropY: target.cropY, scaleX: target.scaleX, scaleY: target.scaleY, }, } as unknown as Transform; } test('right edge expands width when flipX is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 50, elementWidth: 300, elementHeight: 300, flipX: true, }); canvas.add(image); // Right edge: originX='left' (anchor opposite side) transform = prepareEdgeTransform(image, 'left', 'center', 'mr'); const initialWidth = image.width; // Drag outward (positive x in local coords after flip transform) changeImageWidthWithAutoCover(eventData, transform, 180, 50); expect(image.width).toBeGreaterThan(initialWidth); expect(image.width).toBe(150); expect(image.cropX).toBe(0); // eaten all crop expect(image.scaleX).toBe(1.2); // 20% of 150 to get to 180 }); test('left edge expands into cropX when flipX is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 100, cropY: 50, elementWidth: 300, elementHeight: 300, flipX: true, }); canvas.add(image); // Left edge: originX='right' (anchor opposite side) transform = prepareEdgeTransform(image, 'right', 'center', 'ml'); const initialCropX = image.cropX; // Drag outward (negative x expands left edge) changeImageWidthWithAutoCover(eventData, transform, -180, 50); expect(image.cropX).toBe(initialCropX); expect(image.width).toBe(200); expect(image.scaleX).toBe(1.4); // 40% of 200 to go from 100 to 280 }); test('bottom edge expands height when flipY is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 50, elementWidth: 300, elementHeight: 300, flipY: true, }); canvas.add(image); transform = prepareEdgeTransform(image, 'center', 'top', 'mb'); const initialHeight = image.height; changeImageHeightWithAutoCover(eventData, transform, 50, 180); expect(image.height).toBeGreaterThan(initialHeight); expect(image.height).toBe(150); expect(image.cropY).toBe(0); expect(image.scaleY).toBe(1.2); }); test('top edge expands into cropY when flipY is true', () => { image = createMockImage({ width: 100, height: 100, cropX: 50, cropY: 100, elementWidth: 300, elementHeight: 300, flipY: true, }); canvas.add(image); transform = prepareEdgeTransform(image, 'center', 'bottom', 'mt'); const initialCropY = image.cropY; changeImageHeightWithAutoCover(eventData, transform, 50, -180); expect(image.cropY).toBe(initialCropY); expect(image.height).toBe(200); expect(image.scaleY).toBe(1.4); }); }); });