fix(cockpit): entry to the built-in views is as before: aircraft, satellite and vessel keep their fixed order whenever each was selected, and an application's subject takes entry only when it was selected after every built-in one present, so without providers nothing changes

This commit is contained in:
Denis Groshev committed 2026-11-12 16:58:38 +01:00
1 parent 6fbc98952f
commit a4f321a9ef
6 files changed
+119 -40

No files matched your search

+2 -2
View File
@@ -273,8 +273,8 @@ export function cockpitSubjectProviders(
* A provider's candidate: `{id, name, face, selectedAt}`, or null without a
* usable id. `name` falls back to the id; `face` is null unless well formed;
* `selectedAt` (a number or a short string, else null) is a token the
* provider changes each time its subject is selected, even again: entry
* takes the most recently selected subject (entryCandidates).
* provider changes each time its subject is selected, even again: a
* subject selected after the built-in ones takes entry (entryCandidates).
* @param {unknown} raw What `candidate()` returned.
*/
export function cockpitSubjectCandidate(raw) {
+3 -3
View File
@@ -603,7 +603,7 @@ test('the readout entry reads BRIDGE for a selected vessel', async (t) => {
assert.match(readoutEntry.textContent, /BRIDGE/);
});
test('without providers too, the later selection takes entry: a vessel after an aircraft reads BRIDGE', async (t) => {
test('without providers the built-in order decides: a vessel selected after an aircraft still reads COCKPIT', async (t) => {
const { $, cockpit, store, selectVessel } = fixture(t, {
enabled: ['flights', 'ais-live-vessels'],
tracked: { layerId: 'flights', info: CIVIL },
@@ -614,8 +614,8 @@ test('without providers too, the later selection takes entry: a vessel after an
selectVessel(VESSEL);
cockpit.syncEntry();
await nextTick();
assert.equal(store.presence.entryKind, 'vessel');
assert.match($('#cockpit-entry-readout').textContent, /BRIDGE/);
assert.equal(store.presence.entryKind, 'aircraft');
assert.match($('#cockpit-entry-readout').textContent, /COCKPIT/);
// And the other way round: an aircraft tracked after the vessel.
const later = fixture(t, {
enabled: ['flights', 'ais-live-vessels'],
+61 -1
View File
@@ -158,6 +158,7 @@ function fixture(t, { providers = [], vessel = null, layerOn = true } = {}) {
trackedEntityChanged: listeners(),
};
let vesselSelected = vessel;
let trackedAircraftInfo = null;
let floorM = FLOOR_M;
const warned = [];
t.mock.method(console, 'warn', (message) => warned.push(String(message)));
@@ -168,7 +169,7 @@ function fixture(t, { providers = [], vessel = null, layerOn = true } = {}) {
const dropped = [];
const cockpit = new CockpitViewController(viewer, {
services: {
flightsLayer: { getTrackedInfo: () => null },
flightsLayer: { getTrackedInfo: () => trackedAircraftInfo },
militaryFlightsLayer: { getTrackedInfo: () => null },
aisLiveVesselsLayer: {
getCockpitSubject: () =>
@@ -235,6 +236,11 @@ function fixture(t, { providers = [], vessel = null, layerOn = true } = {}) {
selectVessel(record) {
vesselSelected = record;
},
/** Track the fixture's aircraft as Live Flights does, or untrack it. */
trackAircraft(info) {
trackedAircraftInfo = info;
viewer.trackedEntity = info ? aircraft : undefined;
},
setFloor(value) {
floorM = value;
},
@@ -283,6 +289,15 @@ const VESSEL = Object.freeze({
missedRefreshes: 0,
});
/** The fixture's aircraft as Live Flights reports it once tracked. */
const TRACKED_AIRCRAFT = Object.freeze({
icao24: 'abc123',
callsign: 'UAL123',
track: 92,
latitude: LAT,
longitude: LON,
});
test('a provider candidate offers its own entry face; a shut gate hides it', async (t) => {
const ride = subjectProvider();
const { $, cockpit, store } = fixture(t, { providers: [ride] });
@@ -869,6 +884,51 @@ test('the most recently selected subject wins entry', async (t) => {
assert.equal(cockpit.enter(), false);
});
test('the built-in kinds keep their order; a subject takes entry only when selected after all of them', async (t) => {
const clock = manualClock(t, 10_000);
const ride = subjectProvider();
ride.subject.candidate = null;
const { cockpit, selectVessel, trackAircraft } = fixture(t, {
providers: [ride],
});
trackAircraft(TRACKED_AIRCRAFT);
cockpit.syncEntry();
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
// A subject selected after the aircraft was tracked takes entry.
clock.advance(500);
ride.subject.candidate = { id: 'a1', name: 'Example 12' };
cockpit.syncEntry();
assert.equal(cockpit.resolveEntryKind(), 'example-kind');
// A vessel selected after it: a built-in kind is newer, so the built-in
// order decides, as without providers, and the aircraft comes first.
clock.advance(500);
selectVessel(VESSEL);
cockpit.syncEntry();
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
// The subject selected again is newer than every built-in one.
clock.advance(500);
ride.subject.candidate = { id: 'a1', name: 'Example 12', selectedAt: 2 };
cockpit.syncEntry();
assert.equal(cockpit.resolveEntryKind(), 'example-kind');
// An aircraft tracked after it takes entry back.
selectVessel(null);
trackAircraft(null);
cockpit.syncEntry();
clock.advance(500);
trackAircraft(TRACKED_AIRCRAFT);
cockpit.syncEntry();
assert.equal(cockpit.resolveEntryKind(), 'aircraft');
});
test('a subject first seen together with a built-in one comes after it', async (t) => {
manualClock(t, 10_000);
const ride = subjectProvider();
const { cockpit, store } = fixture(t, { providers: [ride], vessel: VESSEL });
cockpit.syncEntry();
assert.equal(cockpit.resolveEntryKind(), 'vessel');
assert.equal(store.presence.entryKind, 'vessel');
});
test('a provider view with look turns with the arrow keys and words its readout', async (t) => {
const clock = manualClock(t, 10_000);
const ride = subjectProvider();
+35 -18
View File
@@ -93,7 +93,7 @@ export function subjectInfo(frame) {
};
}
/** Entry's fixed order, for candidates first seen at the same moment. */
/** The built-in kinds' fixed entry order. */
const BUILT_IN_ENTRY_ORDER = Object.freeze({
aircraft: 0,
satellite: 1,
@@ -101,14 +101,19 @@ const BUILT_IN_ENTRY_ORDER = Object.freeze({
});
/**
* Every subject entry could ride now, the most recently selected first: a
* tracked aircraft, the tracked satellite, the selected vessel and each
* provider's candidate. A candidate's time is when this controller first saw
* it (it looks on its entry cadence and on tracking changes), or saw it
* selected again: a provider candidate whose `selectedAt` changes, the same
* subject clicked again, moves to the front. Candidates first seen together
* keep the fixed order (aircraft, satellite, vessel, then providers by
* priority). Each carries `allowed()`, its kind's entry gate.
* Every subject entry could ride now, in entry order: a tracked aircraft,
* the tracked satellite, the selected vessel and each provider's candidate.
* The built-in kinds keep their fixed order (aircraft, satellite, vessel),
* whenever each was selected. A provider's candidate goes ahead of them only
* when it was selected after every built-in one present: a train picked
* after an aircraft was tracked takes entry, an aircraft tracked after it
* takes it back. Providers' candidates are ordered most recently selected
* first, then by priority. A candidate's time is when this controller first
* saw it (it looks on its entry cadence and on tracking changes), or saw it
* selected again (a provider candidate whose `selectedAt` changes). One
* first seen together with a built-in candidate comes after it. With no
* provider, entry is the built-in order alone. Each carries `allowed()`, its
* kind's entry gate.
* @param {number} [nowMs]
* @returns {Array<{kind: string, id: string, seenAtMs: number, order: number,
* allowed: () => boolean, provider?: object, candidate?: object}>}
@@ -167,13 +172,25 @@ export function entryCandidates(nowMs = performance.now()) {
// A subject gone and back counts as selected again.
for (const kind of [...seen.keys()])
if (!list.some((entry) => entry.kind === kind)) seen.delete(kind);
return list.sort((a, b) => b.seenAtMs - a.seenAtMs || a.order - b.order);
const builtIns = list
.filter((entry) => !entry.provider)
.sort((a, b) => a.order - b.order);
const provided = list
.filter((entry) => entry.provider)
.sort((a, b) => b.seenAtMs - a.seenAtMs || a.order - b.order);
const builtInSeenAtMs = Math.max(
-Infinity,
...builtIns.map((entry) => entry.seenAtMs),
);
const later = provided.filter((entry) => entry.seenAtMs > builtInSeenAtMs);
const earlier = provided.filter((entry) => entry.seenAtMs <= builtInSeenAtMs);
return [...later, ...builtIns, ...earlier];
}
/**
* The subject entry would ride: the most recently selected one. It decides
* alone, so when its own gate is shut entry is unavailable rather than taken
* by an older selection.
* The subject entry would ride: the first in entry order (entryCandidates).
* It decides alone, so when its own gate is shut entry is unavailable rather
* than taken by the next one.
* @returns {object|null} An entry of entryCandidates, or null.
*/
export function resolveEntry(nowMs = performance.now()) {
@@ -199,9 +216,9 @@ export function readSubjectLayerId() {
}
/**
* The kind entry would take: the most recently selected subject's
* (resolveEntry): a Cesium-tracked aircraft, the tracked satellite, the
* selected vessel or a provider's candidate.
* The kind entry would take, the first in entry order's (resolveEntry): a
* Cesium-tracked aircraft, the tracked satellite, the selected vessel or a
* provider's candidate.
* @returns {string|null}
*/
export function resolveEntryKind() {
@@ -296,7 +313,7 @@ export function syncEntry() {
const candidates = this.entryCandidates(performance.now());
const tracked = candidates.some(({ kind }) => kind === 'aircraft');
this.syncTr3bToggle(tracked ? this.readAircraftInfo() : null);
// The most recently selected subject decides (resolveEntry).
// The first in entry order decides alone (resolveEntry).
const chosen = candidates[0] ?? null;
const entryKind = chosen?.allowed() ? chosen.kind : null;
const face =
@@ -518,7 +535,7 @@ function enterSubject(provider) {
}
/**
* Enter first person on the most recently selected subject (resolveEntry): a
* Enter first person on the first subject in entry order (resolveEntry): a
* tracked aircraft, the tracked satellite's orbital view, the selected
* vessel's bridge or an application provider's subject.
* @param {{kind?: string|null}} [options] Restrict entry to one subject kind;