fix: replace hardcoded localhost:3001 with relative API URL + add canvas mock and integration test
- Fix frontend Network error: API_BASE now defaults to empty string (same-origin) - Fix 3 files: api.ts, AuthContext.tsx, Dashboard.tsx - Add Canvas 2D context mock in tests/setup.ts for jsdom - Fix -0 vs +0 in ZoomPanController.getViewport() - Add IntegrationWorkflow.test.ts (35 tests, full CAD workflow)
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user