Files
gods-eye-view/src/ui/cockpitController.test.mjs
T
Sameh Khamis ffe14e50f5 feat(ui): render the Cockpit Contact card from a store
The Cockpit controller no longer writes the Contact card's scope,
navigation state, cohort counts, nearest contact, bearing, uncertainty,
update time or weather toggle: it keeps them as plain state, starting
from cockpitContextState.js, and publishes a copy to the gev-cockpit
store, and a new CockpitContext component renders it. A lost contact
still holds its last readout. The weather toggle flips from that state,
not the button's attribute. The card's visibility, collapse and layout
stay with the controller. The controller's publishBrief option becomes
a publish object, one entry per Cockpit view.
2026-10-31 14:08:19 -07:00

247 lines
7.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { CockpitViewController } from './cockpitController.js';
function environment() {
const original = { document: globalThis.document, window: globalThis.window };
const document = Object.assign(new EventTarget(), {
getElementById: () => null,
querySelectorAll: () => [],
body: { classList: { remove() {} } },
});
const window = Object.assign(new EventTarget(), { clearTimeout, setTimeout });
globalThis.document = document;
globalThis.window = window;
const callbacks = new Set();
const event = {
addEventListener(fn) {
callbacks.add(fn);
return () => callbacks.delete(fn);
},
};
const viewer = {
scene: {
preUpdate: event,
screenSpaceCameraController: { enableInputs: false },
},
trackedEntityChanged: event,
entities: { contains: () => false },
};
return {
viewer,
callbacks,
restore() {
Object.assign(globalThis, original);
},
};
}
test('Cockpit subscriptions are released once and retained actions cannot restart it', () => {
const env = environment();
try {
const owner = new CockpitViewController(env.viewer, { services: {} });
assert.equal(env.callbacks.size, 2);
owner.dispose();
owner.dispose();
assert.equal(env.callbacks.size, 0);
assert.equal(owner.enter(), false);
assert.equal(owner.navigateContext(1), false);
assert.equal(owner.toggleTrackedTr3b(), false);
assert.equal(owner.update(), false);
assert.equal(owner.active, false);
} finally {
env.restore();
}
});
test('disposing an active Cockpit releases the supplied render owner without retracking', () => {
const env = environment();
try {
const released = [];
const owner = new CockpitViewController(env.viewer, {
services: { releaseContinuousRender: (reason) => released.push(reason) },
});
owner.active = true;
owner.dispose();
owner.dispose();
assert.deepEqual(released, ['cockpit']);
assert.equal(
env.viewer.scene.screenSpaceCameraController.enableInputs,
true,
);
assert.equal(env.viewer.trackedEntity, undefined);
assert.equal(owner.active, false);
} finally {
env.restore();
}
});
test('aircraft identity is read from the supplied layer instances', () => {
const env = environment();
try {
const civilian = {
icao24: 'abc123',
layerId: 'flights',
callsign: 'CIVIL',
};
const military = {
icao24: 'def456',
layerId: 'military',
callsign: 'MILITARY',
};
const owner = new CockpitViewController(env.viewer, {
services: {
flightsLayer: { getTrackedInfo: () => civilian },
militaryFlightsLayer: { getTrackedInfo: () => military },
},
});
env.viewer.trackedEntity = { gevTrackedId: 'military:def456' };
assert.deepEqual(owner.readAircraftInfo(), military);
env.viewer.trackedEntity = { gevTrackedId: 'flights:abc123' };
assert.deepEqual(owner.readAircraftInfo(), civilian);
owner.dispose();
} finally {
env.restore();
}
});
test('an obsolete regional briefing cannot publish after Cockpit disposal', async () => {
const env = environment();
try {
let finish;
let signal;
const owner = new CockpitViewController(env.viewer, {
services: {
regionalDistanceM: () => Infinity,
fetchRegionalBrief: (_lat, _lon, options) => {
signal = options.signal;
return new Promise((resolve) => {
finish = resolve;
});
},
releaseContinuousRender() {},
},
});
const published = [];
owner.renderRegionalBrief = (payload) => published.push(payload);
owner.active = true;
owner.maybeRefreshRegionalBrief({
layerId: 'flights',
icao24: 'abc123',
latitude: 30,
longitude: -97,
});
owner.dispose();
assert.equal(signal.aborted, true);
finish({ articles: [] });
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(published, []);
assert.equal(owner.regionalBriefAbort, null);
} finally {
env.restore();
}
});
test('stopping Cockpit revokes camera actions before deferred disposal releases rendering', () => {
const env = environment();
try {
const released = [];
const owner = new CockpitViewController(env.viewer, {
services: {
releaseContinuousRender: (reason) => released.push(reason),
},
});
owner.active = true;
owner.stop();
assert.equal(env.callbacks.size, 0);
assert.equal(owner.update(), false);
assert.equal(owner.navigateContext(1), false);
assert.deepEqual(released, []);
owner.dispose();
assert.deepEqual(released, ['cockpit']);
assert.equal(owner.active, false);
} finally {
env.restore();
}
});
test('stopped Cockpit cannot rearm briefing rotation or request a fresh region', () => {
const env = environment();
try {
const frames = new Map();
let next = 0;
let fetches = 0;
let paints = 0;
env.viewer.scene.screenSpaceCameraController.enableInputs = true;
window.setTimeout = (callback) => {
frames.set(++next, callback);
return next;
};
window.clearTimeout = (id) => frames.delete(id);
const owner = new CockpitViewController(env.viewer, {
services: {
regionalDistanceM: () => Infinity,
fetchRegionalBrief: () => {
fetches++;
return Promise.resolve({ articles: [] });
},
releaseContinuousRender() {},
},
});
owner.active = true;
owner.showBriefPage = () => {
paints++;
};
owner.setBriefAutoRotate(true);
assert.equal(frames.size, 1);
const queued = [...frames.values()][0];
owner.stop();
assert.equal(frames.size, 0);
queued();
owner.setBriefAutoRotate(true);
owner.startBriefRotation();
owner.maybeRefreshRegionalBrief({ latitude: 30, longitude: -97 });
assert.equal(frames.size, 0);
assert.equal(paints, 0);
assert.equal(fetches, 0);
owner.dispose();
} finally {
env.restore();
}
});
test('the weather toggle flips from its own state, however fast it is pressed', () => {
const env = environment();
try {
const toggle = new EventTarget();
document.getElementById = (id) =>
id === 'cockpit-weather-toggle' ? toggle : null;
const requested = [];
window.addEventListener('gev:cockpit-weather-toggle', (event) =>
requested.push(event.detail.enabled),
);
const published = [];
const owner = new CockpitViewController(env.viewer, {
services: {},
publish: { context: (view) => published.push(view.weather) },
});
// Two presses before any view renders still alternate.
toggle.dispatchEvent(new Event('click'));
toggle.dispatchEvent(new Event('click'));
assert.deepEqual(requested, [true, false]);
assert.deepEqual(published, [true, false]);
// The effects owner reports its state; the next press flips that.
window.dispatchEvent(
new CustomEvent('gev:cockpit-weather-state', {
detail: { enabled: true },
}),
);
toggle.dispatchEvent(new Event('click'));
assert.deepEqual(requested, [true, false, false]);
assert.deepEqual(published, [true, false, true, false]);
owner.dispose();
} finally {
env.restore();
}
});