/** * ZoomPanController Tests – Zoom & Pan Transformation */ import { describe, it, expect, beforeEach } from 'vitest'; import { ZoomPanController } from '../src/canvas/ZoomPanController'; function createMockCanvas(w = 800, h = 600): HTMLCanvasElement { const canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; const ctx = canvas.getContext('2d'); if (ctx) { ctx.fillRect = (() => {}) as any; ctx.strokeRect = (() => {}) as any; ctx.beginPath = (() => {}) as any; ctx.moveTo = (() => {}) as any; ctx.lineTo = (() => {}) as any; ctx.stroke = (() => {}) as any; ctx.fill = (() => {}) as any; ctx.save = (() => {}) as any; ctx.restore = (() => {}) as any; ctx.scale = (() => {}) as any; ctx.arc = (() => {}) as any; ctx.setLineDash = (() => {}) as any; ctx.clearRect = (() => {}) as any; ctx.translate = (() => {}) as any; ctx.rotate = (() => {}) as any; } return canvas; } describe('ZoomPanController', () => { let canvas: HTMLCanvasElement; let zpc: ZoomPanController; beforeEach(() => { canvas = createMockCanvas(800, 600); zpc = new ZoomPanController(canvas); }); describe('initial state', () => { it('should have scale=1, offsetX=0, offsetY=0', () => { const t = zpc.getTransform(); expect(t.a).toBe(1); expect(t.d).toBe(1); expect(t.e).toBe(0); expect(t.f).toBe(0); }); it('getScale should return 1 initially', () => { expect(zpc.getScale()).toBe(1); }); }); describe('getViewport', () => { it('should return viewport covering full canvas at scale=1', () => { const vp = zpc.getViewport(); expect(vp.minX).toBe(0); expect(vp.minY).toBe(0); expect(vp.maxX).toBe(800); expect(vp.maxY).toBe(600); }); it('should shrink visible area when zoomed in', () => { zpc.zoomAt(400, 300, 2); const vp = zpc.getViewport(); const w = vp.maxX - vp.minX; const h = vp.maxY - vp.minY; expect(w).toBeCloseTo(400, 1); expect(h).toBeCloseTo(300, 1); }); }); describe('zoomAt', () => { it('should increase scale with factor > 1', () => { zpc.zoomAt(100, 100, 2); expect(zpc.getScale()).toBe(2); }); it('should decrease scale with factor < 1', () => { zpc.zoomAt(100, 100, 0.5); expect(zpc.getScale()).toBe(0.5); }); it('should keep the zoom center point stable', () => { zpc.zoomAt(400, 300, 2); const screen = zpc.worldToScreen(400, 300); expect(screen.x).toBeCloseTo(400, 0); expect(screen.y).toBeCloseTo(300, 0); }); it('should clamp scale to minimum 0.01', () => { zpc.zoomAt(0, 0, 0.001); expect(zpc.getScale()).toBeGreaterThanOrEqual(0.01); }); it('should clamp scale to maximum 100', () => { for (let i = 0; i < 20; i++) { zpc.zoomAt(400, 300, 10); } expect(zpc.getScale()).toBeLessThanOrEqual(100); }); }); describe('pan', () => { it('should update offsetX and offsetY', () => { zpc.pan(50, 30); const t = zpc.getTransform(); expect(t.e).toBe(50); expect(t.f).toBe(30); }); it('should accumulate pan calls', () => { zpc.pan(10, 20); zpc.pan(30, 40); const t = zpc.getTransform(); expect(t.e).toBe(40); expect(t.f).toBe(60); }); }); describe('reset', () => { it('should restore scale=1 and offset=0,0', () => { zpc.zoomAt(100, 100, 3); zpc.pan(50, 50); zpc.reset(); const t = zpc.getTransform(); expect(t.a).toBe(1); expect(t.d).toBe(1); expect(t.e).toBe(0); expect(t.f).toBe(0); expect(zpc.getScale()).toBe(1); }); }); describe('worldToScreen & screenToWorld', () => { it('should convert world to screen at identity transform', () => { const s = zpc.worldToScreen(100, 200); expect(s.x).toBe(100); expect(s.y).toBe(200); }); it('should convert world to screen with scale and offset', () => { zpc.zoomAt(0, 0, 2); zpc.pan(50, 50); const s = zpc.worldToScreen(100, 100); expect(s.x).toBe(250); expect(s.y).toBe(250); }); }); describe('zoomFit', () => { it('should not crash with empty elements', () => { zpc.zoomFit([]); expect(zpc.getScale()).toBe(1); }); it('should fit elements within canvas', () => { zpc.zoomFit([ { x: 0, y: 0, width: 200, height: 200 }, { x: 400, y: 400, width: 200, height: 200 }, ]); expect(zpc.getScale()).toBeGreaterThan(0); expect(zpc.getScale()).toBeLessThan(100); }); }); describe('zoomToRect', () => { it('should zoom to a given rect', () => { zpc.zoomToRect({ minX: 0, minY: 0, maxX: 400, maxY: 300 }); expect(zpc.getScale()).toBeGreaterThan(0); }); it('should do nothing for zero-size rect', () => { const before = zpc.getScale(); zpc.zoomToRect({ minX: 0, minY: 0, maxX: 0, maxY: 0 }); expect(zpc.getScale()).toBe(before); }); }); });