Files
gods-eye-view/src/ui/shellLifetime.test.mjs
T
German KhokhlovandClaude Opus 5.5 36b675b928 fix(street-level): reconnect the panel once the data manager attaches, so a switch-on opens it
The panel connected before the shell had its data manager, so it never
heard switch-on requests and a user switch-on left it collapsed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-08 15:22:36 +04:00

164 lines
5.8 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { LocationNavigation } from './locationNavigation.js';
import { LayerBindings } from './layerBindings.js';
function replaceWindow(t, value) {
const previous = Object.getOwnPropertyDescriptor(globalThis, 'window');
Object.defineProperty(globalThis, 'window', { configurable: true, writable: true, value });
t.after(() => {
if (previous) Object.defineProperty(globalThis, 'window', previous);
else delete globalThis.window;
});
}
function location(t) {
t.mock.timers.enable({ apis: ['setTimeout'] });
replaceWindow(t, { setTimeout });
const calls = [];
let hooks;
const owner = new LocationNavigation({
viewer: { camera: {
cancelFlight() {}, lookAtTransform() {},
positionCartographic: { latitude: 0, longitude: 0, height: 20000000 },
} },
services: {
OrbitController: class { stop() { calls.push('orbit-stop'); } },
GLOBE_VIEW: { heightM: 20000000 },
flyToGlobeView(_viewer, callbacks) { hooks = callbacks; return {}; },
interruptCameraMotion() {},
militaryAwarenessLayer: {}, satellitesLayer: {}, rocketLaunchesLayer: {},
trafficLayer: {
beginWorldJump() { calls.push('begin'); },
endWorldJump() { calls.push('end'); },
},
suspendDetection() { calls.push('suspend'); },
resumeDetection() { calls.push('resume'); },
},
elements: {}, navigation: {}, readCockpit: () => null,
operations: {
_stampNavigation() {}, _updateTrafficSyncChip() {},
_runExplicitNavigation(_noun, run) { return run(); },
},
});
return { owner, calls, readHooks: () => hooks };
}
test('shell disposal settles a pending globe reset once and revokes its timeout', async (t) => {
const { owner, calls, readHooks } = location(t);
const pending = owner.resetToGlobeView();
assert.equal(owner.resetToGlobeView(), pending);
owner.destroy();
assert.equal((await pending).cancelled, true);
const after = [...calls];
readHooks().onComplete();
t.mock.timers.tick(10000);
owner.destroy();
assert.deepEqual(calls, after);
assert.equal(calls.filter(c => c === 'resume').length, 1);
assert.equal((await owner.resetToGlobeView()).cancelled, true);
});
test('world jump completion cannot revive navigation after shell disposal', (t) => {
const { owner, calls } = location(t);
let complete;
owner._flyWithTransition(true, ({ onStart, onComplete }) => {
onStart(); complete = onComplete;
});
owner.destroy();
const after = [...calls];
complete();
t.mock.timers.tick(10000);
assert.deepEqual(calls, after);
assert.equal(calls.filter(c => c === 'resume').length, 1);
});
function bindings(t) {
replaceWindow(t, new EventTarget());
const events = [];
const controls = {
hud: { attachDataManager() {} },
_contextControls: { connect() {} },
_cctvControls: { connect() {} }, _radioControls: { connect() {} },
_streetLevelControls: { connects: 0, connect() { this.connects += 1; } },
};
const owner = new LayerBindings({
viewer: {}, services: {}, readControls: () => controls,
feedback: {}, shareRestoration: { connect() {} },
operations: {
_updateTrafficSyncChip() {}, _updateGlobalLoadingFeedback() {},
_syncContextModeButtons() {},
},
});
function manager(id, directions = true) {
return {
layers: new Map(directions ? [['directions', { module: {
attachShellServices(value) { events.push([id, value ? 'attach' : 'detach']); },
} }]] : []),
subscribe() { events.push([id, 'subscribe']); return () => events.push([id, 'unsubscribe']); },
};
}
return { owner, manager, events, controls };
}
test('manager replacement releases Directions even when the new manager has no route layer', (t) => {
const { owner, manager, events } = bindings(t);
owner.attachDataManager(manager('old'));
owner.attachDataManager(manager('new', false));
assert.deepEqual(events, [
['old', 'subscribe'], ['old', 'attach'], ['old', 'unsubscribe'],
['new', 'subscribe'], ['old', 'detach'],
]);
owner.stop(); owner.disconnect();
assert.deepEqual(events.at(-1), ['new', 'unsubscribe']);
const count = events.length;
owner.attachDataManager(manager('late'));
owner.stop(); owner.disconnect();
assert.equal(events.length, count);
});
test('attaching the manager reconnects Street Level, so its switch-on requests reach the panel', (t) => {
const { owner, manager, controls } = bindings(t);
owner.attachDataManager(manager('first'));
assert.equal(controls._streetLevelControls.connects, 1);
owner.stop(); owner.disconnect();
});
test('camera-entry listeners are removed before any asynchronous layer cleanup', (t) => {
const { owner } = bindings(t);
let changes = 0;
let trackedCallback;
owner.viewer = { trackedEntityChanged: {
addEventListener(callback) {
trackedCallback = callback;
return () => { changes += 1; };
},
} };
owner._stampNavigation = () => { changes += 100; };
owner.observeCamera();
owner.stop();
trackedCallback({});
owner.stop();
assert.equal(changes, 1);
});
test('weather shell supplies live imagery hosts to wind and observations and releases them', (t) => {
const { owner } = bindings(t);
let host = { collection: {}, kind: 'tileset' };
owner.services.imageryHost = () => host;
const attached = new Map();
const ids = ['wind', 'weather-radar', 'weather-satellite', 'weather-lightning'];
owner.attachDataManager({
layers: new Map(ids.map(id => [id, { module: {
attachShellServices(services) { attached.set(id, services); },
} }])),
subscribe: () => () => {},
});
for (const id of ids) assert.equal(attached.get(id).imageryHost(), host);
host = { collection: null, kind: 'none' };
for (const id of ids) assert.equal(attached.get(id).imageryHost(), host);
owner.disconnect();
for (const id of ids) assert.equal(attached.get(id), null);
});