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() {} });