import { describe, expect, it } from 'vitest'; import type { StateSnapshot } from './game/state'; import { planesGeoJson, routesGeoJson, type AirportCoord } from './mapData'; const AIRPORTS = new Map([ ['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 { 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); }); });