mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-10 21:32:23 +00:00
fix(cyclones): name the classification and mark a stale cache in shared context
- Publish classificationName next to the raw NHC code; unknown codes stay unexpanded. - Publish staleCacheFetchedAt only while the proxy serves its cache. - Touch the shared store on clear only after this layer published, so a feed failure no longer evicts another layer's dangling selection. - Drop the unreachable no-storm branch and the unused return value. - Tests: split the heading fixture into due north and stationary, cover the unavailable and failure paths with the real store, and check that clear/disable leave another layer's selection alone; fold the fake-store test into the real-store one.
This commit is contained in:
1 parent
aca5317a2a
commit
4e518821b5
3 files changed
+247
-147
No files matched your search
@@ -743,12 +743,12 @@ NHC movement speed is converted from miles per hour to rounded knots by the
|
|||||||
proxy; advisory wind intensity is already in knots and is passed through.
|
proxy; advisory wind intensity is already in knots and is passed through.
|
||||||
Cyclone markers, labels, tracks and cones are hidden beyond the horizon on every map source.
|
Cyclone markers, labels, tracks and cones are hidden beyond the horizon on every map source.
|
||||||
Explicit cyclone selections publish one shared `weather-cyclones` record
|
Explicit cyclone selections publish one shared `weather-cyclones` record
|
||||||
with official advisory provenance and source position. Switching storms removes
|
with official advisory provenance, source position, the English classification
|
||||||
the prior record; automatic row selection never claims global context, and
|
name and, while the proxy serves its cache, the cache fetch time. Switching
|
||||||
refreshes do not re-announce selection. Clearing or losing an explicitly selected
|
storms selects the replacement before pruning the old record; automatic row
|
||||||
storm removes shared context rather than assigning a fallback.
|
selection never claims global context, and refreshes do not re-announce
|
||||||
Deliberate clear/disable reports `deliberate`; feed disappearance reports
|
selection. Deliberate clear/disable reports `deliberate`; losing the selected
|
||||||
`evicted`. Switching storms selects the replacement before pruning the old record.
|
storm from the feed reports `evicted` and assigns no fallback.
|
||||||
Consult the linked official advisory for safety decisions.
|
Consult the linked official advisory for safety decisions.
|
||||||
|
|
||||||
Voice and HUD snapshots reuse the existing feedState classifier. Analyst follow-ups retain their original data provenance; current-view results append provenance without replacing legacy fields. HUD context and deterministic telemetry include non-nominal feed state.
|
Voice and HUD snapshots reuse the existing feedState classifier. Analyst follow-ups retain their original data provenance; current-view results append provenance without replacing legacy fields. HUD context and deterministic telemetry include non-nominal feed state.
|
||||||
|
|||||||
@@ -96,19 +96,19 @@ export function createCyclonesLayer({
|
|||||||
const selected = () =>
|
const selected = () =>
|
||||||
snapshot?.storms.find((storm) => storm.id === selectedId) || null;
|
snapshot?.storms.find((storm) => storm.id === selectedId) || null;
|
||||||
function clearSelectedContext({ evicted = false } = {}) {
|
function clearSelectedContext({ evicted = false } = {}) {
|
||||||
if (!evicted)
|
if (selectedContextStormId) {
|
||||||
context?.clearSelectedEntityContextForLayer?.('weather-cyclones');
|
if (!evicted)
|
||||||
context?.removeEntityContextsForLayer?.('weather-cyclones');
|
context?.clearSelectedEntityContextForLayer?.('weather-cyclones');
|
||||||
|
context?.removeEntityContextsForLayer?.('weather-cyclones');
|
||||||
|
}
|
||||||
selectedContextStormId = null;
|
selectedContextStormId = null;
|
||||||
selectedContextEntity = null;
|
selectedContextEntity = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Callers publish only while selectedId names a storm in the snapshot.
|
||||||
function publishSelectedContext({ announce = false } = {}) {
|
function publishSelectedContext({ announce = false } = {}) {
|
||||||
const storm = selected();
|
const storm = selected();
|
||||||
if (!storm || !context) {
|
if (!context) return;
|
||||||
if (!storm) clearSelectedContext({ evicted: true });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
selectedContextEntity ||= { show: true };
|
selectedContextEntity ||= { show: true };
|
||||||
const contextId = `cyclone:${storm.id}`;
|
const contextId = `cyclone:${storm.id}`;
|
||||||
const record = context.registerEntityContext?.(selectedContextEntity, {
|
const record = context.registerEntityContext?.(selectedContextEntity, {
|
||||||
@@ -116,13 +116,20 @@ export function createCyclonesLayer({
|
|||||||
layerId: 'weather-cyclones',
|
layerId: 'weather-cyclones',
|
||||||
layerName: 'Cyclone advisories',
|
layerName: 'Cyclone advisories',
|
||||||
source: 'NOAA NHC / CPHC',
|
source: 'NOAA NHC / CPHC',
|
||||||
// Layer identity is already carried separately. Keep source names literal
|
// Keep official storm names verbatim; layerName identifies the layer.
|
||||||
// rather than adding an untranslated user-facing prefix.
|
|
||||||
label: storm.name,
|
label: storm.name,
|
||||||
latitude: storm.position.latitude,
|
latitude: storm.position.latitude,
|
||||||
longitude: storm.position.longitude,
|
longitude: storm.position.longitude,
|
||||||
|
// At most 12 properties reach the voice summary; keep the list short.
|
||||||
properties: {
|
properties: {
|
||||||
classification: storm.classification,
|
classification: storm.classification,
|
||||||
|
// English source meaning; unknown codes stay unexpanded.
|
||||||
|
classificationName: Object.hasOwn(
|
||||||
|
CLASSIFICATION_NAMES,
|
||||||
|
storm.classification,
|
||||||
|
)
|
||||||
|
? CLASSIFICATION_NAMES[storm.classification]
|
||||||
|
: undefined,
|
||||||
basin: storm.basin,
|
basin: storm.basin,
|
||||||
advisoryNumber: storm.advisoryNumber,
|
advisoryNumber: storm.advisoryNumber,
|
||||||
issuedAt: storm.issuedAt,
|
issuedAt: storm.issuedAt,
|
||||||
@@ -133,15 +140,19 @@ export function createCyclonesLayer({
|
|||||||
movementDirectionDegrees: storm.movement?.directionDegrees,
|
movementDirectionDegrees: storm.movement?.directionDegrees,
|
||||||
movementSpeedKt: storm.movement?.speedKt,
|
movementSpeedKt: storm.movement?.speedKt,
|
||||||
advisoryUrl: storm.advisoryUrl,
|
advisoryUrl: storm.advisoryUrl,
|
||||||
|
// Present only while the server serves its cache with NHC unreachable.
|
||||||
|
staleCacheFetchedAt:
|
||||||
|
snapshot.stale && Number.isFinite(snapshot.fetchedAt)
|
||||||
|
? new Date(snapshot.fetchedAt).toISOString()
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!record) return null;
|
if (!record) return;
|
||||||
selectedContextStormId = storm.id;
|
selectedContextStormId = storm.id;
|
||||||
if (announce) context.selectEntityContext?.(selectedContextEntity);
|
if (announce) context.selectEntityContext?.(selectedContextEntity);
|
||||||
context.removeEntityContextsForLayer?.('weather-cyclones', {
|
context.removeEntityContextsForLayer?.('weather-cyclones', {
|
||||||
retainIds: new Set([contextId]),
|
retainIds: new Set([contextId]),
|
||||||
});
|
});
|
||||||
return record;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function select(id) {
|
function select(id) {
|
||||||
|
|||||||
+219
-130
@@ -405,96 +405,19 @@ function harness(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
test('explicit cyclone selection publishes shared context without auto-claiming it', async () => {
|
// Runs a test body against the real shared-context store on a stand-in window,
|
||||||
let next = snapshot([storm(), storm('ep162026')]);
|
// with the voice runner reading the same store.
|
||||||
const records = new Map();
|
async function withSharedContext(t, body) {
|
||||||
let selectedContext = null;
|
|
||||||
let selectedEvents = 0;
|
|
||||||
const context = {
|
|
||||||
registerEntityContext(entity, metadata) {
|
|
||||||
entity.__gevContextId = metadata.id;
|
|
||||||
const record = { ...metadata, entity };
|
|
||||||
records.set(metadata.id, record);
|
|
||||||
return record;
|
|
||||||
},
|
|
||||||
selectEntityContext(entity) {
|
|
||||||
selectedContext = records.get(entity.__gevContextId) || null;
|
|
||||||
selectedEvents += 1;
|
|
||||||
return selectedContext;
|
|
||||||
},
|
|
||||||
removeEntityContextsForLayer(layerId, { retainIds } = {}) {
|
|
||||||
for (const [id, record] of records) {
|
|
||||||
if (record.layerId === layerId && !retainIds?.has(id))
|
|
||||||
records.delete(id);
|
|
||||||
}
|
|
||||||
if (selectedContext && !records.has(selectedContext.id))
|
|
||||||
selectedContext = null;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const h = harness({ getSnapshot: async () => next }, { context });
|
|
||||||
h.layer.enable();
|
|
||||||
await h.layer.update();
|
|
||||||
|
|
||||||
assert.equal(
|
|
||||||
selectedEvents,
|
|
||||||
0,
|
|
||||||
'the automatic first-storm row selection must not claim shared context',
|
|
||||||
);
|
|
||||||
assert.equal(records.size, 0);
|
|
||||||
|
|
||||||
h.layer.setParams({ stormId: 'ep162026' });
|
|
||||||
assert.equal(selectedEvents, 1);
|
|
||||||
assert.equal(selectedContext?.layerId, 'weather-cyclones');
|
|
||||||
assert.equal(selectedContext?.source, 'NOAA NHC / CPHC');
|
|
||||||
assert.equal(selectedContext?.label, 'Another system');
|
|
||||||
assert.equal(selectedContext?.latitude, 15.5);
|
|
||||||
assert.equal(selectedContext?.longitude, -125.8);
|
|
||||||
assert.equal(selectedContext?.properties.advisoryNumber, '10');
|
|
||||||
|
|
||||||
h.layer.setParams({ stormId: 'ep152026' });
|
|
||||||
assert.deepEqual([...records.keys()], ['cyclone:ep152026']);
|
|
||||||
assert.equal(selectedContext?.id, 'cyclone:ep152026');
|
|
||||||
h.layer.setParams({ stormId: 'ep162026' });
|
|
||||||
assert.deepEqual([...records.keys()], ['cyclone:ep162026']);
|
|
||||||
assert.equal(selectedContext?.id, 'cyclone:ep162026');
|
|
||||||
assert.equal(selectedEvents, 3, 'each explicit storm switch announces once');
|
|
||||||
|
|
||||||
await h.layer.update();
|
|
||||||
assert.equal(
|
|
||||||
selectedEvents,
|
|
||||||
3,
|
|
||||||
'refresh updates the selected record without re-announcing the click',
|
|
||||||
);
|
|
||||||
|
|
||||||
next = snapshot([storm()]);
|
|
||||||
await h.layer.update();
|
|
||||||
assert.equal(
|
|
||||||
selectedContext,
|
|
||||||
null,
|
|
||||||
'a vanished user-selected storm must clear instead of transferring context to the fallback row',
|
|
||||||
);
|
|
||||||
assert.equal(records.size, 0);
|
|
||||||
|
|
||||||
h.layer.setParams({ stormId: 'ep152026' });
|
|
||||||
assert.equal(selectedEvents, 4);
|
|
||||||
h.layer.setParams({ clear: true });
|
|
||||||
assert.equal(selectedContext, null);
|
|
||||||
assert.equal(records.size, 0);
|
|
||||||
|
|
||||||
h.layer.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('shared cyclone context distinguishes switches, deliberate clears and feed eviction', async (t) => {
|
|
||||||
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
||||||
const host = new EventTarget();
|
const host = new EventTarget();
|
||||||
Object.defineProperty(globalThis, 'window', {
|
Object.defineProperty(globalThis, 'window', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: host,
|
value: host,
|
||||||
});
|
});
|
||||||
let h;
|
const layers = [];
|
||||||
t.after(() => {
|
t.after(() => {
|
||||||
try {
|
try {
|
||||||
h?.layer.destroy();
|
for (const layer of layers) layer.destroy();
|
||||||
} finally {
|
} finally {
|
||||||
if (previousWindow)
|
if (previousWindow)
|
||||||
Object.defineProperty(globalThis, 'window', previousWindow);
|
Object.defineProperty(globalThis, 'window', previousWindow);
|
||||||
@@ -506,9 +429,9 @@ test('shared cyclone context distinguishes switches, deliberate clears and feed
|
|||||||
events.push(['selected', detail.id]),
|
events.push(['selected', detail.id]),
|
||||||
);
|
);
|
||||||
host.addEventListener('gev:entity-selection-cleared', ({ detail }) =>
|
host.addEventListener('gev:entity-selection-cleared', ({ detail }) =>
|
||||||
events.push(['cleared', detail.reason]),
|
events.push(['cleared', detail.layerId, detail.reason]),
|
||||||
);
|
);
|
||||||
// The voice runner reads the same store and bounds its basemap wait.
|
// The voice runner bounds its basemap wait with the window timers.
|
||||||
Object.assign(host, { setTimeout, clearTimeout });
|
Object.assign(host, { setTimeout, clearTimeout });
|
||||||
const position = Cesium.Cartesian3.fromDegrees(-125.8, 15.5, 5_000_000);
|
const position = Cesium.Cartesian3.fromDegrees(-125.8, 15.5, 5_000_000);
|
||||||
const runner = createGevActionRunner({
|
const runner = createGevActionRunner({
|
||||||
@@ -535,55 +458,221 @@ test('shared cyclone context distinguishes switches, deliberate clears and feed
|
|||||||
assert.equal(result.scope, 'selected');
|
assert.equal(result.scope, 'selected');
|
||||||
return result.selected.properties;
|
return result.selected.properties;
|
||||||
};
|
};
|
||||||
let next = snapshot([
|
const store = contextStore.getContextStore();
|
||||||
{ ...storm(), movement: { directionDegrees: 315, speedKt: 12 } },
|
const open = (feed) => {
|
||||||
{ ...storm('ep162026'), movement: { directionDegrees: 0, speedKt: 0 } },
|
const h = harness(feed, { context: contextStore });
|
||||||
]);
|
layers.push(h.layer);
|
||||||
h = harness({ getSnapshot: async () => next }, { context: contextStore });
|
return h;
|
||||||
h.layer.enable();
|
};
|
||||||
await h.layer.update();
|
await body({ events, selectedProperties, store, open });
|
||||||
h.layer.setParams({ stormId: 'ep152026' });
|
}
|
||||||
let properties = await selectedProperties();
|
|
||||||
assert.equal(
|
|
||||||
properties.movementDirectionDegrees,
|
|
||||||
'315',
|
|
||||||
'selected storm movement should survive voice summarization',
|
|
||||||
);
|
|
||||||
assert.equal(properties.movementSpeedKt, '12');
|
|
||||||
h.layer.setParams({ stormId: 'ep162026' });
|
|
||||||
properties = await selectedProperties();
|
|
||||||
assert.equal(
|
|
||||||
properties.movementDirectionDegrees,
|
|
||||||
'0',
|
|
||||||
'a due-north heading is a value, not a missing one',
|
|
||||||
);
|
|
||||||
assert.equal(properties.movementSpeedKt, '0', 'stationary is a value');
|
|
||||||
assert.deepEqual(events, [
|
|
||||||
['selected', 'cyclone:ep152026'],
|
|
||||||
['selected', 'cyclone:ep162026'],
|
|
||||||
]);
|
|
||||||
assert.deepEqual(
|
|
||||||
[...contextStore.getContextStore().entities.keys()],
|
|
||||||
['cyclone:ep162026'],
|
|
||||||
);
|
|
||||||
await h.layer.update();
|
|
||||||
assert.equal(events.length, 2, 'refresh must not announce selection');
|
|
||||||
|
|
||||||
h.layer.setParams({ clear: true });
|
test('shared cyclone context distinguishes switches, deliberate clears and feed eviction', async (t) => {
|
||||||
assert.deepEqual(events.at(-1), ['cleared', 'deliberate']);
|
await withSharedContext(
|
||||||
assert.equal(contextStore.getContextStore().entities.size, 0);
|
t,
|
||||||
|
async ({ events, selectedProperties, store, open }) => {
|
||||||
|
let next = snapshot([
|
||||||
|
{ ...storm(), movement: { directionDegrees: 315, speedKt: 12 } },
|
||||||
|
{
|
||||||
|
...storm('ep162026'),
|
||||||
|
movement: { directionDegrees: 0, speedKt: 10 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const h = open({ getSnapshot: async () => next });
|
||||||
|
h.layer.enable();
|
||||||
|
await h.layer.update();
|
||||||
|
assert.deepEqual(
|
||||||
|
events,
|
||||||
|
[],
|
||||||
|
'the automatic first-storm row selection must not claim shared context',
|
||||||
|
);
|
||||||
|
assert.equal(store.entities.size, 0);
|
||||||
|
|
||||||
h.layer.setParams({ stormId: 'ep162026' });
|
h.layer.setParams({ stormId: 'ep152026' });
|
||||||
next = snapshot([storm()]);
|
const record = store.entities.get('cyclone:ep152026');
|
||||||
await h.layer.update();
|
assert.equal(record.layerId, 'weather-cyclones');
|
||||||
assert.deepEqual(events.at(-1), ['cleared', 'evicted']);
|
assert.equal(record.source, 'NOAA NHC / CPHC');
|
||||||
assert.equal(contextStore.getContextStore().entities.size, 0);
|
assert.equal(record.label, 'Fifteen-E');
|
||||||
assert.equal(contextStore.getContextStore().selectedEntityId, null);
|
assert.equal(record.latitude, 15.5);
|
||||||
|
assert.equal(record.longitude, -125.8);
|
||||||
|
let properties = await selectedProperties();
|
||||||
|
assert.equal(properties.classification, 'PTC');
|
||||||
|
assert.equal(properties.classificationName, 'Post-tropical cyclone');
|
||||||
|
assert.equal(properties.advisoryNumber, '10');
|
||||||
|
assert.equal(
|
||||||
|
properties.movementDirectionDegrees,
|
||||||
|
'315',
|
||||||
|
'selected storm movement should survive voice summarization',
|
||||||
|
);
|
||||||
|
assert.equal(properties.movementSpeedKt, '12');
|
||||||
|
assert.equal(
|
||||||
|
properties.advisoryUrl,
|
||||||
|
'https://www.nhc.noaa.gov/text/MIATCMEP5.shtml',
|
||||||
|
);
|
||||||
|
assert.equal(properties.staleCacheFetchedAt, undefined);
|
||||||
|
|
||||||
h.layer.setParams({ stormId: 'ep152026' });
|
h.layer.setParams({ stormId: 'ep162026' });
|
||||||
h.layer.disable();
|
properties = await selectedProperties();
|
||||||
assert.deepEqual(events.at(-1), ['cleared', 'deliberate']);
|
assert.equal(
|
||||||
assert.equal(contextStore.getContextStore().entities.size, 0);
|
properties.movementDirectionDegrees,
|
||||||
|
'0',
|
||||||
|
'a due-north heading is a value, not a missing one',
|
||||||
|
);
|
||||||
|
assert.equal(properties.movementSpeedKt, '10');
|
||||||
|
assert.deepEqual(events, [
|
||||||
|
['selected', 'cyclone:ep152026'],
|
||||||
|
['selected', 'cyclone:ep162026'],
|
||||||
|
]);
|
||||||
|
assert.deepEqual([...store.entities.keys()], ['cyclone:ep162026']);
|
||||||
|
|
||||||
|
next = snapshot([
|
||||||
|
{ ...storm(), movement: { directionDegrees: 315, speedKt: 12 } },
|
||||||
|
{ ...storm('ep162026'), movement: { directionDegrees: 0, speedKt: 0 } },
|
||||||
|
]);
|
||||||
|
await h.layer.update();
|
||||||
|
assert.equal(events.length, 2, 'refresh must not announce selection');
|
||||||
|
properties = await selectedProperties();
|
||||||
|
assert.equal(
|
||||||
|
properties.movementSpeedKt,
|
||||||
|
'0',
|
||||||
|
'stationary is a value, refreshed into the selected record',
|
||||||
|
);
|
||||||
|
|
||||||
|
h.layer.setParams({ clear: true });
|
||||||
|
assert.deepEqual(events.at(-1), [
|
||||||
|
'cleared',
|
||||||
|
'weather-cyclones',
|
||||||
|
'deliberate',
|
||||||
|
]);
|
||||||
|
assert.equal(store.entities.size, 0);
|
||||||
|
|
||||||
|
h.layer.setParams({ stormId: 'ep162026' });
|
||||||
|
next = snapshot([storm()]);
|
||||||
|
await h.layer.update();
|
||||||
|
assert.deepEqual(
|
||||||
|
events.at(-1),
|
||||||
|
['cleared', 'weather-cyclones', 'evicted'],
|
||||||
|
'a vanished user-selected storm must clear instead of transferring context to the fallback row',
|
||||||
|
);
|
||||||
|
assert.equal(store.entities.size, 0);
|
||||||
|
assert.equal(store.selectedEntityId, null);
|
||||||
|
|
||||||
|
h.layer.setParams({ stormId: 'ep152026' });
|
||||||
|
h.layer.disable();
|
||||||
|
assert.deepEqual(events.at(-1), [
|
||||||
|
'cleared',
|
||||||
|
'weather-cyclones',
|
||||||
|
'deliberate',
|
||||||
|
]);
|
||||||
|
assert.equal(store.entities.size, 0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('shared cyclone context is evicted when the source fails and marks a stale cache', async (t) => {
|
||||||
|
await withSharedContext(
|
||||||
|
t,
|
||||||
|
async ({ events, selectedProperties, store, open }) => {
|
||||||
|
let next = snapshot([{ ...storm(), classification: 'XX' }]);
|
||||||
|
let fail = false;
|
||||||
|
const h = open({
|
||||||
|
getSnapshot: async () => {
|
||||||
|
if (fail) throw new Error('Network unavailable');
|
||||||
|
return next;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
h.layer.enable();
|
||||||
|
await h.layer.update();
|
||||||
|
h.layer.setParams({ stormId: 'ep152026' });
|
||||||
|
let properties = await selectedProperties();
|
||||||
|
assert.equal(properties.classification, 'XX');
|
||||||
|
assert.equal(
|
||||||
|
properties.classificationName,
|
||||||
|
undefined,
|
||||||
|
'an unknown code is not given a guessed meaning',
|
||||||
|
);
|
||||||
|
|
||||||
|
next = { ...snapshot(), stale: true };
|
||||||
|
await h.layer.update();
|
||||||
|
properties = await selectedProperties();
|
||||||
|
assert.equal(properties.staleCacheFetchedAt, time);
|
||||||
|
assert.equal(events.length, 1, 'refresh must not announce selection');
|
||||||
|
|
||||||
|
next = {
|
||||||
|
...snapshot([]),
|
||||||
|
unavailable: true,
|
||||||
|
reason: 'Cyclone data unavailable',
|
||||||
|
};
|
||||||
|
await h.layer.update();
|
||||||
|
assert.deepEqual(events.at(-1), [
|
||||||
|
'cleared',
|
||||||
|
'weather-cyclones',
|
||||||
|
'evicted',
|
||||||
|
]);
|
||||||
|
assert.equal(store.entities.size, 0);
|
||||||
|
|
||||||
|
next = snapshot();
|
||||||
|
await h.layer.update();
|
||||||
|
h.layer.setParams({ stormId: 'ep152026' });
|
||||||
|
assert.equal(store.selectedEntityId, 'cyclone:ep152026');
|
||||||
|
fail = true;
|
||||||
|
await h.layer.update();
|
||||||
|
assert.deepEqual(events.at(-1), [
|
||||||
|
'cleared',
|
||||||
|
'weather-cyclones',
|
||||||
|
'evicted',
|
||||||
|
]);
|
||||||
|
assert.equal(store.entities.size, 0);
|
||||||
|
assert.equal(store.selectedEntityId, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cyclone clears leave another layer selection and its records alone', async (t) => {
|
||||||
|
await withSharedContext(t, async ({ events, store, open }) => {
|
||||||
|
const h = open({ getSnapshot: async () => snapshot() });
|
||||||
|
h.layer.enable();
|
||||||
|
await h.layer.update();
|
||||||
|
h.layer.setParams({ stormId: 'ep152026' });
|
||||||
|
const foreign = {};
|
||||||
|
contextStore.registerEntityContext(foreign, {
|
||||||
|
id: 'flight:abc123',
|
||||||
|
layerId: 'flights',
|
||||||
|
label: 'Foreign',
|
||||||
|
});
|
||||||
|
contextStore.selectEntityContext(foreign);
|
||||||
|
const before = events.length;
|
||||||
|
|
||||||
|
h.layer.setParams({ clear: true });
|
||||||
|
assert.equal(store.selectedEntityId, 'flight:abc123');
|
||||||
|
assert.ok(store.entities.has('flight:abc123'));
|
||||||
|
assert.equal(store.entities.has('cyclone:ep152026'), false);
|
||||||
|
assert.equal(
|
||||||
|
events.length,
|
||||||
|
before,
|
||||||
|
'no clear is reported for a foreign selection',
|
||||||
|
);
|
||||||
|
|
||||||
|
h.layer.disable();
|
||||||
|
assert.equal(store.selectedEntityId, 'flight:abc123');
|
||||||
|
assert.equal(events.length, before);
|
||||||
|
|
||||||
|
// A foreign record removed without clearing its selection: a cyclone
|
||||||
|
// feed failure must not report that selection as evicted by cyclones.
|
||||||
|
store.entities.delete('flight:abc123');
|
||||||
|
let fail = false;
|
||||||
|
const other = open({
|
||||||
|
getSnapshot: async () => {
|
||||||
|
if (fail) throw new Error('Network unavailable');
|
||||||
|
return snapshot();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
other.layer.enable();
|
||||||
|
await other.layer.update();
|
||||||
|
fail = true;
|
||||||
|
await other.layer.update();
|
||||||
|
assert.equal(events.length, before);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('refresh preserves selection intent, including explicit clears and missing storms', async () => {
|
test('refresh preserves selection intent, including explicit clears and missing storms', async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user