Files
NODEDC_DESIGN_GUIDELINE/scripts/map-provider-startup.test.mjs
T

164 lines
6.0 KiB
JavaScript

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/);
});