mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
ShellFeedback publishes the loading status and the road network chip, and CctvControls the camera grid chip, to the gev-status store; the scene chrome renders them. SplitFlapText plays the departure-board cascade that splitFlap.js plans, writing only the label's one text node, and rests when its owner is destroyed. setSplitFlapText and disposeSplitFlap are gone.
740 lines
36 KiB
JavaScript
740 lines
36 KiB
JavaScript
import { ShareRestoration } from './ui/shareRestoration.js';
|
|
import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs';
|
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
|
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
const retrySite = (stats = {}) => ({ id: 'military-installations', name: 'Mapped Installations', enabled: true,
|
|
stats: { status: 'unavailable', error: 'Unavailable', retryAt: Date.now() + 30000, ...stats } });
|
|
|
|
test('installation retry remains visible after failure dwell without a false spinner', () => {
|
|
const summary = aggregateLayerLoading([retrySite()]);
|
|
const view = presentLoadingFeedback(createLoadingFeedbackState(), summary, 100);
|
|
assert.equal(view.state, 'retry');
|
|
assert.equal(view.label, 'OVERPASS TEMPORARILY UNAVAILABLE');
|
|
assert.match(view.detail, /retrying in 30s/);
|
|
assert.equal(presentLoadingFeedback(createLoadingFeedbackState(), aggregateLayerLoading([{ ...retrySite(), enabled: false }]), 100), null);
|
|
});
|
|
test('an installation retry never conceals another participant failure', () => {
|
|
const state = { visible: true, phase: 'terminal', terminal: 'error', activeIds: ['military-installations', 'flights'] };
|
|
const summary = aggregateLayerLoading([retrySite(), { id: 'flights', enabled: true, stats: { error: 'Failed' } }]);
|
|
assert.equal(presentLoadingFeedback(state, summary, 100).label, 'LOAD FAILED');
|
|
const healthyNow = aggregateLayerLoading([retrySite()]);
|
|
assert.equal(presentLoadingFeedback({ ...state, failedEventIds: ['flights'] }, healthyNow, 100).label, 'LOAD FAILED');
|
|
});
|
|
test('a fresh installation retry can finish successfully without inheriting the old error', () => {
|
|
let state = { ...createLoadingFeedbackState(), phase: 'terminal', terminal: 'error', visible: true, activeIds: ['military-installations'] };
|
|
const loading = aggregateLayerLoading([retrySite({ status: 'loading', error: null, loading: true, retryAt: 0, retrying: true })]);
|
|
state = reduceLoadingFeedback(state, loading, 1000);
|
|
state = reduceLoadingFeedback(state, loading, 1200);
|
|
assert.equal(presentLoadingFeedback(state, loading, 1200).label, 'RETRYING MAPPED SITES');
|
|
const done = aggregateLayerLoading([retrySite({ status: 'ready', error: null, loading: false, retryAt: 0, retrying: false, count: 3 })]);
|
|
state = reduceLoadingFeedback(state, done, 1500);
|
|
assert.equal(presentLoadingFeedback(state, done, 1500).label, 'MAPPED SITES LOADED');
|
|
});
|
|
test('turning off a retrying installation layer does not report the old fetch failure as a disable failure', () => {
|
|
const stopping = aggregateLayerLoading([{ ...retrySite(), lifecycleState: 'disabling' }]);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), stopping, 1000);
|
|
state = reduceLoadingFeedback(state, stopping, 1200);
|
|
const off = aggregateLayerLoading([{ ...retrySite({ status: 'idle', error: null, retryAt: 0 }), enabled: false }]);
|
|
state = reduceLoadingFeedback(state, off, 1400);
|
|
assert.equal(presentLoadingFeedback(state, off, 1400).label, 'LIVE DATA OFF');
|
|
});
|
|
import {
|
|
aggregateLayerLoading,
|
|
canPresentDeferredStatusNotice,
|
|
createGlobalStatusNotice,
|
|
createLoadingFeedbackState,
|
|
createTrafficSyncFeedbackState,
|
|
LOADING_FAILURE_DWELL_MS,
|
|
normalizeLayerLoading,
|
|
presentGlobalLoadingStatus,
|
|
presentGlobalStatusNotice,
|
|
presentLoadingFeedback,
|
|
reduceLoadingFeedback,
|
|
reduceTrafficSyncFeedback,
|
|
TRAFFIC_SYNC_CONFIRM_MS,
|
|
} from './loadingFeedback.js';
|
|
|
|
test('universal status notices reuse the standard failure dwell', () => {
|
|
const notice = createGlobalStatusNotice('Shared satellite is unavailable', 1000);
|
|
assert.equal(notice.hideAt, null, 'finite dwell waits until first presentation');
|
|
assert.equal(presentGlobalStatusNotice(notice, 1001).label, 'Shared satellite is unavailable');
|
|
assert.equal(notice.hideAt, 1001 + LOADING_FAILURE_DWELL_MS);
|
|
assert.deepEqual(presentGlobalStatusNotice(notice, notice.hideAt - 1), {
|
|
state: 'error',
|
|
label: 'Shared satellite is unavailable',
|
|
detail: '',
|
|
});
|
|
assert.equal(presentGlobalStatusNotice(notice, notice.hideAt), null);
|
|
});
|
|
|
|
test('acquiring notices persist without a dwell until explicitly cleared', () => {
|
|
const notice = createGlobalStatusNotice('ACQUIRING', 1000, {
|
|
state: 'acquiring',
|
|
detail: 'SHARED FLIGHT',
|
|
persistent: true,
|
|
});
|
|
assert.equal(notice.hideAt, null);
|
|
assert.deepEqual(presentGlobalStatusNotice(notice, 1_000_000), {
|
|
state: 'acquiring',
|
|
label: 'ACQUIRING',
|
|
detail: 'SHARED FLIGHT',
|
|
});
|
|
});
|
|
|
|
test('deferred terminal notices lose ownership to newer acquisition epochs and disposal', () => {
|
|
assert.equal(canPresentDeferredStatusNotice(4, 4, false), true);
|
|
assert.equal(canPresentDeferredStatusNotice(4, 5, false), false,
|
|
'a newer ACQUIRING epoch blocks the older deferred failure');
|
|
assert.equal(canPresentDeferredStatusNotice(5, 5, true), false,
|
|
'disposal blocks even the current deferred notice');
|
|
});
|
|
|
|
test('share-follow failures use the universal top-center status instead of the bottom toast', () => {
|
|
const handler = shellMethod('_handleShareTrackingRestoreStatus').toString();
|
|
const present = shellMethod('_presentAfterStartupCover').toString();
|
|
assert.match(
|
|
handler,
|
|
/this\.initialRestorePromise\.then\(\(\) =>\s*this\._presentAfterStartupCover\(message, noticeGeneration\),?\s*\)/,
|
|
);
|
|
assert.match(present, /this\.showStatus\(message\)/);
|
|
assert.match(present, /this\._lifetime\.frame\(\(\) => \{/);
|
|
assert.match(present, /this\._lifetime\.listen\(\s*startupCover,\s*'transitionend',\s*showOnce,\s*\{ once: true \},?\s*\)/);
|
|
assert.match(present, /fallbackTimer = this\._lifetime\.timeout\(showOnce, 1000\)/);
|
|
assert.match(present, /canPresentDeferredStatusNotice\(/);
|
|
for (const source of [handler, present]) {
|
|
assert.doesNotMatch(source, /this\._showToast\(message\)/);
|
|
assert.doesNotMatch(source, /pushCockpitSignal/);
|
|
}
|
|
assert.match(handler, /result\.classification === 'pending'/);
|
|
assert.match(handler, /state: 'acquiring'/);
|
|
assert.match(handler, /persistent: true/);
|
|
assert.match(handler, /this\._shareTrackingNoticeGeneration \+= 1/);
|
|
assert.match(handler, /if \(this\._shareTrackingAcquiringKey\) return/);
|
|
assert.match(handler, /result\.classification === 'followed'/);
|
|
assert.match(handler, /result\.classification === 'cancelled'\) return/);
|
|
});
|
|
|
|
test('universal notice masks active loading only for its own fixed dwell', () => {
|
|
const summary = aggregateLayerLoading([{ id: 'satellites', name: 'Satellites', lifecycleState: 'enabling' }]);
|
|
let loading = reduceLoadingFeedback(createLoadingFeedbackState(), summary, 0);
|
|
loading = reduceLoadingFeedback(loading, summary, 200);
|
|
const notice = createGlobalStatusNotice('Shared satellite is unavailable', 300);
|
|
|
|
assert.equal(presentGlobalLoadingStatus(notice, loading, summary, 301).label, 'Shared satellite is unavailable');
|
|
assert.equal(presentGlobalLoadingStatus(notice, loading, summary, notice.hideAt).label, 'LOADING LIVE DATA');
|
|
});
|
|
|
|
test('terminal failure preempts a finite notice, whose full dwell starts afterward', () => {
|
|
const active = aggregateLayerLoading([{ id: 'satellites', name: 'Satellites', lifecycleState: 'enabling' }]);
|
|
let loading = reduceLoadingFeedback(createLoadingFeedbackState(), active, 0);
|
|
loading = reduceLoadingFeedback(loading, active, 200);
|
|
loading = reduceLoadingFeedback(loading, aggregateLayerLoading([]), 300, {
|
|
type: 'visibility-failed', layerId: 'satellites', error: new Error('offline'),
|
|
});
|
|
const notice = createGlobalStatusNotice('Shared satellite is unavailable', 400);
|
|
|
|
assert.equal(
|
|
presentGlobalLoadingStatus(notice, loading, aggregateLayerLoading([]), 401).label,
|
|
'LOAD FAILED',
|
|
);
|
|
assert.equal(notice.hideAt, null, 'masked finite notice has not started its dwell');
|
|
loading = reduceLoadingFeedback(loading, aggregateLayerLoading([]), 5300);
|
|
assert.equal(
|
|
presentGlobalLoadingStatus(notice, loading, aggregateLayerLoading([]), 5300).label,
|
|
'Shared satellite is unavailable',
|
|
);
|
|
assert.equal(notice.hideAt, 5300 + LOADING_FAILURE_DWELL_MS);
|
|
assert.equal(
|
|
presentGlobalLoadingStatus(notice, loading, aggregateLayerLoading([]), notice.hideAt - 1).label,
|
|
'Shared satellite is unavailable',
|
|
);
|
|
assert.equal(presentGlobalLoadingStatus(notice, loading, aggregateLayerLoading([]), notice.hideAt), null);
|
|
});
|
|
|
|
test('persistent acquisition never hides an unrelated manager failure', () => {
|
|
const active = aggregateLayerLoading([{
|
|
id: 'traffic', name: 'Street Traffic', lifecycleState: 'enabling',
|
|
}]);
|
|
const idle = aggregateLayerLoading([]);
|
|
let loading = reduceLoadingFeedback(createLoadingFeedbackState(), active, 0);
|
|
loading = reduceLoadingFeedback(loading, active, 200);
|
|
loading = reduceLoadingFeedback(loading, idle, 300, {
|
|
type: 'visibility-failed', layerId: 'traffic', error: new Error('offline'),
|
|
});
|
|
const acquiring = createGlobalStatusNotice('ACQUIRING', 100, {
|
|
state: 'acquiring',
|
|
detail: 'SHARED FLIGHT',
|
|
persistent: true,
|
|
});
|
|
|
|
assert.deepEqual(presentGlobalLoadingStatus(acquiring, loading, idle, 301), {
|
|
state: 'error',
|
|
label: 'LOAD FAILED',
|
|
detail: '',
|
|
});
|
|
assert.equal(
|
|
presentGlobalLoadingStatus(acquiring, loading, idle, 300 + LOADING_FAILURE_DWELL_MS - 1).label,
|
|
'LOAD FAILED',
|
|
);
|
|
loading = reduceLoadingFeedback(loading, idle, 300 + LOADING_FAILURE_DWELL_MS);
|
|
assert.equal(
|
|
presentGlobalLoadingStatus(acquiring, loading, idle, 300 + LOADING_FAILURE_DWELL_MS).label,
|
|
'ACQUIRING',
|
|
'the still-owned acquisition resumes only after the full failure dwell is visible',
|
|
);
|
|
});
|
|
|
|
test('replacement, repetition, and hidden-tab elapsed time use the newest fixed deadline', () => {
|
|
const first = createGlobalStatusNotice('Shared satellite is unavailable', 100);
|
|
const repeated = createGlobalStatusNotice('Shared satellite is unavailable', 200);
|
|
const replacement = createGlobalStatusNotice('Shared satellite follow expired', 300);
|
|
|
|
presentGlobalStatusNotice(first, 100);
|
|
presentGlobalStatusNotice(repeated, 200);
|
|
presentGlobalStatusNotice(replacement, 300);
|
|
|
|
assert.ok(repeated.hideAt > first.hideAt, 'a repeated event is a new accessible notice epoch');
|
|
assert.equal(presentGlobalStatusNotice(replacement, replacement.hideAt - 1).label, 'Shared satellite follow expired');
|
|
assert.equal(presentGlobalStatusNotice(replacement, replacement.hideAt), null,
|
|
'elapsed wall time while hidden expires the notice instead of replaying it');
|
|
});
|
|
|
|
test('universal notice lifecycle clears on dispose and uses the one top-center live region', async () => {
|
|
const ui = readShellSource();
|
|
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
|
const disposeStart = ui.indexOf(' async dispose() {');
|
|
const disposeEnd = ui.indexOf('\n }\n', disposeStart);
|
|
const dispose = ui.slice(disposeStart, disposeEnd);
|
|
|
|
assert.match(dispose, /this\._feedback\.clearLoadingStatus\(\);/);
|
|
assert.match(dispose, /this\._shareRestoration\.destroy\(\)/);
|
|
assert.match(ShareRestoration.prototype.destroy.toString(), /this\._shareTrackingNoticeGeneration \+= 1;/);
|
|
assert.match(html, /<div id="global-loading-status" role="status" aria-live="polite" aria-atomic="true" hidden>/);
|
|
});
|
|
|
|
test('normalizes lifecycle and refresh loading without owning manager state', () => {
|
|
assert.equal(normalizeLayerLoading({ lifecycleState: 'enabling' }).loading, true);
|
|
assert.equal(normalizeLayerLoading({ lifecycleState: 'disabling' }).disabling, true);
|
|
assert.equal(normalizeLayerLoading({ enabled: true, stats: { loading: true, count: 8 } }).refresh, true);
|
|
assert.equal(normalizeLayerLoading({ enabled: true, stats: { refreshing: true } }).refresh, true);
|
|
assert.match(
|
|
normalizeLayerLoading({ stats: { managerRefreshError: 'refresh failed' } }).error,
|
|
/refresh failed/,
|
|
);
|
|
});
|
|
|
|
test('delays initial loading so instant operations never flash', () => {
|
|
const summary = aggregateLayerLoading([{ id: 'a', name: 'A', lifecycleState: 'enabling' }]);
|
|
const pending = reduceLoadingFeedback(createLoadingFeedbackState(), summary, 100);
|
|
assert.equal(pending.visible, false);
|
|
const finished = reduceLoadingFeedback(pending, aggregateLayerLoading([]), 150);
|
|
assert.equal(presentLoadingFeedback(finished, aggregateLayerLoading([]), 150), null);
|
|
});
|
|
|
|
test('reveals sustained loading and then a bounded completion state', () => {
|
|
const summary = aggregateLayerLoading([{ id: 'a', name: 'A', lifecycleState: 'enabling' }]);
|
|
const pending = reduceLoadingFeedback(createLoadingFeedbackState(), summary, 100);
|
|
const visible = reduceLoadingFeedback(pending, summary, 300);
|
|
assert.equal(presentLoadingFeedback(visible, summary, 300).label, 'LOADING LIVE DATA');
|
|
const complete = reduceLoadingFeedback(visible, aggregateLayerLoading([]), 350);
|
|
assert.equal(presentLoadingFeedback(complete, aggregateLayerLoading([]), 350).label, 'LOAD COMPLETE');
|
|
});
|
|
|
|
test('terminal loading feedback centers its label without an empty detail slot', () => {
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
assert.match(
|
|
css,
|
|
/#global-loading-status:is\(\s*\[data-state='complete'\],\s*\[data-state='cancelled'\],\s*\[data-state='error'\]\s*\)\s*\{[\s\S]*?justify-content:\s*center;[\s\S]*?text-align:\s*center;/,
|
|
);
|
|
assert.match(
|
|
css,
|
|
/#global-loading-status:is\(\s*\[data-state='complete'\],\s*\[data-state='cancelled'\],\s*\[data-state='error'\]\s*\)\s*#global-loading-detail\s*\{\s*display:\s*none;/,
|
|
);
|
|
});
|
|
|
|
test('distinguishes accepted-data refresh from initial loading', () => {
|
|
const summary = aggregateLayerLoading([{
|
|
id: 'a', name: 'A', enabled: true, lifecycleState: 'enabled', stats: { loading: true, count: 4 },
|
|
}]);
|
|
assert.equal(summary.refresh, true);
|
|
});
|
|
|
|
test('surfaces cancellation and failure terminal states', () => {
|
|
const summary = aggregateLayerLoading([{ id: 'a', lifecycleState: 'enabling' }]);
|
|
const pending = reduceLoadingFeedback(createLoadingFeedbackState(), summary, 0);
|
|
const visible = reduceLoadingFeedback(pending, summary, 200);
|
|
assert.equal(reduceLoadingFeedback(visible, aggregateLayerLoading([]), 250, {
|
|
type: 'visibility-cancelled', layerId: 'a', cancelled: true,
|
|
}).terminal, 'cancelled');
|
|
assert.equal(reduceLoadingFeedback(visible, aggregateLayerLoading([]), 250, {
|
|
type: 'visibility-failed', layerId: 'a', error: new Error('no'),
|
|
}).terminal, 'error');
|
|
});
|
|
|
|
test('surfaces manager-owned refresh failure and recovery through the shared banner', () => {
|
|
const refreshing = aggregateLayerLoading([{
|
|
id: 'satellites',
|
|
name: 'Satellites',
|
|
enabled: true,
|
|
lifecycleState: 'enabled',
|
|
stats: { refreshing: true, count: 0, lastUpdate: null },
|
|
}]);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), refreshing, 0, {
|
|
type: 'refresh-transition', layerId: 'satellites', refreshEpoch: 1,
|
|
});
|
|
state = reduceLoadingFeedback(state, refreshing, 200);
|
|
assert.equal(presentLoadingFeedback(state, refreshing, 200).label, 'REFRESHING LIVE DATA');
|
|
state = reduceLoadingFeedback(state, aggregateLayerLoading([]), 250, {
|
|
type: 'refresh-failed', layerId: 'satellites', error: new Error('offline'), refreshEpoch: 1,
|
|
});
|
|
assert.equal(presentLoadingFeedback(state, aggregateLayerLoading([]), 250).label, 'LOAD FAILED');
|
|
|
|
const recovered = reduceLoadingFeedback(state, refreshing, 6000, {
|
|
type: 'refresh-transition', layerId: 'satellites', refreshEpoch: 2,
|
|
});
|
|
const complete = reduceLoadingFeedback(recovered, aggregateLayerLoading([]), 6200, {
|
|
type: 'refresh', layerId: 'satellites', refreshEpoch: 2,
|
|
});
|
|
assert.equal(complete.terminal, 'complete');
|
|
});
|
|
|
|
test('AIS first-connect grace expiry reports failure even without a terminal manager event', () => {
|
|
const waiting = aggregateLayerLoading([{
|
|
id: 'ais-live-vessels',
|
|
name: 'AIS Vessels',
|
|
enabled: true,
|
|
lifecycleState: 'enabled',
|
|
stats: { loading: true, count: 0, lastUpdate: null },
|
|
}]);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), waiting, 0);
|
|
state = reduceLoadingFeedback(state, waiting, 200);
|
|
|
|
const unavailable = aggregateLayerLoading([{
|
|
id: 'ais-live-vessels',
|
|
name: 'AIS Vessels',
|
|
enabled: true,
|
|
lifecycleState: 'enabled',
|
|
stats: {
|
|
loading: false,
|
|
count: 0,
|
|
lastUpdate: null,
|
|
status: 'unavailable',
|
|
error: 'awaiting first AIS message…',
|
|
},
|
|
}]);
|
|
state = reduceLoadingFeedback(state, unavailable, 300);
|
|
|
|
assert.equal(state.terminal, 'error');
|
|
assert.equal(presentLoadingFeedback(state, unavailable, 300).label, 'LOAD FAILED');
|
|
});
|
|
|
|
test('participant stats failure outranks a simultaneous visibility completion', () => {
|
|
const enabling = aggregateLayerLoading([{
|
|
id: 'ais-live-vessels',
|
|
name: 'AIS Vessels',
|
|
lifecycleState: 'enabling',
|
|
stats: { loading: true },
|
|
}]);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), enabling, 0);
|
|
state = reduceLoadingFeedback(state, enabling, 200);
|
|
|
|
const missingKey = aggregateLayerLoading([{
|
|
id: 'ais-live-vessels',
|
|
name: 'AIS Vessels',
|
|
enabled: true,
|
|
lifecycleState: 'enabled',
|
|
stats: { loading: false, keyRequired: true },
|
|
}]);
|
|
state = reduceLoadingFeedback(state, missingKey, 250, {
|
|
type: 'visibility', layerId: 'ais-live-vessels', enabled: true,
|
|
});
|
|
|
|
assert.equal(state.terminal, 'error');
|
|
});
|
|
|
|
test('retains the worst terminal outcome until every concurrent load drains', () => {
|
|
const both = aggregateLayerLoading([
|
|
{ id: 'a', name: 'A', lifecycleState: 'enabling' },
|
|
{ id: 'b', name: 'B', lifecycleState: 'enabling' },
|
|
]);
|
|
const onlyB = aggregateLayerLoading([
|
|
{ id: 'b', name: 'B', lifecycleState: 'enabling' },
|
|
]);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), both, 0, {
|
|
type: 'visibility-transition', layerId: 'a',
|
|
});
|
|
state = reduceLoadingFeedback(state, both, 200, {
|
|
type: 'visibility-transition', layerId: 'b',
|
|
});
|
|
state = reduceLoadingFeedback(state, onlyB, 250, {
|
|
type: 'visibility-failed', layerId: 'a', error: new Error('A failed'),
|
|
});
|
|
assert.deepEqual(state.activeIds, ['a', 'b']);
|
|
assert.equal(state.batchOutcome, 'error');
|
|
state = reduceLoadingFeedback(state, onlyB, 300);
|
|
state = reduceLoadingFeedback(state, aggregateLayerLoading([]), 350, {
|
|
type: 'visibility', layerId: 'b', enabled: true,
|
|
});
|
|
assert.equal(state.terminal, 'error');
|
|
assert.equal(presentLoadingFeedback(state, aggregateLayerLoading([]), 350).label, 'LOAD FAILED');
|
|
});
|
|
|
|
test('retains cancellation across overlapping success and resets it for a later epoch', () => {
|
|
const both = aggregateLayerLoading([
|
|
{ id: 'a', name: 'A', lifecycleState: 'enabling' },
|
|
{ id: 'b', name: 'B', lifecycleState: 'enabling' },
|
|
]);
|
|
const onlyB = aggregateLayerLoading([{ id: 'b', name: 'B', lifecycleState: 'enabling' }]);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), both, 0);
|
|
state = reduceLoadingFeedback(state, onlyB, 200, {
|
|
type: 'visibility-cancelled', layerId: 'a', cancelled: true,
|
|
});
|
|
state = reduceLoadingFeedback(state, aggregateLayerLoading([]), 250, {
|
|
type: 'visibility', layerId: 'b', enabled: true,
|
|
});
|
|
assert.equal(state.terminal, 'cancelled');
|
|
|
|
const next = aggregateLayerLoading([{ id: 'c', name: 'C', lifecycleState: 'enabling' }]);
|
|
state = reduceLoadingFeedback(state, next, 300, {
|
|
type: 'visibility-transition', layerId: 'c',
|
|
});
|
|
assert.equal(state.batchOutcome, null);
|
|
assert.deepEqual(state.activeIds, ['c']);
|
|
state = reduceLoadingFeedback(state, aggregateLayerLoading([]), 500, {
|
|
type: 'visibility', layerId: 'c', enabled: true,
|
|
});
|
|
assert.equal(state.terminal, 'complete');
|
|
});
|
|
|
|
test('ignores terminal events from layers outside the active loading epoch', () => {
|
|
const active = aggregateLayerLoading([{ id: 'a', name: 'A', lifecycleState: 'enabling' }]);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), active, 0);
|
|
state = reduceLoadingFeedback(state, active, 200, {
|
|
type: 'visibility-failed', layerId: 'unrelated', error: new Error('not this batch'),
|
|
});
|
|
assert.equal(state.batchOutcome, null);
|
|
state = reduceLoadingFeedback(state, aggregateLayerLoading([]), 250, {
|
|
type: 'visibility', layerId: 'a', enabled: true,
|
|
});
|
|
assert.equal(state.terminal, 'complete');
|
|
});
|
|
|
|
test('a final failure outranks an earlier success in the same loading epoch', () => {
|
|
const both = aggregateLayerLoading([
|
|
{ id: 'a', lifecycleState: 'enabling' },
|
|
{ id: 'b', lifecycleState: 'enabling' },
|
|
]);
|
|
const onlyB = aggregateLayerLoading([{ id: 'b', lifecycleState: 'enabling' }]);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), both, 0);
|
|
state = reduceLoadingFeedback(state, onlyB, 200, {
|
|
type: 'visibility', layerId: 'a', enabled: true,
|
|
});
|
|
state = reduceLoadingFeedback(state, aggregateLayerLoading([]), 250, {
|
|
type: 'visibility-failed', layerId: 'b', error: new Error('B failed'),
|
|
});
|
|
assert.equal(state.terminal, 'error');
|
|
});
|
|
|
|
test('describes disable work without reporting it as a completed load', () => {
|
|
const summary = aggregateLayerLoading([{
|
|
id: 'a', name: 'A', enabled: true, lifecycleState: 'disabling',
|
|
}]);
|
|
const pending = reduceLoadingFeedback(createLoadingFeedbackState(), summary, 0);
|
|
const visible = reduceLoadingFeedback(pending, summary, 200);
|
|
assert.equal(presentLoadingFeedback(visible, summary, 200).label, 'TURNING OFF LIVE DATA');
|
|
const complete = reduceLoadingFeedback(visible, aggregateLayerLoading([]), 250);
|
|
assert.equal(presentLoadingFeedback(complete, aggregateLayerLoading([]), 250).label, 'LIVE DATA OFF');
|
|
});
|
|
|
|
test('a flow failure landing after the roads settle still ends the batch as LOAD FAILED', () => {
|
|
// Traffic's 250 ms paint race lets a TomTom request outlive the road load
|
|
// that started it. The layer keeps stats.loading true while it still owns
|
|
// that request, so the batch cannot close early and report LOAD COMPLETE
|
|
// over a failure that has not landed yet.
|
|
const sample = (stats) => aggregateLayerLoading([
|
|
{ id: 'traffic', name: 'Street Traffic', enabled: true, lifecycleState: 'enabled', stats },
|
|
]);
|
|
// Roads loading; flow request outstanding.
|
|
const roadsLoading = sample({ loading: true, count: 0, mode: 'live', error: null });
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), roadsLoading, 0);
|
|
state = reduceLoadingFeedback(state, roadsLoading, 200);
|
|
assert.equal(state.visible, true);
|
|
// Cached roads have painted, but the flow fetch has NOT settled: the layer
|
|
// still reports loading, so the batch stays open.
|
|
const flowStillPending = sample({ loading: true, count: 544, mode: 'live', error: null });
|
|
state = reduceLoadingFeedback(state, flowStillPending, 400);
|
|
assert.equal(state.phase, 'loading');
|
|
// Flow fails late.
|
|
const flowFailed = sample({
|
|
loading: false,
|
|
count: 544,
|
|
mode: 'live',
|
|
error: 'SIMULATED — TomTom daily budget reached',
|
|
});
|
|
state = reduceLoadingFeedback(state, flowFailed, 900);
|
|
assert.equal(state.terminal, 'error');
|
|
assert.equal(
|
|
presentLoadingFeedback(state, flowFailed, 900).label,
|
|
'LOAD FAILED',
|
|
'a late flow failure must not be announced as LOAD COMPLETE',
|
|
);
|
|
});
|
|
|
|
test('keeps cold idle traffic hidden even with a truthful zero-coverage label', () => {
|
|
let state = createTrafficSyncFeedbackState();
|
|
const sample = {
|
|
enabled: true,
|
|
stats: { loading: false, loadingLabel: 'LIVE · TomTom flow · 0% cov', flowCoveragePct: 0 },
|
|
};
|
|
for (const now of [0, 220, 440, 2200]) state = reduceTrafficSyncFeedback(state, sample, now);
|
|
assert.equal(state.visible, false);
|
|
assert.equal(state.busy, false);
|
|
});
|
|
|
|
test('shows traffic busy work and one fixed busy-to-idle confirmation', () => {
|
|
const busySample = {
|
|
enabled: true,
|
|
stats: { loading: true, loadingLabel: 'syncing LIVE traffic flow' },
|
|
};
|
|
const idleSample = {
|
|
enabled: true,
|
|
stats: { loading: false, loadingLabel: 'LIVE · TomTom flow · 0% cov' },
|
|
};
|
|
let state = reduceTrafficSyncFeedback(createTrafficSyncFeedbackState(), busySample, 100);
|
|
assert.deepEqual({ visible: state.visible, progress: state.progressText }, { visible: true, progress: '...' });
|
|
state = reduceTrafficSyncFeedback(state, busySample, 320);
|
|
state = reduceTrafficSyncFeedback(state, idleSample, 500);
|
|
const fixedDeadline = state.confirmationUntil;
|
|
assert.equal(fixedDeadline, 500 + TRAFFIC_SYNC_CONFIRM_MS);
|
|
assert.equal(state.progressText, '');
|
|
state = reduceTrafficSyncFeedback(state, idleSample, 900);
|
|
assert.equal(state.confirmationUntil, fixedDeadline);
|
|
state = reduceTrafficSyncFeedback(state, idleSample, fixedDeadline + 1);
|
|
assert.equal(state.visible, false);
|
|
});
|
|
|
|
test('the settled traffic chip shows exactly one percentage — the coverage it measured', () => {
|
|
// "LIVE · TomTom flow · 0% cov" beside a hard-coded "100%" read as a chip
|
|
// arguing with itself. The 100% was never a measurement: a settled chip is
|
|
// complete by definition, so the progress slot goes quiet and the label's
|
|
// coverage figure is the only number left.
|
|
const idleSample = {
|
|
enabled: true,
|
|
stats: { loading: false, loadingLabel: 'LIVE · TomTom flow · 0% cov' },
|
|
};
|
|
let state = reduceTrafficSyncFeedback(
|
|
createTrafficSyncFeedbackState(),
|
|
{ enabled: true, stats: { loading: true, loadingLabel: 'syncing LIVE traffic flow' } },
|
|
0,
|
|
);
|
|
state = reduceTrafficSyncFeedback(state, idleSample, 100);
|
|
assert.equal(state.visible, true);
|
|
assert.equal(state.label, 'LIVE · TomTom flow · 0% cov');
|
|
assert.equal(state.progressText, '');
|
|
const rendered = `${state.label} ${state.progressText}`.trim();
|
|
assert.equal(rendered.match(/\d+%/g).length, 1, 'the settled chip must carry one percentage');
|
|
assert.doesNotMatch(rendered, /100%/);
|
|
});
|
|
|
|
test('an emptied progress slot collapses rather than leaving a min-width stub', () => {
|
|
// ShellFeedback publishes the empty settled progress
|
|
// (src/ui/statusChipOwners.test.mjs); the slot must then take no room.
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
assert.match(css, /#traffic-sync-progress:empty \{\s*display: none;\s*\}/);
|
|
});
|
|
|
|
test('work still in flight keeps its progress number beside a label that has none', () => {
|
|
const state = reduceTrafficSyncFeedback(
|
|
createTrafficSyncFeedbackState(),
|
|
{ enabled: true, stats: { phaseLabel: 'warming roads', phaseProgressPct: 42 } },
|
|
0,
|
|
);
|
|
assert.deepEqual(
|
|
{ busy: state.busy, label: state.label, progress: state.progressText },
|
|
{ busy: true, label: 'warming roads', progress: '42%' },
|
|
);
|
|
assert.doesNotMatch(state.label, /%/, 'a busy label must not carry its own percentage');
|
|
});
|
|
|
|
test('resets traffic feedback on disable and permits a later fresh cycle', () => {
|
|
const busy = { enabled: true, stats: { loading: true } };
|
|
const idle = { enabled: true, stats: { loading: false, loadingLabel: 'simulated traffic' } };
|
|
let state = reduceTrafficSyncFeedback(createTrafficSyncFeedbackState(), busy, 0);
|
|
state = reduceTrafficSyncFeedback(state, idle, 100);
|
|
state = reduceTrafficSyncFeedback(state, { enabled: false, stats: {}, forceShow: true }, 200);
|
|
assert.deepEqual(state, createTrafficSyncFeedbackState());
|
|
state = reduceTrafficSyncFeedback(state, idle, 300);
|
|
assert.equal(state.visible, false);
|
|
state = reduceTrafficSyncFeedback(state, busy, 400);
|
|
state = reduceTrafficSyncFeedback(state, idle, 500);
|
|
assert.equal(state.visible, true);
|
|
});
|
|
|
|
test('new busy work replaces confirmation and force-show remains bounded', () => {
|
|
const idle = { enabled: true, stats: { loadingLabel: 'simulated traffic' } };
|
|
const busy = {
|
|
enabled: true,
|
|
stats: { phaseProgressPct: -20, prewarmQueueDepth: 1, phaseLabel: 'warming roads' },
|
|
};
|
|
let state = reduceTrafficSyncFeedback(createTrafficSyncFeedbackState(), idle, 0);
|
|
state = reduceTrafficSyncFeedback(state, { ...idle, forceShow: true }, 100);
|
|
const forcedDeadline = state.confirmationUntil;
|
|
state = reduceTrafficSyncFeedback(state, { ...idle, forceShow: true }, 300);
|
|
assert.equal(state.confirmationUntil, forcedDeadline);
|
|
state = reduceTrafficSyncFeedback(state, busy, 400);
|
|
assert.deepEqual({ busy: state.busy, progress: state.progressText }, { busy: true, progress: '0%' });
|
|
state = reduceTrafficSyncFeedback(state, idle, 500);
|
|
assert.equal(state.confirmationUntil, 500 + TRAFFIC_SYNC_CONFIRM_MS);
|
|
state = reduceTrafficSyncFeedback(state, idle, 500 + TRAFFIC_SYNC_CONFIRM_MS + 1);
|
|
assert.equal(state.visible, false);
|
|
});
|
|
|
|
test('aggregates Mapped Installations refresh beside CCTV without changing either owner', () => {
|
|
const summary = aggregateLayerLoading([
|
|
{
|
|
id: 'cctv', name: 'CCTV', enabled: true, lifecycleState: 'enabled',
|
|
stats: { count: 500, lastUpdate: 10, loading: true },
|
|
},
|
|
{
|
|
id: 'military-installations', name: 'Mapped Installations', enabled: true,
|
|
lifecycleState: 'enabled',
|
|
stats: { count: 4, lastUpdate: 20, loading: true },
|
|
},
|
|
]);
|
|
assert.deepEqual(summary.activeIds, ['cctv', 'military-installations']);
|
|
assert.equal(summary.refresh, true);
|
|
const pending = reduceLoadingFeedback(createLoadingFeedbackState(), summary, 0);
|
|
const visible = reduceLoadingFeedback(pending, summary, 200);
|
|
assert.deepEqual(
|
|
presentLoadingFeedback(visible, summary, 200),
|
|
{ state: 'refresh', label: 'REFRESHING LIVE DATA', detail: 'CCTV · Mapped Installations' },
|
|
);
|
|
});
|
|
|
|
// The reducer above is pure and fully covered; its DRIVER lives inside the
|
|
// StyleManager class (a full viewer to instantiate), so — as with the panel
|
|
// stack layout contract — the ticker's lifecycle is pinned against ui.js
|
|
// source. (perf rebase 2026-08-17)
|
|
test('the loading ticker never runs hidden and stops after loading and notices settle', () => {
|
|
const ui = readFileSync(new URL('./ui/shellFeedback.js', import.meta.url), 'utf8');
|
|
// Scope every assertion to _armLoadingFeedbackTicker's own body. The
|
|
// neighbouring _startTrafficChipTicker is a deliberately PERMANENT 500ms
|
|
// safety-net poll, so its `if (document.hidden) return;` is correct there
|
|
// and must not be confused with this self-stopping ticker's leak.
|
|
const armBody = ui.slice(ui.indexOf('_armLoadingFeedbackTicker() {'));
|
|
assert.ok(armBody.startsWith('_armLoadingFeedbackTicker() {'), 'ticker function not found in the UI shell source');
|
|
const arm = armBody.slice(0, armBody.indexOf('\n }\n') + 5);
|
|
|
|
// 1. The ARM guard itself refuses while hidden. Previously the only hidden
|
|
// check sat INSIDE the interval body, so a batch that completed while
|
|
// hidden left the 60ms timer scheduled for the whole hidden period: the
|
|
// idle check that clears it was unreachable behind the hidden `return`.
|
|
assert.match(
|
|
arm,
|
|
/if \(this\._loadingFeedbackTicker \|\| document\.hidden\) return;/,
|
|
'the ticker must not arm while the document is hidden',
|
|
);
|
|
|
|
// 2. Going hidden STOPS the interval rather than idling inside it.
|
|
assert.match(
|
|
arm,
|
|
/if \(document\.hidden\) \{\s*this\._stopLoadingFeedbackTicker\(\);\s*return;\s*\}/,
|
|
'a hidden tick must clear the interval, not just skip the work',
|
|
);
|
|
assert.doesNotMatch(
|
|
arm,
|
|
/setInterval\(\(\) => \{\s*if \(document\.hidden\) return;/,
|
|
'the bare hidden `return` inside this interval is the leak — it must be gone',
|
|
);
|
|
|
|
// 3. Reaching idle stops it after any time-driven notice has completed.
|
|
// Persistent ACQUIRING notices remain visible without a 60ms timer.
|
|
assert.match(
|
|
arm,
|
|
/const noticeNeedsTicker = Number\.isFinite\(\s*this\._globalStatusNotice\?\.hideAt,?\s*\);[\s\S]*?if \(\s*this\._loadingFeedbackState\?\.phase === 'idle'\s*&&\s*!noticeNeedsTicker\s*\) \{\s*this\._stopLoadingFeedbackTicker\(\);\s*\}/,
|
|
'an idle phase with no expiring notice must stop the ticker',
|
|
);
|
|
assert.match(
|
|
ui,
|
|
/_stopLoadingFeedbackTicker\(\)\s*\{[\s\S]*?clearInterval\(this\._loadingFeedbackTicker\);[\s\S]*?this\._loadingFeedbackTicker = null;/,
|
|
'the shared stopper must actually clear and null the handle',
|
|
);
|
|
|
|
// 4. Returning to visible resamples, so a batch that finished while hidden
|
|
// is reconciled and a still-running one re-arms its ticker. The handler
|
|
// must live on the class that OWNS _updateGlobalLoadingFeedback
|
|
// (ShellFeedback) — wiring it into a neighbouring controller instead
|
|
// throws on every visibilitychange — and must be torn down with the rest.
|
|
assert.match(
|
|
ui,
|
|
/this\._loadingVisibilityHandler = \(\) => \{\s*if \(!document\.hidden\) this\._updateGlobalLoadingFeedback\(\);\s*else this\._stopLoadingFeedbackTicker\(\);\s*\};\s*document\.addEventListener\(\s*'visibilitychange',\s*this\._loadingVisibilityHandler,?\s*\);/,
|
|
'visibilitychange must resample the chip on return',
|
|
);
|
|
const styleManager = ui.slice(ui.indexOf('export class ShellFeedback'));
|
|
assert.ok(
|
|
styleManager.includes('this._loadingVisibilityHandler'),
|
|
'the resample handler must be owned by ShellFeedback, which defines _updateGlobalLoadingFeedback',
|
|
);
|
|
assert.match(
|
|
ui,
|
|
/document\.removeEventListener\(\s*'visibilitychange',\s*this\._loadingVisibilityHandler,?\s*\);/,
|
|
'the resample handler must be removed on teardown',
|
|
);
|
|
});
|
|
|
|
test('a guidance status such as zoom-in never counts as a participant failure', () => {
|
|
const zoomIn = { id: 'military-installations', name: 'Mapped Installations', enabled: true,
|
|
stats: { status: 'zoom-in', error: 'Zoom in to load mapped installation context', loading: true, count: 0 } };
|
|
const loading = aggregateLayerLoading([zoomIn]);
|
|
assert.equal(loading.records[0].error, null);
|
|
assert.equal(loading.records[0].degraded, false);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), loading, 1000);
|
|
state = reduceLoadingFeedback(state, loading, 1200);
|
|
const settled = aggregateLayerLoading([{ ...zoomIn, stats: { ...zoomIn.stats, loading: false } }]);
|
|
state = reduceLoadingFeedback(state, settled, 1500);
|
|
assert.equal(state.terminal, 'complete');
|
|
assert.equal(presentLoadingFeedback(state, settled, 1500).label, 'MAPPED SITES LOADED');
|
|
});
|
|
|
|
|
|
test('guidance does not suppress independent manager and feed failures', () => {
|
|
for (const field of ['lastError', 'managerRefreshError']) {
|
|
const record = normalizeLayerLoading({
|
|
id: 'militaryInstallations', enabled: true,
|
|
stats: { status: 'zoom-in', error: 'Zoom in to load mapped sites.', [field]: 'Network unavailable' },
|
|
});
|
|
assert.equal(record.error, 'Network unavailable');
|
|
}
|
|
});
|
|
|
|
test('ALPR retries show a countdown, preserve other failures, and clear when disabled', () => {
|
|
const camera = { ...retrySite({ error: 'Overpass rate-limited' }), id: 'alpr-cameras', name: 'ALPR cameras' };
|
|
const summary = aggregateLayerLoading([camera]);
|
|
const view = presentLoadingFeedback(createLoadingFeedbackState(), summary, 0);
|
|
assert.equal(view.state, 'retry');
|
|
assert.equal(view.label, 'OVERPASS RATE-LIMITED');
|
|
assert.match(view.detail, /ALPR cameras · retrying in 30s/);
|
|
const failed = { visible: true, phase: 'terminal', terminal: 'error', activeIds: ['alpr-cameras', 'flights'] };
|
|
const otherFailure = aggregateLayerLoading([camera, { id: 'flights', enabled: true, stats: { error: 'Failed' } }]);
|
|
assert.equal(presentLoadingFeedback(failed, otherFailure, 0).label, 'LOAD FAILED');
|
|
assert.equal(presentLoadingFeedback({ ...failed, failedEventIds: ['flights'] }, summary, 0).label, 'LOAD FAILED');
|
|
assert.equal(presentLoadingFeedback(createLoadingFeedbackState(), aggregateLayerLoading([{ ...camera, enabled: false }]), 0), null);
|
|
});
|
|
|
|
test('ALPR retry success does not inherit its prior error, including turning the layer off', () => {
|
|
const camera = stats => ({ id: 'alpr-cameras', enabled: true, stats });
|
|
const loading = aggregateLayerLoading([camera({ status: 'loading', loading: true, retrying: true })]);
|
|
let state = reduceLoadingFeedback(createLoadingFeedbackState(), loading, 0);
|
|
state = reduceLoadingFeedback(state, loading, 200);
|
|
assert.equal(presentLoadingFeedback(state, loading, 200).label, 'RETRYING ALPR CAMERAS');
|
|
const done = aggregateLayerLoading([camera({ status: 'ready', count: 3 })]);
|
|
state = reduceLoadingFeedback(state, done, 300);
|
|
assert.equal(presentLoadingFeedback(state, done, 300).label, 'LOAD COMPLETE');
|
|
const stopping = normalizeLayerLoading({ ...camera({ status: 'unavailable', error: 'Old failure' }), lifecycleState: 'disabling' });
|
|
assert.equal(stopping.error, null);
|
|
assert.equal(stopping.unavailable, false);
|
|
});
|