MotorSignal-App/website/tests/telemetry-camera-follow.tes...

110 lines
4.4 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { computeCameraFollowStep } from '../lib/telemetry-camera-follow';
describe('telemetry-camera-follow engine', () => {
const createMockMap = (center = { lat: 35.0, lng: -115.0 }, size = { x: 600, y: 400 }) => {
return {
getCenter: vi.fn(() => ({ ...center })),
getSize: vi.fn(() => ({ ...size })),
latLngToContainerPoint: vi.fn(([lat, lng]: [number, number]) => {
// Linear scale for testing: 1 deg = 1000 px, centered at (300, 200)
const x = (size.x / 2) + (lng - center.lng) * 1000;
const y = (size.y / 2) - (lat - center.lat) * 1000;
return { x, y };
}),
containerPointToLatLng: vi.fn((pt: { x: number; y: number }) => {
const lng = center.lng + (pt.x - (size.x / 2)) / 1000;
const lat = center.lat - (pt.y - (size.y / 2)) / 1000;
return { lat, lng };
}),
};
};
it('keeps camera stationary when motorcycle marker is within dead zone radius', () => {
const map = createMockMap({ lat: 35.0, lng: -115.0 });
// Marker offset by only 10 pixels (dead zone is 32px)
// 10 px / 1000 = 0.01 deg
const markerLatLng = { lat: 35.005, lng: -115.005 }; // hypot is ~7 px
const result = computeCameraFollowStep(map, markerLatLng, 16.67, { deadZoneRadiusPx: 32 });
expect(result.shouldPan).toBe(false);
expect(result.excessPx).toBe(0);
expect(result.targetLatLng.lat).toBe(35.0);
expect(result.targetLatLng.lng).toBe(-115.0);
});
it('smoothly follows towards marker when outside dead zone', () => {
const map = createMockMap({ lat: 35.0, lng: -115.0 });
// Marker offset to the right by 100 pixels (dx = 100, dy = 0)
// lng = -115.0 + 100 / 1000 = -114.90
const markerLatLng = { lat: 35.0, lng: -114.90 };
const result = computeCameraFollowStep(map, markerLatLng, 16.67, {
deadZoneRadiusPx: 30,
maxStepPxPerFrame: 20,
dampingLambda: 4.5,
});
expect(result.shouldPan).toBe(true);
expect(result.distanceFromCenterPx).toBeCloseTo(100, 1);
expect(result.excessPx).toBeCloseTo(70, 1);
expect(result.panDeltaPx.x).toBeGreaterThan(0);
expect(result.panDeltaPx.y).toBeCloseTo(0, 4);
// Target longitude should move east (towards -114.90)
expect(result.targetLatLng.lng).toBeGreaterThan(-115.0);
expect(result.targetLatLng.lng).toBeLessThan(-114.90);
expect(result.targetLatLng.lat).toBeCloseTo(35.0, 5);
});
it('strictly clamps maximum pan step per frame to prevent outrunning tile loading', () => {
const map = createMockMap({ lat: 35.0, lng: -115.0 });
// Marker very far away (500 pixels away)
const markerLatLng = { lat: 35.0, lng: -114.50 };
const maxStep = 10;
const result = computeCameraFollowStep(map, markerLatLng, 100, {
deadZoneRadiusPx: 30,
maxStepPxPerFrame: maxStep,
dampingLambda: 10, // high damping
});
expect(result.shouldPan).toBe(true);
const stepLength = Math.hypot(result.panDeltaPx.x, result.panDeltaPx.y);
expect(stepLength).toBeCloseTo(maxStep, 2);
});
it('adjusts smoothly to higher playback speeds and larger frame deltas', () => {
const map = createMockMap({ lat: 35.0, lng: -115.0 });
const markerLatLng = { lat: 35.0, lng: -114.90 }; // 100px away
const stepShort = computeCameraFollowStep(map, markerLatLng, 16.67, { maxStepPxPerFrame: 50 });
const stepLong = computeCameraFollowStep(map, markerLatLng, 50, { maxStepPxPerFrame: 50 });
expect(stepLong.panDeltaPx.x).toBeGreaterThan(stepShort.panDeltaPx.x);
});
it('falls back safely to LatLng interpolation when container projection methods are missing', () => {
const mapWithoutProjection = {
getCenter: () => ({ lat: 35.0, lng: -115.0 }),
};
const markerLatLng = { lat: 35.01, lng: -115.01 }; // outside ~0.0003 deg dead zone
const result = computeCameraFollowStep(mapWithoutProjection, markerLatLng, 16.67);
expect(result.shouldPan).toBe(true);
expect(result.targetLatLng.lat).toBeGreaterThan(35.0);
expect(result.targetLatLng.lng).toBeLessThan(-115.0);
});
it('handles null, undefined, and non-finite coordinates gracefully without throwing', () => {
const map = createMockMap();
const resultNull = computeCameraFollowStep(null, null as any);
expect(resultNull.shouldPan).toBe(false);
const resultNaN = computeCameraFollowStep(map, { lat: NaN, lng: 10 });
expect(resultNaN.shouldPan).toBe(false);
});
});