diff --git a/src/layers/launches/failedEntry.test.mjs b/src/layers/launches/failedEntry.test.mjs index 4330cf0d8..d43a5c6e9 100644 --- a/src/layers/launches/failedEntry.test.mjs +++ b/src/layers/launches/failedEntry.test.mjs @@ -1,8 +1,7 @@ // The real manager, Context coordinator and launches ingestion run against a -// local /api/launches, so a failed first load takes the browser's path. +// stubbed /api/launches, so a failed first load takes the browser's path. import test from 'node:test'; import assert from 'node:assert/strict'; -import http from 'node:http'; import * as Cesium from 'cesium'; import { DataLayerManager } from '../../data/manager.js'; import { ContextControls } from '../../ui/contextControls.js'; @@ -93,24 +92,15 @@ function fakeCamera() { async function fixture(t) { const upstream = { status: 429, results: [launchRecord] }; - const server = http.createServer(async (req, res) => { - if (req.url === '/api/celestrak/active') { - res.end(''); - return; - } - if (upstream.hold) await upstream.hold; - if (upstream.status !== 200) { - res.writeHead(upstream.status, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ error: 'Launch Library 2 unavailable' })); - return; - } - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ results: upstream.results })); - }); - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); - const origin = `http://127.0.0.1:${server.address().port}`; const source = createLaunchSource({ - fetchImpl: (url, options) => fetch(new URL(url, origin), options), + async fetchImpl(url) { + if (url === '/api/celestrak/active') return new Response(''); + if (upstream.hold) await upstream.hold; + return Response.json( + upstream.status === 200 ? { results: upstream.results } : {}, + { status: upstream.status }, + ); + }, }); const services = { @@ -201,14 +191,13 @@ async function fixture(t) { manager.register(missions); missions.attachDataManager(manager); - const toasts = []; const controls = new ContextControls({ elements: {}, installations: {}, actions: { getCockpit: () => null, claimVisualAuthority: noop, - showToast: (message) => toasts.push(message), + showToast: noop, setClearBusy: noop, syncDetection: noop, scheduleLayout: noop, @@ -224,8 +213,6 @@ async function fixture(t) { for (const id of ['rocket-launches', 'satellites', 'earthquakes', 'wind']) await manager.setEnabled(id, false); await missions.destroy(viewer); - server.closeAllConnections(); - await new Promise((resolve) => server.close(resolve)); }); const enabledIds = () => [...manager.getEnabledLayerIds()].sort(); @@ -250,56 +237,46 @@ async function fixture(t) { state, camera, rosters, - toasts, enabledIds, view, settle, }; } -test('a direct Space Missions entry that gets HTTP 429 rolls back instead of isolating', async (t) => { - const f = await fixture(t); - assert.equal( - await f.manager.setEnabled('earthquakes', true, { origin: 'user' }), - true, - ); - const before = f.view(); +const entries = { + direct: (f) => + f.manager.setEnabled('rocket-launches', true, { origin: 'user' }), + 'Context-panel': async (f) => + ( + await f.controls.setContextMode('space-missions', { + claimVisualAuthority: false, + }) + ).ok, +}; - const entered = await f.manager.setEnabled('rocket-launches', true, { - origin: 'user', +for (const [path, enter] of Object.entries(entries)) + test(`a ${path} Space Missions entry that gets HTTP 429 rolls back instead of isolating`, async (t) => { + const f = await fixture(t); + await f.manager.setEnabled('earthquakes', true, { origin: 'user' }); + const before = f.view(); + + const entered = await enter(f); + await f.settle(); + + assert.equal(entered, false, 'a failed mission index fails the enable'); + assert.deepEqual(f.enabledIds(), ['earthquakes']); + assert.equal(f.controls.getContextModeState().mode, null); + assert.equal(f.controls.getContextModeState().active, false); + assert.equal(f.missions.getStats().error, 'HTTP 429'); + assert.deepEqual(f.view(), before, 'the pre-entry view comes back'); + assert.ok(f.camera.cancelled > 0, 'the full-globe flight is cancelled'); + + assert.equal( + await f.manager.setEnabled('wind', true, { origin: 'user' }), + true, + 'an unrelated layer is not blocked after the failed entry', + ); }); - await f.settle(); - - assert.equal(entered, false, 'a failed mission index fails the enable'); - assert.deepEqual(f.enabledIds(), ['earthquakes']); - assert.equal(f.controls.getContextModeState().mode, null); - assert.equal(f.controls.getContextModeState().active, false); - assert.equal(f.missions.getStats().error, 'HTTP 429'); - assert.deepEqual(f.view(), before, 'the pre-entry view comes back'); - assert.ok(f.camera.cancelled > 0, 'the full-globe flight is cancelled'); - - assert.equal( - await f.manager.setEnabled('wind', true, { origin: 'user' }), - true, - 'an unrelated layer is not blocked after the failed entry', - ); -}); - -test('a Context-panel Space Missions entry that gets HTTP 429 restores the previous layers', async (t) => { - const f = await fixture(t); - await f.manager.setEnabled('earthquakes', true, { origin: 'user' }); - const before = f.view(); - - const result = await f.controls.setContextMode('space-missions', { - claimVisualAuthority: false, - }); - await f.settle(); - - assert.equal(result.ok, false); - assert.deepEqual(f.enabledIds(), ['earthquakes']); - assert.equal(f.controls.getContextModeState().mode, null); - assert.deepEqual(f.view(), before); -}); // Holds the /api/launches reply until the returned release is called. function holdReply(f) { diff --git a/src/layers/launches/ingestion.js b/src/layers/launches/ingestion.js index be6190815..fbd3778e1 100644 --- a/src/layers/launches/ingestion.js +++ b/src/layers/launches/ingestion.js @@ -236,7 +236,6 @@ export function createIngestion({ parts.panel.renderMissionRoster(); throw error; } - layerState._entryCameraView = null; } } } diff --git a/src/layers/launches/lifecycle.js b/src/layers/launches/lifecycle.js index 13150e805..252ca7ab1 100644 --- a/src/layers/launches/lifecycle.js +++ b/src/layers/launches/lifecycle.js @@ -241,12 +241,12 @@ export function createLifecycle({ // A new attempt at an index that never loaded starts from loading. layerState._lastError = null; parts.panel.renderMissionRoster(); + // Kept until the mission index loads, so an entry that never does + // gives the view back. A newer flight takes the camera and drops it. + layerState._entryCameraView = captureCameraView(layerState._viewer); + layerState._entryLandedView = null; } - // Kept until the mission index loads, so an entry that never does - // gives the view back. A newer flight takes the camera and drops it. const token = layerState._lifecycleToken; - layerState._entryCameraView = captureCameraView(layerState._viewer); - layerState._entryLandedView = null; const flying = parts.selection.focusFullGlobe(layerState._viewer, { complete: () => { if (token === layerState._lifecycleToken) @@ -305,7 +305,6 @@ export function createLifecycle({ layerState._sourceController.abort(); releaseContinuousRender('rocket-launches'); // direct-destroy path (perf wave 2 fix) layerState._enabled = false; - layerState._entryCameraView = null; layerState._lifecycleToken++; layerState._updateDirty = false; await parts.ingestion.restoreSatelliteDependency();