import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; import { runStagedMapProviders, waitForGlobeViewportReady, } from "../apps/catalog/src/mapProviderStartup.mjs"; function eventHarness() { const listeners = new Set(); return { event: { addEventListener(listener) { listeners.add(listener); return () => listeners.delete(listener); }, }, raise(...args) { for (const listener of [...listeners]) listener(...args); }, get size() { return listeners.size; }, }; } test("providers start in base-map order", async () => { const calls = []; const result = await runStagedMapProviders({ loadImagery: () => calls.push("imagery"), waitAfterImagery: () => calls.push("imagery-ready"), loadTerrain: () => calls.push("terrain"), waitAfterTerrain: () => calls.push("terrain-ready"), loadBuildings: () => calls.push("buildings"), loadDeferred: () => calls.push("deferred"), }); assert.equal(result, "complete"); assert.deepEqual(calls, ["imagery", "imagery-ready", "terrain", "terrain-ready", "buildings", "deferred"]); }); test("imagery failure is reported without blocking terrain or buildings", async () => { const calls = []; const errors = []; await runStagedMapProviders({ loadImagery: () => { calls.push("imagery"); throw new Error("imagery failed"); }, waitAfterImagery: () => calls.push("imagery-ready"), loadTerrain: () => calls.push("terrain"), waitAfterTerrain: () => calls.push("terrain-ready"), loadBuildings: () => calls.push("buildings"), onProviderError: (provider, error) => errors.push([provider, error.message]), }); assert.deepEqual(calls, ["imagery", "terrain", "terrain-ready", "buildings"]); assert.deepEqual(errors, [["imagery", "imagery failed"]]); }); test("terrain failure is reported without blocking buildings", async () => { const calls = []; const errors = []; await runStagedMapProviders({ loadImagery: () => calls.push("imagery"), waitAfterImagery: () => calls.push("imagery-ready"), loadTerrain: () => { calls.push("terrain"); throw new Error("terrain failed"); }, waitAfterTerrain: () => calls.push("terrain-ready"), loadBuildings: () => calls.push("buildings"), onProviderError: (provider, error) => errors.push([provider, error.message]), }); assert.deepEqual(calls, ["imagery", "imagery-ready", "terrain", "buildings"]); assert.deepEqual(errors, [["terrain", "terrain failed"]]); }); test("cancellation prevents all later stages", async () => { const controller = new AbortController(); const calls = []; const result = await runStagedMapProviders({ signal: controller.signal, loadImagery: () => { calls.push("imagery"); controller.abort(); }, waitAfterImagery: () => calls.push("imagery-ready"), loadTerrain: () => calls.push("terrain"), loadBuildings: () => calls.push("buildings"), }); assert.equal(result, "cancelled"); assert.deepEqual(calls, ["imagery"]); }); test("a stalled provider times out without inserting late or blocking later stages", async () => { const calls = []; const errors = []; let releaseImagery; const stalledImagery = new Promise((resolve) => { releaseImagery = resolve; }); await runStagedMapProviders({ providerInitializationTimeoutMs: 1, loadImagery: async ({ signal }) => { calls.push("imagery-start"); await stalledImagery; if (!signal.aborted) calls.push("imagery-insert"); }, loadTerrain: () => calls.push("terrain"), loadBuildings: () => calls.push("buildings"), onProviderError: (provider, error) => errors.push([provider, error.message]), }); releaseImagery(); await Promise.resolve(); assert.deepEqual(calls, ["imagery-start", "terrain", "buildings"]); assert.deepEqual(errors, [["imagery", "imagery_startup_timeout"]]); }); test("viewport readiness waits for discovery frames and cleans listeners", async () => { const progress = eventHarness(); const postRender = eventHarness(); const globe = { tilesLoaded: true, tileLoadProgressEvent: progress.event }; const scene = { postRender: postRender.event, requestRender() {} }; let settled = false; const ready = waitForGlobeViewportReady({ globe, scene, timeoutMs: 100, minimumRenderFrames: 2 }) .then((reason) => { settled = true; return reason; }); postRender.raise(); await Promise.resolve(); assert.equal(settled, false); postRender.raise(); assert.equal(await ready, "loaded"); assert.equal(progress.size, 0); assert.equal(postRender.size, 0); }); test("viewport readiness times out and aborts without leaking listeners", async () => { const timeoutProgress = eventHarness(); const timeoutPostRender = eventHarness(); assert.equal(await waitForGlobeViewportReady({ globe: { tilesLoaded: false, tileLoadProgressEvent: timeoutProgress.event }, scene: { postRender: timeoutPostRender.event, requestRender() {} }, timeoutMs: 1, }), "timeout"); assert.equal(timeoutProgress.size, 0); assert.equal(timeoutPostRender.size, 0); const abortProgress = eventHarness(); const abortPostRender = eventHarness(); const controller = new AbortController(); const pending = waitForGlobeViewportReady({ globe: { tilesLoaded: false, tileLoadProgressEvent: abortProgress.event }, scene: { postRender: abortPostRender.event, requestRender() {} }, signal: controller.signal, timeoutMs: 100, }); controller.abort(); assert.equal(await pending, "cancelled"); assert.equal(abortProgress.size, 0); assert.equal(abortPostRender.size, 0); }); test("the live renderer uses the staged provider coordinator", async () => { const renderer = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"); assert.match(renderer, /runStagedMapProviders\(\{/); assert.match(renderer, /waitForGlobeViewportReady\(\{/); assert.match(renderer, /providerStartupAbort\.abort\(\)/); assert.doesNotMatch(renderer, /Do not serialize provider startup/); });