Merge branch 'german/refactor-e12b-fake-engine-tests' into german/navara-refactor

This commit is contained in:
German Khokhlov committed 2026-11-10 17:47:29 +04:00
commit 8caff876cb
76 files changed
+668 -564

No files matched your search

+2 -1
View File
@@ -4,6 +4,7 @@
"fixedSleeps": 69,
"loadingScreenReadiness": 0,
"cesiumOnlyReads": 47,
"ownWaitHelpers": 16
"ownWaitHelpers": 16,
"testEngineLiterals": 2
}
}
+80 -1
View File
@@ -2,17 +2,19 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import {
QA_HARNESS_METRICS,
engineLiteralLines,
harnessGrowth,
measureQaHarness,
readBaseline,
readQaSources,
readTestSources,
} from './report-qa-harness.mjs';
test('QA harness duplication does not grow past the baseline', () => {
assert.deepEqual(
harnessGrowth(measureQaHarness(), readBaseline().metrics),
[],
'QA harness duplication grew; use scripts/qa-strict-harness.mjs helpers for browser launch, engine readiness and camera waits instead.',
'QA duplication grew; use the shared browser harness and src/globe/testing/fakeEngine.js for test engines.',
);
});
@@ -43,6 +45,83 @@ test('QA sources include the harness and exclude tests and other reports', () =>
assert.ok(!names.includes('scripts/report-qa-harness.mjs'));
});
test('test sources include nested tests and exclude runtime files', () => {
const names = readTestSources().map(({ file }) => file);
assert.ok(names.includes('src/globe/testing/fakeEngine.test.mjs'));
assert.ok(names.includes('scripts/qaHarness.test.mjs'));
assert.ok(names.every((name) => name.endsWith('.test.mjs')));
});
test('engine literals include assignments, inline options and factory returns', () => {
for (const source of [
'const engine = {};',
'let engine; engine = { camera: {} };',
'scene.engine = { host: {} };',
"scene['engine'] = {};",
'init({ engine: {} });',
'const state = { _engine: {} };',
'const engineB = {}, trafficEngine = {};',
"function createEngine() { return { kind: 'cesium' }; }",
'const engine = () => ({ camera: {} });',
'const fakeEngine = () => { return { host: {} }; };',
'const factory = { createEngine() { return { render: {} }; } };',
'inspect({ host: { size() {} }, camera: {} });',
"const navara = { kind: 'navara', render: {} };",
])
assert.ok(engineLiteralLines(source).length > 0, source);
assert.deepEqual(
engineLiteralLines('const engine = {};\ninit({ engine: {} });'),
[1, 2],
);
});
test('engine literal counting ignores source text, options and harness observations', () => {
for (const source of [
'// const engine = {};',
"const source = 'const engine = {};';",
'const source = `init({ engine: {} });`;',
'const engine = createFakeEngine();',
'init({ engine: createFakeEngine() });',
'const options = { engineOptions: {} };',
'const engineOptions = { canvas: {}, width: 800 };',
"const headers = { host: 'localhost' };",
'const overlay = { host: { setEntries() {} } };',
'result[engine] = { camera: {} };',
'function observedEngine() { return { engine, camera }; }',
'function observedEngine() { return { snapshots: [] }; }',
'function observedEngine() { return records.map(() => ({ camera: {} })); }',
])
assert.deepEqual(engineLiteralLines(source), [], source);
});
test('engine literal metric counts test files once and rejects baseline growth', () => {
const metric = QA_HARNESS_METRICS.find(
({ id }) => id === 'testEngineLiterals',
);
const measured = metric.measure([
{
file: 'src/one.test.mjs',
text: 'const engine = {}; init({ engine: {} });',
},
{
file: 'scripts/two.test.mjs',
text: 'const engine = () => ({ host: {} });',
},
{ file: 'src/three.test.mjs', text: 'const engine = createFakeEngine();' },
{ file: 'src/runtime.js', text: 'const engine = {};' },
]);
assert.equal(measured, 2);
const baseline = Object.fromEntries(
QA_HARNESS_METRICS.map(({ id }) => [id, 0]),
);
assert.deepEqual(
harnessGrowth({ ...baseline, testEngineLiterals: measured }, baseline).map(
({ id }) => id,
),
['testEngineLiterals'],
);
});
test('metrics count scripts once and distinguish helpers from their callers', () => {
const measure = (id, ...sources) =>
QA_HARNESS_METRICS.find((metric) => metric.id === id).measure(
+106 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* Browser QA still doing work owned by scripts/qa-strict-harness.mjs.
* Each metric counts scripts, so repeated uses in one script count once.
* Browser QA and tests still duplicating the shared harness or fake engine.
* Each metric counts files, so repeated uses in one file count once.
*
* node scripts/report-qa-harness.mjs
* node scripts/report-qa-harness.mjs --json
@@ -10,6 +10,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { parsers } from 'prettier/plugins/babel.mjs';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
export const BASELINE_PATH = path.join(
@@ -18,6 +19,80 @@ export const BASELINE_PATH = path.join(
'qa-harness-baseline.json',
);
/** Engine assignments, inline options and engine factory returns, not strings or comments. */
export function engineLiteralLines(text) {
const lines = [];
const engineName = (name) =>
!/Options$/.test(name ?? '') &&
/^(?:_?engine(?:[A-Z0-9]\w*)?|\w+Engine(?:[A-Z0-9]\w*)?)$/.test(name ?? '');
function walk(node, parent, factory) {
if (!node || typeof node !== 'object') return;
if (/Function|ObjectMethod/.test(node.type))
factory = node.id?.name ?? parent?.id?.name ?? node.key?.name;
if (node.type === 'ObjectExpression') {
const name =
parent?.type === 'VariableDeclarator'
? parent.id?.name
: parent?.type === 'AssignmentExpression'
? parent.left?.computed
? parent.left.property?.value
: (parent.left?.property?.name ?? parent.left?.name)
: parent?.type === 'ObjectProperty'
? (parent.key?.name ?? parent.key?.value)
: null;
const keys = node.properties.map((property) => property.key?.name);
const host = node.properties.find(
(property) => property.key?.name === 'host',
)?.value;
const globeHost =
host?.type === 'ObjectExpression' &&
host.properties.some((property) =>
['canvas', 'container', 'size'].includes(property.key?.name),
);
const adapterKind = node.properties.some(
(property) =>
property.key?.name === 'kind' &&
['cesium', 'navara'].includes(property.value?.value),
);
const returned =
parent?.type === 'ReturnStatement' ||
parent?.type === 'ArrowFunctionExpression';
// A factory's { engine, ...observations } is a harness, not its engine.
if (
engineName(name) ||
globeHost ||
(adapterKind &&
keys.some((key) =>
['camera', 'render', 'credits', 'maps'].includes(key),
)) ||
(returned &&
engineName(factory) &&
!keys.includes('engine') &&
keys.some((key) =>
[
'kind',
'host',
'camera',
'frames',
'render',
'credits',
'maps',
].includes(key),
))
)
lines.push(node.loc.start.line);
}
for (const [key, value] of Object.entries(node)) {
if (['loc', 'comments', 'tokens'].includes(key)) continue;
if (Array.isArray(value))
for (const child of value) walk(child, node, factory);
else if (value && typeof value === 'object') walk(value, node, factory);
}
}
walk(parsers.babel.parse(text));
return lines;
}
const readsCesiumState = (text) => {
const names = [
'__godsEyeView',
@@ -78,6 +153,15 @@ export const QA_HARNESS_METRICS = Object.freeze([
),
).length,
},
{
id: 'testEngineLiterals',
label: 'Test files with their own engine literal',
measure: (files) =>
files.filter(
({ file, text }) =>
file.endsWith('.test.mjs') && engineLiteralLines(text).length,
).length,
},
]);
/** QA scripts, including the shared harness itself, but excluding tests. */
@@ -92,10 +176,29 @@ export function readQaSources(root = ROOT) {
}));
}
export function readTestSources(root = ROOT) {
return ['build', 'scripts', 'server', 'src'].flatMap((directory) => {
const base = path.join(root, directory);
if (!fs.existsSync(base)) return [];
return fs
.readdirSync(base, { recursive: true })
.sort()
.filter((name) => name.endsWith('.test.mjs'))
.map((name) => ({
file: `${directory}/${name.split(path.sep).join('/')}`,
text: fs.readFileSync(path.join(base, name), 'utf8'),
}));
});
}
export function measureQaHarness(root = ROOT) {
const files = readQaSources(root);
const tests = readTestSources(root);
return Object.fromEntries(
QA_HARNESS_METRICS.map(({ id, measure }) => [id, measure(files)]),
QA_HARNESS_METRICS.map(({ id, measure }) => [
id,
measure(id === 'testEngineLiterals' ? tests : files),
]),
);
}
+10 -3
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createAnnotationEngine } from './annotationEngine.js';
@@ -69,7 +70,7 @@ test('the marks survive a globe leaving and are drawn again on the next one', as
log.length = 0;
engine.attachRenderer({
engine: {},
engine: createFakeEngine(),
renderer: recordingRenderer(log, 'next'),
});
assert.deepEqual(log, ['next add Capitol']);
@@ -88,8 +89,14 @@ test('detaching twice and attaching over an attached globe release each drawing
});
engine.detachRenderer();
engine.detachRenderer();
engine.attachRenderer({ engine: {}, renderer: recordingRenderer(log, 'b') });
engine.attachRenderer({ engine: {}, renderer: recordingRenderer(log, 'c') });
engine.attachRenderer({
engine: createFakeEngine(),
renderer: recordingRenderer(log, 'b'),
});
engine.attachRenderer({
engine: createFakeEngine(),
renderer: recordingRenderer(log, 'c'),
});
assert.deepEqual(log, ['a destroy', 'b destroy']);
engine.destroy();
});
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// Behavioural tests for the draw tool: the module is driven against fake DOM
// and viewer doubles and its OBSERVABLE effects are asserted — which lease it
// holds, which viewer input actions it borrowed and gave back, what it leaves
@@ -975,7 +976,7 @@ test('a page without the Draw controls gets no draw tool', () => {
try {
let connected = false;
const tool = initDrawTool({
engine: {},
engine: createFakeEngine(),
annotations: fakeAnnotations(),
connect: () => {
connected = true;
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { installDomClasses } from '../testSupport/domClasses.mjs';
import { test } from 'node:test';
import assert from 'node:assert/strict';
@@ -127,10 +128,9 @@ function fakeViewer(lon, lat) {
},
};
// The Cesium engine's credits over the viewer's credit display.
const engine = {
credits: createCesiumCredits(viewer).credits,
render: { request: () => scene.requestRender() },
};
const engine = createFakeEngine();
engine.credits = createCesiumCredits(viewer).credits;
engine.render.request = () => scene.requestRender();
enginesOf.set(viewer, engine);
return { dataSources, frames, viewer, engine };
}
+30 -36
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
import * as geo from '../geo/index.js';
@@ -9,18 +10,11 @@ test('a depth pick on high terrain close to the camera is kept', () => {
const camera = geo.cartesian3.fromDegrees(86.9, 27.9, 4030);
const ground = geo.cartesian3.fromDegrees(86.9, 27.9, 4000);
const seaLevel = geo.cartesian3.fromDegrees(86.9, 27.9, 0);
const engine = {
host: { size: () => ({ width: 100, height: 100 }) },
picker: {
pickPositionSupported: true,
pickPosition: () => geo.cartesian3.clone(ground),
pickGlobe: () => geo.cartesian3.clone(ground),
},
camera: {
livePosition: camera,
pickEllipsoid: () => geo.cartesian3.clone(seaLevel),
},
};
const engine = createFakeEngine({ width: 100, height: 100 });
engine.fake.setPose({ position: camera });
engine.fake.script('pickPosition', () => geo.cartesian3.clone(ground));
engine.fake.script('pickGlobe', () => geo.cartesian3.clone(ground));
engine.fake.script('pickEllipsoid', () => geo.cartesian3.clone(seaLevel));
const picked = pickWorldFromScreen(engine, 0.5, 0.5);
assert.ok(
Math.abs(picked.height - 4000) < 1,
@@ -29,35 +23,35 @@ test('a depth pick on high terrain close to the camera is kept', () => {
});
test('a near-camera depth artifact falls back to terrain before sea level', () => {
const engine = {
host: { size: () => ({ width: 100, height: 100 }) },
picker: {
pickPositionSupported: true,
pickPosition: () => geo.cartesian3.fromDegrees(86.9, 27.9, 4029),
pickGlobe: () => geo.cartesian3.fromDegrees(86.9, 27.9, 4000),
},
camera: {
livePosition: geo.cartesian3.fromDegrees(86.9, 27.9, 4030),
pickEllipsoid: () => geo.cartesian3.fromDegrees(86.9, 27.9, 0),
},
};
const engine = createFakeEngine({ width: 100, height: 100 });
engine.fake.setPose({
position: geo.cartesian3.fromDegrees(86.9, 27.9, 4030),
});
engine.fake.script('pickPosition', () =>
geo.cartesian3.fromDegrees(86.9, 27.9, 4029),
);
engine.fake.script('pickGlobe', () =>
geo.cartesian3.fromDegrees(86.9, 27.9, 4000),
);
engine.fake.script('pickEllipsoid', () =>
geo.cartesian3.fromDegrees(86.9, 27.9, 0),
);
assert.ok(Math.abs(pickWorldFromScreen(engine, 0.5, 0.5).height - 4000) < 1);
});
test('a far camera still treats a near-plane depth hit as a miss', () => {
// 100 km up: a preview drawn at the near plane reads back 50 m away.
const camera = geo.cartesian3.fromDegrees(86.9, 27.9, 100_000);
const engine = {
host: { size: () => ({ width: 100, height: 100 }) },
picker: {
pickPositionSupported: true,
pickPosition: () => geo.cartesian3.fromDegrees(86.9, 27.9, 99_950),
pickGlobe: () => geo.cartesian3.fromDegrees(86.9, 27.9, 4000),
},
camera: {
livePosition: camera,
pickEllipsoid: () => geo.cartesian3.fromDegrees(86.9, 27.9, 0),
},
};
const engine = createFakeEngine({ width: 100, height: 100 });
engine.fake.setPose({ position: camera });
engine.fake.script('pickPosition', () =>
geo.cartesian3.fromDegrees(86.9, 27.9, 99_950),
);
engine.fake.script('pickGlobe', () =>
geo.cartesian3.fromDegrees(86.9, 27.9, 4000),
);
engine.fake.script('pickEllipsoid', () =>
geo.cartesian3.fromDegrees(86.9, 27.9, 0),
);
assert.ok(Math.abs(pickWorldFromScreen(engine, 0.5, 0.5).height - 4000) < 1);
});
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
@@ -111,7 +112,7 @@ test('startup shares one promise and teardown keeps data alive for controls', as
test('later components receive the scene engine beside its viewer', async () => {
const viewer = { name: 'viewer' };
const engine = { kind: 'cesium' };
const engine = createFakeEngine({ kind: 'cesium' });
const seen = [];
const receive =
(phase) =>
+3 -2
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// A candidate engine that fails after the shell attached to it (a later
// part: data, Cockpit clouds) must leave the shell detached, so the working
// engine attaches again.
@@ -29,7 +30,7 @@ test('a candidate failing in a later part leaves the shell ready for the working
const cesium = {
kind: 'cesium',
viewer: null,
engine: { kind: 'cesium' },
engine: createFakeEngine({ kind: 'cesium' }),
catalog,
};
await sceneHost.attach(cesium);
@@ -49,7 +50,7 @@ test('a candidate failing in a later part leaves the shell ready for the working
const navara = {
kind: 'navara',
viewer: null,
engine: { kind: 'navara' },
engine: createFakeEngine({ kind: 'navara' }),
catalog,
};
await assert.rejects(sceneHost.attach(navara), /layer attachment failed/);
+7 -8
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
import {
@@ -12,15 +13,13 @@ import { createView } from '../view/index.js';
const fakeEngine = () => {
const flights = [];
return {
flights,
camera: {
flyTo(options) {
flights.push(options);
options.complete();
},
},
const engine = createFakeEngine();
engine.flights = flights;
engine.camera.flyTo = (options) => {
flights.push(options);
options.complete();
};
return engine;
};
const fakeLayers = (enabled) => ({
getAll: () =>
+4 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
@@ -23,7 +24,9 @@ test('the scene engine is built on the viewer and destroyed by the scene lifetim
defer: (dispose) => cleanups.push(dispose),
createEngine(target, options) {
calls.push([target, options]);
return { kind: 'cesium', destroy: () => calls.push('destroy') };
const engine = createFakeEngine();
engine.destroy = () => calls.push('destroy');
return engine;
},
});
assert.equal(engine.kind, 'cesium');
+4 -3
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
@@ -73,6 +74,8 @@ function boot(
};
},
createStandaloneApplication: ({ engineKind }) => {
const engine = createFakeEngine({ kind: engineKind });
engine.camera.completeFlight = () => flushes.push('flight');
const styleManager = {
initialRestorePromise,
shareLinkManager: { flush: () => flushes.push('share link') },
@@ -92,9 +95,7 @@ function boot(
}),
getComponents: () => ({
scene: {
engine: {
camera: { completeFlight: () => flushes.push('flight') },
},
engine: engine,
},
controls: { styleManager },
}),
+2 -4
View File
@@ -42,10 +42,8 @@ test('Navara draws the page session board and its switch takes only the drawing'
const lifetime = new AbortController();
const layerSession = { lifetime };
const { views, meshes } = fakeViews();
const engine = {
...createFakeEngine({ kind: 'navara' }),
extensions: { navara: views },
};
const engine = createFakeEngine({ kind: 'navara' });
engine.extensions = { navara: views };
// As src/app/tools.js attaches the board on the backend's scene.
const attach = () => {
const cleanups = [];
+3 -12
View File
@@ -1,21 +1,12 @@
import { createFakeEngine } from './globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { CelestialRing } from './celestialRing.js';
test('full globe focus uses the engine without a Cesium global', () => {
let flight;
const engine = {
host: { canvas: { clientWidth: 1200, clientHeight: 800 } },
render: { request() {} },
camera: {
pose: () => ({
cartographic: { longitude: 0, latitude: 0 },
heading: 0,
fovy: Math.PI / 3,
}),
flyTo: (options) => { flight = options; },
},
};
const engine = createFakeEngine({ canvas: { clientWidth: 1200, clientHeight: 800 }, fovy: Math.PI / 3 });
engine.camera.flyTo = (options) => { flight = options; };
const ring = Object.assign(Object.create(CelestialRing.prototype), {
_engine: engine,
});
+11 -20
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
@@ -3142,11 +3143,8 @@ test('Bhote Koshi republishes its evidence cards to the incoming engine overlay'
globalThis.window = { setTimeout: () => 1, clearTimeout() {} };
const first = eventViewer();
const second = eventViewer();
const engineB = {
id: 'B',
host: { canvas: second.scene.canvas },
camera: second.camera,
};
const engineB = createFakeEngine({ canvas: second.scene.canvas });
engineB.id = 'B';
const hosts = new Map();
const hostFor = (engine) => {
const key = engine ?? 'A';
@@ -3216,16 +3214,11 @@ async function navaraEvidenceCard({ sample, signal }) {
installEventDom();
globalThis.window = { setTimeout: () => 1, clearTimeout() {} };
const entries = [];
const engine = {
terrain: 3500,
host: { canvas: eventViewer().scene.canvas },
surface: {
terrainHeight: () => engine.terrain,
sampleTerrainMostDetailed: sample,
},
maps: { terrainRevision: () => 1 },
camera: { livePosition: Cesium.Cartesian3.fromDegrees(0, 0, 1e7) },
};
const engine = createFakeEngine({ canvas: eventViewer().scene.canvas });
engine.terrain = 3500;
engine.fake.script('terrainHeight', () => engine.terrain);
engine.fake.script('sampleTerrainMostDetailed', sample);
engine.maps.terrainRevision = () => 1;
const layer = buildEventLayer({
eventLoader: async () => event,
imageLoader: async (url) => ({ url }),
@@ -3330,11 +3323,9 @@ test('Navara evidence anchors without a terrain sample follow the terrain near t
assert.ok(Math.abs(height() - pack) < 0.01, 'the pack elevation');
card.position();
assert.ok(Math.abs(height() - pack) < 0.01, 'a far camera keeps it');
engine.camera.livePosition = Cesium.Cartesian3.fromDegrees(
first.lon,
first.lat,
6000,
);
engine.fake.setPose({
position: Cesium.Cartesian3.fromDegrees(first.lon, first.lat, 6000),
});
engine.terrain = 2000;
assert.ok(card.position(), 'the first card is shown');
assert.ok(
+4 -10
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// Edge cases for bhote-koshi-locator: a boundary that resolves late, after its
// request was cancelled, does not change the live scene; both engines show the
// same area and the same target.
@@ -42,11 +43,8 @@ function locator(engine) {
remove: (source) => shown.delete(source),
},
};
const navara = {
kind: 'navara',
render: { request: (reason) => renders.push(reason) },
credits: { add() {}, remove() {} },
};
const navara = createFakeEngine({ kind: 'navara' });
navara.render.request = (reason) => renders.push(reason);
const layer = createBhoteKoshiLocatorLayer({
boundaryResolver: (signal) => {
const lookup = deferred();
@@ -176,11 +174,7 @@ test('edge:bhote-koshi-locator: an engine swap while the boundary resolves lands
const enabling = layer.enable(viewer);
layer.detachRenderer();
layer.attachRenderer({
engine: {
kind: 'navara',
render: { request() {} },
credits: { add() {}, remove() {} },
},
engine: createFakeEngine({ kind: 'navara' }),
});
lookup.resolve({ ring: LIVE_RING });
assert.equal(await enabling, true);
+3 -3
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
@@ -94,10 +95,9 @@ test('OpenStreetMap has one generic data credit with separate tile and names dis
// A Cesium engine's credits over a viewer double's credit display.
async function cesiumCreditsEngine(viewer) {
const { createCesiumCredits } = await import('../globe/cesium/credits.js');
return {
return Object.assign(createFakeEngine(), {
credits: createCesiumCredits(viewer).credits,
render: { request() {} },
};
});
}
test('inline OSM attribution persists until its last display owner leaves', async (t) => {
+12 -8
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { LayerLifecycle } from './lifecycle.js';
@@ -46,7 +47,7 @@ function catalog(...modules) {
};
}
const engine = (name) => ({ kind: 'test', name });
const engine = (name) => Object.assign(createFakeEngine({ kind: 'test' }), { name });
const firstViewer = { name: 'first viewer' };
const secondViewer = { name: 'second viewer' };
@@ -86,6 +87,7 @@ const detaches = (calls) => calls.filter((call) => call[0] === 'detach');
const attaches = (calls) => calls.filter((call) => call[0] === 'attach');
test('one failing detachRenderer leaves the others released and the manager unbound', async () => {
const second = engine('second');
let failures = 1;
const broken = sceneLayer('broken', {
detach() {
@@ -122,16 +124,17 @@ test('one failing detachRenderer leaves the others released and the manager unbo
// The next scene binds and both retained layers attach to it.
await bindCatalog(
manager,
engine('second'),
second,
catalog(...modules),
);
assert.equal(manager.viewer, secondViewer);
assert.deepEqual(attaches(broken.calls).at(-1), ['attach', engine('second')]);
assert.deepEqual(attaches(after.calls).at(-1), ['attach', engine('second')]);
assert.deepEqual(attaches(broken.calls).at(-1), ['attach', second]);
assert.deepEqual(attaches(after.calls).at(-1), ['attach', second]);
assert.equal(manager.isEnabled('broken'), true);
});
test('bindCatalog after a failed detach retries the release before attaching', async () => {
const second = engine('second');
let failures = 1;
const broken = sceneLayer('broken', {
detach() {
@@ -146,13 +149,13 @@ test('bindCatalog after a failed detach retries the release before attaching', a
await bindCatalog(
manager,
engine('second'),
second,
catalog(...modules),
);
assert.deepEqual(broken.calls.slice(-3), [
['detach', firstViewer],
['detach', firstViewer],
['attach', engine('second')],
['attach', second],
]);
// The scene is settled: the next detach releases the second viewer once.
await manager.detachScene();
@@ -419,6 +422,7 @@ test('an explicit refresh between scenes ends false when its caller aborts', asy
});
test('a layer enabled between scenes initializes on the next scene', async () => {
const second = engine('second');
const late = sceneLayer('late');
const present = sceneLayer('present');
const { manager, modules } = await boundManager(present);
@@ -429,9 +433,9 @@ test('a layer enabled between scenes initializes on the next scene', async () =>
const enabled = manager.setEnabled('late', true, { origin: 'user' });
await tick();
assert.deepEqual(late.calls, [], 'no init against a missing scene');
await bindCatalog(manager, engine('second'), catalog(...all));
await bindCatalog(manager, second, catalog(...all));
assert.equal(await enabled, true);
assert.deepEqual(late.calls.slice(0, 2), ['init', ['attach', engine('second')]]);
assert.deepEqual(late.calls.slice(0, 2), ['init', ['attach', second]]);
});
test('a detach does not deadlock on an enable parked for the next scene', async () => {
+9 -8
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { readFileSync } from 'node:fs';
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import { test } from 'node:test';
@@ -77,14 +78,14 @@ const {
} = militaryAwarenessLayer;
const viewerEngines = new WeakMap();
const viewerEngine = (viewer) => {
if (!viewerEngines.has(viewer)) viewerEngines.set(viewer, {
kind: 'cesium', viewer,
render: {
request: () => viewer.scene.requestRender?.(),
setContinuous: (enabled) => { viewer.scene.requestRenderMode = !enabled; },
isContinuous: () => viewer.scene.requestRenderMode === false,
},
});
if (!viewerEngines.has(viewer)) {
const engine = createFakeEngine();
engine.viewer = viewer;
engine.render.request = () => viewer.scene.requestRender?.();
engine.render.setContinuous = (enabled) => { viewer.scene.requestRenderMode = !enabled; };
engine.render.isContinuous = () => viewer.scene.requestRenderMode === false;
viewerEngines.set(viewer, engine);
}
return viewerEngines.get(viewer);
};
+11 -14
View File
@@ -12,11 +12,8 @@ import { createFakeEngine } from '../globe/testing/fakeEngine.js';
/** A layer on the fake engine, whose imagery drapes where `surface()` says. */
function setup(overlay = OSM_OVERLAYS[0]) {
let surface = 'globe';
const fake = createFakeEngine({ kind: 'cesium' });
const engine = {
...fake,
imagery: { ...fake.imagery, surface: () => surface },
};
const engine = createFakeEngine({ kind: 'cesium' });
engine.imagery.surface = () => surface;
const layer = createOsmOverlayLayer(overlay);
layer.init(null, { engine });
return {
@@ -120,15 +117,15 @@ test('destroy and a renderer detach remove the owned imagery', () => {
test('on Navara an overlay drapes through engine.maps and follows the globe', () => {
const added = [];
const engineWith = () => ({
maps: {
addOverlay(options) {
const entry = { options, removed: false };
added.push(entry);
return () => (entry.removed = true);
},
},
});
const engineWith = () => {
const engine = createFakeEngine({ kind: 'navara' });
engine.maps.addOverlay = (options) => {
const entry = { options, removed: false };
added.push(entry);
return () => (entry.removed = true);
};
return engine;
};
const [seaMarks] = createOsmOverlayLayers();
seaMarks.init(null, { engine: engineWith() });
assert.equal(added.length, 0, 'nothing until enabled');
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
@@ -11,7 +12,7 @@ globalThis.CustomEvent ||= class CustomEvent {
globalThis.dispatchEvent ||= () => true;
const { registerEntityContext } = await import('./contextStore.js');
const engine = {};
const engine = createFakeEngine();
const { pickRegistryFor } = await import('./pickRegistry.js');
const { registerPickOwner, unregisterPickOwner, findPickOwner } = pickRegistryFor(engine);
const { resolvePickedContext, pickedKindForLayer } = await import('./pickedContext.js');
+3 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
import {
@@ -1638,7 +1639,8 @@ test('Radio recovery preserves closer altitude and caps extreme full-globe zoom'
test('Radio camera navigation yields to every live tracked-entity owner', () => {
let following = null;
const engine = { camera: { following: () => following } };
const engine = createFakeEngine();
engine.camera.following = () => following;
assert.equal(radioCameraNavigationAllowed(engine), true);
for (const gevTrackedId of ['flights:abc123', 'military:def456']) {
const trackedEntity = { gevTrackedId };
@@ -221,9 +221,11 @@ test('cable paths drape on exactly the active surface on every stack', () => {
// Boot fires no stack event; the first drape comes from the engine:
// photoreal tiles shown ⇔ the globe hidden.
const engine = (photoreal) => ({
maps: { photorealActive: () => photoreal },
});
const engine = (photoreal) => {
const globe = createFakeEngine();
globe.maps.photorealActive = () => photoreal;
return globe;
};
assert.equal(cableDrapeForEngine(engine(true)), 'tiles');
assert.equal(cableDrapeForEngine(engine(false)), 'terrain');
assert.equal(cableDrapeForEngine(null), 'both');
+7 -10
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
// src/data/trackedModelRegime.test.mjs
//
@@ -172,16 +173,12 @@ function viewerAtHeight(height, { tilesLoaded = true, sampleHeight = () => undef
trackedEntity: undefined,
entities: { remove() {} },
};
viewer.engine = {
isDestroyed: () => false,
surface: {
tilesLoaded: () => viewer.tileset.tilesLoaded === true,
sampleHeightDetailed(place, { exclude } = {}) {
const h = viewer.scene.sampleHeight(place, exclude);
return Number.isFinite(h) ? { height: h, source: 'tiles' } : undefined;
},
},
};
viewer.engine = createFakeEngine();
viewer.engine.fake.script('tilesLoaded', () => viewer.tileset.tilesLoaded === true);
viewer.engine.fake.script('sampleHeightDetailed', (place, { exclude } = {}) => {
const h = viewer.scene.sampleHeight(place, exclude);
return Number.isFinite(h) ? { height: h, source: 'tiles' } : undefined;
});
return viewer;
}
+3 -12
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createTrackedReadout } from './trackedReadout.js';
@@ -19,18 +20,8 @@ test('followed aircraft card anchors to the model the person sees (gevVisualPosi
position: () => native.gevDisplayPosition(),
},
};
const listeners = [];
const engine = {
camera: {
following: () => handle,
followChanged: {
addEventListener: (f) => {
listeners.push(f);
return () => {};
},
},
},
};
const engine = createFakeEngine();
engine.camera.following = () => handle;
let entries = null;
const overlay = {
setEntries(_id, e) {
+28 -57
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import { mkdtempSync, rmSync } from 'node:fs';
@@ -214,68 +215,38 @@ test('traffic timing pairs real ordering to the scheduling change and guards re-
const changed = eventChannel();
let longitude = -97.74;
const cartographic = { latitude: Math.PI / 6, longitude: 0, height: 5000 };
const camera = {
moveEnd,
changed,
percentageChanged: 0.5,
livePosition: { x: 0, y: 0, z: 0 },
pose: () => ({ cartographic: { ...cartographic }, heading: 0, pitch: -Math.PI / 2 }),
viewRectangle() {
const west = longitude * Math.PI / 180;
return {
south: 30 * Math.PI / 180,
west,
north: 30.02 * Math.PI / 180,
east: west + 0.02 * Math.PI / 180,
};
},
pickEllipsoid: () => undefined,
};
// A zero-size canvas has no reticle footprint; the view rectangle bounds the fetch.
const engine = createFakeEngine({
width: 0,
height: 0,
canvas: { clientWidth: 0, clientHeight: 0, width: 0, height: 0 },
});
engine.camera.moveEnd = moveEnd;
engine.camera.changed = changed;
engine.camera.pose = () => ({ cartographic: { ...cartographic }, heading: 0, pitch: -Math.PI / 2 });
engine.fake.script('viewRectangle', () => {
const west = longitude * Math.PI / 180;
return {
south: 30 * Math.PI / 180,
west,
north: 30.02 * Math.PI / 180,
east: west + 0.02 * Math.PI / 180,
};
});
engine.fake.script('pickEllipsoid', undefined);
engine.fake.script('worldToWindow', undefined);
engine.frames.preUpdate = preUpdate;
engine.frames.preRender = preRender;
engine.frames.postRender = postRender;
engine.projector.viewProjection = (result = new Array(16).fill(0)) => result;
engine.projector.horizonVisible = () => true;
engine.picker.pickPositionSupported = false;
engine.surface.supported = false;
const setLongitude = (next) => {
longitude = next;
cartographic.longitude = next * Math.PI / 180;
};
setLongitude(longitude);
const noSet = () => ({
set() {},
setCount() {},
commit() {},
replace() {},
show() {},
destroy() {},
});
// An engine on a zero-size canvas: no pick ray (no reticle footprint),
// so the fetch is bounded by the view rectangle.
const engine = {
kind: 'cesium',
host: {
canvas: { clientWidth: 0, clientHeight: 0, width: 0, height: 0 },
size: () => ({ width: 0, height: 0 }),
},
camera,
frames: { preUpdate, preRender, postRender },
projector: {
worldToWindow: () => undefined,
viewProjection: (result = new Array(16).fill(0)) => result,
horizonVisible: () => true,
},
picker: { pickPositionSupported: false },
surface: {
supported: false,
terrainHeight: () => 0,
sampleHeight: () => undefined,
tilesLoaded: () => true,
},
maps: {
photorealActive: () => false,
current: () => ({ baseImagery: null, photoreal: null }),
},
render: { request() {} },
credits: { addStatic: () => () => {} },
movers: { createSet: noSet },
ground: { createSet: noSet },
destroy() {},
};
performance.clearMarks();
performance.clearMeasures();
+8 -10
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
import {
@@ -42,16 +43,13 @@ function harness() {
},
};
const pickers = new Set();
const engine = {
render: { request: () => requests++ },
camera: { livePosition: { x: 0, y: 0, z: 0 }, pickRay: () => null },
picker: {
pickPosition: () => undefined,
registerMeshPicker(pick) {
pickers.add(pick);
return () => pickers.delete(pick);
},
},
const engine = createFakeEngine({ kind: 'navara' });
engine.render.request = () => requests++;
engine.camera.pickRay = () => null;
engine.fake.script('pickPosition', undefined);
engine.picker.registerMeshPicker = (pick) => {
pickers.add(pick);
return () => pickers.delete(pick);
};
const port = createNavaraMeshes(view, {
overlay,
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
import {
@@ -140,7 +141,7 @@ test('pack meshes draw in the overlay and test the scene depth', () => {
camera: {},
picker: { registerMeshPicker: () => () => {} },
});
const engine = { meshes: port.meshes, render: { request() {} } };
const engine = Object.assign(createFakeEngine(), { meshes: port.meshes });
createPackRenderer(engine).image(
{ id: 'p', placement: { bounds: [0, 0, 0.01, 0.01], height: 300 } },
{},
+4 -3
View File
@@ -1,8 +1,8 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
import { createAircraftSceneBinding } from './sceneBinding.js';
import { LayerBindings } from '../../ui/layerBindings.js';
import { createEngineEvent } from '../../globe/contract.js';
import { readLayerSource } from '../../testSupport/readLayerSource.mjs';
function observedShell(t) {
@@ -11,13 +11,14 @@ function observedShell(t) {
t.after(() => {
globalThis.window = previous;
});
const followChanged = createEngineEvent();
const engine = createFakeEngine();
const followChanged = engine.camera.followChanged;
const shell = {
generation: 1,
followChanged,
};
const bindings = new LayerBindings({
engine: { camera: { followChanged } },
engine: engine,
services: {},
operations: {
_stampNavigation() {
@@ -111,10 +111,8 @@ function viewerAt(height) {
trackedEntity: undefined,
entities: { remove() {} },
};
viewer.engine = {
isDestroyed: () => false,
surface: { tilesLoaded: () => true, sampleHeightDetailed: () => undefined },
};
viewer.engine = createFakeEngine();
viewer.engine.fake.script('sampleHeightDetailed', undefined);
return viewer;
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js';
@@ -50,7 +51,7 @@ function harness(layer, { groundM = null, track = [] } = {}) {
_trailPositions: [],
_trailBackfillToken: 0,
_renderer: {
engine: {},
engine: createFakeEngine(),
follow: { onChanged: () => () => {} },
},
};
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
// The ALPR paint lane must initialize in an insecure context (plain HTTP over
// the HOST=0.0.0.0 LAN opt-in), where crypto.randomUUID does not exist.
import { test } from 'node:test';
@@ -34,7 +35,7 @@ test('ALPR overlay init works without crypto.randomUUID', (t) => {
},
},
},
engine: {},
engine: createFakeEngine(),
markers: new Map(),
syncMarker() {},
sampleHeight() {},
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
// CCTV icon thinning must use the camera's real vertical FOV. In a 16:9
// window with Cesium's default fov 60deg the vertical FOV is about 36deg;
// main read it from viewer.camera.frustum.fovy. Two cameras 17 m apart seen
@@ -33,7 +34,7 @@ test('icon thinning uses the camera vertical FOV, not a pi/3 fallback', () => {
distance: () => 2000,
cameraPose: () => pose,
fovy: () => pose.fovy,
engine: { camera: { pose: () => pose } },
engine: createFakeEngine({ fovy: FOVY_16_9 }),
};
const { refreshHorizonCulling } = createRendering({
state,
+1 -1
View File
@@ -82,7 +82,7 @@ test('edge:earthquakes: an expired record is dropped and a duplicate never forks
FIXED_SAMPLE,
WITH_DUPLICATE,
]);
layer.init(null, { engine: {} });
layer.init(null, { engine: createFakeEngine() });
layer.enable();
assert.equal(await layer.update(), true);
assert.equal(await layer.update(), false);
+5 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createViewport } from './viewport.js';
@@ -22,11 +23,14 @@ function harness() {
const moveEnd = signal(),
preRender = signal(),
requests = [];
const engine = createFakeEngine();
engine.camera.moveEnd = moveEnd;
engine.frames.preRender = preRender;
const layerState = {
_enabled: true,
_destroyed: false,
_view: { camera: {} },
_renderer: { engine: { camera: { moveEnd }, frames: { preRender } } },
_renderer: { engine: engine },
};
const viewport = createViewport({
layerState,
+5 -4
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import {
element,
kmz,
@@ -74,7 +75,7 @@ function harness(t) {
let listener = null;
const layer = createGhostMapsLayer({
storage: null,
createRenderer: (engine) => fakeRenderer(engine.name, live),
createRenderer: (engine) => fakeRenderer(engine.kind, live),
source: {
getCatalog: async () => catalog('v1'),
getFile: (_id, version) => files.get(version).promise,
@@ -84,7 +85,7 @@ function harness(t) {
},
},
});
layer.init(null, { engine: { name: 'cesium' } });
layer.init(null, { engine: createFakeEngine({ kind: 'cesium' }) });
layer.enable();
t.after(() => layer.destroy());
const file = (version) => {
@@ -113,7 +114,7 @@ test('edge:s2-ghostmaps: an edition and engine change mid-parse shows only the s
await new Promise(setImmediate);
push(catalog('v3'));
layer.detachRenderer();
await layer.attachRenderer({ engine: { name: 'navara' } });
await layer.attachRenderer({ engine: createFakeEngine({ kind: 'navara' }) });
v2.resolve(kmz('edition two'));
v3.resolve(kmz('edition three'));
// Let the queued syncs settle.
@@ -135,7 +136,7 @@ test('edge:s2-ghostmaps: a bad archive keeps the working edition on the globe',
push(catalog('v2'));
// The engine changes while the broken file is still on its way.
layer.detachRenderer();
await layer.attachRenderer({ engine: { name: 'navara' } });
await layer.attachRenderer({ engine: createFakeEngine({ kind: 'navara' }) });
broken.resolve(new Uint8Array([1, 2, 3, 4]));
for (let i = 0; i < 20; i += 1) await new Promise(setImmediate);
+7 -4
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import {
element,
kmz,
@@ -58,10 +59,10 @@ for (const action of ['expire', 'disable']) {
onClick: () => () => {},
draw(id, features) {
const gate = deferred();
if (engine.name === 'navara') gates.push(gate);
if (engine.kind === 'navara') gates.push(gate);
else gate.resolve();
const paint = {
engine: engine.name,
engine: engine.kind,
names: [...features.values()].map((feature) => feature.name),
dropped: [],
ready: gate.promise,
@@ -84,7 +85,7 @@ for (const action of ['expire', 'disable']) {
getFile: async () => kmz('old event'),
},
});
layer.init(null, { engine: { name: 'cesium' } });
layer.init(null, { engine: createFakeEngine({ kind: 'cesium' }) });
layer.enable();
t.after(() => layer.destroy());
await layer.update();
@@ -92,7 +93,9 @@ for (const action of ['expire', 'disable']) {
// Engine switch: the new drawing is still decoding its icons.
layer.detachRenderer();
const attached = layer.attachRenderer({ engine: { name: 'navara' } });
const attached = layer.attachRenderer({
engine: createFakeEngine({ kind: 'navara' }),
});
await new Promise(setImmediate);
assert.equal(gates.length, 1);
// The event leaves its age window; the expiry pass runs meanwhile.
+22 -25
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import { createGnssDisplay } from './render/display.js';
import assert from 'node:assert/strict';
import test from 'node:test';
@@ -82,18 +83,15 @@ const threeHealthy = () => [
const flush = () => new Promise((resolve) => setImmediate(resolve));
test('a globe-wide rectangle uses the visible screen centre as the query anchor', () => {
const anchor = defaultGnssViewAnchor({
host: { size: () => ({ width: 1000, height: 800 }) },
camera: {
pickEllipsoid(pixel) {
assert.deepEqual(pixel, { x: 500, y: 400 });
return geo.cartesian3.fromDegrees(-97, 30);
},
viewRectangle: () => {
throw new Error('centre hit must not compute a rectangle');
},
},
const engine = createFakeEngine({ width: 1000, height: 800 });
engine.fake.script('pickEllipsoid', (pixel) => {
assert.deepEqual(pixel, { x: 500, y: 400 });
return geo.cartesian3.fromDegrees(-97, 30);
});
engine.fake.script('viewRectangle', () => {
throw new Error('centre hit must not compute a rectangle');
});
const anchor = defaultGnssViewAnchor(engine);
assert.ok(Math.abs(anchor.latitude - 30) < 1e-8);
assert.ok(Math.abs(anchor.longitude + 97) < 1e-8);
});
@@ -189,7 +187,10 @@ test('movement during a pending refresh follows up at the newest view; disable c
test('the layer requires a snapshot source and cannot be initialized twice', () => {
assert.throws(() => createGnssIntegrityLayer({}), TypeError);
const { layer } = harness(async () => ({ rows: [] }));
assert.throws(() => layer.init(null, { engine: {} }), /already/);
assert.throws(
() => layer.init(null, { engine: createFakeEngine() }),
/already/,
);
});
test('an enabled refresh bins the snapshot into coloured ground cells', async () => {
@@ -694,19 +695,15 @@ test('the cells stay out of picks', async () => {
test('view anchor falls back to rectangle, then nadir, then no camera', () => {
assert.equal(defaultGnssViewAnchor({}), null);
const radians = geo.math.toRadians;
const engine = {
host: { size: () => ({ width: 1000, height: 800 }) },
camera: {
pickEllipsoid: () => undefined,
viewRectangle: () => ({
west: radians(20),
south: radians(40),
east: radians(22),
north: radians(42),
}),
pose: () => assert.fail('a rectangle must not read the pose'),
},
};
const engine = createFakeEngine({ width: 1000, height: 800 });
engine.fake.script('pickEllipsoid', undefined);
engine.fake.script('viewRectangle', {
west: radians(20),
south: radians(40),
east: radians(22),
north: radians(42),
});
engine.camera.pose = () => assert.fail('a rectangle must not read the pose');
const rectangleAnchor = defaultGnssViewAnchor(engine);
assert.ok(Math.abs(rectangleAnchor.latitude - 41) < 1e-10);
assert.ok(Math.abs(rectangleAnchor.longitude - 21) < 1e-10);
+5 -5
View File
@@ -33,7 +33,7 @@ test('a bundled file delivered between planets keeps multipart IDs and is read o
lod() {},
}),
);
layer.init(null, { engine: {} });
layer.init(null, { engine: createFakeEngine() });
const enabled = layer.enable();
await new Promise(setImmediate);
layer.detachRenderer();
@@ -57,9 +57,9 @@ test('a bundled file delivered between planets keeps multipart IDs and is read o
await enabled;
assert.equal(layer.getAnalystRecords().length, 2);
assert.equal(captures.length, 0);
await layer.attachRenderer({ engine: {} });
await layer.attachRenderer({ engine: createFakeEngine() });
layer.detachRenderer();
await layer.attachRenderer({ engine: {} });
await layer.attachRenderer({ engine: createFakeEngine() });
assert.equal(captures[0], captures[1]);
assert.deepEqual(
captures[0].records.map((r) => r.id),
@@ -89,13 +89,13 @@ test('a released scene stops waiting for a stalled dataset load, which stays in
lod() {},
}),
);
layer.init(null, { engine: {} });
layer.init(null, { engine: createFakeEngine() });
const enabled = layer.enable();
await new Promise(setImmediate);
layer.detachRenderer();
const scene = new AbortController();
const attaching = layer.attachRenderer({
engine: {},
engine: createFakeEngine(),
signal: scene.signal,
});
scene.abort();
+6 -7
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js';
@@ -26,14 +27,12 @@ test('orbital ground focus reads the drawn height and the engine pick id', () =>
test('focus frames a selected installation through the globe engine camera', () => {
const record = { id: 'osm:node:12', latitude: 70, longitude: 179.8 };
let flight = null;
const engine = createFakeEngine();
engine.camera.flyToSphere = (sphere, options) => {
flight = { sphere, options };
};
const state = {
engine: {
camera: {
flyToSphere: (sphere, options) => {
flight = { sphere, options };
},
},
},
engine: engine,
recordById: new Map([[record.id, record]]),
};
const controls = createControls({
+11 -10
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
@@ -162,10 +163,12 @@ test('an aborting real ingestion cannot clear the next scheduled view key', asyn
test('Navara installation ingestion loads from its engine camera without a viewer', async () => {
const { createIngestion } = await import('./ingestion.js');
const rectangle = Cesium.Rectangle.fromDegrees(-123, 37, -122, 38);
const engine = createFakeEngine({ kind: 'navara' });
engine.fake.script('viewRectangle', () => rectangle);
const state = {
enabled: true,
viewer: null,
engine: { camera: { viewRectangle: () => rectangle } },
engine: engine,
records: [],
recordById: new Map(),
coverage: { kind: 'viewport' },
@@ -242,18 +245,16 @@ test('the engine camera drives installation viewport requests without a viewer',
east: Cesium.Math.toRadians(-178),
north: Cesium.Math.toRadians(16),
};
const engine = createFakeEngine();
engine.fake.script('viewRectangle', () => rectangle);
engine.camera.pose = () => ({
position: { x: 1, y: 2, z: 3 },
direction: { x: -1, y: 0, z: 0 },
});
const state = {
enabled: true,
viewer: null,
engine: {
camera: {
viewRectangle: () => rectangle,
pose: () => ({
position: { x: 1, y: 2, z: 3 },
direction: { x: -1, y: 0, z: 0 },
}),
},
},
engine: engine,
};
const loads = [];
const parts = {
+4 -6
View File
@@ -1,15 +1,13 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createEngineEvent } from '../../globe/contract.js';
import { createLaunchHover } from './render/display.js';
// What the hover reads of an engine: its canvas, drillPick and camera motion.
function hoverEngine(drillPick, style = { cursor: '' }) {
return {
host: { canvas: { style } },
picker: { drillPick },
camera: { moveStart: createEngineEvent(), moveEnd: createEngineEvent() },
};
const engine = createFakeEngine({ canvas: { style } });
engine.fake.script('drillPick', drillPick);
return engine;
}
function fakeTimers() {
+6 -6
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js';
@@ -10,12 +11,11 @@ test('selected pad frame uses neutral geometry and the physical orbit epoch', ()
epoch;
const state = {
orbitNow: worldTimeFor('satellites'),
_engine: {
camera: {
livePosition: geo.cartesian3.fromDegrees(launch.lon, launch.lat, 500),
height: () => 500,
},
},
_engine: createFakeEngine({
longitude: geo.math.toRadians(launch.lon),
latitude: geo.math.toRadians(launch.lat),
altitude: 500,
}),
_enabled: true,
_selectedLaunchId: 'qa',
_launches: [launch],
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createLifecycle } from './lifecycle.js';
@@ -8,7 +9,7 @@ test('missions preserve the satellite target before their focus flight releases
let target = 25544,
snapshot = null;
const lifecycle = createLifecycle({
state: { _engine: {} },
state: { _engine: createFakeEngine() },
services: { render: {} },
getRenderer: () => ({ setVisible() {} }),
parts: {
@@ -46,7 +47,7 @@ test('missions begin their entry flight while the satellite catalog loads', asyn
});
let flew = false;
const lifecycle = createLifecycle({
state: { _engine: {} },
state: { _engine: createFakeEngine() },
services: { render: {} },
getRenderer: () => ({ setVisible() {} }),
parts: {
+2 -3
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createState } from './state.js';
@@ -48,9 +49,7 @@ test('changing replay speed during the countdown keeps its real-time liftoff', (
test('resuming a replay restores its retained chase heading', () => {
const { state } = replayRig();
state._engine = {
frames: { preUpdate: { addEventListener: () => () => {} } },
};
state._engine = createFakeEngine();
state._launches = [{ id: 'mission' }];
state._replayCameraLaunchId = 'mission';
state._replayCameraHeading = 1.2;
+1 -1
View File
@@ -26,7 +26,7 @@ test('ground snapping excludes the aircraft by its engine pick id', async () =>
},
frustumTester: () => () => true,
horizon: () => ({ isPointVisible: () => true }),
engine: {},
engine: createFakeEngine(),
},
};
const exclusions = [];
+2 -4
View File
@@ -728,14 +728,12 @@ function failingMapController() {
const controller = new MapSourceController({
registry,
initialStack: 'photoreal',
engine: {
kind: 'cesium',
render: { request() {} },
engine: Object.assign(createFakeEngine(), {
maps: createCesiumMaps(viewer, {
credits: createCesiumCredits(viewer).credits,
createImageryLayer: (provider) => ({ provider }),
}).maps,
},
}),
onError: () => {},
});
const calls = [];
@@ -93,7 +93,7 @@ test('restored orbit rings receive their current rotation before the first frame
},
});
lifecycle.methods.attachRenderer({
engine: {},
engine: createFakeEngine(),
viewRestored: new Promise(() => {}),
});
assert.equal(rotated, 1);
+10 -15
View File
@@ -13,21 +13,16 @@ import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
// the near plane, as the engines answer).
function cameraEngine(scene) {
const eye = new C.Cartesian3();
return {
host: {
size: () => ({
width: scene.canvas.clientWidth,
height: scene.canvas.clientHeight,
}),
},
projector: {
worldToWindow(point, result) {
C.Matrix4.multiplyByPoint(scene.camera.viewMatrix, point, eye);
if (eye.z >= -scene.camera.frustum.near) return undefined;
return C.SceneTransforms.worldToWindowCoordinates(scene, point, result);
},
},
};
const engine = createFakeEngine({
width: scene.canvas.clientWidth,
height: scene.canvas.clientHeight,
});
engine.fake.script('worldToWindow', (point, result) => {
C.Matrix4.multiplyByPoint(scene.camera.viewMatrix, point, eye);
if (eye.z >= -scene.camera.frustum.near) return undefined;
return C.SceneTransforms.worldToWindowCoordinates(scene, point, result);
});
return engine;
}
const bounds = { west: 0, east: 0.01, south: 0, north: 0.01 };
+30 -26
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js';
@@ -35,32 +36,35 @@ test('oblique horizon bearings are bounded and tile caps hold at city latitudes'
]) {
// An engine-shaped stub: the camera's pose and its ellipsoid and ray
// picks (the sky above the 300 px line), and the canvas.
const engine = {
camera: {
pose: () => ({
cartographic: geo.cartographic.fromDegrees(lon, lat, 300),
heading: 0,
pitch: -Math.PI / 12,
}),
pickEllipsoid(p) {
if (p.y < 300) return undefined;
return geo.cartesian3.fromDegrees(
lon + (p.x - 640) / 100000,
lat + (800 - p.y) / 50000,
0,
);
},
pickRay: () => ({
origin: geo.cartesian3.create(),
direction: geo.cartesian3.create(
-Math.sin((lat * Math.PI) / 180) * Math.cos((lon * Math.PI) / 180),
-Math.sin((lat * Math.PI) / 180) * Math.sin((lon * Math.PI) / 180),
Math.cos((lat * Math.PI) / 180),
),
}),
},
host: { canvas: { width: 1280, height: 800 } },
};
const engine = createFakeEngine({
longitude: geo.math.toRadians(lon),
latitude: geo.math.toRadians(lat),
altitude: 300,
canvas: { width: 1280, height: 800 },
});
engine.fake.setPose({
longitude: geo.math.toRadians(lon),
latitude: geo.math.toRadians(lat),
height: 300,
heading: 0,
pitch: -Math.PI / 12,
});
engine.fake.script('pickEllipsoid', (p) => {
if (p.y < 300) return undefined;
return geo.cartesian3.fromDegrees(
lon + (p.x - 640) / 100000,
lat + (800 - p.y) / 50000,
0,
);
});
engine.camera.pickRay = () => ({
origin: geo.cartesian3.create(),
direction: geo.cartesian3.create(
-Math.sin((lat * Math.PI) / 180) * Math.cos((lon * Math.PI) / 180),
-Math.sin((lat * Math.PI) / 180) * Math.sin((lon * Math.PI) / 180),
Math.cos((lat * Math.PI) / 180),
),
});
const result = createReticleFootprint()(engine, 300);
assert.ok(result.coverage.coarse.length <= 16);
assert.ok(result.coverage.detail.length <= 16);
+6 -6
View File
@@ -3695,12 +3695,12 @@ test('actual Traffic, Bikeshare and Transit lifecycles retain sensitivity in eve
// Traffic claims through its engine's camera, as bikeshare does: the
// viewer's. The stub has what binding its scene subscribes to.
const listeners = { addEventListener: () => noop };
const trafficEngine = {
camera: app.viewer.camera,
frames: { preRender: listeners, postRender: listeners },
projector: { viewProjection: noop },
surface: { tilesLoaded: () => false },
};
const trafficEngine = createFakeEngine();
trafficEngine.camera = app.engine.camera;
trafficEngine.frames.preRender = listeners;
trafficEngine.frames.postRender = listeners;
trafficEngine.projector.viewProjection = noop;
trafficEngine.fake.script('tilesLoaded', false);
const traffic = trafficLifecycle({
state: { _engine: trafficEngine, _loadGeneration: 0 },
services: {
+8 -9
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
// Edge cases for wind: on a fixed
// grid direction, speed and legend agree whichever way the centre is picked,
// and an empty field is never presented as calm.
@@ -24,15 +25,13 @@ const options = {
status: 'Model forecast',
};
/** Inspect either the current map centre or a captured position. */
const viaEngine = (snapshot, point) =>
inspectWindAtCenter(
snapshot,
{
camera: { pickEllipsoid: () => point },
host: { canvas: { clientWidth: 800, clientHeight: 600 } },
},
options,
);
const viaEngine = (snapshot, point) => {
const engine = createFakeEngine({
canvas: { clientWidth: 800, clientHeight: 600 },
});
engine.fake.script('pickEllipsoid', point);
return inspectWindAtCenter(snapshot, engine, options);
};
const viaPosition = (snapshot, point) =>
inspectWindAtCenter(snapshot, null, { ...options, position: point });
const windLine = (reading) =>
+5 -4
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js';
@@ -27,10 +28,10 @@ test('center inspection uses a fixed forecast reading without double-converting
values: Float32Array.from([20, 20, 20, 20]),
},
};
const engine = {
camera: { pickEllipsoid: () => geo.cartesian3.fromDegrees(0, 0) },
host: { canvas: { clientWidth: 800, clientHeight: 600 } },
};
const engine = createFakeEngine({
canvas: { clientWidth: 800, clientHeight: 600 },
});
engine.fake.script('pickEllipsoid', () => geo.cartesian3.fromDegrees(0, 0));
const result = inspectWindAtCenter(snapshot, engine, {
overlay: 'temperature',
units: 'km/h',
+8 -9
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
// src/layers/wind/inspectionMarker.test.mjs — the wind inspection marker
// follows only its captured point, renders in its container, and releases
// its frame listener.
@@ -47,15 +48,13 @@ function fixture(t) {
requestRender() {},
},
};
const engine = {
host: { canvas: viewer.scene.canvas },
frames: { postRender: viewer.scene.postRender },
projector: {
worldToWindow: viewer.scene.cartesianToCanvasCoordinates,
horizonVisible: () => visibility.visible,
},
render: { request() {} },
};
const engine = createFakeEngine({ canvas: viewer.scene.canvas });
engine.frames.postRender = viewer.scene.postRender;
engine.fake.script(
'worldToWindow',
viewer.scene.cartesianToCanvasCoordinates,
);
engine.projector.horizonVisible = () => visibility.visible;
const store = useWindInspectionStore(pinia);
visibility.visible = true;
const owner = createWindInspectionMarker({
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { createCesiumMaps } from '../globe/cesium/maps.js';
import { createCesiumCredits } from '../globe/cesium/credits.js';
import test from 'node:test';
@@ -72,7 +73,7 @@ function fixture(registry, options = {}) {
});
const controller = new MapSourceController({
registry,
engine: { kind: 'cesium', render: { request() {} }, maps: mapsOwner.maps },
engine: Object.assign(createFakeEngine(), { maps: mapsOwner.maps }),
onChange: (state) => changes.push(state),
...options,
});
+3 -3
View File
@@ -2070,7 +2070,7 @@ test('a caught run failure resolves as run-failed, not success (#971)', async ()
test('a failed scene-pack rebind releases the partial renderer and a stopped shot unblocks', async () => {
const director = Object.create(SceneDirector.prototype);
const drawn = new Set();
director.engine = { host: { container: {} } };
director.engine = createFakeEngine();
director._dataPacks = {
setScene(viewer) {
if (!viewer) drawn.clear();
@@ -2088,7 +2088,7 @@ test('a failed scene-pack rebind releases the partial renderer and a stopped sho
assert.throws(
() =>
director.attachScene(
{ engine: { host: { container: {} } } },
{ engine: createFakeEngine() },
{},
() => true,
),
@@ -2100,7 +2100,7 @@ test('a failed scene-pack rebind releases the partial renderer and a stopped sho
abort.abort();
assert.equal(await stopped, false);
const waiting = director._waitForScene();
director.engine = { host: {} };
director.engine = createFakeEngine();
director.styleManager = {};
director.resumeScene();
assert.equal(await waiting, true);
+33 -17
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from './globe/testing/fakeEngine.js';
import { test, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import {
@@ -242,7 +243,9 @@ test('scope appearance follows direct state changes and install/destroy lifecycl
setScopeMaskEnabled(false);
assert.equal(dom.documentElement.dataset.scopeMask, 'off');
delete dom.documentElement.dataset.scopeMask;
installScopeMask({ host: { container: dom.container } });
installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
assert.equal(
dom.documentElement.dataset.scopeMask,
'off',
@@ -254,7 +257,9 @@ test('scope appearance follows direct state changes and install/destroy lifecycl
undefined,
'destroy clears the document marker',
);
installScopeMask({ host: { container: dom.container } });
installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
assert.equal(
dom.documentElement.dataset.scopeMask,
'off',
@@ -282,7 +287,9 @@ test('scope appearance follows direct state changes and install/destroy lifecycl
test('a DPR change with no resize still repaints the backing store', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try {
installScopeMask({ host: { container: dom.container } });
installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
// 1x monitor: backing store matches CSS pixels.
assert.equal(dom.canvas.width, 1000);
assert.equal(dom.canvas.height, 800);
@@ -314,7 +321,9 @@ test('a DPR change with no resize still repaints the backing store', () => {
test('destroy tears the DPR watch down (no redraw after teardown)', () => {
const dom = stubScopeMaskDom({ width: 640, height: 480, dpr: 1 });
try {
installScopeMask({ host: { container: dom.container } });
installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
assert.equal(dom.canvas.width, 640);
destroyScopeMask();
dom.setDpr(2);
@@ -439,7 +448,9 @@ test('quantization snaps to the repaint grid', () => {
test('repaint gate: only a QUANTIZED step repaints; hovering costs nothing', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try {
installScopeMask({ host: { container: dom.container } });
installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
const base = getScopeTerminusRepaintCount();
// Parked at true full-globe altitude: the seeded alpha already matches, so
@@ -477,7 +488,9 @@ test('repaint gate: only a QUANTIZED step repaints; hovering costs nothing', ()
test('a full zoom-in gesture costs only a handful of repaints', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try {
installScopeMask({ host: { container: dom.container } });
installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
const base = getScopeTerminusRepaintCount();
// 20 Mm → ground in 10 km steps: ~2000 samples across the whole descent,
@@ -510,7 +523,9 @@ test('a full zoom-in gesture costs only a handful of repaints', () => {
test('an override pins the terminus and null restores the ramp', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try {
installScopeMask({ host: { container: dom.container } });
installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
setScopeTerminusOverride(0.97);
assert.equal(getScopeTerminusOverride(), 0.97);
assert.equal(getScopeTerminusAlpha(), 0.97);
@@ -536,7 +551,9 @@ test('an override pins the terminus and null restores the ramp', () => {
test('the hard-crop (feather 0) path honors the same terminus alpha', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try {
installScopeMask({ host: { container: dom.container } });
installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
updateScopeTerminusForHeight(SCOPE_TERMINUS_FAR_M + 5_000_000); // true full-globe view
setScopeMaskFeather(0); // hard crop — the evenodd rect-minus-circle path
// The globe-scale seed paint is legitimately in the history, so assert on
@@ -583,15 +600,12 @@ function stubScopeEngine(container, heightM) {
set.add(listener);
return () => set.delete(listener);
};
const engine = createFakeEngine({ container, altitude: height });
engine.frames.preRender.addEventListener = addTo(preRenderListeners);
engine.camera.moveEnd.addEventListener = addTo(moveEndListeners);
engine.camera.height = () => height;
return {
engine: {
host: { container },
frames: { preRender: { addEventListener: addTo(preRenderListeners) } },
camera: {
height: () => height,
moveEnd: { addEventListener: addTo(moveEndListeners) },
},
},
engine,
setHeight(next) {
height = next;
},
@@ -748,7 +762,9 @@ test('terminus percents clamp into the supported band; junk is absent, not zero'
test('a pinned override is floored to the band at every entry point', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try {
installScopeMask({ host: { container: dom.container } });
installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
setScopeTerminusOverride(0); // an sce=0 link, or any stale caller
assert.equal(
getScopeTerminusOverride(),
+3 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// The standalone application is created once; an engine switch detaches and
// attaches only its engine-bound parts. A candidate that fails releases its
// renderer, and the working engine attaches again on the same parts.
@@ -48,7 +49,8 @@ function standalone({ failOpen = null, beforeOpen, beforeRelease, lifecycle = fa
createStandaloneScene: async ({ engineKind, defer }) => {
await beforeOpen?.(engineKind);
if (failOpen === engineKind) throw new Error(`${engineKind} refused`);
const engine = { kind: engineKind, id: opened.length };
const engine = createFakeEngine({ kind: engineKind });
engine.id = opened.length;
opened.push(engine);
defer(async () => {
await beforeRelease?.(engine);
+7 -18
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { createSessionHandoff } from '../app/sessionHandoff.js';
import assert from 'node:assert/strict';
import test from 'node:test';
@@ -479,10 +480,7 @@ test('pagehide while the outgoing engine is released starts nothing', async () =
test('a slow first frame never rolls back a started engine', async () => {
const timers = [];
const engine = {
frames: { postRender: { addEventListener: () => () => {} } },
render: { request() {} },
};
const engine = createFakeEngine();
const h = harness();
let frame;
const machine = createEngineSwitch({
@@ -525,21 +523,12 @@ test('the first-frame wait resolves on a frame, on a hidden tab and on timeout',
};
let draw;
let requests = 0;
const engine = {
frames: {
postRender: {
addEventListener(fn) {
draw = fn;
return () => {
draw = null;
};
},
},
},
render: {
request: () => requests++,
},
const engine = createFakeEngine();
engine.frames.postRender.addEventListener = (fn) => {
draw = fn;
return () => { draw = null; };
};
engine.render.request = () => requests++;
const timers = new Map();
const options = {
document: doc,
+2 -4
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
import { createRendererCache } from './rendererCache.js';
@@ -51,10 +52,7 @@ function fakeBuild(released) {
resume() {
this.running = true;
},
engine: {
host: { container, canvas },
render: { request() {} },
},
engine: createFakeEngine({ container, canvas }),
};
};
}
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createSceneSwitcher } from './sceneSwitcher.js';
@@ -193,7 +194,7 @@ test('the restored camera precedes retained follow and replay', async () => {
const app = {
getState: () => ({ status: 'ready' }),
getComponents: () => ({
scene: { engine: { kind: current } },
scene: { engine: createFakeEngine({ kind: current }) },
controls: { styleManager: style },
}),
async detachScene() {
+3 -6
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// src/ui/displayPanel.test.mjs — the Display panel renders bloom and sharpen
// from what VisualSettings publishes, and shows its initial markup before.
import '../testSupport/registerDom.mjs';
@@ -145,6 +146,7 @@ test('a publish never rewrites a slider the user is dragging', async (t) => {
/** VisualSettings with a stand-in detection engine. */
function detectionSettings(store) {
const { owner } = settings(store);
// This is detection state, not a globe engine.
const engine = {
mode: 'OFF',
tuning: { densityPct: 50, allocationStrategy: 'ELASTIC' },
@@ -425,12 +427,7 @@ test('the imported files render what the file import publishes and keep focus',
assert.equal($('geo-import-hint').textContent, '');
// A globe engine double the files draw on.
const engine = {
host: { container: document.createElement('div'), canvas: null },
camera: { moveEnd: { addEventListener: () => () => {} } },
surface: { tilesLoaded: () => true },
render: { request() {} },
};
const engine = createFakeEngine({ container: document.createElement('div') });
const tool = createGeoFileImport({
drawFile: async () => ({
report: { undrawn: 0 },
+7 -13
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createFrameRateMonitor } from './frameRateMonitor.js';
@@ -17,15 +18,10 @@ function fixture(t) {
addEventListener: (name, handler) => keys.add(handler),
removeEventListener: (name, handler) => keys.delete(handler),
};
const engine = {
frames: {
postRender: {
addEventListener(handler) {
frames.add(handler);
return () => frames.delete(handler);
},
},
},
const engine = createFakeEngine();
engine.frames.postRender.addEventListener = (handler) => {
frames.add(handler);
return () => frames.delete(handler);
};
const monitor = createFrameRateMonitor({ engine, documentRef, store });
t.after(() => monitor.destroy());
@@ -108,7 +104,7 @@ test('a page without the title bar gets no readout and keeps the key', () => {
let listened = false;
const store = { publishFps: () => assert.fail('nothing to show it in') };
const monitor = createFrameRateMonitor({
engine: { frames: { postRender: { addEventListener: () => () => {} } } },
engine: createFakeEngine(),
documentRef: {
getElementById: () => null,
addEventListener: () => (listened = true),
@@ -130,9 +126,7 @@ test("destroying an earlier monitor leaves a newer one's readout", () => {
addEventListener() {},
removeEventListener() {},
};
const engine = {
frames: { postRender: { addEventListener: () => () => {} } },
};
const engine = createFakeEngine();
const first = createFrameRateMonitor({ engine, documentRef, store });
const second = createFrameRateMonitor({ engine, documentRef, store });
const shown = store.fps;
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// src/ui/frameRateView.test.mjs — the title bar's FPS readout renders what
// createFrameRateMonitor() publishes and toggles with the backquote key.
import '../testSupport/registerDom.mjs';
@@ -19,7 +20,7 @@ test('the FPS readout sits in the title bar, hidden until the backquote key', as
document.querySelector('#title-bar .frame-rate-readout');
assert.equal(readout(), null, 'no readout without a monitor');
const monitor = createFrameRateMonitor({
engine: { frames: { postRender: { addEventListener: () => () => {} } } },
engine: createFakeEngine(),
store: useStatusStore(pinia),
});
t.after(() => monitor.destroy());
+12 -15
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// Behavioral tests for DISPLAY ▸ Import: the controller is driven against
// fake DOM and engine doubles with an injected loader and drawing, and its
// observable effects are asserted — drawings made and destroyed, the rows
@@ -65,22 +66,18 @@ class FakeElement {
/** A globe engine double: its host, camera moves and render requests. */
function fakeEngine() {
const moveEnd = new Set();
const engine = {
const engine = createFakeEngine({
kind: 'fake',
host: {
container: new FakeElement('div', 'cesiumContainer'),
canvas: new FakeElement('canvas'),
},
camera: {
moveEnd: {
addEventListener: (fn) => (moveEnd.add(fn), () => moveEnd.delete(fn)),
},
},
surface: { tilesLoaded: () => true },
renders: 0,
render: { request: () => (engine.renders += 1) },
moveEnd,
};
container: new FakeElement('div', 'cesiumContainer'),
canvas: new FakeElement('canvas'),
});
engine.camera.moveEnd.addEventListener = (fn) => (
moveEnd.add(fn),
() => moveEnd.delete(fn)
);
engine.renders = 0;
engine.render.request = () => (engine.renders += 1);
engine.moveEnd = moveEnd;
return engine;
}
+2 -6
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// A file removed while its drawing on the next engine is still pending
// (icon decode) must not leave that drawing on the globe.
import test from 'node:test';
@@ -10,12 +11,7 @@ const el = () => ({
addEventListener() {},
removeEventListener() {},
});
const engine = () => ({
host: { container: el(), canvas: el() },
camera: { moveEnd: { addEventListener: () => () => {} } },
surface: { tilesLoaded: () => true },
render: { request() {} },
});
const engine = () => createFakeEngine({ container: el(), canvas: el() });
test('removing a file during its redraw destroys the late drawing', async () => {
const document = {
+7 -10
View File
@@ -445,16 +445,13 @@ test('the followed entity is read in the share format, and its offset round-trip
gevDisplayPosition: () => target,
},
};
const engine = {
camera: {
pose: () => ({ position: cameraPosition }),
following: () =>
viewer.trackedEntity && {
native: viewer.trackedEntity,
target: () => target,
},
},
};
const engine = createFakeEngine();
engine.fake.setPose({ position: cameraPosition });
engine.camera.following = () =>
viewer.trackedEntity && {
native: viewer.trackedEntity,
target: () => target,
};
const subject = readShareFollowSubject({ engine, nowMs: NOW_MS });
assert.equal(subject.kind, 'military_aircraft');
assert.equal(subject.id, 'ae1234');
+5 -6
View File
@@ -127,12 +127,11 @@ test('camera-entry listeners are removed before any asynchronous layer cleanup',
const { owner } = bindings(t);
let changes = 0;
let trackedCallback;
owner.engine = { camera: { followChanged: {
addEventListener(callback) {
trackedCallback = callback;
return () => { changes += 1; };
},
} } };
owner.engine = createFakeEngine();
owner.engine.camera.followChanged.addEventListener = (callback) => {
trackedCallback = callback;
return () => { changes += 1; };
};
owner._stampNavigation = () => { changes += 100; };
owner.observeCamera();
owner.stop();
+13 -12
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// The shell lives for the page: an engine switch detaches only its
// globe-bound parts and attaches them to the next globe, keeping the shell's
// session objects (navigation, location search, HUD, share link, display
@@ -37,7 +38,7 @@ function fakeShell() {
},
};
Object.assign(shell, {
_scene: { viewer: {}, engine: { kind: 'cesium' }, mapStackController: {} },
_scene: { viewer: {}, engine: createFakeEngine({ kind: 'cesium' }), mapStackController: {} },
_disposed: false,
_layerSession: null,
services: {
@@ -143,7 +144,7 @@ test('the next globe gets new globe-bound parts and the restore runs on it', asy
const { shell, session, log } = fakeShell();
await shell.detachScene({ handoff: true });
log.length = 0;
const engine = { kind: 'navara' };
const engine = createFakeEngine({ kind: 'navara' });
await shell.attachScene({
viewer: null,
engine,
@@ -176,7 +177,7 @@ test('the next globe gets new globe-bound parts and the restore runs on it', asy
assert.equal(shell._navigation, session.navigation);
await shell.attachScene({ engine });
await assert.rejects(
shell.attachScene({ engine: { kind: 'cesium' } }),
shell.attachScene({ engine: createFakeEngine({ kind: 'cesium' }) }),
/attached to a globe/,
);
});
@@ -186,7 +187,7 @@ test('a panel whose layer is another instance in the new catalog is rebuilt', as
await shell.detachScene({ handoff: true });
log.length = 0;
await shell.attachScene({
engine: { kind: 'cesium' },
engine: createFakeEngine({ kind: 'cesium' }),
services: { cctvLayer: 'other' },
});
assert.ok(log.includes('_initCctvPanel'));
@@ -209,7 +210,7 @@ test('an attachment that throws part-way leaves the shell detached and attachabl
throw new Error('overlay host failed');
};
await assert.rejects(
shell.attachScene({ engine: { kind: 'navara' } }),
shell.attachScene({ engine: createFakeEngine({ kind: 'navara' }) }),
/overlay host failed/,
);
// The shell releases what it built and is detached when that settles.
@@ -217,7 +218,7 @@ test('an attachment that throws part-way leaves the shell detached and attachabl
assert.equal(shell.engine, null);
log.length = 0;
shell._initSceneOverlays = () => log.push('_initSceneOverlays');
const engine = { kind: 'cesium' };
const engine = createFakeEngine({ kind: 'cesium' });
await shell.attachScene({ engine });
assert.equal(shell.engine, engine);
assert.ok(log.includes('shareRestoration.start'));
@@ -244,7 +245,7 @@ test('between engines a style key waits for the next engine instead of the relea
shell._layerBindings = { observeCamera() {}, stop() {}, disconnect() {} };
shell._visualSettings = { setStyle: (name) => applied.push(name) };
};
await shell.attachScene({ engine: { kind: 'cesium' } });
await shell.attachScene({ engine: createFakeEngine({ kind: 'cesium' }) });
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(applied, ['crt']);
@@ -262,13 +263,13 @@ test('shell recovery waits for failed attachment cleanup and serializes repeated
shell._initSceneOverlays = () => {
throw new Error('overlay failed');
};
const failed = shell.attachScene({ engine: { kind: 'navara' } });
const failed = shell.attachScene({ engine: createFakeEngine({ kind: 'navara' }) });
const rejected = assert.rejects(failed, /overlay failed/);
await Promise.resolve();
const detach = shell.detachScene();
const again = shell.detachScene();
shell._initSceneOverlays = () => {};
const engine = { kind: 'cesium' };
const engine = createFakeEngine({ kind: 'cesium' });
const recovered = shell.attachScene({ engine });
await Promise.resolve();
assert.notEqual(shell.engine, engine, 'recovery waits for cleanup');
@@ -288,7 +289,7 @@ test('shell attach waits for a deferred outgoing detach', async () => {
release = resolve;
});
const leaving = shell.detachScene({ handoff: true });
const engine = { kind: 'navara' };
const engine = createFakeEngine({ kind: 'navara' });
const entering = shell.attachScene({ engine });
await Promise.resolve();
assert.notEqual(shell.engine, engine);
@@ -302,7 +303,7 @@ test('an engine without Wildlife Sightings gets no card instead of a failed star
// The stand-in a catalog gives for a layer the engine leaves out.
Object.assign(shell, {
services: { wildlifeSightingsLayer: {} },
_scene: { viewer: null, engine: { kind: 'navara' } },
_scene: { viewer: null, engine: createFakeEngine({ kind: 'navara' }) },
});
assert.doesNotThrow(() => shell._initWildlifeSightingCard());
assert.equal(shell._wildlifeSightingCard, null);
@@ -324,7 +325,7 @@ test('a hosted shell uses the enclosing transition while direct calls wait outsi
entered();
return new Promise((resolve) => { release = resolve; });
};
const next = { engine: { kind: 'navara' }, catalog };
const next = { engine: createFakeEngine({ kind: 'navara' }), catalog };
const switching = host.run(async (transaction) => {
await host.detach({ handoff: true }, transaction);
await host.attach(next, transaction);
+5 -5
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
import { NavigationController } from './navigationController.js';
@@ -69,7 +70,7 @@ test('share teardown settles its promise, removes gestures and rejects a retaine
let stamps = 0;
let applies = 0;
const owner = new ShareRestoration({
engine: { host: { canvas } },
engine: createFakeEngine({ canvas }),
navigation: {
_beginDeferredNavigation: () => 1,
_reassertNavigationHandoff: () => true,
@@ -164,12 +165,11 @@ test('on Navara, presets set the Sharpen slider to 80 % and the engine draws it'
_sharpenSliderRow: element(),
_sharpenSliderValue: element(),
};
const engine = createFakeEngine({ kind: 'navara' });
engine.render.setSharpen = (options) => sharpen.push(options);
const owner = new VisualSettings({
viewer: null,
engine: {
kind: 'navara',
render: { setSharpen: (options) => sharpen.push(options) },
},
engine: engine,
elements,
operations: {
_syncShareState() {},
+3 -4
View File
@@ -57,7 +57,7 @@ function fixture() {
createStage: (options) => ({ ...options, enabled: true }),
},
);
const engine = { render: cesium.render, isDestroyed: () => false };
const engine = Object.assign(createFakeEngine(), { render: cesium.render });
const effects = new VisualEffects({
engine,
requestRender: (reason) => requests.push(reason),
@@ -351,9 +351,8 @@ test('existing baseline and military presets keep one detection default', () =>
test('an engine sharpens with the same amount scale, only while on and above 0', () => {
const sharpen = [];
const requests = [];
const engine = {
render: { setSharpen: (options) => sharpen.push(options) },
};
const engine = createFakeEngine();
engine.render.setSharpen = (options) => sharpen.push(options);
const effects = new VisualEffects({
engine,
requestRender: (reason) => requests.push(reason),
+9 -12
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// Pure-logic unit tests for the voice-lifecycle size guards (Batch 11, M13).
// These helpers are DOM/WebRTC-free so they pin the screenshot down-scaling and
// payload-byte estimation that keep an oversized dc.send from stranding a turn.
@@ -2859,10 +2860,8 @@ test('isBenignViewportDeleteError is false for non-error payloads and junk', ()
test('hidden document yields no fresh frame — capture must not label a stale canvas Current', async () => {
const originalDocument = globalThis.document;
let requested = 0;
const engine = {
frames: { postRender: { addEventListener() { return () => {}; } } },
render: { request() { requested += 1; } },
};
const engine = createFakeEngine();
engine.render.request = () => { requested += 1; };
try {
globalThis.document = { hidden: true };
const fresh = await renderFreshCesiumFrame(engine);
@@ -2878,10 +2877,9 @@ test('visible document with a rendering scene reports a fresh frame', async () =
try {
globalThis.document = { hidden: false };
let fire = null;
const engine = {
frames: { postRender: { addEventListener(listener) { fire = listener; return () => { fire = null; }; } } },
render: { request() { queueMicrotask(() => fire?.()); } },
};
const engine = createFakeEngine();
engine.frames.postRender.addEventListener = (listener) => { fire = listener; return () => { fire = null; }; };
engine.render.request = () => { queueMicrotask(() => fire?.()); };
const fresh = await renderFreshCesiumFrame(engine);
assert.equal(fresh, true);
} finally {
@@ -2895,10 +2893,9 @@ test('a tab switch during the bounded render wait invalidates freshness', async
const doc = { hidden: false };
globalThis.document = doc;
let fire = null;
const engine = {
frames: { postRender: { addEventListener(listener) { fire = listener; return () => { fire = null; }; } } },
render: { request() { doc.hidden = true; queueMicrotask(() => fire?.()); } },
};
const engine = createFakeEngine();
engine.frames.postRender.addEventListener = (listener) => { fire = listener; return () => { fire = null; }; };
engine.render.request = () => { doc.hidden = true; queueMicrotask(() => fire?.()); };
const fresh = await renderFreshCesiumFrame(engine);
assert.equal(fresh, false, 'freshness rechecked after the await');
} finally {
+3 -2
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
// Cross-feature voice contracts, driven through the real action runner:
// one result contract (engine fields → model payload → voice card), shared
// pointing for imagery, result-set ownership between OSM and the analyst,
@@ -687,7 +688,7 @@ test('an engine switch keeps the last answer for a follow-up on the next engine'
build: () =>
harness({ runnerOptions: { analystMemory, layerEnabledAt } }).runner,
});
run.attach({ engine: { kind: 'cesium' } });
run.attach({ engine: createFakeEngine({ kind: 'cesium' }) });
const first = await run('analyst_query', {
layers: ['flights'],
scope: { kind: 'anywhere' },
@@ -695,7 +696,7 @@ test('an engine switch keeps the last answer for a follow-up on the next engine'
assert.equal(first.ok, true);
// The real runner of the outgoing engine is disposed by the switch.
run.detach();
run.attach({ engine: { kind: 'navara' } });
run.attach({ engine: createFakeEngine({ kind: 'navara' }) });
const followUp = await run('analyst_query', {
layers: ['flights'],
followUp: true,
+14 -24
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict';
import test from 'node:test';
import {
@@ -96,23 +97,16 @@ test('crop: a slow render gets one bounded fresh-frame recovery request', async
let listener = null;
try {
globalThis.document = { hidden: false };
const engine = {
frames: {
postRender: {
addEventListener(next) {
listener = next;
return () => {
if (listener === next) listener = null;
};
},
},
},
render: {
request() {
requests += 1;
if (requests === 2) queueMicrotask(() => listener?.());
},
},
const engine = createFakeEngine();
engine.frames.postRender.addEventListener = (next) => {
listener = next;
return () => {
if (listener === next) listener = null;
};
};
engine.render.request = () => {
requests += 1;
if (requests === 2) queueMicrotask(() => listener?.());
};
assert.equal(await renderFreshPointerFrame(engine), true);
assert.equal(requests, 2, 'only the pointer path retries once');
@@ -127,13 +121,9 @@ test('crop: a hidden document never starts the recovery request', async () => {
let requests = 0;
try {
globalThis.document = { hidden: true };
const engine = {
frames: { postRender: { addEventListener: () => () => {} } },
render: {
request() {
requests += 1;
},
},
const engine = createFakeEngine();
engine.render.request = () => {
requests += 1;
};
assert.equal(await renderFreshPointerFrame(engine), false);
assert.equal(requests, 0);
+2 -1
View File
@@ -1,8 +1,9 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createSceneRunner, engineSwitchingRefusal } from './sceneRunner.js';
const engine = (kind) => ({ kind });
const engine = (kind) => createFakeEngine({ kind });
function runnerFor(log) {
return (scene) => {
+2 -5
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test';
import assert from 'node:assert/strict';
import { createSceneVoice } from './gevRealtime.js';
@@ -6,11 +7,7 @@ import { createTestVoiceUi } from '../testSupport/voiceUi.mjs';
function fakeEngine(kind) {
const canvas = new EventTarget();
canvas.getBoundingClientRect = () => ({ left: 0, top: 0 });
return {
kind,
host: { canvas, size: () => ({ width: 0, height: 0 }) },
camera: { pose: () => ({}) },
};
return createFakeEngine({ kind, canvas, width: 0, height: 0 });
}
function fixture() {