mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
714 lines
34 KiB
JavaScript
714 lines
34 KiB
JavaScript
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 ui = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8');
|
|
const start = ui.indexOf(' _handleShareTrackingRestoreStatus(result) {');
|
|
const end = ui.indexOf('\n _initGlobalContextPanel() {', start);
|
|
const handler = ui.slice(start, end);
|
|
assert.match(handler, /this\._showGlobalStatusNotice\(message\)/);
|
|
assert.match(handler, /this\.initialRestorePromise\.then\(showAfterStartupCover\)/);
|
|
assert.match(handler, /this\._lifetime\.frame\(\(\) => \{/);
|
|
assert.match(handler, /this\._lifetime\.listen\(startupCover, 'transitionend', showOnce, \{ once: true \}\)/);
|
|
assert.match(handler, /fallbackTimer = this\._lifetime\.timeout\(showOnce, 1000\)/);
|
|
assert.doesNotMatch(handler, /this\._showToast\(message\)/);
|
|
assert.doesNotMatch(handler, /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, /canPresentDeferredStatusNotice\(/);
|
|
assert.match(handler, /if \(this\._shareTrackingAcquiringKey\) return/);
|
|
assert.match(handler, /result\.classification === 'followed' \|\| result\.classification === 'cancelled'/);
|
|
});
|
|
|
|
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', () => {
|
|
const ui = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8');
|
|
const html = 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\._globalStatusNotice = null;/);
|
|
assert.match(dispose, /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\(\[data-state='complete'\], \[data-state='cancelled'\], \[data-state='error'\]\)\s*\{[\s\S]*?justify-content:\s*center;[\s\S]*?text-align:\s*center;/,
|
|
);
|
|
assert.match(
|
|
css,
|
|
/#global-loading-status:is\(\[data-state='complete'\], \[data-state='cancelled'\], \[data-state='error'\]\) #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('the chip renderer clears the progress slot instead of stranding the last value', () => {
|
|
const ui = readFileSync(new URL('./ui/shellFeedback.js', import.meta.url), 'utf8');
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
const start = ui.indexOf(' _updateTrafficSyncChip(');
|
|
assert.ok(start > 0, '_updateTrafficSyncChip is missing');
|
|
const body = ui.slice(start, ui.indexOf('\n }', start));
|
|
// A truthiness guard here would leave the busy "..." sitting beside the
|
|
// settled label, which is the contradiction wearing a different hat.
|
|
assert.doesNotMatch(body, /if \(presentation\.progressText\s*\n?\s*&&/);
|
|
assert.match(
|
|
body,
|
|
/if \(this\._trafficSyncProgress\.textContent !== presentation\.progressText\) \{/,
|
|
);
|
|
// …and the emptied slot must collapse rather than leave a min-width stub.
|
|
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 ui.js');
|
|
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\(this\._globalStatusNotice\?\.hideAt\);[\s\S]*?if \(this\._loadingFeedbackState\?\.phase === 'idle' && !noticeNeedsTicker\) \{\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\('visibilitychange', this\._loadingVisibilityHandler\);/,
|
|
'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\('visibilitychange', this\._loadingVisibilityHandler\);/,
|
|
'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');
|
|
}
|
|
});
|