diff --git a/src/cameraHandoff.test.mjs b/src/cameraHandoff.test.mjs index b4a594412..79497fd1a 100644 --- a/src/cameraHandoff.test.mjs +++ b/src/cameraHandoff.test.mjs @@ -114,10 +114,10 @@ test('voice Cockpit entry reaches the camera only through stamping seams', () => assert.doesNotMatch(cockpitTracking, /trackedEntity\s*=/); assert.doesNotMatch(cockpitTracking, /flyTo/); - // Seam 1: any tracker handoff stamps, so the adoption step is covered. + // Seam 1: a new tracker handoff stamps; restoring the session does not. assert.match( ui, - /viewer\.trackedEntityChanged\.addEventListener\(\s*\(entity\) => \{\s*if \(entity && !this\._disposed\)\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\);/, + /viewer\.trackedEntityChanged\.addEventListener\(\s*\(entity\) => \{[\s\S]*?if \(\s*entity &&\s*!this\._disposed &&\s*entity\.gevSelectionOrigin !== 'share-restore'\s*\)\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\);/, 'tracker handoff must stamp', ); // Seam 2 is pinned by "Cockpit takeover invalidates deferred work" above. diff --git a/src/layers/aircraft/sceneBinding.js b/src/layers/aircraft/sceneBinding.js index fb8530d54..d6ffaf1bd 100644 --- a/src/layers/aircraft/sceneBinding.js +++ b/src/layers/aircraft/sceneBinding.js @@ -58,6 +58,7 @@ export function createAircraftSceneBinding({ parts.snapshots.presentRecords(); if (flightState._trackedIcao) { parts.tracking._trackFlight(flightState._trackedIcao, { + origin: 'share-restore', resume: true, follow: resumeFollow, }); diff --git a/src/layers/aircraft/sceneBinding.test.mjs b/src/layers/aircraft/sceneBinding.test.mjs new file mode 100644 index 000000000..ac5d81748 --- /dev/null +++ b/src/layers/aircraft/sceneBinding.test.mjs @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createAircraftSceneBinding } from './sceneBinding.js'; +import { LayerBindings } from '../../ui/layerBindings.js'; +import { createEngineEvent } from '../../globe/contract.js'; + +test('resuming an aircraft on Cesium preserves the pending share navigation', (t) => { + const previous = globalThis.window; + globalThis.window = new EventTarget(); + t.after(() => { + globalThis.window = previous; + }); + const trackedEntityChanged = createEngineEvent(); + let generation = 1; + const shell = new LayerBindings({ + viewer: { trackedEntityChanged }, + services: {}, + operations: { + _stampNavigation() { + generation++; + }, + }, + }); + shell.observeCamera(); + t.after(() => shell.stop()); + const renderer = { + fleet: { attach() {}, setVisible() {}, restoreOrder() {} }, + models: { setVisible() {} }, + }; + let restored; + const binding = createAircraftSceneBinding({ + id: 'flights', + flightState: { + initialized: true, + _trackedIcao: 'a00000', + _preloadModel: {}, + }, + services: { renderer: { create: () => renderer } }, + parts: { + snapshots: { presentRecords() {} }, + tracking: { + _trackFlight(id, options) { + restored = { id, ...options }; + trackedEntityChanged.raiseEvent({ + gevSelectionOrigin: options.origin, + }); + }, + }, + }, + }); + binding.attachRenderer({}); + assert.equal(restored.id, 'a00000'); + assert.equal(restored.resume, true); + assert.equal( + generation, + 1, + 'restored tracking cannot supersede and clear itself', + ); + for (const origin of ['user', 'voice', 'programmatic']) { + trackedEntityChanged.raiseEvent({ gevSelectionOrigin: origin }); + } + assert.equal(generation, 4, 'new tracking still takes navigation authority'); +}); diff --git a/src/ui/layerBindings.js b/src/ui/layerBindings.js index dfd27d07e..7b9f8100a 100644 --- a/src/ui/layerBindings.js +++ b/src/ui/layerBindings.js @@ -86,7 +86,13 @@ export class LayerBindings { if (this.viewer) this._navigationOwnerChangedRemover = this.viewer.trackedEntityChanged.addEventListener((entity) => { - if (entity && !this._disposed) + // Rebinding the session's aircraft is part of the pending share + // restoration, not a new navigation that supersedes that restore. + if ( + entity && + !this._disposed && + entity.gevSelectionOrigin !== 'share-restore' + ) this._stampNavigation({ cancelPendingSelection: false }); }); // Vessel/installation focus flies without ever assigning a tracked entity,