diff --git a/src/data/cctvCalgary.test.mjs b/src/data/cctvCalgary.test.mjs
index d13952a4e..a67f73b06 100644
--- a/src/data/cctvCalgary.test.mjs
+++ b/src/data/cctvCalgary.test.mjs
@@ -1,3 +1,4 @@
+import { streamingResponse } from '../testSupport/streamingResponse.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
@@ -19,24 +20,6 @@ import { allocateSourceCap } from '../../server/providers/cctv/cap.js';
import { createCctvCatalog } from '../../server/providers/cctv/catalog.js';
import { directionToHeading } from './directionText.js';
-/**
- * A response whose body is a live stream, plus a flag that flips when the
- * stream is cancelled. A rejection path that returns without cancelling holds
- * the transport open, so the flag is what the refusal tests actually assert.
- */
-const streamingResponse = (init = {}) => {
- const state = { cancelled: false };
- const body = new ReadableStream({
- start(controller) {
- controller.enqueue(new TextEncoder().encode('['));
- },
- cancel() {
- state.cancelled = true;
- },
- });
- return { response: new Response(body, init), state };
-};
-
/** One Open Calgary row, shaped like the live `k7p9-kppz` payload. */
const row = (overrides = {}) => ({
camera_url: {
diff --git a/src/data/cctvCataloniaSource.test.mjs b/src/data/cctvCataloniaSource.test.mjs
index be472784f..824f9bedc 100644
--- a/src/data/cctvCataloniaSource.test.mjs
+++ b/src/data/cctvCataloniaSource.test.mjs
@@ -1,3 +1,4 @@
+import { withEnv } from '../testSupport/environment.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -17,17 +18,6 @@ import {
} from '../../server/providers/cctv/sources.js';
/** Set (or, for `undefined`, delete) environment variables for one test. */
-function withEnv(t, env) {
- for (const [name, value] of Object.entries(env)) {
- const previous = process.env[name];
- t.after(() => {
- if (previous === undefined) delete process.env[name];
- else process.env[name] = previous;
- });
- if (value === undefined) delete process.env[name];
- else process.env[name] = value;
- }
-}
/** Silence the loaders' progress and failure logging. */
function quiet(t) {
diff --git a/src/data/cctvDriveBcSource.test.mjs b/src/data/cctvDriveBcSource.test.mjs
index e3833c4aa..1cad19080 100644
--- a/src/data/cctvDriveBcSource.test.mjs
+++ b/src/data/cctvDriveBcSource.test.mjs
@@ -1,3 +1,4 @@
+import { withEnv } from '../testSupport/environment.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -28,17 +29,6 @@ import {
} from '../../server/providers/cctv/sources.js';
/** Set (or, for `undefined`, delete) environment variables for one test. */
-function withEnv(t, env) {
- for (const [name, value] of Object.entries(env)) {
- const previous = process.env[name];
- t.after(() => {
- if (previous === undefined) delete process.env[name];
- else process.env[name] = previous;
- });
- if (value === undefined) delete process.env[name];
- else process.env[name] = value;
- }
-}
/** Silence the loaders' progress and failure logging. */
function quiet(t) {
diff --git a/src/data/cctvIceland.test.mjs b/src/data/cctvIceland.test.mjs
index 1db511478..2f4bf7374 100644
--- a/src/data/cctvIceland.test.mjs
+++ b/src/data/cctvIceland.test.mjs
@@ -1,3 +1,4 @@
+import { withEnv } from '../testSupport/environment.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -19,17 +20,6 @@ import {
} from '../../server/providers/cctv/sources.js';
/** Set (or, for `undefined`, delete) environment variables for one test. */
-function withEnv(t, env) {
- for (const [name, value] of Object.entries(env)) {
- const previous = process.env[name];
- t.after(() => {
- if (previous === undefined) delete process.env[name];
- else process.env[name] = previous;
- });
- if (value === undefined) delete process.env[name];
- else process.env[name] = value;
- }
-}
/** One row shaped like the live `vefmyndavelar2014_1` payload. */
const row = (over = {}) => ({
diff --git a/src/data/cctvKingCountySource.test.mjs b/src/data/cctvKingCountySource.test.mjs
index 5da0ae708..92c7797cc 100644
--- a/src/data/cctvKingCountySource.test.mjs
+++ b/src/data/cctvKingCountySource.test.mjs
@@ -1,3 +1,4 @@
+import { withEnv } from '../testSupport/environment.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
@@ -14,17 +15,6 @@ import { cctvProxy } from '../../server/providers/cctv.js';
import { createCatalog } from '../layers/cctv/catalog.js';
/** Set (or, for `undefined`, delete) environment variables for one test. */
-function withEnv(t, env) {
- for (const [name, value] of Object.entries(env)) {
- const previous = process.env[name];
- t.after(() => {
- if (previous === undefined) delete process.env[name];
- else process.env[name] = previous;
- });
- if (value === undefined) delete process.env[name];
- else process.env[name] = value;
- }
-}
/** Silence the loaders' progress and failure logging. */
function quiet(t) {
diff --git a/src/data/cctvMissingNumbers.test.mjs b/src/data/cctvMissingNumbers.test.mjs
index 68911110f..3bc5e84d2 100644
--- a/src/data/cctvMissingNumbers.test.mjs
+++ b/src/data/cctvMissingNumbers.test.mjs
@@ -1,3 +1,4 @@
+import { withEnv } from '../testSupport/environment.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
@@ -22,17 +23,6 @@ import { createModel } from '../layers/cctv/model.js';
const ABSENT = [null, undefined, '', ' ', false, true];
/** Set env vars for one test and restore them afterwards. */
-function withEnv(t, env) {
- for (const [name, value] of Object.entries(env)) {
- const previous = process.env[name];
- t.after(() => {
- if (previous === undefined) delete process.env[name];
- else process.env[name] = previous;
- });
- if (value === undefined) delete process.env[name];
- else process.env[name] = value;
- }
-}
/** Silence the loaders' progress and failure logging. */
function quiet(t) {
diff --git a/src/data/cctvNyc.test.mjs b/src/data/cctvNyc.test.mjs
index f2e429dee..580e4b9da 100644
--- a/src/data/cctvNyc.test.mjs
+++ b/src/data/cctvNyc.test.mjs
@@ -1,3 +1,4 @@
+import { streamingResponse } from '../testSupport/streamingResponse.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
@@ -22,24 +23,6 @@ import {
} from '../../server/providers/cctv/normalize.js';
import { createCctvCatalog } from '../../server/providers/cctv/catalog.js';
-/**
- * A response whose body is a live stream, plus a flag that flips when the
- * stream is cancelled. A rejection path that returns without cancelling holds
- * the transport open, so the flag is what the refusal tests actually assert.
- */
-const streamingResponse = (init = {}) => {
- const state = { cancelled: false };
- const body = new ReadableStream({
- start(controller) {
- controller.enqueue(new TextEncoder().encode('['));
- },
- cancel() {
- state.cancelled = true;
- },
- });
- return { response: new Response(body, init), state };
-};
-
const MIDTOWN_ID = '8a6bc417-4877-4ebe-8052-88c1b261baf1';
const BROOKLYN_ID = '11111111-2222-3333-4444-555555555555';
diff --git a/src/data/cctvOttawa.test.mjs b/src/data/cctvOttawa.test.mjs
index e4bd0f290..2cc498559 100644
--- a/src/data/cctvOttawa.test.mjs
+++ b/src/data/cctvOttawa.test.mjs
@@ -1,3 +1,4 @@
+import { withEnv } from '../testSupport/environment.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -21,17 +22,6 @@ import {
const CERT = { OTTAWA_CAMERA_CERTIFICATE: 'CERT123' };
/** Set (or, for `undefined`, delete) environment variables for one test. */
-function withEnv(t, env) {
- for (const [name, value] of Object.entries(env)) {
- const previous = process.env[name];
- t.after(() => {
- if (previous === undefined) delete process.env[name];
- else process.env[name] = previous;
- });
- if (value === undefined) delete process.env[name];
- else process.env[name] = value;
- }
-}
/** Silence the loader's progress and failure logging. */
function quiet(t) {
diff --git a/src/data/cctvWorldQuebec.test.mjs b/src/data/cctvWorldQuebec.test.mjs
index 605831382..3caef66ca 100644
--- a/src/data/cctvWorldQuebec.test.mjs
+++ b/src/data/cctvWorldQuebec.test.mjs
@@ -1,3 +1,4 @@
+import { withEnv } from '../testSupport/environment.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -29,17 +30,6 @@ function quiet(t) {
}
/** Set (or, for `undefined`, delete) environment variables for one test. */
-function withEnv(t, env) {
- for (const [name, value] of Object.entries(env)) {
- const previous = process.env[name];
- t.after(() => {
- if (previous === undefined) delete process.env[name];
- else process.env[name] = previous;
- });
- if (value === undefined) delete process.env[name];
- else process.env[name] = value;
- }
-}
/** A temporary source root holding the given World and Québec catalogs. */
function sourceRoot(t, { world = [], quebec = null } = {}) {
diff --git a/src/data/cctvWsdotSource.test.mjs b/src/data/cctvWsdotSource.test.mjs
index 6b2ad3398..deecfdacd 100644
--- a/src/data/cctvWsdotSource.test.mjs
+++ b/src/data/cctvWsdotSource.test.mjs
@@ -1,3 +1,4 @@
+import { withEnv } from '../testSupport/environment.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -15,17 +16,6 @@ import {
} from '../../server/providers/cctv/sources.js';
/** Set (or, for `undefined`, delete) environment variables for one test. */
-function withEnv(t, env) {
- for (const [name, value] of Object.entries(env)) {
- const previous = process.env[name];
- t.after(() => {
- if (previous === undefined) delete process.env[name];
- else process.env[name] = previous;
- });
- if (value === undefined) delete process.env[name];
- else process.env[name] = value;
- }
-}
/** Silence the loaders' progress and failure logging. */
function quiet(t) {
diff --git a/src/hudAltitudeDatum.test.mjs b/src/hudAltitudeDatum.test.mjs
index ec19f9f08..8ed1420fe 100644
--- a/src/hudAltitudeDatum.test.mjs
+++ b/src/hudAltitudeDatum.test.mjs
@@ -1,3 +1,4 @@
+import './testSupport/stubMgrs.mjs';
// The HUD's ALT readout datum. Field report (2026-08-22, cockpit parked at
// SFO): the bottom-right OSD read "ALT: -15M" — and JFK, earlier, "ALT: -18M".
// Cesium reports the camera's height against the WGS84 ELLIPSOID, and San
@@ -25,27 +26,8 @@ import { createFakeEngine } from './globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import { hudReadouts } from './testSupport/hudReadouts.mjs';
import { readFileSync } from 'node:fs';
-import { registerHooks } from 'node:module';
import { ensureGeoidReady } from './data/geoid.js';
-const MGRS_STUB_URL = 'gev-test-stub:mgrs';
-registerHooks({
- resolve(specifier, context, next) {
- if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
- return next(specifier, context);
- },
- load(url, context, next) {
- if (url === MGRS_STUB_URL) {
- return {
- format: 'module',
- shortCircuit: true,
- source:
- 'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
- };
- }
- return next(url, context);
- },
-});
const { IntelHUD } = await import('./hud.js');
diff --git a/src/hudDms.test.mjs b/src/hudDms.test.mjs
index 728a86888..ca879e0a6 100644
--- a/src/hudDms.test.mjs
+++ b/src/hudDms.test.mjs
@@ -1,3 +1,4 @@
+import './testSupport/stubMgrs.mjs';
import { viewerEngine } from './testSupport/viewerEngine.mjs';
// The HUD's LAT/LON readout in degrees-minutes-seconds. Seconds are printed to
// two decimals, so a value within 0.005" of the next whole minute rounds up.
@@ -11,26 +12,6 @@ import { viewerEngine } from './testSupport/viewerEngine.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { hudReadouts } from './testSupport/hudReadouts.mjs';
-import { registerHooks } from 'node:module';
-
-const MGRS_STUB_URL = 'gev-test-stub:mgrs';
-registerHooks({
- resolve(specifier, context, next) {
- if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
- return next(specifier, context);
- },
- load(url, context, next) {
- if (url === MGRS_STUB_URL) {
- return {
- format: 'module',
- shortCircuit: true,
- source:
- 'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
- };
- }
- return next(url, context);
- },
-});
const { IntelHUD } = await import('./hud.js');
diff --git a/src/hudPageVisibility.test.mjs b/src/hudPageVisibility.test.mjs
index 2ab025a68..bf84bd672 100644
--- a/src/hudPageVisibility.test.mjs
+++ b/src/hudPageVisibility.test.mjs
@@ -1,3 +1,4 @@
+import './testSupport/stubMgrs.mjs';
import { viewerEngine } from './testSupport/viewerEngine.mjs';
// The HUD's 250 ms telemetry readout skips ticks in a hidden page, except in
// an inline panel, whose host may report it hidden while it is on screen.
@@ -8,25 +9,7 @@ import { viewerEngine } from './testSupport/viewerEngine.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { hudReadouts } from './testSupport/hudReadouts.mjs';
-import { registerHooks } from 'node:module';
-const MGRS_STUB_URL = 'gev-test-stub:mgrs';
-registerHooks({
- resolve(specifier, context, next) {
- if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
- return next(specifier, context);
- },
- load(url, context, next) {
- if (url === MGRS_STUB_URL) {
- return {
- format: 'module',
- shortCircuit: true,
- source: 'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
- };
- }
- return next(url, context);
- },
-});
const { IntelHUD } = await import('./hud.js');
diff --git a/src/hudSummaryGate.test.mjs b/src/hudSummaryGate.test.mjs
index e932dc828..c533a7087 100644
--- a/src/hudSummaryGate.test.mjs
+++ b/src/hudSummaryGate.test.mjs
@@ -1,3 +1,4 @@
+import './testSupport/stubMgrs.mjs';
import { viewerEngine } from './testSupport/viewerEngine.mjs';
// The HUD's place lookups are paid Google requests made only to feed the AI
// summary, so a server without one must not be asked to make them.
@@ -8,26 +9,6 @@ import { viewerEngine } from './testSupport/viewerEngine.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { hudReadouts } from './testSupport/hudReadouts.mjs';
-import { registerHooks } from 'node:module';
-
-const MGRS_STUB_URL = 'gev-test-stub:mgrs';
-registerHooks({
- resolve(specifier, context, next) {
- if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
- return next(specifier, context);
- },
- load(url, context, next) {
- if (url === MGRS_STUB_URL) {
- return {
- format: 'module',
- shortCircuit: true,
- source:
- 'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
- };
- }
- return next(url, context);
- },
-});
const { IntelHUD } = await import('./hud.js');
diff --git a/src/layers/ghostMaps/edgeCases.test.mjs b/src/layers/ghostMaps/edgeCases.test.mjs
index d12f73a31..1f159aac8 100644
--- a/src/layers/ghostMaps/edgeCases.test.mjs
+++ b/src/layers/ghostMaps/edgeCases.test.mjs
@@ -1,3 +1,8 @@
+import {
+ element,
+ kmz,
+ deferred,
+} from '../../testSupport/ghostMapsFixtures.mjs';
// Edge cases for s2-ghostmaps:
// change the KMZ edition and the engine before parsing finishes; only the
// selected edition is shown, and a bad archive does not erase the working one.
@@ -5,25 +10,6 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import { createGhostMapsLayer } from './index.js';
-function element(localName, children = [], value = '') {
- const node = {
- localName,
- children,
- get textContent() {
- return value + children.map((child) => child.textContent).join('');
- },
- getAttribute: () => null,
- getElementsByTagName(name) {
- return children.flatMap((child) => [
- ...(name === '*' || name === child.localName ? [child] : []),
- ...child.getElementsByTagName(name),
- ]);
- },
- };
- for (const child of children) child.parentElement = node;
- return node;
-}
-
// The XML double turns `NAME` into one placemark called NAME.
function documentFor(xml) {
const name = /(.*)<\/kml>/.exec(xml)?.[1] ?? 'unnamed';
@@ -39,37 +25,6 @@ function documentFor(xml) {
]);
}
-/** A stored (uncompressed) KMZ holding doc.kml only. */
-function kmz(name) {
- const key = Buffer.from('doc.kml');
- const bytes = Buffer.from(`${name}`);
- const header = Buffer.alloc(30);
- header.writeUInt32LE(0x04034b50);
- header.writeUInt32LE(bytes.length, 18);
- header.writeUInt32LE(bytes.length, 22);
- header.writeUInt16LE(key.length, 26);
- const directory = Buffer.alloc(46);
- directory.writeUInt32LE(0x02014b50);
- directory.writeUInt32LE(bytes.length, 20);
- directory.writeUInt32LE(bytes.length, 24);
- directory.writeUInt16LE(key.length, 28);
- directory.writeUInt32LE(0, 42);
- const central = Buffer.concat([directory, key]);
- const end = Buffer.alloc(22);
- end.writeUInt32LE(0x06054b50);
- end.writeUInt16LE(1, 8);
- end.writeUInt16LE(1, 10);
- end.writeUInt32LE(central.length, 12);
- end.writeUInt32LE(header.length + key.length + bytes.length, 16);
- return new Uint8Array(Buffer.concat([header, key, bytes, central, end]));
-}
-
-function deferred() {
- let resolve;
- const promise = new Promise((done) => (resolve = done));
- return { promise, resolve };
-}
-
const catalog = (version) => ({
products: [{ id: 'cip', version, defaultOn: true }],
});
diff --git a/src/layers/ghostMaps/pendingPaint.test.mjs b/src/layers/ghostMaps/pendingPaint.test.mjs
index be1b6a00e..d7d3bb898 100644
--- a/src/layers/ghostMaps/pendingPaint.test.mjs
+++ b/src/layers/ghostMaps/pendingPaint.test.mjs
@@ -1,26 +1,12 @@
+import {
+ element,
+ kmz,
+ deferred,
+} from '../../testSupport/ghostMapsFixtures.mjs';
import assert from 'node:assert/strict';
import test from 'node:test';
import { createGhostMapsLayer } from './index.js';
-function element(localName, children = [], value = '') {
- const node = {
- localName,
- children,
- get textContent() {
- return value + children.map((child) => child.textContent).join('');
- },
- getAttribute: () => null,
- getElementsByTagName(name) {
- return children.flatMap((child) => [
- ...(name === '*' || name === child.localName ? [child] : []),
- ...child.getElementsByTagName(name),
- ]);
- },
- };
- for (const child of children) child.parentElement = node;
- return node;
-}
-
// The XML double turns `NAME` into one placemark called NAME.
function documentFor(xml) {
const name = /(.*)<\/kml>/.exec(xml)?.[1] ?? 'unnamed';
@@ -41,37 +27,6 @@ function documentFor(xml) {
]);
}
-/** A stored (uncompressed) KMZ holding doc.kml only. */
-function kmz(name) {
- const key = Buffer.from('doc.kml');
- const bytes = Buffer.from(`${name}`);
- const header = Buffer.alloc(30);
- header.writeUInt32LE(0x04034b50);
- header.writeUInt32LE(bytes.length, 18);
- header.writeUInt32LE(bytes.length, 22);
- header.writeUInt16LE(key.length, 26);
- const directory = Buffer.alloc(46);
- directory.writeUInt32LE(0x02014b50);
- directory.writeUInt32LE(bytes.length, 20);
- directory.writeUInt32LE(bytes.length, 24);
- directory.writeUInt16LE(key.length, 28);
- directory.writeUInt32LE(0, 42);
- const central = Buffer.concat([directory, key]);
- const end = Buffer.alloc(22);
- end.writeUInt32LE(0x06054b50);
- end.writeUInt16LE(1, 8);
- end.writeUInt16LE(1, 10);
- end.writeUInt32LE(central.length, 12);
- end.writeUInt32LE(header.length + key.length + bytes.length, 16);
- return new Uint8Array(Buffer.concat([header, key, bytes, central, end]));
-}
-
-function deferred() {
- let resolve;
- const promise = new Promise((done) => (resolve = done));
- return { promise, resolve };
-}
-
const catalog = (version) => ({
products: [{ id: 'cip', version, defaultOn: true }],
});
diff --git a/src/layers/satellites/cockpitSubject.test.mjs b/src/layers/satellites/cockpitSubject.test.mjs
index 415482e4d..a4f636150 100644
--- a/src/layers/satellites/cockpitSubject.test.mjs
+++ b/src/layers/satellites/cockpitSubject.test.mjs
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
import test from 'node:test';
import * as geo from '../../geo/index.js';
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
-import { createSatellitesDisplay } from './render/display.js';
+import { satelliteServices } from '../../testSupport/satelliteServices.mjs';
import { constantRgba } from '../shared/color.js';
import { createState } from './state.js';
import { createControls } from './controls.js';
@@ -98,31 +98,9 @@ async function fixture(t) {
readSensors: async () => SENSORS,
};
const noop = () => {};
- const services = {
- renderer: { create: ({ engine }) => createSatellitesDisplay(engine) },
- overlays: {
- setEntries: noop,
- setVisible: noop,
- clearSource: noop,
- },
- layerState: { isExplicitLayerStateOrigin: () => false },
- focus: {
- clearFocusTarget: noop,
- publishFocusTargetFromCachedPosition: noop,
- getFocusTarget: () => null,
- focusNowMs: (now) => now,
- focusPassIsNeeded: () => false,
- },
- context: {
- clearTrackedSubjectContext: noop,
- getContextStore: () => ({ entities: new Map() }),
- refreshTrackedSubjectContext: noop,
- selectTrackedSubjectContext: noop,
- },
- readout: { refreshTrackedReadout: noop },
- render: { holdContinuousRender: noop, releaseContinuousRender: noop },
- picking: { registerPickOwner: noop, unregisterPickOwner: noop },
- };
+ const services = satelliteServices();
+ services.focus.focusNowMs = (now) => now;
+ services.focus.focusPassIsNeeded = () => false;
const previousDocument = globalThis.document;
globalThis.document = new EventTarget();
const state = createState({ services });
diff --git a/src/layers/satellites/stale.test.mjs b/src/layers/satellites/stale.test.mjs
index 4f7e91683..ac60d66a2 100644
--- a/src/layers/satellites/stale.test.mjs
+++ b/src/layers/satellites/stale.test.mjs
@@ -1,4 +1,4 @@
-import { createSatellitesDisplay } from './render/display.js';
+import { satelliteServices } from '../../testSupport/satelliteServices.mjs';
import assert from 'node:assert/strict';
import http from 'node:http';
import { EventEmitter, once } from 'node:events';
@@ -76,32 +76,7 @@ async function fixture(t) {
// Application host services and native EventTargets; the display on the
// fake engine, its input, events, SGP4, ingestion and lifecycle run without
// a WebGL scene.
- const noop = () => {};
- const services = {
- renderer: {
- create: ({ engine }) => createSatellitesDisplay(engine),
- },
- overlays: {
- setEntries: noop,
- setVisible: noop,
- clearSource: noop,
- },
- layerState: { isExplicitLayerStateOrigin: () => false },
- focus: {
- clearFocusTarget: noop,
- publishFocusTargetFromCachedPosition: noop,
- getFocusTarget: () => null,
- },
- context: {
- clearTrackedSubjectContext: noop,
- getContextStore: () => ({ entities: new Map() }),
- refreshTrackedSubjectContext: noop,
- selectTrackedSubjectContext: noop,
- },
- readout: { refreshTrackedReadout: noop },
- render: { holdContinuousRender: noop, releaseContinuousRender: noop },
- picking: { registerPickOwner: noop, unregisterPickOwner: noop },
- };
+ const services = satelliteServices();
const previousDocument = globalThis.document;
globalThis.document = new EventTarget();
const state = createState({ services });
diff --git a/src/sharelink.navigation.test.mjs b/src/sharelink.navigation.test.mjs
index 521b560d8..13cad1dcb 100644
--- a/src/sharelink.navigation.test.mjs
+++ b/src/sharelink.navigation.test.mjs
@@ -1,3 +1,4 @@
+import { installManualTimers } from './testSupport/manualTimers.mjs';
import { viewerEngine } from './testSupport/viewerEngine.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
@@ -72,23 +73,6 @@ function installTab(t, initialHash = '') {
};
}
-/** Queue timers instead of running them, so delayed work runs only on demand. */
-function installManualTimers(t) {
- const prior = {
- setTimeout: globalThis.setTimeout,
- clearTimeout: globalThis.clearTimeout,
- };
- const timers = new Map();
- let timerId = 0;
- globalThis.setTimeout = (fn) => {
- timers.set(++timerId, fn);
- return timerId;
- };
- globalThis.clearTimeout = (id) => timers.delete(id);
- t.after(() => Object.assign(globalThis, prior));
- return timers;
-}
-
function fakeViewer() {
return viewerEngine({
camera: {
@@ -175,7 +159,7 @@ test('a non-share hash is ignored and Back/Forward restores each entry once', (t
test('a newer navigation supersedes an older restore and its pending write', (t) => {
const tab = installTab(t, SHARE_A);
- const timers = installManualTimers(t);
+ const timers = installManualTimers(t).timers;
const manager = new ShareLinkManager(fakeViewer());
manager.completeInitialRestore(manager.parseInitialHash().restoreToken);
@@ -237,7 +221,7 @@ function deferred() {
/** A ShareRestoration over a real ShareLinkManager with a stub restore sink. */
function restorationHarness(t, initialHash, navigationOverrides = {}) {
const tab = installTab(t, initialHash);
- const timers = installManualTimers(t);
+ const timers = installManualTimers(t).timers;
const restores = [];
let navigationGeneration = 0;
@@ -427,7 +411,7 @@ test('the layer coordinator replaces in-flight share layers with the navigated s
test('a manager held for an engine-changing visit neither stamps nor restores that entry', (t) => {
const tab = installTab(t, SHARE_A);
- const timers = installManualTimers(t);
+ const timers = installManualTimers(t).timers;
const manager = new ShareLinkManager(fakeViewer());
manager.completeInitialRestore(manager.parseInitialHash().restoreToken);
// The page's own popstate listener runs first (src/main.js): the visit
diff --git a/src/testSupport/environment.mjs b/src/testSupport/environment.mjs
new file mode 100644
index 000000000..481821265
--- /dev/null
+++ b/src/testSupport/environment.mjs
@@ -0,0 +1,11 @@
+export function withEnv(t, env) {
+ for (const [name, value] of Object.entries(env)) {
+ const previous = process.env[name];
+ t.after(() => {
+ if (previous === undefined) delete process.env[name];
+ else process.env[name] = previous;
+ });
+ if (value === undefined) delete process.env[name];
+ else process.env[name] = value;
+ }
+}
diff --git a/src/testSupport/ghostMapsFixtures.mjs b/src/testSupport/ghostMapsFixtures.mjs
new file mode 100644
index 000000000..8e30b8291
--- /dev/null
+++ b/src/testSupport/ghostMapsFixtures.mjs
@@ -0,0 +1,49 @@
+export function element(localName, children = [], value = '') {
+ const node = {
+ localName,
+ children,
+ get textContent() {
+ return value + children.map((child) => child.textContent).join('');
+ },
+ getAttribute: () => null,
+ getElementsByTagName(name) {
+ return children.flatMap((child) => [
+ ...(name === '*' || name === child.localName ? [child] : []),
+ ...child.getElementsByTagName(name),
+ ]);
+ },
+ };
+ for (const child of children) child.parentElement = node;
+ return node;
+}
+
+/** A stored (uncompressed) KMZ holding doc.kml only. */
+export function kmz(name) {
+ const key = Buffer.from('doc.kml');
+ const bytes = Buffer.from(`${name}`);
+ const header = Buffer.alloc(30);
+ header.writeUInt32LE(0x04034b50);
+ header.writeUInt32LE(bytes.length, 18);
+ header.writeUInt32LE(bytes.length, 22);
+ header.writeUInt16LE(key.length, 26);
+ const directory = Buffer.alloc(46);
+ directory.writeUInt32LE(0x02014b50);
+ directory.writeUInt32LE(bytes.length, 20);
+ directory.writeUInt32LE(bytes.length, 24);
+ directory.writeUInt16LE(key.length, 28);
+ directory.writeUInt32LE(0, 42);
+ const central = Buffer.concat([directory, key]);
+ const end = Buffer.alloc(22);
+ end.writeUInt32LE(0x06054b50);
+ end.writeUInt16LE(1, 8);
+ end.writeUInt16LE(1, 10);
+ end.writeUInt32LE(central.length, 12);
+ end.writeUInt32LE(header.length + key.length + bytes.length, 16);
+ return new Uint8Array(Buffer.concat([header, key, bytes, central, end]));
+}
+
+export function deferred() {
+ let resolve;
+ const promise = new Promise((done) => (resolve = done));
+ return { promise, resolve };
+}
diff --git a/src/testSupport/manualTimers.mjs b/src/testSupport/manualTimers.mjs
new file mode 100644
index 000000000..826c6545d
--- /dev/null
+++ b/src/testSupport/manualTimers.mjs
@@ -0,0 +1,24 @@
+/** Queue timers instead of running them, so delayed work runs only on demand. */
+export function installManualTimers(t) {
+ const prior = {
+ setTimeout: globalThis.setTimeout,
+ clearTimeout: globalThis.clearTimeout,
+ };
+ const timers = new Map();
+ let timerId = 0;
+ globalThis.setTimeout = (fn) => {
+ timers.set(++timerId, fn);
+ return timerId;
+ };
+ globalThis.clearTimeout = (id) => timers.delete(id);
+ t.after(() => Object.assign(globalThis, prior));
+ return {
+ timers,
+ runAll() {
+ for (const [id, fn] of [...timers]) {
+ timers.delete(id);
+ fn();
+ }
+ },
+ };
+}
diff --git a/src/testSupport/satelliteServices.mjs b/src/testSupport/satelliteServices.mjs
new file mode 100644
index 000000000..dd29d4f0c
--- /dev/null
+++ b/src/testSupport/satelliteServices.mjs
@@ -0,0 +1,30 @@
+import { createSatellitesDisplay } from '../layers/satellites/render/display.js';
+
+export function satelliteServices() {
+ const noop = () => {};
+ return {
+ renderer: {
+ create: ({ engine }) => createSatellitesDisplay(engine),
+ },
+ overlays: {
+ setEntries: noop,
+ setVisible: noop,
+ clearSource: noop,
+ },
+ layerState: { isExplicitLayerStateOrigin: () => false },
+ focus: {
+ clearFocusTarget: noop,
+ publishFocusTargetFromCachedPosition: noop,
+ getFocusTarget: () => null,
+ },
+ context: {
+ clearTrackedSubjectContext: noop,
+ getContextStore: () => ({ entities: new Map() }),
+ refreshTrackedSubjectContext: noop,
+ selectTrackedSubjectContext: noop,
+ },
+ readout: { refreshTrackedReadout: noop },
+ render: { holdContinuousRender: noop, releaseContinuousRender: noop },
+ picking: { registerPickOwner: noop, unregisterPickOwner: noop },
+ };
+}
diff --git a/src/testSupport/streamingResponse.mjs b/src/testSupport/streamingResponse.mjs
new file mode 100644
index 000000000..a431ed1b4
--- /dev/null
+++ b/src/testSupport/streamingResponse.mjs
@@ -0,0 +1,17 @@
+/**
+ * A response whose body is a live stream, plus a flag that flips when the
+ * stream is cancelled. A rejection path that returns without cancelling holds
+ * the transport open, so the flag is what the refusal tests actually assert.
+ */
+export const streamingResponse = (init = {}) => {
+ const state = { cancelled: false };
+ const body = new ReadableStream({
+ start(controller) {
+ controller.enqueue(new TextEncoder().encode('['));
+ },
+ cancel() {
+ state.cancelled = true;
+ },
+ });
+ return { response: new Response(body, init), state };
+};
diff --git a/src/ui/shareLinks.test.mjs b/src/ui/shareLinks.test.mjs
index 1d27393f5..1208c69d6 100644
--- a/src/ui/shareLinks.test.mjs
+++ b/src/ui/shareLinks.test.mjs
@@ -1,3 +1,4 @@
+import { installManualTimers } from '../testSupport/manualTimers.mjs';
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { createCesiumMoversEngine } from '../testSupport/cesiumMoversEngine.mjs';
import { enuOffset } from '../globe/follow.js';
@@ -77,30 +78,6 @@ function installPage(t, href) {
return { location, writes, target };
}
-function installManualTimers(t) {
- const prior = {
- setTimeout: globalThis.setTimeout,
- clearTimeout: globalThis.clearTimeout,
- };
- const timers = new Map();
- let timerId = 0;
- globalThis.setTimeout = (fn) => {
- timers.set(++timerId, fn);
- return timerId;
- };
- globalThis.clearTimeout = (id) => timers.delete(id);
- t.after(() => Object.assign(globalThis, prior));
- return {
- timers,
- runAll() {
- for (const [id, fn] of [...timers]) {
- timers.delete(id);
- fn();
- }
- },
- };
-}
-
/** A viewer whose camera flights land at once. */
function fakeViewer() {
const flights = [];
diff --git a/src/ui/views/intelHudView.test.mjs b/src/ui/views/intelHudView.test.mjs
index 5ffea4981..996834078 100644
--- a/src/ui/views/intelHudView.test.mjs
+++ b/src/ui/views/intelHudView.test.mjs
@@ -1,3 +1,4 @@
+import '../../testSupport/stubMgrs.mjs';
import { viewerEngine } from '../../testSupport/viewerEngine.mjs';
// src/ui/views/intelHudView.test.mjs — the Intel HUD view renders what
// IntelHUD publishes: its readouts, visibility, layout variant and colors.
@@ -10,30 +11,10 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { createApp, nextTick } from 'vue';
import { createPinia } from 'pinia';
-import { registerHooks } from 'node:module';
import { presentSelectedVessel } from '../../hudReadouts.js';
import { useHudStore } from '../stores/hud.js';
import IntelHudView from './IntelHudView.vue';
-const MGRS_STUB_URL = 'gev-test-stub:mgrs';
-registerHooks({
- resolve(specifier, context, next) {
- if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
- return next(specifier, context);
- },
- load(url, context, next) {
- if (url === MGRS_STUB_URL) {
- return {
- format: 'module',
- shortCircuit: true,
- source:
- 'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
- };
- }
- return next(url, context);
- },
-});
-
const { IntelHUD } = await import('../../hud.js');
const listeners = () => ({ addEventListener() {}, removeEventListener() {} });