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, /