Fold duplicate coverage tests into their neighbours

This commit is contained in:
German Khokhlov committed 2026-11-06 22:01:39 +04:00
1 parent 5c4918affb
commit ea76fc4bc0
9 files changed
+94 -328

No files matched your search

@@ -1,6 +1,4 @@
- Say why Transit and Bike Share are empty outside coverage. Over a city with
no feed or system (London, Tokyo) their Layers rows read
`No feed here yet · 17 regions available` and
`No bike share here yet · 61 systems available`, or how low to fly, instead
of `never`; leaving coverage clears the last-update time (German, @gekh,
#1576).
no feed or system (London, Tokyo) their Layers rows say so and count the
covered regions or systems, or say how low to fly, instead of `never`;
leaving coverage clears the last-update time (German, @gekh, #1576).
-12
View File
@@ -15,18 +15,6 @@ export class DataLayerManager extends LayerLifecycle {
this._presentation.refresh();
}
/**
* Repaint the toggle panel now. For layers whose data arrives outside their
* manager tick (camera-driven loads such as Transit's proximity polls), so a
* row shows its count when the data lands instead of at the next interval.
* One DOM pass; skipped while the document is hidden.
* @param {string} [layerId] - The layer whose row changed.
*/
refreshLayerStats(layerId) {
this._dropRecoveredRefreshError(layerId);
this._refreshTogglePanel();
}
_buildMetaText(layer) {
return this._presentation.panel._buildMetaText(layer);
}
@@ -1,121 +0,0 @@
// BIKESHARE COVERAGE STATUS — a view with no system says so.
//
// With no system in range (London, Tokyo) the layer says why instead of
// "never", and keeps no date for data it no longer draws.
//
// Run with: npm test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createState } from './state.js';
import { createControls } from './controls.js';
import { createModel } from './model.js';
import { createViewport } from './viewport.js';
import { GBFS_CITY_REGISTRY } from './registry.js';
const LONDON = { lat: 51.5072, lon: -0.1276 };
function makeLayer() {
const services = {
overlays: {
setOverlayEntries() {},
setOverlaySourceVisible() {},
clearOverlaySource() {},
},
render: { governorRequestRender() {} },
};
const state = createState({ services });
const parts = {
queries: { collectDetectableStations: () => [] },
model: createModel({ state, services, parts: {}, source: null }),
ingestion: { abortInFlight() {} },
rendering: { removeCityPoints() {} },
};
parts.viewport = createViewport({ state, services, parts, source: null });
const { methods } = createControls({ state, services, parts, source: {} });
state._enabled = true;
return { state, methods, viewport: parts.viewport };
}
/** A viewer looking straight down at `at` from `height` metres. */
function viewerAt(at, height) {
const rad = Math.PI / 180;
return {
camera: {
positionCartographic: {
height,
latitude: at.lat * rad,
longitude: at.lon * rad,
},
computeViewRectangle: () => undefined,
},
scene: { globe: {} },
};
}
test('above the altitude gate the row says how low to fly', async () => {
const { state, methods, viewport } = makeLayer();
state._viewer = viewerAt(LONDON, 2_000_000);
await viewport.runProximityCheck();
const stats = methods.getStats();
assert.equal(stats.status, 'zoom-in');
assert.equal(stats.statusMessage, 'Fly below 48 km to a covered city');
assert.equal(stats.error, undefined);
});
test('a city with no system says so and counts the ones there are', async () => {
const { state, methods, viewport } = makeLayer();
state._viewer = viewerAt(LONDON, 8_000);
await viewport.runProximityCheck();
const stats = methods.getStats();
assert.equal(stats.status, 'zoom-in');
assert.equal(
stats.statusMessage,
`No bike share here yet · ${GBFS_CITY_REGISTRY.length} systems available`,
);
assert.equal(stats.count, 0);
});
test('a city with a system in range is not given the prompt', () => {
const { state, methods } = makeLayer();
state._altitudeGateEnabled = true;
state._activeCityIds.add('boston-bluebikes');
assert.equal(methods.getStats().status, undefined);
});
test('a loading or failed city reports itself, not the prompt', () => {
const { state, methods } = makeLayer();
state._altitudeGateEnabled = true;
state._loading = true;
assert.equal(methods.getStats().status, undefined);
// A city that failed is dropped from range, and still owns the row.
state._loading = false;
state._failedCityIds.add('boston-bluebikes');
const stats = methods.getStats();
assert.equal(stats.status, undefined);
assert.equal(stats.error, 'GBFS fetch error');
});
test('a disabled layer reports no prompt', () => {
const { state, methods } = makeLayer();
state._enabled = false;
assert.equal(methods.getStats().status, undefined);
});
test('leaving every system behind drops the date of the data it drew', async () => {
for (const height of [8_000, 2_000_000]) {
const { state, methods, viewport } = makeLayer();
state._lastUpdate = Date.now();
// Boston's stations were drawn and dated; the camera moves to London.
state._activeCityIds.add('boston-bluebikes');
state._cityRuntime.set('boston-bluebikes', {});
state._viewer = viewerAt(LONDON, height);
await viewport.runProximityCheck();
assert.equal(state._activeCityIds.size, 0);
assert.equal(methods.getStats().lastUpdate, null, `at ${height} m`);
}
});
+76 -3
View File
@@ -1,11 +1,13 @@
// Bike Share: after the camera settles, the last row the panel painted must
// match what the layer reports now. Each case drives the real viewport,
// Bike Share's Layers row. With no system in range it says why instead of
// "never" and keeps no date for data it no longer draws. After the camera
// settles, the last row the panel painted must match what the layer reports
// now. Each case drives the real viewport,
// ingestion, rendering and controls; the manager is a spy that records the
// row text (LayerPanel._buildMetaText) at every refreshLayerStats() call.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { CITY_BY_ID } from './registry.js';
import { CITY_BY_ID, GBFS_CITY_REGISTRY } from './registry.js';
import { createState } from './state.js';
import { createControls } from './controls.js';
import { createIngestion } from './ingestion.js';
@@ -175,6 +177,7 @@ function makeLiveLayer() {
source,
painted,
lastPainted: () => painted.at(-1),
stats: () => controls.getStats(),
live: () => rowText(controls.getStats()),
moveTo(place, height = LOW_M) {
Object.assign(camera, place, { height });
@@ -291,3 +294,73 @@ test('leaving the failed city of two repaints DEGRADED away', async () => {
assert.doesNotMatch(layer.live(), /^degraded/);
assert.equal(layer.lastPainted(), layer.live());
});
test('above the altitude gate the row says how low to fly', async () => {
const layer = makeLiveLayer();
await layer.enable();
await layer.moveTo(LONDON, 2_000_000);
const stats = layer.stats();
assert.equal(stats.status, 'zoom-in');
assert.equal(stats.statusMessage, 'Fly below 48 km to a covered city');
assert.equal(stats.error, undefined);
});
test('a city with no system says so and counts the ones there are', async () => {
const layer = makeLiveLayer();
await layer.enable();
const stats = layer.stats();
assert.equal(stats.status, 'zoom-in');
assert.equal(
stats.statusMessage,
`No bike share here yet · ${GBFS_CITY_REGISTRY.length} systems available`,
);
assert.equal(stats.count, 0);
});
test('a city in range, loading, failed or a disabled layer gets no prompt', async () => {
const layer = makeLiveLayer();
assert.equal(layer.stats().status, undefined, 'disabled');
const info = deferred();
layer.source.gates.set(`${BOSTON_ID}:info`, info);
await layer.enable();
const entering = layer.moveTo(BOSTON);
assert.equal(layer.stats().status, undefined, 'loading');
info.resolve();
await entering;
await settle();
assert.equal(layer.stats().status, undefined, 'loaded');
// Left mid-load: range is empty, but the aborted load still owns the row.
const leaving = makeLiveLayer();
leaving.source.gates.set(`${BOSTON_ID}:info`, deferred());
await leaving.enable();
const loading = leaving.moveTo(BOSTON);
await settle();
const left = leaving.moveTo(LONDON);
assert.equal(leaving.state._activeCityIds.size, 0);
assert.equal(leaving.stats().loading, true);
assert.equal(leaving.stats().status, undefined, 'leaving mid-load');
await Promise.all([loading, left]);
const failed = makeLiveLayer();
failed.source.dead.add(BOSTON_ID);
await failed.enable();
await failed.moveTo(BOSTON);
await settle();
assert.equal(failed.stats().status, undefined, 'failed');
assert.equal(failed.stats().error, 'GBFS fetch error');
});
test('leaving every system behind drops the date of the data it drew', async () => {
for (const height of [LOW_M, 2_000_000]) {
const layer = makeLiveLayer();
await layer.enable();
await layer.moveTo(BOSTON);
await settle();
assert.ok(layer.stats().lastUpdate, 'Boston is dated');
await layer.moveTo(LONDON, height);
assert.equal(layer.stats().lastUpdate, null, `at ${height} m`);
}
});
+1 -3
View File
@@ -243,11 +243,9 @@ export function createViewport({ state: layerState, services, parts, source }) {
nextActive.size !== layerState._activeCityIds.size ||
[...nextActive].some((cityId) => !layerState._activeCityIds.has(cityId));
layerState._activeCityIds = nextActive;
if (layerState._activeCityIds.size === 0) {
if (nextActive.size === 0) {
layerState._count = 0;
layerState._lastUpdate = null;
if (changed) refreshRow();
return;
}
if (changed) refreshRow();
-120
View File
@@ -1,120 +0,0 @@
// A camera move that changes a Transit or Bike Share coverage prompt repaints
// the row at once, not at the next manager tick (15 s, 60 s).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createState as createBikeState } from './bikeshare/state.js';
import { createControls as createBikeControls } from './bikeshare/controls.js';
import { createModel as createBikeModel } from './bikeshare/model.js';
import { createViewport as createBikeViewport } from './bikeshare/viewport.js';
import { createState as createTransitState } from './transit/state.js';
import { createViewport as createTransitViewport } from './transit/viewport.js';
import { createQueries as createTransitQueries } from './transit/queries.js';
const rad = Math.PI / 180;
const LONDON = { lat: 51.5072, lon: -0.1276 };
const BOSTON = { lat: 42.3601, lon: -71.0589 };
const services = () => ({
overlays: {
setOverlayEntries() {},
setOverlaySourceVisible() {},
clearOverlaySource() {},
},
render: { governorRequestRender() {} },
});
function viewer(at, height) {
return {
camera: {
positionCartographic: {
height,
latitude: at.lat * rad,
longitude: at.lon * rad,
},
computeViewRectangle: () => undefined,
},
scene: { globe: {} },
};
}
function spyManager() {
const dm = { repaints: 0 };
dm.refreshLayerStats = () => {
dm.repaints++;
};
return dm;
}
test('Bike Share: flying from London into Boston repaints the prompt row', async () => {
const svc = services();
const state = createBikeState({ services: svc });
const parts = {
queries: { collectDetectableStations: () => [] },
model: createBikeModel({ state, services: svc, parts: {}, source: null }),
ingestion: {
abortInFlight() {},
loadCityStationInfo: async () => new Map(),
loadCityStationStatus: async () => new Map(),
},
rendering: {
removeCityPoints() {},
ensureCityPoints() {},
applyStatusToPoints() {},
},
};
parts.viewport = createBikeViewport({
state,
services: svc,
parts,
source: null,
});
const { methods } = createBikeControls({
state,
services: svc,
parts,
source: {},
});
state._enabled = true;
const dm = spyManager();
methods.attachDataManager(dm);
state._viewer = viewer(LONDON, 8_000);
await parts.viewport.runProximityCheck();
assert.equal(methods.getStats().status, 'zoom-in');
const before = dm.repaints;
state._viewer = viewer(BOSTON, 3_000);
await parts.viewport.runProximityCheck();
assert.equal(methods.getStats().status, undefined, 'Boston is covered');
assert.ok(dm.repaints > before, 'the row was asked to repaint');
});
test('Transit: leaving Boston for London repaints the row', () => {
const svc = services();
const state = createTransitState({ services: svc });
const parts = {
ingestion: {
feedStatus: () => ({ count: 0 }),
pollFeed() {},
removeFeedVehicles() {},
sweepAgedVehicles() {},
},
rendering: { requestVisibility() {}, syncRenderHold() {} },
};
parts.viewport = createTransitViewport({ state, services: svc, parts });
const queries = createTransitQueries({ state, parts }).methods;
state._enabled = true;
const dm = spyManager();
state._dataManager = dm;
state._viewer = viewer(BOSTON, 4_000);
parts.viewport.runProximityCheck();
assert.ok(state._activeFeeds.size > 0, 'Boston has a feed');
state._lastUpdate = Date.now();
const before = dm.repaints;
state._viewer = viewer(LONDON, 4_000);
parts.viewport.runProximityCheck();
assert.equal(queries.getStats().status, 'zoom-in');
assert.ok(dm.repaints > before, 'the row was asked to repaint');
});
-64
View File
@@ -1,64 +0,0 @@
// The Transit prompt names the altitude the gate opens at: at 2,900 km over
// Boston (under 3,000 km, above the 2,850 km enter threshold) nothing loads.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createState } from './state.js';
import { createViewport } from './viewport.js';
import { createQueries } from './queries.js';
import { ACTIVATION_ENTER_ALTITUDE_M } from './policy.js';
const BOSTON = { lat: 42.3601, lon: -71.0589 };
function makeLayer() {
const services = {
overlays: {
setOverlayEntries() {},
setOverlaySourceVisible() {},
clearOverlaySource() {},
},
render: { governorRequestRender() {} },
};
const state = createState({ services });
const polled = [];
const parts = {
ingestion: {
feedStatus: () => ({ count: 0 }),
pollFeed: (feed) => polled.push(feed.id),
removeFeedVehicles() {},
sweepAgedVehicles() {},
},
rendering: { requestVisibility() {}, syncRenderHold() {} },
};
parts.viewport = createViewport({ state, services, parts });
const queries = createQueries({ state, parts }).methods;
state._enabled = true;
const rad = Math.PI / 180;
state._viewer = {
camera: {
positionCartographic: {
height: 0,
latitude: BOSTON.lat * rad,
longitude: BOSTON.lon * rad,
},
computeViewRectangle: () => undefined,
},
scene: { globe: {} },
};
return { state, queries, viewport: parts.viewport, polled };
}
test('below the prompted altitude but above the gate, the prompt names the gate', () => {
const { state, queries, viewport, polled } = makeLayer();
state._viewer.camera.positionCartographic.height = 2_900_000;
viewport.runProximityCheck();
// Nothing loads at 2,900 km: the gate opens at the enter threshold.
assert.equal(state._altitudeGateOpen, false);
assert.deepEqual(polled, []);
const km = Math.round(ACTIVATION_ENTER_ALTITUDE_M / 1000).toLocaleString(
'en-US',
);
const { statusMessage } = queries.getStats();
assert.equal(statusMessage, `Fly below ${km} km to a covered region`);
});
+12
View File
@@ -4021,3 +4021,15 @@ test('a view with no feed reads its coverage prompt, not an undated feed', async
'Fly below 2,850 km to a covered region',
);
});
test('the prompt names the altitude the gate opens at, not 3,000 km', async (t) => {
// 2,900 km is under ACTIVATION_ALTITUDE_M but above the enter threshold.
const app = harness(t, { altitude: 2_900_000 });
app.layer.enable(app.viewer);
await app.layer.update();
assert.deepEqual(app.requested, [], 'nothing loads over Boston');
assert.equal(
app.layer.getStats().statusMessage,
'Fly below 2,850 km to a covered region',
);
});
@@ -91,8 +91,10 @@ test('leaving a feed whose tick failed repaints its error away', async () => {
await lifecycle.refreshLayer('transit');
assert.match(row(), /feed unavailable/, 'the tick recorded the failure');
const before = painted.length;
state._viewer = viewer(LONDON, 4_000);
parts.viewport.runProximityCheck();
assert.equal(queries.getStats().status, 'zoom-in');
assert.ok(painted.length > before, 'leaving Boston repainted the row');
assert.doesNotMatch(painted.at(-1), /feed unavailable/);
});