import { readLayerSource } from '../testSupport/readLayerSource.mjs'; import { dispatchCockpitModeChanged, enter, exit, _adoptTrackedEntity } from '../ui/cockpitTrackingController.js'; import test from 'node:test'; import assert from 'node:assert/strict'; import * as Cesium from 'cesium'; import flightsLayer, { _setCockpitDetectionSubjectForTest as setFlightsCockpitSubject, _setTrackedFlightRefreshStateForTest, } from './flights.js'; import militaryFlightsLayer, { _setCockpitDetectionSubjectForTest as setMilitaryCockpitSubject, _setTrackedMilitaryRefreshStateForTest, } from './militaryFlights.js'; const SUBJECT = 'abc123'; const NEXT_SUBJECT = 'def456'; const FLIGHTS_SOURCE = readLayerSource(new URL('./flights.js', import.meta.url)); const MILITARY_SOURCE = readLayerSource(new URL('./militaryFlights.js', import.meta.url)); const LAYERS = [ { name: 'commercial', layer: flightsLayer, setSubject: setFlightsCockpitSubject, seed(icao24) { _setTrackedFlightRefreshStateForTest({ icao24, entity: null, billboard: candidateBillboard(icao24), billboardCollection: { show: true, remove() {} }, viewer: candidateViewer(), tracked: false, meta: { callsign: `${icao24.toUpperCase()} `, altitude: 10_668, klass: 'airliner', onGround: false, }, }); }, }, { name: 'military', layer: militaryFlightsLayer, setSubject: setMilitaryCockpitSubject, seed(icao24) { _setTrackedMilitaryRefreshStateForTest({ icao24, entity: null, billboard: candidateBillboard(icao24), billboardCollection: { show: true, remove() {} }, viewer: candidateViewer(), tracked: false, meta: { callsign: `${icao24.toUpperCase()} `, altitudeFt: 35_000, klass: 'fighter', onGround: false, }, }); }, }, ]; function candidateBillboard(icao24) { const offset = Number.parseInt(icao24.slice(-2), 16) || 1; return { position: Cesium.Cartesian3.fromDegrees(-97.7 + offset * 0.001, 30.2, 10_668), color: Cesium.Color.WHITE, show: true, }; } function candidateViewer() { return { camera: { positionCartographic: null }, scene: {}, }; } function candidateIds(layer) { return layer.getDetectableObjects({ maxCount: 10 }).map((candidate) => candidate.sourceId); } test('Cockpit lifecycle publishes one normalized aircraft identity to both detection owners', () => { const previousWindow = globalThis.window; const details = []; globalThis.window = { dispatchEvent: event => details.push(event.detail) }; try { dispatchCockpitModeChanged(true, { icao24: ' ABC123 ', layerId: 'military' }); dispatchCockpitModeChanged(true, { icao24: ' DEF456 ', layerId: 'other' }); dispatchCockpitModeChanged(false); assert.deepEqual(details, [ { active: true, subjectId: 'abc123', layerId: 'military' }, { active: true, subjectId: 'def456', layerId: null }, { active: false, subjectId: null, layerId: null }, ]); } finally { globalThis.window = previousWindow; } for (const action of [enter, _adoptTrackedEntity]) { assert.match(action.toString(), /this\.dispatchCockpitModeChanged\(true, info\);/, 'entry and in-Cockpit handoff each publish the active subject'); } assert.match(exit.toString(), /this\.dispatchCockpitModeChanged\(false\);/, 'exit clears the active subject'); for (const [name, source] of [ ['commercial', FLIGHTS_SOURCE], ['military', MILITARY_SOURCE], ]) { const consumer = /function\s*(?:parts\.\w+\.)?_applyCockpitState\(\s*detail\s*=\s*\{\},?\s*\)\s*\{[\s\S]*?\n {0,2}\}/ .exec(source)?.[0]; assert.ok(consumer, `${name} Cockpit consumer is defined`); assert.match(consumer, /detail\?\.subjectId/); assert.match(consumer, /\.trim\(\s*,?\s*\)\s*\.toLowerCase\(\s*,?\s*\)/); assert.doesNotMatch(consumer, /layerId/, `${name} must also suppress a duplicate subject originating in the sibling AIR feed`); } }); for (const fixture of LAYERS) { test(`${fixture.name} detection omits only the active Cockpit AIR subject`, () => { try { // The event contract normalizes ICAO24 before storing it. Exercise that // boundary with the uppercase form while the real candidate key stays // lowercase. fixture.setSubject(true, SUBJECT.toUpperCase()); fixture.seed(SUBJECT); assert.deepEqual( candidateIds(fixture.layer), [], 'the active first-person subject must not emit its own moving bracket', ); fixture.seed(NEXT_SUBJECT); assert.deepEqual( candidateIds(fixture.layer), [NEXT_SUBJECT], 'a nearby aircraft must keep its detection bracket in Cockpit', ); // A Cockpit handoff flips the suppression atomically: the old subject // becomes an ordinary nearby contact, and the new subject disappears. fixture.setSubject(true, NEXT_SUBJECT.toUpperCase()); fixture.seed(SUBJECT); assert.deepEqual(candidateIds(fixture.layer), [SUBJECT], 'handoff restores the old subject'); fixture.seed(NEXT_SUBJECT); assert.deepEqual(candidateIds(fixture.layer), [], 'handoff suppresses the new subject'); // Exiting Cockpit restores the same candidate without touching Detection // mode or the aircraft layer. fixture.setSubject(false, null); fixture.seed(NEXT_SUBJECT); assert.deepEqual(candidateIds(fixture.layer), [NEXT_SUBJECT], 'Cockpit exit restores the bracket'); } finally { fixture.setSubject(false, null); } }); } test('a Cockpit subject duplicated across commercial and military feeds is suppressed in both', () => { const realDocument = globalThis.document; globalThis.document = { createElement() { return { getContext() { return { clearRect() {}, save() {}, beginPath() {}, arc() {}, stroke() {}, fill() {}, restore() {}, }; }, toDataURL() { return 'data:image/png;base64,cockpit-contact-test'; }, }; }, }; try { // Both layers consume the shared subject identity regardless of which // aircraft layer originated the Cockpit event. This prevents one feed's // duplicate from leaving a second bracket on the pilot's own aircraft. setFlightsCockpitSubject(true, SUBJECT); setMilitaryCockpitSubject(true, SUBJECT); LAYERS[0].seed(SUBJECT); LAYERS[1].seed(SUBJECT); assert.deepEqual(candidateIds(flightsLayer), []); assert.deepEqual(candidateIds(militaryFlightsLayer), []); } finally { setFlightsCockpitSubject(false, null); setMilitaryCockpitSubject(false, null); if (realDocument === undefined) delete globalThis.document; else globalThis.document = realDocument; } });