Files
Flight-Ops/lib/mapData.test.ts
T

156 lines
6.7 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { StateSnapshot } from './game/state';
import { planesGeoJson, routesGeoJson, type AirportCoord } from './mapData';
const AIRPORTS = new Map<string, AirportCoord>([
['EDDF', { lat: 50.03, lon: 8.56 }],
['EDDV', { lat: 52.46, lon: 9.68 }],
['EHAM', { lat: 52.31, lon: 4.76 }],
]);
function snapshot(overrides: Partial<StateSnapshot> = {}): StateSnapshot {
return {
account: { cash: 0, reputation: 0 },
simState: { clockMs: 0, timeScale: 1, paused: false },
planes: [],
contracts: [],
catalog: [{ model: 'atr72', label: 'ATR 72-600', price: 200_000, maxFuel: 6300, burnPerKm: 1.75, speedKmh: 510 }],
fuelPricePerL: 0.8,
sellFraction: 0.5,
...overrides,
};
}
describe('planesGeoJson', () => {
it('returns an empty collection for a null snapshot', () => {
expect(planesGeoJson(null)).toEqual({ type: 'FeatureCollection', features: [] });
});
it('emits one Point per plane at its polled lat/lon with popup properties', () => {
const fc = planesGeoJson(
snapshot({
planes: [
{ id: 'plane-starter', model: 'atr72', status: 'available', fuel: 6300, maxFuel: 6300, locationIcao: 'EDDF', locationName: 'Frankfurt Main Airport', contractId: null, departureSimTime: null, lat: 50.03, lon: 8.56 },
{ id: 'plane-2', model: 'atr72', status: 'flying', fuel: 4000, maxFuel: 6300, locationIcao: 'EDDF', locationName: 'Frankfurt Main Airport', contractId: 'c1', departureSimTime: 1000, lat: 51.2, lon: 9.1 },
],
contracts: [
{ id: 'c1', originIcao: 'EDDF', originName: 'Frankfurt Main Airport', destIcao: 'EDDV', destName: 'Hannover Airport', reward: 100, cargoType: 'mail', cargoCount: 5, deadlineSimTime: 9e8, status: 'accepted', planeId: 'plane-2', createdSimTime: 0 },
],
}),
);
expect(fc.features).toHaveLength(2);
const grounded = fc.features[0];
expect(grounded.geometry.coordinates).toEqual([8.56, 50.03]); // [lon, lat]
expect(grounded.properties.flying).toBe(false);
expect(grounded.properties.modelLabel).toBe('ATR 72-600'); // catalog label, not raw model id
expect(grounded.properties.fuelText).toBe('6300 / 6300 L');
expect(grounded.properties.routeText).toBe('');
const flying = fc.features[1];
expect(flying.geometry.coordinates).toEqual([9.1, 51.2]); // live polled position
expect(flying.properties.flying).toBe(true);
expect(flying.properties.status).toBe('flying');
expect(flying.properties.fuelText).toBe('4000 / 6300 L');
expect(flying.properties.routeText).toBe('Frankfurt Main Airport → Hannover Airport');
});
it('falls back to the raw model id when the catalog has no entry', () => {
const fc = planesGeoJson(
snapshot({
catalog: [],
planes: [
{ id: 'p1', model: 'mystery', status: 'available', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'x', contractId: null, departureSimTime: null, lat: 0, lon: 0 },
],
}),
);
expect(fc.features[0].properties.modelLabel).toBe('mystery');
});
it('leaves routeText empty for a flying plane whose contract is missing from the snapshot', () => {
const fc = planesGeoJson(
snapshot({
planes: [
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'x', contractId: 'gone', departureSimTime: 0, lat: 0, lon: 0 },
],
}),
);
expect(fc.features[0].properties.routeText).toBe('');
});
});
describe('routesGeoJson', () => {
it('returns an empty collection for a null snapshot', () => {
expect(routesGeoJson(null, AIRPORTS)).toEqual({ type: 'FeatureCollection', features: [] });
});
it('draws nothing for open/completed/expired contracts', () => {
const mk = (status: string) => ({ id: `c-${status}`, originIcao: 'EDDF', originName: 'F', destIcao: 'EDDV', destName: 'H', reward: 1, cargoType: 'mail', cargoCount: 1, deadlineSimTime: 9e8, status, planeId: null, createdSimTime: 0 });
const fc = routesGeoJson(
snapshot({ contracts: [mk('open'), mk('completed'), mk('expired')] as StateSnapshot['contracts'] }),
AIRPORTS,
);
expect(fc.features).toHaveLength(0);
});
it('accepted-but-undispatched contract → pending line origin→dest', () => {
const fc = routesGeoJson(
snapshot({
contracts: [
{ id: 'c1', originIcao: 'EDDF', originName: 'F', destIcao: 'EHAM', destName: 'A', reward: 1, cargoType: 'mail', cargoCount: 1, deadlineSimTime: 9e8, status: 'accepted', planeId: null, createdSimTime: 0 },
],
}),
AIRPORTS,
);
expect(fc.features).toHaveLength(1);
expect(fc.features[0].properties).toEqual({ contractId: 'c1', kind: 'pending' });
expect(fc.features[0].geometry.coordinates).toEqual([
[8.56, 50.03], // EDDF [lon, lat]
[4.76, 52.31], // EHAM
]);
});
it('accepted + plane flying → inflight line', () => {
const fc = routesGeoJson(
snapshot({
planes: [
{ id: 'p1', model: 'atr72', status: 'flying', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', contractId: 'c1', departureSimTime: 0, lat: 51, lon: 9 },
],
contracts: [
{ id: 'c1', originIcao: 'EDDF', originName: 'F', destIcao: 'EDDV', destName: 'H', reward: 1, cargoType: 'mail', cargoCount: 1, deadlineSimTime: 9e8, status: 'accepted', planeId: 'p1', createdSimTime: 0 },
],
}),
AIRPORTS,
);
expect(fc.features).toHaveLength(1);
expect(fc.features[0].properties.kind).toBe('inflight');
});
it('accepted + planeId set but plane grounded → still pending (defensive)', () => {
const fc = routesGeoJson(
snapshot({
planes: [
{ id: 'p1', model: 'atr72', status: 'available', fuel: 1, maxFuel: 2, locationIcao: 'EDDF', locationName: 'F', contractId: null, departureSimTime: null, lat: 50.03, lon: 8.56 },
],
contracts: [
{ id: 'c1', originIcao: 'EDDF', originName: 'F', destIcao: 'EDDV', destName: 'H', reward: 1, cargoType: 'mail', cargoCount: 1, deadlineSimTime: 9e8, status: 'accepted', planeId: 'p1', createdSimTime: 0 },
],
}),
AIRPORTS,
);
expect(fc.features[0].properties.kind).toBe('pending');
});
it('skips a contract whose endpoint is missing from the airport dataset', () => {
const fc = routesGeoJson(
snapshot({
contracts: [
{ id: 'c1', originIcao: 'ZZZZ', originName: '?', destIcao: 'EDDV', destName: 'H', reward: 1, cargoType: 'mail', cargoCount: 1, deadlineSimTime: 9e8, status: 'accepted', planeId: null, createdSimTime: 0 },
],
}),
AIRPORTS,
);
expect(fc.features).toHaveLength(0);
});
});