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, "fixedSleeps": 69,
"loadingScreenReadiness": 0, "loadingScreenReadiness": 0,
"cesiumOnlyReads": 47, "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 assert from 'node:assert/strict';
import { import {
QA_HARNESS_METRICS, QA_HARNESS_METRICS,
engineLiteralLines,
harnessGrowth, harnessGrowth,
measureQaHarness, measureQaHarness,
readBaseline, readBaseline,
readQaSources, readQaSources,
readTestSources,
} from './report-qa-harness.mjs'; } from './report-qa-harness.mjs';
test('QA harness duplication does not grow past the baseline', () => { test('QA harness duplication does not grow past the baseline', () => {
assert.deepEqual( assert.deepEqual(
harnessGrowth(measureQaHarness(), readBaseline().metrics), 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')); 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', () => { test('metrics count scripts once and distinguish helpers from their callers', () => {
const measure = (id, ...sources) => const measure = (id, ...sources) =>
QA_HARNESS_METRICS.find((metric) => metric.id === id).measure( QA_HARNESS_METRICS.find((metric) => metric.id === id).measure(
+106 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
/** /**
* Browser QA still doing work owned by scripts/qa-strict-harness.mjs. * Browser QA and tests still duplicating the shared harness or fake engine.
* Each metric counts scripts, so repeated uses in one script count once. * Each metric counts files, so repeated uses in one file count once.
* *
* node scripts/report-qa-harness.mjs * node scripts/report-qa-harness.mjs
* node scripts/report-qa-harness.mjs --json * node scripts/report-qa-harness.mjs --json
@@ -10,6 +10,7 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { parsers } from 'prettier/plugins/babel.mjs';
const ROOT = fileURLToPath(new URL('..', import.meta.url)); const ROOT = fileURLToPath(new URL('..', import.meta.url));
export const BASELINE_PATH = path.join( export const BASELINE_PATH = path.join(
@@ -18,6 +19,80 @@ export const BASELINE_PATH = path.join(
'qa-harness-baseline.json', '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 readsCesiumState = (text) => {
const names = [ const names = [
'__godsEyeView', '__godsEyeView',
@@ -78,6 +153,15 @@ export const QA_HARNESS_METRICS = Object.freeze([
), ),
).length, ).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. */ /** 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) { export function measureQaHarness(root = ROOT) {
const files = readQaSources(root); const files = readQaSources(root);
const tests = readTestSources(root);
return Object.fromEntries( 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 { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createAnnotationEngine } from './annotationEngine.js'; 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; log.length = 0;
engine.attachRenderer({ engine.attachRenderer({
engine: {}, engine: createFakeEngine(),
renderer: recordingRenderer(log, 'next'), renderer: recordingRenderer(log, 'next'),
}); });
assert.deepEqual(log, ['next add Capitol']); 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.detachRenderer(); engine.detachRenderer();
engine.attachRenderer({ engine: {}, renderer: recordingRenderer(log, 'b') }); engine.attachRenderer({
engine.attachRenderer({ engine: {}, renderer: recordingRenderer(log, 'c') }); engine: createFakeEngine(),
renderer: recordingRenderer(log, 'b'),
});
engine.attachRenderer({
engine: createFakeEngine(),
renderer: recordingRenderer(log, 'c'),
});
assert.deepEqual(log, ['a destroy', 'b destroy']); assert.deepEqual(log, ['a destroy', 'b destroy']);
engine.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 // 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 // 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 // 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 { try {
let connected = false; let connected = false;
const tool = initDrawTool({ const tool = initDrawTool({
engine: {}, engine: createFakeEngine(),
annotations: fakeAnnotations(), annotations: fakeAnnotations(),
connect: () => { connect: () => {
connected = true; connected = true;
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { installDomClasses } from '../testSupport/domClasses.mjs'; import { installDomClasses } from '../testSupport/domClasses.mjs';
import { test } from 'node:test'; import { test } from 'node:test';
import assert from 'node:assert/strict'; 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. // The Cesium engine's credits over the viewer's credit display.
const engine = { const engine = createFakeEngine();
credits: createCesiumCredits(viewer).credits, engine.credits = createCesiumCredits(viewer).credits;
render: { request: () => scene.requestRender() }, engine.render.request = () => scene.requestRender();
};
enginesOf.set(viewer, engine); enginesOf.set(viewer, engine);
return { dataSources, frames, 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 assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import * as geo from '../geo/index.js'; 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 camera = geo.cartesian3.fromDegrees(86.9, 27.9, 4030);
const ground = geo.cartesian3.fromDegrees(86.9, 27.9, 4000); const ground = geo.cartesian3.fromDegrees(86.9, 27.9, 4000);
const seaLevel = geo.cartesian3.fromDegrees(86.9, 27.9, 0); const seaLevel = geo.cartesian3.fromDegrees(86.9, 27.9, 0);
const engine = { const engine = createFakeEngine({ width: 100, height: 100 });
host: { size: () => ({ width: 100, height: 100 }) }, engine.fake.setPose({ position: camera });
picker: { engine.fake.script('pickPosition', () => geo.cartesian3.clone(ground));
pickPositionSupported: true, engine.fake.script('pickGlobe', () => geo.cartesian3.clone(ground));
pickPosition: () => geo.cartesian3.clone(ground), engine.fake.script('pickEllipsoid', () => geo.cartesian3.clone(seaLevel));
pickGlobe: () => geo.cartesian3.clone(ground),
},
camera: {
livePosition: camera,
pickEllipsoid: () => geo.cartesian3.clone(seaLevel),
},
};
const picked = pickWorldFromScreen(engine, 0.5, 0.5); const picked = pickWorldFromScreen(engine, 0.5, 0.5);
assert.ok( assert.ok(
Math.abs(picked.height - 4000) < 1, 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', () => { test('a near-camera depth artifact falls back to terrain before sea level', () => {
const engine = { const engine = createFakeEngine({ width: 100, height: 100 });
host: { size: () => ({ width: 100, height: 100 }) }, engine.fake.setPose({
picker: { position: geo.cartesian3.fromDegrees(86.9, 27.9, 4030),
pickPositionSupported: true, });
pickPosition: () => geo.cartesian3.fromDegrees(86.9, 27.9, 4029), engine.fake.script('pickPosition', () =>
pickGlobe: () => geo.cartesian3.fromDegrees(86.9, 27.9, 4000), geo.cartesian3.fromDegrees(86.9, 27.9, 4029),
}, );
camera: { engine.fake.script('pickGlobe', () =>
livePosition: geo.cartesian3.fromDegrees(86.9, 27.9, 4030), geo.cartesian3.fromDegrees(86.9, 27.9, 4000),
pickEllipsoid: () => geo.cartesian3.fromDegrees(86.9, 27.9, 0), );
}, 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); 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', () => { 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. // 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 camera = geo.cartesian3.fromDegrees(86.9, 27.9, 100_000);
const engine = { const engine = createFakeEngine({ width: 100, height: 100 });
host: { size: () => ({ width: 100, height: 100 }) }, engine.fake.setPose({ position: camera });
picker: { engine.fake.script('pickPosition', () =>
pickPositionSupported: true, geo.cartesian3.fromDegrees(86.9, 27.9, 99_950),
pickPosition: () => geo.cartesian3.fromDegrees(86.9, 27.9, 99_950), );
pickGlobe: () => geo.cartesian3.fromDegrees(86.9, 27.9, 4000), engine.fake.script('pickGlobe', () =>
}, geo.cartesian3.fromDegrees(86.9, 27.9, 4000),
camera: { );
livePosition: camera, engine.fake.script('pickEllipsoid', () =>
pickEllipsoid: () => geo.cartesian3.fromDegrees(86.9, 27.9, 0), geo.cartesian3.fromDegrees(86.9, 27.9, 0),
}, );
};
assert.ok(Math.abs(pickWorldFromScreen(engine, 0.5, 0.5).height - 4000) < 1); 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 test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process'; 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 () => { test('later components receive the scene engine beside its viewer', async () => {
const viewer = { name: 'viewer' }; const viewer = { name: 'viewer' };
const engine = { kind: 'cesium' }; const engine = createFakeEngine({ kind: 'cesium' });
const seen = []; const seen = [];
const receive = const receive =
(phase) => (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 // 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 // part: data, Cockpit clouds) must leave the shell detached, so the working
// engine attaches again. // engine attaches again.
@@ -29,7 +30,7 @@ test('a candidate failing in a later part leaves the shell ready for the working
const cesium = { const cesium = {
kind: 'cesium', kind: 'cesium',
viewer: null, viewer: null,
engine: { kind: 'cesium' }, engine: createFakeEngine({ kind: 'cesium' }),
catalog, catalog,
}; };
await sceneHost.attach(cesium); 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 = { const navara = {
kind: 'navara', kind: 'navara',
viewer: null, viewer: null,
engine: { kind: 'navara' }, engine: createFakeEngine({ kind: 'navara' }),
catalog, catalog,
}; };
await assert.rejects(sceneHost.attach(navara), /layer attachment failed/); 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 assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { import {
@@ -12,15 +13,13 @@ import { createView } from '../view/index.js';
const fakeEngine = () => { const fakeEngine = () => {
const flights = []; const flights = [];
return { const engine = createFakeEngine();
flights, engine.flights = flights;
camera: { engine.camera.flyTo = (options) => {
flyTo(options) { flights.push(options);
flights.push(options); options.complete();
options.complete();
},
},
}; };
return engine;
}; };
const fakeLayers = (enabled) => ({ const fakeLayers = (enabled) => ({
getAll: () => getAll: () =>
+4 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import fs from 'node:fs'; 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), defer: (dispose) => cleanups.push(dispose),
createEngine(target, options) { createEngine(target, options) {
calls.push([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'); 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 '../testSupport/registerDom.mjs';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
@@ -73,6 +74,8 @@ function boot(
}; };
}, },
createStandaloneApplication: ({ engineKind }) => { createStandaloneApplication: ({ engineKind }) => {
const engine = createFakeEngine({ kind: engineKind });
engine.camera.completeFlight = () => flushes.push('flight');
const styleManager = { const styleManager = {
initialRestorePromise, initialRestorePromise,
shareLinkManager: { flush: () => flushes.push('share link') }, shareLinkManager: { flush: () => flushes.push('share link') },
@@ -92,9 +95,7 @@ function boot(
}), }),
getComponents: () => ({ getComponents: () => ({
scene: { scene: {
engine: { engine: engine,
camera: { completeFlight: () => flushes.push('flight') },
},
}, },
controls: { styleManager }, 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 lifetime = new AbortController();
const layerSession = { lifetime }; const layerSession = { lifetime };
const { views, meshes } = fakeViews(); const { views, meshes } = fakeViews();
const engine = { const engine = createFakeEngine({ kind: 'navara' });
...createFakeEngine({ kind: 'navara' }), engine.extensions = { navara: views };
extensions: { navara: views },
};
// As src/app/tools.js attaches the board on the backend's scene. // As src/app/tools.js attaches the board on the backend's scene.
const attach = () => { const attach = () => {
const cleanups = []; const cleanups = [];
+3 -12
View File
@@ -1,21 +1,12 @@
import { createFakeEngine } from './globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { CelestialRing } from './celestialRing.js'; import { CelestialRing } from './celestialRing.js';
test('full globe focus uses the engine without a Cesium global', () => { test('full globe focus uses the engine without a Cesium global', () => {
let flight; let flight;
const engine = { const engine = createFakeEngine({ canvas: { clientWidth: 1200, clientHeight: 800 }, fovy: Math.PI / 3 });
host: { canvas: { clientWidth: 1200, clientHeight: 800 } }, engine.camera.flyTo = (options) => { flight = options; };
render: { request() {} },
camera: {
pose: () => ({
cartographic: { longitude: 0, latitude: 0 },
heading: 0,
fovy: Math.PI / 3,
}),
flyTo: (options) => { flight = options; },
},
};
const ring = Object.assign(Object.create(CelestialRing.prototype), { const ring = Object.assign(Object.create(CelestialRing.prototype), {
_engine: engine, _engine: engine,
}); });
+11 -20
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import test from 'node:test'; 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() {} }; globalThis.window = { setTimeout: () => 1, clearTimeout() {} };
const first = eventViewer(); const first = eventViewer();
const second = eventViewer(); const second = eventViewer();
const engineB = { const engineB = createFakeEngine({ canvas: second.scene.canvas });
id: 'B', engineB.id = 'B';
host: { canvas: second.scene.canvas },
camera: second.camera,
};
const hosts = new Map(); const hosts = new Map();
const hostFor = (engine) => { const hostFor = (engine) => {
const key = engine ?? 'A'; const key = engine ?? 'A';
@@ -3216,16 +3214,11 @@ async function navaraEvidenceCard({ sample, signal }) {
installEventDom(); installEventDom();
globalThis.window = { setTimeout: () => 1, clearTimeout() {} }; globalThis.window = { setTimeout: () => 1, clearTimeout() {} };
const entries = []; const entries = [];
const engine = { const engine = createFakeEngine({ canvas: eventViewer().scene.canvas });
terrain: 3500, engine.terrain = 3500;
host: { canvas: eventViewer().scene.canvas }, engine.fake.script('terrainHeight', () => engine.terrain);
surface: { engine.fake.script('sampleTerrainMostDetailed', sample);
terrainHeight: () => engine.terrain, engine.maps.terrainRevision = () => 1;
sampleTerrainMostDetailed: sample,
},
maps: { terrainRevision: () => 1 },
camera: { livePosition: Cesium.Cartesian3.fromDegrees(0, 0, 1e7) },
};
const layer = buildEventLayer({ const layer = buildEventLayer({
eventLoader: async () => event, eventLoader: async () => event,
imageLoader: async (url) => ({ url }), 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'); assert.ok(Math.abs(height() - pack) < 0.01, 'the pack elevation');
card.position(); card.position();
assert.ok(Math.abs(height() - pack) < 0.01, 'a far camera keeps it'); assert.ok(Math.abs(height() - pack) < 0.01, 'a far camera keeps it');
engine.camera.livePosition = Cesium.Cartesian3.fromDegrees( engine.fake.setPose({
first.lon, position: Cesium.Cartesian3.fromDegrees(first.lon, first.lat, 6000),
first.lat, });
6000,
);
engine.terrain = 2000; engine.terrain = 2000;
assert.ok(card.position(), 'the first card is shown'); assert.ok(card.position(), 'the first card is shown');
assert.ok( 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 // 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 // request was cancelled, does not change the live scene; both engines show the
// same area and the same target. // same area and the same target.
@@ -42,11 +43,8 @@ function locator(engine) {
remove: (source) => shown.delete(source), remove: (source) => shown.delete(source),
}, },
}; };
const navara = { const navara = createFakeEngine({ kind: 'navara' });
kind: 'navara', navara.render.request = (reason) => renders.push(reason);
render: { request: (reason) => renders.push(reason) },
credits: { add() {}, remove() {} },
};
const layer = createBhoteKoshiLocatorLayer({ const layer = createBhoteKoshiLocatorLayer({
boundaryResolver: (signal) => { boundaryResolver: (signal) => {
const lookup = deferred(); 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); const enabling = layer.enable(viewer);
layer.detachRenderer(); layer.detachRenderer();
layer.attachRenderer({ layer.attachRenderer({
engine: { engine: createFakeEngine({ kind: 'navara' }),
kind: 'navara',
render: { request() {} },
credits: { add() {}, remove() {} },
},
}); });
lookup.resolve({ ring: LIVE_RING }); lookup.resolve({ ring: LIVE_RING });
assert.equal(await enabling, true); 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 { test } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { 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. // A Cesium engine's credits over a viewer double's credit display.
async function cesiumCreditsEngine(viewer) { async function cesiumCreditsEngine(viewer) {
const { createCesiumCredits } = await import('../globe/cesium/credits.js'); const { createCesiumCredits } = await import('../globe/cesium/credits.js');
return { return Object.assign(createFakeEngine(), {
credits: createCesiumCredits(viewer).credits, credits: createCesiumCredits(viewer).credits,
render: { request() {} }, });
};
} }
test('inline OSM attribution persists until its last display owner leaves', async (t) => { 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 test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { LayerLifecycle } from './lifecycle.js'; 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 firstViewer = { name: 'first viewer' };
const secondViewer = { name: 'second 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'); const attaches = (calls) => calls.filter((call) => call[0] === 'attach');
test('one failing detachRenderer leaves the others released and the manager unbound', async () => { test('one failing detachRenderer leaves the others released and the manager unbound', async () => {
const second = engine('second');
let failures = 1; let failures = 1;
const broken = sceneLayer('broken', { const broken = sceneLayer('broken', {
detach() { 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. // The next scene binds and both retained layers attach to it.
await bindCatalog( await bindCatalog(
manager, manager,
engine('second'), second,
catalog(...modules), catalog(...modules),
); );
assert.equal(manager.viewer, secondViewer); assert.equal(manager.viewer, secondViewer);
assert.deepEqual(attaches(broken.calls).at(-1), ['attach', engine('second')]); assert.deepEqual(attaches(broken.calls).at(-1), ['attach', second]);
assert.deepEqual(attaches(after.calls).at(-1), ['attach', engine('second')]); assert.deepEqual(attaches(after.calls).at(-1), ['attach', second]);
assert.equal(manager.isEnabled('broken'), true); assert.equal(manager.isEnabled('broken'), true);
}); });
test('bindCatalog after a failed detach retries the release before attaching', async () => { test('bindCatalog after a failed detach retries the release before attaching', async () => {
const second = engine('second');
let failures = 1; let failures = 1;
const broken = sceneLayer('broken', { const broken = sceneLayer('broken', {
detach() { detach() {
@@ -146,13 +149,13 @@ test('bindCatalog after a failed detach retries the release before attaching', a
await bindCatalog( await bindCatalog(
manager, manager,
engine('second'), second,
catalog(...modules), catalog(...modules),
); );
assert.deepEqual(broken.calls.slice(-3), [ assert.deepEqual(broken.calls.slice(-3), [
['detach', firstViewer], ['detach', firstViewer],
['detach', firstViewer], ['detach', firstViewer],
['attach', engine('second')], ['attach', second],
]); ]);
// The scene is settled: the next detach releases the second viewer once. // The scene is settled: the next detach releases the second viewer once.
await manager.detachScene(); 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 () => { test('a layer enabled between scenes initializes on the next scene', async () => {
const second = engine('second');
const late = sceneLayer('late'); const late = sceneLayer('late');
const present = sceneLayer('present'); const present = sceneLayer('present');
const { manager, modules } = await boundManager(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' }); const enabled = manager.setEnabled('late', true, { origin: 'user' });
await tick(); await tick();
assert.deepEqual(late.calls, [], 'no init against a missing scene'); 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.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 () => { 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 { readFileSync } from 'node:fs';
import { readLayerSource } from '../testSupport/readLayerSource.mjs'; import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import { test } from 'node:test'; import { test } from 'node:test';
@@ -77,14 +78,14 @@ const {
} = militaryAwarenessLayer; } = militaryAwarenessLayer;
const viewerEngines = new WeakMap(); const viewerEngines = new WeakMap();
const viewerEngine = (viewer) => { const viewerEngine = (viewer) => {
if (!viewerEngines.has(viewer)) viewerEngines.set(viewer, { if (!viewerEngines.has(viewer)) {
kind: 'cesium', viewer, const engine = createFakeEngine();
render: { engine.viewer = viewer;
request: () => viewer.scene.requestRender?.(), engine.render.request = () => viewer.scene.requestRender?.();
setContinuous: (enabled) => { viewer.scene.requestRenderMode = !enabled; }, engine.render.setContinuous = (enabled) => { viewer.scene.requestRenderMode = !enabled; };
isContinuous: () => viewer.scene.requestRenderMode === false, engine.render.isContinuous = () => viewer.scene.requestRenderMode === false;
}, viewerEngines.set(viewer, engine);
}); }
return viewerEngines.get(viewer); 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. */ /** A layer on the fake engine, whose imagery drapes where `surface()` says. */
function setup(overlay = OSM_OVERLAYS[0]) { function setup(overlay = OSM_OVERLAYS[0]) {
let surface = 'globe'; let surface = 'globe';
const fake = createFakeEngine({ kind: 'cesium' }); const engine = createFakeEngine({ kind: 'cesium' });
const engine = { engine.imagery.surface = () => surface;
...fake,
imagery: { ...fake.imagery, surface: () => surface },
};
const layer = createOsmOverlayLayer(overlay); const layer = createOsmOverlayLayer(overlay);
layer.init(null, { engine }); layer.init(null, { engine });
return { 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', () => { test('on Navara an overlay drapes through engine.maps and follows the globe', () => {
const added = []; const added = [];
const engineWith = () => ({ const engineWith = () => {
maps: { const engine = createFakeEngine({ kind: 'navara' });
addOverlay(options) { engine.maps.addOverlay = (options) => {
const entry = { options, removed: false }; const entry = { options, removed: false };
added.push(entry); added.push(entry);
return () => (entry.removed = true); return () => (entry.removed = true);
}, };
}, return engine;
}); };
const [seaMarks] = createOsmOverlayLayers(); const [seaMarks] = createOsmOverlayLayers();
seaMarks.init(null, { engine: engineWith() }); seaMarks.init(null, { engine: engineWith() });
assert.equal(added.length, 0, 'nothing until enabled'); 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 assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
@@ -11,7 +12,7 @@ globalThis.CustomEvent ||= class CustomEvent {
globalThis.dispatchEvent ||= () => true; globalThis.dispatchEvent ||= () => true;
const { registerEntityContext } = await import('./contextStore.js'); const { registerEntityContext } = await import('./contextStore.js');
const engine = {}; const engine = createFakeEngine();
const { pickRegistryFor } = await import('./pickRegistry.js'); const { pickRegistryFor } = await import('./pickRegistry.js');
const { registerPickOwner, unregisterPickOwner, findPickOwner } = pickRegistryFor(engine); const { registerPickOwner, unregisterPickOwner, findPickOwner } = pickRegistryFor(engine);
const { resolvePickedContext, pickedKindForLayer } = await import('./pickedContext.js'); 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 assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { 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', () => { test('Radio camera navigation yields to every live tracked-entity owner', () => {
let following = null; let following = null;
const engine = { camera: { following: () => following } }; const engine = createFakeEngine();
engine.camera.following = () => following;
assert.equal(radioCameraNavigationAllowed(engine), true); assert.equal(radioCameraNavigationAllowed(engine), true);
for (const gevTrackedId of ['flights:abc123', 'military:def456']) { for (const gevTrackedId of ['flights:abc123', 'military:def456']) {
const trackedEntity = { gevTrackedId }; 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: // Boot fires no stack event; the first drape comes from the engine:
// photoreal tiles shown ⇔ the globe hidden. // photoreal tiles shown ⇔ the globe hidden.
const engine = (photoreal) => ({ const engine = (photoreal) => {
maps: { photorealActive: () => photoreal }, const globe = createFakeEngine();
}); globe.maps.photorealActive = () => photoreal;
return globe;
};
assert.equal(cableDrapeForEngine(engine(true)), 'tiles'); assert.equal(cableDrapeForEngine(engine(true)), 'tiles');
assert.equal(cableDrapeForEngine(engine(false)), 'terrain'); assert.equal(cableDrapeForEngine(engine(false)), 'terrain');
assert.equal(cableDrapeForEngine(null), 'both'); 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'; import { readLayerSource } from '../testSupport/readLayerSource.mjs';
// src/data/trackedModelRegime.test.mjs // src/data/trackedModelRegime.test.mjs
// //
@@ -172,16 +173,12 @@ function viewerAtHeight(height, { tilesLoaded = true, sampleHeight = () => undef
trackedEntity: undefined, trackedEntity: undefined,
entities: { remove() {} }, entities: { remove() {} },
}; };
viewer.engine = { viewer.engine = createFakeEngine();
isDestroyed: () => false, viewer.engine.fake.script('tilesLoaded', () => viewer.tileset.tilesLoaded === true);
surface: { viewer.engine.fake.script('sampleHeightDetailed', (place, { exclude } = {}) => {
tilesLoaded: () => viewer.tileset.tilesLoaded === true, const h = viewer.scene.sampleHeight(place, exclude);
sampleHeightDetailed(place, { exclude } = {}) { return Number.isFinite(h) ? { height: h, source: 'tiles' } : undefined;
const h = viewer.scene.sampleHeight(place, exclude); });
return Number.isFinite(h) ? { height: h, source: 'tiles' } : undefined;
},
},
};
return viewer; return viewer;
} }
+3 -12
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createTrackedReadout } from './trackedReadout.js'; import { createTrackedReadout } from './trackedReadout.js';
@@ -19,18 +20,8 @@ test('followed aircraft card anchors to the model the person sees (gevVisualPosi
position: () => native.gevDisplayPosition(), position: () => native.gevDisplayPosition(),
}, },
}; };
const listeners = []; const engine = createFakeEngine();
const engine = { engine.camera.following = () => handle;
camera: {
following: () => handle,
followChanged: {
addEventListener: (f) => {
listeners.push(f);
return () => {};
},
},
},
};
let entries = null; let entries = null;
const overlay = { const overlay = {
setEntries(_id, e) { 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 assert from 'node:assert/strict';
import { readLayerSource } from '../testSupport/readLayerSource.mjs'; import { readLayerSource } from '../testSupport/readLayerSource.mjs';
import { mkdtempSync, rmSync } from 'node:fs'; 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(); const changed = eventChannel();
let longitude = -97.74; let longitude = -97.74;
const cartographic = { latitude: Math.PI / 6, longitude: 0, height: 5000 }; const cartographic = { latitude: Math.PI / 6, longitude: 0, height: 5000 };
const camera = { // A zero-size canvas has no reticle footprint; the view rectangle bounds the fetch.
moveEnd, const engine = createFakeEngine({
changed, width: 0,
percentageChanged: 0.5, height: 0,
livePosition: { x: 0, y: 0, z: 0 }, canvas: { clientWidth: 0, clientHeight: 0, width: 0, height: 0 },
pose: () => ({ cartographic: { ...cartographic }, heading: 0, pitch: -Math.PI / 2 }), });
viewRectangle() { engine.camera.moveEnd = moveEnd;
const west = longitude * Math.PI / 180; engine.camera.changed = changed;
return { engine.camera.pose = () => ({ cartographic: { ...cartographic }, heading: 0, pitch: -Math.PI / 2 });
south: 30 * Math.PI / 180, engine.fake.script('viewRectangle', () => {
west, const west = longitude * Math.PI / 180;
north: 30.02 * Math.PI / 180, return {
east: west + 0.02 * Math.PI / 180, south: 30 * Math.PI / 180,
}; west,
}, north: 30.02 * Math.PI / 180,
pickEllipsoid: () => undefined, 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) => { const setLongitude = (next) => {
longitude = next; longitude = next;
cartographic.longitude = next * Math.PI / 180; cartographic.longitude = next * Math.PI / 180;
}; };
setLongitude(longitude); 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.clearMarks();
performance.clearMeasures(); performance.clearMeasures();
+8 -10
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../testing/fakeEngine.js';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { import {
@@ -42,16 +43,13 @@ function harness() {
}, },
}; };
const pickers = new Set(); const pickers = new Set();
const engine = { const engine = createFakeEngine({ kind: 'navara' });
render: { request: () => requests++ }, engine.render.request = () => requests++;
camera: { livePosition: { x: 0, y: 0, z: 0 }, pickRay: () => null }, engine.camera.pickRay = () => null;
picker: { engine.fake.script('pickPosition', undefined);
pickPosition: () => undefined, engine.picker.registerMeshPicker = (pick) => {
registerMeshPicker(pick) { pickers.add(pick);
pickers.add(pick); return () => pickers.delete(pick);
return () => pickers.delete(pick);
},
},
}; };
const port = createNavaraMeshes(view, { const port = createNavaraMeshes(view, {
overlay, overlay,
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../testing/fakeEngine.js';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { import {
@@ -140,7 +141,7 @@ test('pack meshes draw in the overlay and test the scene depth', () => {
camera: {}, camera: {},
picker: { registerMeshPicker: () => () => {} }, picker: { registerMeshPicker: () => () => {} },
}); });
const engine = { meshes: port.meshes, render: { request() {} } }; const engine = Object.assign(createFakeEngine(), { meshes: port.meshes });
createPackRenderer(engine).image( createPackRenderer(engine).image(
{ id: 'p', placement: { bounds: [0, 0, 0.01, 0.01], height: 300 } }, { 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 assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { createAircraftSceneBinding } from './sceneBinding.js'; import { createAircraftSceneBinding } from './sceneBinding.js';
import { LayerBindings } from '../../ui/layerBindings.js'; import { LayerBindings } from '../../ui/layerBindings.js';
import { createEngineEvent } from '../../globe/contract.js';
import { readLayerSource } from '../../testSupport/readLayerSource.mjs'; import { readLayerSource } from '../../testSupport/readLayerSource.mjs';
function observedShell(t) { function observedShell(t) {
@@ -11,13 +11,14 @@ function observedShell(t) {
t.after(() => { t.after(() => {
globalThis.window = previous; globalThis.window = previous;
}); });
const followChanged = createEngineEvent(); const engine = createFakeEngine();
const followChanged = engine.camera.followChanged;
const shell = { const shell = {
generation: 1, generation: 1,
followChanged, followChanged,
}; };
const bindings = new LayerBindings({ const bindings = new LayerBindings({
engine: { camera: { followChanged } }, engine: engine,
services: {}, services: {},
operations: { operations: {
_stampNavigation() { _stampNavigation() {
@@ -111,10 +111,8 @@ function viewerAt(height) {
trackedEntity: undefined, trackedEntity: undefined,
entities: { remove() {} }, entities: { remove() {} },
}; };
viewer.engine = { viewer.engine = createFakeEngine();
isDestroyed: () => false, viewer.engine.fake.script('sampleHeightDetailed', undefined);
surface: { tilesLoaded: () => true, sampleHeightDetailed: () => undefined },
};
return viewer; return viewer;
} }
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js'; import * as geo from '../../geo/index.js';
@@ -50,7 +51,7 @@ function harness(layer, { groundM = null, track = [] } = {}) {
_trailPositions: [], _trailPositions: [],
_trailBackfillToken: 0, _trailBackfillToken: 0,
_renderer: { _renderer: {
engine: {}, engine: createFakeEngine(),
follow: { onChanged: () => () => {} }, 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 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. // the HOST=0.0.0.0 LAN opt-in), where crypto.randomUUID does not exist.
import { test } from 'node:test'; import { test } from 'node:test';
@@ -34,7 +35,7 @@ test('ALPR overlay init works without crypto.randomUUID', (t) => {
}, },
}, },
}, },
engine: {}, engine: createFakeEngine(),
markers: new Map(), markers: new Map(),
syncMarker() {}, syncMarker() {},
sampleHeight() {}, 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 // 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; // 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 // 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, distance: () => 2000,
cameraPose: () => pose, cameraPose: () => pose,
fovy: () => pose.fovy, fovy: () => pose.fovy,
engine: { camera: { pose: () => pose } }, engine: createFakeEngine({ fovy: FOVY_16_9 }),
}; };
const { refreshHorizonCulling } = createRendering({ const { refreshHorizonCulling } = createRendering({
state, state,
+1 -1
View File
@@ -82,7 +82,7 @@ test('edge:earthquakes: an expired record is dropped and a duplicate never forks
FIXED_SAMPLE, FIXED_SAMPLE,
WITH_DUPLICATE, WITH_DUPLICATE,
]); ]);
layer.init(null, { engine: {} }); layer.init(null, { engine: createFakeEngine() });
layer.enable(); layer.enable();
assert.equal(await layer.update(), true); assert.equal(await layer.update(), true);
assert.equal(await layer.update(), false); 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 test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createViewport } from './viewport.js'; import { createViewport } from './viewport.js';
@@ -22,11 +23,14 @@ function harness() {
const moveEnd = signal(), const moveEnd = signal(),
preRender = signal(), preRender = signal(),
requests = []; requests = [];
const engine = createFakeEngine();
engine.camera.moveEnd = moveEnd;
engine.frames.preRender = preRender;
const layerState = { const layerState = {
_enabled: true, _enabled: true,
_destroyed: false, _destroyed: false,
_view: { camera: {} }, _view: { camera: {} },
_renderer: { engine: { camera: { moveEnd }, frames: { preRender } } }, _renderer: { engine: engine },
}; };
const viewport = createViewport({ const viewport = createViewport({
layerState, layerState,
+5 -4
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import { import {
element, element,
kmz, kmz,
@@ -74,7 +75,7 @@ function harness(t) {
let listener = null; let listener = null;
const layer = createGhostMapsLayer({ const layer = createGhostMapsLayer({
storage: null, storage: null,
createRenderer: (engine) => fakeRenderer(engine.name, live), createRenderer: (engine) => fakeRenderer(engine.kind, live),
source: { source: {
getCatalog: async () => catalog('v1'), getCatalog: async () => catalog('v1'),
getFile: (_id, version) => files.get(version).promise, 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(); layer.enable();
t.after(() => layer.destroy()); t.after(() => layer.destroy());
const file = (version) => { 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); await new Promise(setImmediate);
push(catalog('v3')); push(catalog('v3'));
layer.detachRenderer(); layer.detachRenderer();
await layer.attachRenderer({ engine: { name: 'navara' } }); await layer.attachRenderer({ engine: createFakeEngine({ kind: 'navara' }) });
v2.resolve(kmz('edition two')); v2.resolve(kmz('edition two'));
v3.resolve(kmz('edition three')); v3.resolve(kmz('edition three'));
// Let the queued syncs settle. // 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')); push(catalog('v2'));
// The engine changes while the broken file is still on its way. // The engine changes while the broken file is still on its way.
layer.detachRenderer(); layer.detachRenderer();
await layer.attachRenderer({ engine: { name: 'navara' } }); await layer.attachRenderer({ engine: createFakeEngine({ kind: 'navara' }) });
broken.resolve(new Uint8Array([1, 2, 3, 4])); broken.resolve(new Uint8Array([1, 2, 3, 4]));
for (let i = 0; i < 20; i += 1) await new Promise(setImmediate); 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 { import {
element, element,
kmz, kmz,
@@ -58,10 +59,10 @@ for (const action of ['expire', 'disable']) {
onClick: () => () => {}, onClick: () => () => {},
draw(id, features) { draw(id, features) {
const gate = deferred(); const gate = deferred();
if (engine.name === 'navara') gates.push(gate); if (engine.kind === 'navara') gates.push(gate);
else gate.resolve(); else gate.resolve();
const paint = { const paint = {
engine: engine.name, engine: engine.kind,
names: [...features.values()].map((feature) => feature.name), names: [...features.values()].map((feature) => feature.name),
dropped: [], dropped: [],
ready: gate.promise, ready: gate.promise,
@@ -84,7 +85,7 @@ for (const action of ['expire', 'disable']) {
getFile: async () => kmz('old event'), getFile: async () => kmz('old event'),
}, },
}); });
layer.init(null, { engine: { name: 'cesium' } }); layer.init(null, { engine: createFakeEngine({ kind: 'cesium' }) });
layer.enable(); layer.enable();
t.after(() => layer.destroy()); t.after(() => layer.destroy());
await layer.update(); await layer.update();
@@ -92,7 +93,9 @@ for (const action of ['expire', 'disable']) {
// Engine switch: the new drawing is still decoding its icons. // Engine switch: the new drawing is still decoding its icons.
layer.detachRenderer(); layer.detachRenderer();
const attached = layer.attachRenderer({ engine: { name: 'navara' } }); const attached = layer.attachRenderer({
engine: createFakeEngine({ kind: 'navara' }),
});
await new Promise(setImmediate); await new Promise(setImmediate);
assert.equal(gates.length, 1); assert.equal(gates.length, 1);
// The event leaves its age window; the expiry pass runs meanwhile. // 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 { createGnssDisplay } from './render/display.js';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
@@ -82,18 +83,15 @@ const threeHealthy = () => [
const flush = () => new Promise((resolve) => setImmediate(resolve)); const flush = () => new Promise((resolve) => setImmediate(resolve));
test('a globe-wide rectangle uses the visible screen centre as the query anchor', () => { test('a globe-wide rectangle uses the visible screen centre as the query anchor', () => {
const anchor = defaultGnssViewAnchor({ const engine = createFakeEngine({ width: 1000, height: 800 });
host: { size: () => ({ width: 1000, height: 800 }) }, engine.fake.script('pickEllipsoid', (pixel) => {
camera: { assert.deepEqual(pixel, { x: 500, y: 400 });
pickEllipsoid(pixel) { return geo.cartesian3.fromDegrees(-97, 30);
assert.deepEqual(pixel, { x: 500, y: 400 });
return geo.cartesian3.fromDegrees(-97, 30);
},
viewRectangle: () => {
throw new Error('centre hit must not compute a rectangle');
},
},
}); });
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.latitude - 30) < 1e-8);
assert.ok(Math.abs(anchor.longitude + 97) < 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', () => { test('the layer requires a snapshot source and cannot be initialized twice', () => {
assert.throws(() => createGnssIntegrityLayer({}), TypeError); assert.throws(() => createGnssIntegrityLayer({}), TypeError);
const { layer } = harness(async () => ({ rows: [] })); 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 () => { 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', () => { test('view anchor falls back to rectangle, then nadir, then no camera', () => {
assert.equal(defaultGnssViewAnchor({}), null); assert.equal(defaultGnssViewAnchor({}), null);
const radians = geo.math.toRadians; const radians = geo.math.toRadians;
const engine = { const engine = createFakeEngine({ width: 1000, height: 800 });
host: { size: () => ({ width: 1000, height: 800 }) }, engine.fake.script('pickEllipsoid', undefined);
camera: { engine.fake.script('viewRectangle', {
pickEllipsoid: () => undefined, west: radians(20),
viewRectangle: () => ({ south: radians(40),
west: radians(20), east: radians(22),
south: radians(40), north: radians(42),
east: radians(22), });
north: radians(42), engine.camera.pose = () => assert.fail('a rectangle must not read the pose');
}),
pose: () => assert.fail('a rectangle must not read the pose'),
},
};
const rectangleAnchor = defaultGnssViewAnchor(engine); const rectangleAnchor = defaultGnssViewAnchor(engine);
assert.ok(Math.abs(rectangleAnchor.latitude - 41) < 1e-10); assert.ok(Math.abs(rectangleAnchor.latitude - 41) < 1e-10);
assert.ok(Math.abs(rectangleAnchor.longitude - 21) < 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() {}, lod() {},
}), }),
); );
layer.init(null, { engine: {} }); layer.init(null, { engine: createFakeEngine() });
const enabled = layer.enable(); const enabled = layer.enable();
await new Promise(setImmediate); await new Promise(setImmediate);
layer.detachRenderer(); layer.detachRenderer();
@@ -57,9 +57,9 @@ test('a bundled file delivered between planets keeps multipart IDs and is read o
await enabled; await enabled;
assert.equal(layer.getAnalystRecords().length, 2); assert.equal(layer.getAnalystRecords().length, 2);
assert.equal(captures.length, 0); assert.equal(captures.length, 0);
await layer.attachRenderer({ engine: {} }); await layer.attachRenderer({ engine: createFakeEngine() });
layer.detachRenderer(); layer.detachRenderer();
await layer.attachRenderer({ engine: {} }); await layer.attachRenderer({ engine: createFakeEngine() });
assert.equal(captures[0], captures[1]); assert.equal(captures[0], captures[1]);
assert.deepEqual( assert.deepEqual(
captures[0].records.map((r) => r.id), 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() {}, lod() {},
}), }),
); );
layer.init(null, { engine: {} }); layer.init(null, { engine: createFakeEngine() });
const enabled = layer.enable(); const enabled = layer.enable();
await new Promise(setImmediate); await new Promise(setImmediate);
layer.detachRenderer(); layer.detachRenderer();
const scene = new AbortController(); const scene = new AbortController();
const attaching = layer.attachRenderer({ const attaching = layer.attachRenderer({
engine: {}, engine: createFakeEngine(),
signal: scene.signal, signal: scene.signal,
}); });
scene.abort(); scene.abort();
+6 -7
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js'; 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', () => { test('focus frames a selected installation through the globe engine camera', () => {
const record = { id: 'osm:node:12', latitude: 70, longitude: 179.8 }; const record = { id: 'osm:node:12', latitude: 70, longitude: 179.8 };
let flight = null; let flight = null;
const engine = createFakeEngine();
engine.camera.flyToSphere = (sphere, options) => {
flight = { sphere, options };
};
const state = { const state = {
engine: { engine: engine,
camera: {
flyToSphere: (sphere, options) => {
flight = { sphere, options };
},
},
},
recordById: new Map([[record.id, record]]), recordById: new Map([[record.id, record]]),
}; };
const controls = createControls({ const controls = createControls({
+11 -10
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import * as Cesium from 'cesium'; 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 () => { test('Navara installation ingestion loads from its engine camera without a viewer', async () => {
const { createIngestion } = await import('./ingestion.js'); const { createIngestion } = await import('./ingestion.js');
const rectangle = Cesium.Rectangle.fromDegrees(-123, 37, -122, 38); const rectangle = Cesium.Rectangle.fromDegrees(-123, 37, -122, 38);
const engine = createFakeEngine({ kind: 'navara' });
engine.fake.script('viewRectangle', () => rectangle);
const state = { const state = {
enabled: true, enabled: true,
viewer: null, viewer: null,
engine: { camera: { viewRectangle: () => rectangle } }, engine: engine,
records: [], records: [],
recordById: new Map(), recordById: new Map(),
coverage: { kind: 'viewport' }, coverage: { kind: 'viewport' },
@@ -242,18 +245,16 @@ test('the engine camera drives installation viewport requests without a viewer',
east: Cesium.Math.toRadians(-178), east: Cesium.Math.toRadians(-178),
north: Cesium.Math.toRadians(16), 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 = { const state = {
enabled: true, enabled: true,
viewer: null, viewer: null,
engine: { engine: engine,
camera: {
viewRectangle: () => rectangle,
pose: () => ({
position: { x: 1, y: 2, z: 3 },
direction: { x: -1, y: 0, z: 0 },
}),
},
},
}; };
const loads = []; const loads = [];
const parts = { const parts = {
+4 -6
View File
@@ -1,15 +1,13 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createEngineEvent } from '../../globe/contract.js';
import { createLaunchHover } from './render/display.js'; import { createLaunchHover } from './render/display.js';
// What the hover reads of an engine: its canvas, drillPick and camera motion. // What the hover reads of an engine: its canvas, drillPick and camera motion.
function hoverEngine(drillPick, style = { cursor: '' }) { function hoverEngine(drillPick, style = { cursor: '' }) {
return { const engine = createFakeEngine({ canvas: { style } });
host: { canvas: { style } }, engine.fake.script('drillPick', drillPick);
picker: { drillPick }, return engine;
camera: { moveStart: createEngineEvent(), moveEnd: createEngineEvent() },
};
} }
function fakeTimers() { function fakeTimers() {
+6 -6
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js'; import * as geo from '../../geo/index.js';
@@ -10,12 +11,11 @@ test('selected pad frame uses neutral geometry and the physical orbit epoch', ()
epoch; epoch;
const state = { const state = {
orbitNow: worldTimeFor('satellites'), orbitNow: worldTimeFor('satellites'),
_engine: { _engine: createFakeEngine({
camera: { longitude: geo.math.toRadians(launch.lon),
livePosition: geo.cartesian3.fromDegrees(launch.lon, launch.lat, 500), latitude: geo.math.toRadians(launch.lat),
height: () => 500, altitude: 500,
}, }),
},
_enabled: true, _enabled: true,
_selectedLaunchId: 'qa', _selectedLaunchId: 'qa',
_launches: [launch], _launches: [launch],
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createLifecycle } from './lifecycle.js'; import { createLifecycle } from './lifecycle.js';
@@ -8,7 +9,7 @@ test('missions preserve the satellite target before their focus flight releases
let target = 25544, let target = 25544,
snapshot = null; snapshot = null;
const lifecycle = createLifecycle({ const lifecycle = createLifecycle({
state: { _engine: {} }, state: { _engine: createFakeEngine() },
services: { render: {} }, services: { render: {} },
getRenderer: () => ({ setVisible() {} }), getRenderer: () => ({ setVisible() {} }),
parts: { parts: {
@@ -46,7 +47,7 @@ test('missions begin their entry flight while the satellite catalog loads', asyn
}); });
let flew = false; let flew = false;
const lifecycle = createLifecycle({ const lifecycle = createLifecycle({
state: { _engine: {} }, state: { _engine: createFakeEngine() },
services: { render: {} }, services: { render: {} },
getRenderer: () => ({ setVisible() {} }), getRenderer: () => ({ setVisible() {} }),
parts: { parts: {
+2 -3
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createState } from './state.js'; 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', () => { test('resuming a replay restores its retained chase heading', () => {
const { state } = replayRig(); const { state } = replayRig();
state._engine = { state._engine = createFakeEngine();
frames: { preUpdate: { addEventListener: () => () => {} } },
};
state._launches = [{ id: 'mission' }]; state._launches = [{ id: 'mission' }];
state._replayCameraLaunchId = 'mission'; state._replayCameraLaunchId = 'mission';
state._replayCameraHeading = 1.2; 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, frustumTester: () => () => true,
horizon: () => ({ isPointVisible: () => true }), horizon: () => ({ isPointVisible: () => true }),
engine: {}, engine: createFakeEngine(),
}, },
}; };
const exclusions = []; const exclusions = [];
+2 -4
View File
@@ -728,14 +728,12 @@ function failingMapController() {
const controller = new MapSourceController({ const controller = new MapSourceController({
registry, registry,
initialStack: 'photoreal', initialStack: 'photoreal',
engine: { engine: Object.assign(createFakeEngine(), {
kind: 'cesium',
render: { request() {} },
maps: createCesiumMaps(viewer, { maps: createCesiumMaps(viewer, {
credits: createCesiumCredits(viewer).credits, credits: createCesiumCredits(viewer).credits,
createImageryLayer: (provider) => ({ provider }), createImageryLayer: (provider) => ({ provider }),
}).maps, }).maps,
}, }),
onError: () => {}, onError: () => {},
}); });
const calls = []; const calls = [];
@@ -93,7 +93,7 @@ test('restored orbit rings receive their current rotation before the first frame
}, },
}); });
lifecycle.methods.attachRenderer({ lifecycle.methods.attachRenderer({
engine: {}, engine: createFakeEngine(),
viewRestored: new Promise(() => {}), viewRestored: new Promise(() => {}),
}); });
assert.equal(rotated, 1); 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). // the near plane, as the engines answer).
function cameraEngine(scene) { function cameraEngine(scene) {
const eye = new C.Cartesian3(); const eye = new C.Cartesian3();
return { const engine = createFakeEngine({
host: { width: scene.canvas.clientWidth,
size: () => ({ height: scene.canvas.clientHeight,
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;
projector: { return C.SceneTransforms.worldToWindowCoordinates(scene, point, result);
worldToWindow(point, result) { });
C.Matrix4.multiplyByPoint(scene.camera.viewMatrix, point, eye); return engine;
if (eye.z >= -scene.camera.frustum.near) return undefined;
return C.SceneTransforms.worldToWindowCoordinates(scene, point, result);
},
},
};
} }
const bounds = { west: 0, east: 0.01, south: 0, north: 0.01 }; 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 test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js'; 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 // An engine-shaped stub: the camera's pose and its ellipsoid and ray
// picks (the sky above the 300 px line), and the canvas. // picks (the sky above the 300 px line), and the canvas.
const engine = { const engine = createFakeEngine({
camera: { longitude: geo.math.toRadians(lon),
pose: () => ({ latitude: geo.math.toRadians(lat),
cartographic: geo.cartographic.fromDegrees(lon, lat, 300), altitude: 300,
heading: 0, canvas: { width: 1280, height: 800 },
pitch: -Math.PI / 12, });
}), engine.fake.setPose({
pickEllipsoid(p) { longitude: geo.math.toRadians(lon),
if (p.y < 300) return undefined; latitude: geo.math.toRadians(lat),
return geo.cartesian3.fromDegrees( height: 300,
lon + (p.x - 640) / 100000, heading: 0,
lat + (800 - p.y) / 50000, pitch: -Math.PI / 12,
0, });
); engine.fake.script('pickEllipsoid', (p) => {
}, if (p.y < 300) return undefined;
pickRay: () => ({ return geo.cartesian3.fromDegrees(
origin: geo.cartesian3.create(), lon + (p.x - 640) / 100000,
direction: geo.cartesian3.create( lat + (800 - p.y) / 50000,
-Math.sin((lat * Math.PI) / 180) * Math.cos((lon * Math.PI) / 180), 0,
-Math.sin((lat * Math.PI) / 180) * Math.sin((lon * Math.PI) / 180), );
Math.cos((lat * Math.PI) / 180), });
), engine.camera.pickRay = () => ({
}), origin: geo.cartesian3.create(),
}, direction: geo.cartesian3.create(
host: { canvas: { width: 1280, height: 800 } }, -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); const result = createReticleFootprint()(engine, 300);
assert.ok(result.coverage.coarse.length <= 16); assert.ok(result.coverage.coarse.length <= 16);
assert.ok(result.coverage.detail.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 // Traffic claims through its engine's camera, as bikeshare does: the
// viewer's. The stub has what binding its scene subscribes to. // viewer's. The stub has what binding its scene subscribes to.
const listeners = { addEventListener: () => noop }; const listeners = { addEventListener: () => noop };
const trafficEngine = { const trafficEngine = createFakeEngine();
camera: app.viewer.camera, trafficEngine.camera = app.engine.camera;
frames: { preRender: listeners, postRender: listeners }, trafficEngine.frames.preRender = listeners;
projector: { viewProjection: noop }, trafficEngine.frames.postRender = listeners;
surface: { tilesLoaded: () => false }, trafficEngine.projector.viewProjection = noop;
}; trafficEngine.fake.script('tilesLoaded', false);
const traffic = trafficLifecycle({ const traffic = trafficLifecycle({
state: { _engine: trafficEngine, _loadGeneration: 0 }, state: { _engine: trafficEngine, _loadGeneration: 0 },
services: { services: {
+8 -9
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
// Edge cases for wind: on a fixed // Edge cases for wind: on a fixed
// grid direction, speed and legend agree whichever way the centre is picked, // grid direction, speed and legend agree whichever way the centre is picked,
// and an empty field is never presented as calm. // and an empty field is never presented as calm.
@@ -24,15 +25,13 @@ const options = {
status: 'Model forecast', status: 'Model forecast',
}; };
/** Inspect either the current map centre or a captured position. */ /** Inspect either the current map centre or a captured position. */
const viaEngine = (snapshot, point) => const viaEngine = (snapshot, point) => {
inspectWindAtCenter( const engine = createFakeEngine({
snapshot, canvas: { clientWidth: 800, clientHeight: 600 },
{ });
camera: { pickEllipsoid: () => point }, engine.fake.script('pickEllipsoid', point);
host: { canvas: { clientWidth: 800, clientHeight: 600 } }, return inspectWindAtCenter(snapshot, engine, options);
}, };
options,
);
const viaPosition = (snapshot, point) => const viaPosition = (snapshot, point) =>
inspectWindAtCenter(snapshot, null, { ...options, position: point }); inspectWindAtCenter(snapshot, null, { ...options, position: point });
const windLine = (reading) => const windLine = (reading) =>
+5 -4
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import * as geo from '../../geo/index.js'; 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]), values: Float32Array.from([20, 20, 20, 20]),
}, },
}; };
const engine = { const engine = createFakeEngine({
camera: { pickEllipsoid: () => geo.cartesian3.fromDegrees(0, 0) }, canvas: { clientWidth: 800, clientHeight: 600 },
host: { canvas: { clientWidth: 800, clientHeight: 600 } }, });
}; engine.fake.script('pickEllipsoid', () => geo.cartesian3.fromDegrees(0, 0));
const result = inspectWindAtCenter(snapshot, engine, { const result = inspectWindAtCenter(snapshot, engine, {
overlay: 'temperature', overlay: 'temperature',
units: 'km/h', 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 // src/layers/wind/inspectionMarker.test.mjs — the wind inspection marker
// follows only its captured point, renders in its container, and releases // follows only its captured point, renders in its container, and releases
// its frame listener. // its frame listener.
@@ -47,15 +48,13 @@ function fixture(t) {
requestRender() {}, requestRender() {},
}, },
}; };
const engine = { const engine = createFakeEngine({ canvas: viewer.scene.canvas });
host: { canvas: viewer.scene.canvas }, engine.frames.postRender = viewer.scene.postRender;
frames: { postRender: viewer.scene.postRender }, engine.fake.script(
projector: { 'worldToWindow',
worldToWindow: viewer.scene.cartesianToCanvasCoordinates, viewer.scene.cartesianToCanvasCoordinates,
horizonVisible: () => visibility.visible, );
}, engine.projector.horizonVisible = () => visibility.visible;
render: { request() {} },
};
const store = useWindInspectionStore(pinia); const store = useWindInspectionStore(pinia);
visibility.visible = true; visibility.visible = true;
const owner = createWindInspectionMarker({ 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 { createCesiumMaps } from '../globe/cesium/maps.js';
import { createCesiumCredits } from '../globe/cesium/credits.js'; import { createCesiumCredits } from '../globe/cesium/credits.js';
import test from 'node:test'; import test from 'node:test';
@@ -72,7 +73,7 @@ function fixture(registry, options = {}) {
}); });
const controller = new MapSourceController({ const controller = new MapSourceController({
registry, registry,
engine: { kind: 'cesium', render: { request() {} }, maps: mapsOwner.maps }, engine: Object.assign(createFakeEngine(), { maps: mapsOwner.maps }),
onChange: (state) => changes.push(state), onChange: (state) => changes.push(state),
...options, ...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 () => { test('a failed scene-pack rebind releases the partial renderer and a stopped shot unblocks', async () => {
const director = Object.create(SceneDirector.prototype); const director = Object.create(SceneDirector.prototype);
const drawn = new Set(); const drawn = new Set();
director.engine = { host: { container: {} } }; director.engine = createFakeEngine();
director._dataPacks = { director._dataPacks = {
setScene(viewer) { setScene(viewer) {
if (!viewer) drawn.clear(); if (!viewer) drawn.clear();
@@ -2088,7 +2088,7 @@ test('a failed scene-pack rebind releases the partial renderer and a stopped sho
assert.throws( assert.throws(
() => () =>
director.attachScene( director.attachScene(
{ engine: { host: { container: {} } } }, { engine: createFakeEngine() },
{}, {},
() => true, () => true,
), ),
@@ -2100,7 +2100,7 @@ test('a failed scene-pack rebind releases the partial renderer and a stopped sho
abort.abort(); abort.abort();
assert.equal(await stopped, false); assert.equal(await stopped, false);
const waiting = director._waitForScene(); const waiting = director._waitForScene();
director.engine = { host: {} }; director.engine = createFakeEngine();
director.styleManager = {}; director.styleManager = {};
director.resumeScene(); director.resumeScene();
assert.equal(await waiting, true); 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 { test, beforeEach } from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { import {
@@ -242,7 +243,9 @@ test('scope appearance follows direct state changes and install/destroy lifecycl
setScopeMaskEnabled(false); setScopeMaskEnabled(false);
assert.equal(dom.documentElement.dataset.scopeMask, 'off'); assert.equal(dom.documentElement.dataset.scopeMask, 'off');
delete dom.documentElement.dataset.scopeMask; delete dom.documentElement.dataset.scopeMask;
installScopeMask({ host: { container: dom.container } }); installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
assert.equal( assert.equal(
dom.documentElement.dataset.scopeMask, dom.documentElement.dataset.scopeMask,
'off', 'off',
@@ -254,7 +257,9 @@ test('scope appearance follows direct state changes and install/destroy lifecycl
undefined, undefined,
'destroy clears the document marker', 'destroy clears the document marker',
); );
installScopeMask({ host: { container: dom.container } }); installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
assert.equal( assert.equal(
dom.documentElement.dataset.scopeMask, dom.documentElement.dataset.scopeMask,
'off', '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', () => { test('a DPR change with no resize still repaints the backing store', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 }); const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try { try {
installScopeMask({ host: { container: dom.container } }); installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
// 1x monitor: backing store matches CSS pixels. // 1x monitor: backing store matches CSS pixels.
assert.equal(dom.canvas.width, 1000); assert.equal(dom.canvas.width, 1000);
assert.equal(dom.canvas.height, 800); 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)', () => { test('destroy tears the DPR watch down (no redraw after teardown)', () => {
const dom = stubScopeMaskDom({ width: 640, height: 480, dpr: 1 }); const dom = stubScopeMaskDom({ width: 640, height: 480, dpr: 1 });
try { try {
installScopeMask({ host: { container: dom.container } }); installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
assert.equal(dom.canvas.width, 640); assert.equal(dom.canvas.width, 640);
destroyScopeMask(); destroyScopeMask();
dom.setDpr(2); 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', () => { test('repaint gate: only a QUANTIZED step repaints; hovering costs nothing', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 }); const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try { try {
installScopeMask({ host: { container: dom.container } }); installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
const base = getScopeTerminusRepaintCount(); const base = getScopeTerminusRepaintCount();
// Parked at true full-globe altitude: the seeded alpha already matches, so // 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', () => { test('a full zoom-in gesture costs only a handful of repaints', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 }); const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try { try {
installScopeMask({ host: { container: dom.container } }); installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
const base = getScopeTerminusRepaintCount(); const base = getScopeTerminusRepaintCount();
// 20 Mm → ground in 10 km steps: ~2000 samples across the whole descent, // 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', () => { test('an override pins the terminus and null restores the ramp', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 }); const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try { try {
installScopeMask({ host: { container: dom.container } }); installScopeMask(
createFakeEngine({ container: dom.container, altitude: 14_000_000 }),
);
setScopeTerminusOverride(0.97); setScopeTerminusOverride(0.97);
assert.equal(getScopeTerminusOverride(), 0.97); assert.equal(getScopeTerminusOverride(), 0.97);
assert.equal(getScopeTerminusAlpha(), 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', () => { test('the hard-crop (feather 0) path honors the same terminus alpha', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 }); const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try { 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 updateScopeTerminusForHeight(SCOPE_TERMINUS_FAR_M + 5_000_000); // true full-globe view
setScopeMaskFeather(0); // hard crop — the evenodd rect-minus-circle path setScopeMaskFeather(0); // hard crop — the evenodd rect-minus-circle path
// The globe-scale seed paint is legitimately in the history, so assert on // The globe-scale seed paint is legitimately in the history, so assert on
@@ -583,15 +600,12 @@ function stubScopeEngine(container, heightM) {
set.add(listener); set.add(listener);
return () => set.delete(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 { return {
engine: { engine,
host: { container },
frames: { preRender: { addEventListener: addTo(preRenderListeners) } },
camera: {
height: () => height,
moveEnd: { addEventListener: addTo(moveEndListeners) },
},
},
setHeight(next) { setHeight(next) {
height = 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', () => { test('a pinned override is floored to the band at every entry point', () => {
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 }); const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
try { 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 setScopeTerminusOverride(0); // an sce=0 link, or any stale caller
assert.equal( assert.equal(
getScopeTerminusOverride(), 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 // The standalone application is created once; an engine switch detaches and
// attaches only its engine-bound parts. A candidate that fails releases its // attaches only its engine-bound parts. A candidate that fails releases its
// renderer, and the working engine attaches again on the same parts. // 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 }) => { createStandaloneScene: async ({ engineKind, defer }) => {
await beforeOpen?.(engineKind); await beforeOpen?.(engineKind);
if (failOpen === engineKind) throw new Error(`${engineKind} refused`); 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); opened.push(engine);
defer(async () => { defer(async () => {
await beforeRelease?.(engine); 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 { createSessionHandoff } from '../app/sessionHandoff.js';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; 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 () => { test('a slow first frame never rolls back a started engine', async () => {
const timers = []; const timers = [];
const engine = { const engine = createFakeEngine();
frames: { postRender: { addEventListener: () => () => {} } },
render: { request() {} },
};
const h = harness(); const h = harness();
let frame; let frame;
const machine = createEngineSwitch({ 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 draw;
let requests = 0; let requests = 0;
const engine = { const engine = createFakeEngine();
frames: { engine.frames.postRender.addEventListener = (fn) => {
postRender: { draw = fn;
addEventListener(fn) { return () => { draw = null; };
draw = fn;
return () => {
draw = null;
};
},
},
},
render: {
request: () => requests++,
},
}; };
engine.render.request = () => requests++;
const timers = new Map(); const timers = new Map();
const options = { const options = {
document: doc, document: doc,
+2 -4
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { createRendererCache } from './rendererCache.js'; import { createRendererCache } from './rendererCache.js';
@@ -51,10 +52,7 @@ function fakeBuild(released) {
resume() { resume() {
this.running = true; this.running = true;
}, },
engine: { engine: createFakeEngine({ container, canvas }),
host: { container, canvas },
render: { request() {} },
},
}; };
}; };
} }
+2 -1
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createSceneSwitcher } from './sceneSwitcher.js'; import { createSceneSwitcher } from './sceneSwitcher.js';
@@ -193,7 +194,7 @@ test('the restored camera precedes retained follow and replay', async () => {
const app = { const app = {
getState: () => ({ status: 'ready' }), getState: () => ({ status: 'ready' }),
getComponents: () => ({ getComponents: () => ({
scene: { engine: { kind: current } }, scene: { engine: createFakeEngine({ kind: current }) },
controls: { styleManager: style }, controls: { styleManager: style },
}), }),
async detachScene() { 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 // src/ui/displayPanel.test.mjs — the Display panel renders bloom and sharpen
// from what VisualSettings publishes, and shows its initial markup before. // from what VisualSettings publishes, and shows its initial markup before.
import '../testSupport/registerDom.mjs'; 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. */ /** VisualSettings with a stand-in detection engine. */
function detectionSettings(store) { function detectionSettings(store) {
const { owner } = settings(store); const { owner } = settings(store);
// This is detection state, not a globe engine.
const engine = { const engine = {
mode: 'OFF', mode: 'OFF',
tuning: { densityPct: 50, allocationStrategy: 'ELASTIC' }, 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, ''); assert.equal($('geo-import-hint').textContent, '');
// A globe engine double the files draw on. // A globe engine double the files draw on.
const engine = { const engine = createFakeEngine({ container: document.createElement('div') });
host: { container: document.createElement('div'), canvas: null },
camera: { moveEnd: { addEventListener: () => () => {} } },
surface: { tilesLoaded: () => true },
render: { request() {} },
};
const tool = createGeoFileImport({ const tool = createGeoFileImport({
drawFile: async () => ({ drawFile: async () => ({
report: { undrawn: 0 }, report: { undrawn: 0 },
+7 -13
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createFrameRateMonitor } from './frameRateMonitor.js'; import { createFrameRateMonitor } from './frameRateMonitor.js';
@@ -17,15 +18,10 @@ function fixture(t) {
addEventListener: (name, handler) => keys.add(handler), addEventListener: (name, handler) => keys.add(handler),
removeEventListener: (name, handler) => keys.delete(handler), removeEventListener: (name, handler) => keys.delete(handler),
}; };
const engine = { const engine = createFakeEngine();
frames: { engine.frames.postRender.addEventListener = (handler) => {
postRender: { frames.add(handler);
addEventListener(handler) { return () => frames.delete(handler);
frames.add(handler);
return () => frames.delete(handler);
},
},
},
}; };
const monitor = createFrameRateMonitor({ engine, documentRef, store }); const monitor = createFrameRateMonitor({ engine, documentRef, store });
t.after(() => monitor.destroy()); 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; let listened = false;
const store = { publishFps: () => assert.fail('nothing to show it in') }; const store = { publishFps: () => assert.fail('nothing to show it in') };
const monitor = createFrameRateMonitor({ const monitor = createFrameRateMonitor({
engine: { frames: { postRender: { addEventListener: () => () => {} } } }, engine: createFakeEngine(),
documentRef: { documentRef: {
getElementById: () => null, getElementById: () => null,
addEventListener: () => (listened = true), addEventListener: () => (listened = true),
@@ -130,9 +126,7 @@ test("destroying an earlier monitor leaves a newer one's readout", () => {
addEventListener() {}, addEventListener() {},
removeEventListener() {}, removeEventListener() {},
}; };
const engine = { const engine = createFakeEngine();
frames: { postRender: { addEventListener: () => () => {} } },
};
const first = createFrameRateMonitor({ engine, documentRef, store }); const first = createFrameRateMonitor({ engine, documentRef, store });
const second = createFrameRateMonitor({ engine, documentRef, store }); const second = createFrameRateMonitor({ engine, documentRef, store });
const shown = store.fps; 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 // src/ui/frameRateView.test.mjs — the title bar's FPS readout renders what
// createFrameRateMonitor() publishes and toggles with the backquote key. // createFrameRateMonitor() publishes and toggles with the backquote key.
import '../testSupport/registerDom.mjs'; 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'); document.querySelector('#title-bar .frame-rate-readout');
assert.equal(readout(), null, 'no readout without a monitor'); assert.equal(readout(), null, 'no readout without a monitor');
const monitor = createFrameRateMonitor({ const monitor = createFrameRateMonitor({
engine: { frames: { postRender: { addEventListener: () => () => {} } } }, engine: createFakeEngine(),
store: useStatusStore(pinia), store: useStatusStore(pinia),
}); });
t.after(() => monitor.destroy()); 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 // Behavioral tests for DISPLAY ▸ Import: the controller is driven against
// fake DOM and engine doubles with an injected loader and drawing, and its // fake DOM and engine doubles with an injected loader and drawing, and its
// observable effects are asserted — drawings made and destroyed, the rows // 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. */ /** A globe engine double: its host, camera moves and render requests. */
function fakeEngine() { function fakeEngine() {
const moveEnd = new Set(); const moveEnd = new Set();
const engine = { const engine = createFakeEngine({
kind: 'fake', kind: 'fake',
host: { container: new FakeElement('div', 'cesiumContainer'),
container: new FakeElement('div', 'cesiumContainer'), canvas: new FakeElement('canvas'),
canvas: new FakeElement('canvas'), });
}, engine.camera.moveEnd.addEventListener = (fn) => (
camera: { moveEnd.add(fn),
moveEnd: { () => moveEnd.delete(fn)
addEventListener: (fn) => (moveEnd.add(fn), () => moveEnd.delete(fn)), );
}, engine.renders = 0;
}, engine.render.request = () => (engine.renders += 1);
surface: { tilesLoaded: () => true }, engine.moveEnd = moveEnd;
renders: 0,
render: { request: () => (engine.renders += 1) },
moveEnd,
};
return engine; 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 // A file removed while its drawing on the next engine is still pending
// (icon decode) must not leave that drawing on the globe. // (icon decode) must not leave that drawing on the globe.
import test from 'node:test'; import test from 'node:test';
@@ -10,12 +11,7 @@ const el = () => ({
addEventListener() {}, addEventListener() {},
removeEventListener() {}, removeEventListener() {},
}); });
const engine = () => ({ const engine = () => createFakeEngine({ container: el(), canvas: el() });
host: { container: el(), canvas: el() },
camera: { moveEnd: { addEventListener: () => () => {} } },
surface: { tilesLoaded: () => true },
render: { request() {} },
});
test('removing a file during its redraw destroys the late drawing', async () => { test('removing a file during its redraw destroys the late drawing', async () => {
const document = { 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, gevDisplayPosition: () => target,
}, },
}; };
const engine = { const engine = createFakeEngine();
camera: { engine.fake.setPose({ position: cameraPosition });
pose: () => ({ position: cameraPosition }), engine.camera.following = () =>
following: () => viewer.trackedEntity && {
viewer.trackedEntity && { native: viewer.trackedEntity,
native: viewer.trackedEntity, target: () => target,
target: () => target, };
},
},
};
const subject = readShareFollowSubject({ engine, nowMs: NOW_MS }); const subject = readShareFollowSubject({ engine, nowMs: NOW_MS });
assert.equal(subject.kind, 'military_aircraft'); assert.equal(subject.kind, 'military_aircraft');
assert.equal(subject.id, 'ae1234'); 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); const { owner } = bindings(t);
let changes = 0; let changes = 0;
let trackedCallback; let trackedCallback;
owner.engine = { camera: { followChanged: { owner.engine = createFakeEngine();
addEventListener(callback) { owner.engine.camera.followChanged.addEventListener = (callback) => {
trackedCallback = callback; trackedCallback = callback;
return () => { changes += 1; }; return () => { changes += 1; };
}, };
} } };
owner._stampNavigation = () => { changes += 100; }; owner._stampNavigation = () => { changes += 100; };
owner.observeCamera(); owner.observeCamera();
owner.stop(); 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 // 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 // globe-bound parts and attaches them to the next globe, keeping the shell's
// session objects (navigation, location search, HUD, share link, display // session objects (navigation, location search, HUD, share link, display
@@ -37,7 +38,7 @@ function fakeShell() {
}, },
}; };
Object.assign(shell, { Object.assign(shell, {
_scene: { viewer: {}, engine: { kind: 'cesium' }, mapStackController: {} }, _scene: { viewer: {}, engine: createFakeEngine({ kind: 'cesium' }), mapStackController: {} },
_disposed: false, _disposed: false,
_layerSession: null, _layerSession: null,
services: { 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(); const { shell, session, log } = fakeShell();
await shell.detachScene({ handoff: true }); await shell.detachScene({ handoff: true });
log.length = 0; log.length = 0;
const engine = { kind: 'navara' }; const engine = createFakeEngine({ kind: 'navara' });
await shell.attachScene({ await shell.attachScene({
viewer: null, viewer: null,
engine, 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); assert.equal(shell._navigation, session.navigation);
await shell.attachScene({ engine }); await shell.attachScene({ engine });
await assert.rejects( await assert.rejects(
shell.attachScene({ engine: { kind: 'cesium' } }), shell.attachScene({ engine: createFakeEngine({ kind: 'cesium' }) }),
/attached to a globe/, /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 }); await shell.detachScene({ handoff: true });
log.length = 0; log.length = 0;
await shell.attachScene({ await shell.attachScene({
engine: { kind: 'cesium' }, engine: createFakeEngine({ kind: 'cesium' }),
services: { cctvLayer: 'other' }, services: { cctvLayer: 'other' },
}); });
assert.ok(log.includes('_initCctvPanel')); 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'); throw new Error('overlay host failed');
}; };
await assert.rejects( await assert.rejects(
shell.attachScene({ engine: { kind: 'navara' } }), shell.attachScene({ engine: createFakeEngine({ kind: 'navara' }) }),
/overlay host failed/, /overlay host failed/,
); );
// The shell releases what it built and is detached when that settles. // 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); assert.equal(shell.engine, null);
log.length = 0; log.length = 0;
shell._initSceneOverlays = () => log.push('_initSceneOverlays'); shell._initSceneOverlays = () => log.push('_initSceneOverlays');
const engine = { kind: 'cesium' }; const engine = createFakeEngine({ kind: 'cesium' });
await shell.attachScene({ engine }); await shell.attachScene({ engine });
assert.equal(shell.engine, engine); assert.equal(shell.engine, engine);
assert.ok(log.includes('shareRestoration.start')); 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._layerBindings = { observeCamera() {}, stop() {}, disconnect() {} };
shell._visualSettings = { setStyle: (name) => applied.push(name) }; 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();
await Promise.resolve(); await Promise.resolve();
assert.deepEqual(applied, ['crt']); assert.deepEqual(applied, ['crt']);
@@ -262,13 +263,13 @@ test('shell recovery waits for failed attachment cleanup and serializes repeated
shell._initSceneOverlays = () => { shell._initSceneOverlays = () => {
throw new Error('overlay failed'); 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/); const rejected = assert.rejects(failed, /overlay failed/);
await Promise.resolve(); await Promise.resolve();
const detach = shell.detachScene(); const detach = shell.detachScene();
const again = shell.detachScene(); const again = shell.detachScene();
shell._initSceneOverlays = () => {}; shell._initSceneOverlays = () => {};
const engine = { kind: 'cesium' }; const engine = createFakeEngine({ kind: 'cesium' });
const recovered = shell.attachScene({ engine }); const recovered = shell.attachScene({ engine });
await Promise.resolve(); await Promise.resolve();
assert.notEqual(shell.engine, engine, 'recovery waits for cleanup'); 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; release = resolve;
}); });
const leaving = shell.detachScene({ handoff: true }); const leaving = shell.detachScene({ handoff: true });
const engine = { kind: 'navara' }; const engine = createFakeEngine({ kind: 'navara' });
const entering = shell.attachScene({ engine }); const entering = shell.attachScene({ engine });
await Promise.resolve(); await Promise.resolve();
assert.notEqual(shell.engine, engine); 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. // The stand-in a catalog gives for a layer the engine leaves out.
Object.assign(shell, { Object.assign(shell, {
services: { wildlifeSightingsLayer: {} }, services: { wildlifeSightingsLayer: {} },
_scene: { viewer: null, engine: { kind: 'navara' } }, _scene: { viewer: null, engine: createFakeEngine({ kind: 'navara' }) },
}); });
assert.doesNotThrow(() => shell._initWildlifeSightingCard()); assert.doesNotThrow(() => shell._initWildlifeSightingCard());
assert.equal(shell._wildlifeSightingCard, null); assert.equal(shell._wildlifeSightingCard, null);
@@ -324,7 +325,7 @@ test('a hosted shell uses the enclosing transition while direct calls wait outsi
entered(); entered();
return new Promise((resolve) => { release = resolve; }); 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) => { const switching = host.run(async (transaction) => {
await host.detach({ handoff: true }, transaction); await host.detach({ handoff: true }, transaction);
await host.attach(next, 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 assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { NavigationController } from './navigationController.js'; 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 stamps = 0;
let applies = 0; let applies = 0;
const owner = new ShareRestoration({ const owner = new ShareRestoration({
engine: { host: { canvas } }, engine: createFakeEngine({ canvas }),
navigation: { navigation: {
_beginDeferredNavigation: () => 1, _beginDeferredNavigation: () => 1,
_reassertNavigationHandoff: () => true, _reassertNavigationHandoff: () => true,
@@ -164,12 +165,11 @@ test('on Navara, presets set the Sharpen slider to 80 % and the engine draws it'
_sharpenSliderRow: element(), _sharpenSliderRow: element(),
_sharpenSliderValue: element(), _sharpenSliderValue: element(),
}; };
const engine = createFakeEngine({ kind: 'navara' });
engine.render.setSharpen = (options) => sharpen.push(options);
const owner = new VisualSettings({ const owner = new VisualSettings({
viewer: null, viewer: null,
engine: { engine: engine,
kind: 'navara',
render: { setSharpen: (options) => sharpen.push(options) },
},
elements, elements,
operations: { operations: {
_syncShareState() {}, _syncShareState() {},
+3 -4
View File
@@ -57,7 +57,7 @@ function fixture() {
createStage: (options) => ({ ...options, enabled: true }), createStage: (options) => ({ ...options, enabled: true }),
}, },
); );
const engine = { render: cesium.render, isDestroyed: () => false }; const engine = Object.assign(createFakeEngine(), { render: cesium.render });
const effects = new VisualEffects({ const effects = new VisualEffects({
engine, engine,
requestRender: (reason) => requests.push(reason), 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', () => { test('an engine sharpens with the same amount scale, only while on and above 0', () => {
const sharpen = []; const sharpen = [];
const requests = []; const requests = [];
const engine = { const engine = createFakeEngine();
render: { setSharpen: (options) => sharpen.push(options) }, engine.render.setSharpen = (options) => sharpen.push(options);
};
const effects = new VisualEffects({ const effects = new VisualEffects({
engine, engine,
requestRender: (reason) => requests.push(reason), 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). // 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 // 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. // 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 () => { test('hidden document yields no fresh frame — capture must not label a stale canvas Current', async () => {
const originalDocument = globalThis.document; const originalDocument = globalThis.document;
let requested = 0; let requested = 0;
const engine = { const engine = createFakeEngine();
frames: { postRender: { addEventListener() { return () => {}; } } }, engine.render.request = () => { requested += 1; };
render: { request() { requested += 1; } },
};
try { try {
globalThis.document = { hidden: true }; globalThis.document = { hidden: true };
const fresh = await renderFreshCesiumFrame(engine); const fresh = await renderFreshCesiumFrame(engine);
@@ -2878,10 +2877,9 @@ test('visible document with a rendering scene reports a fresh frame', async () =
try { try {
globalThis.document = { hidden: false }; globalThis.document = { hidden: false };
let fire = null; let fire = null;
const engine = { const engine = createFakeEngine();
frames: { postRender: { addEventListener(listener) { fire = listener; return () => { fire = null; }; } } }, engine.frames.postRender.addEventListener = (listener) => { fire = listener; return () => { fire = null; }; };
render: { request() { queueMicrotask(() => fire?.()); } }, engine.render.request = () => { queueMicrotask(() => fire?.()); };
};
const fresh = await renderFreshCesiumFrame(engine); const fresh = await renderFreshCesiumFrame(engine);
assert.equal(fresh, true); assert.equal(fresh, true);
} finally { } finally {
@@ -2895,10 +2893,9 @@ test('a tab switch during the bounded render wait invalidates freshness', async
const doc = { hidden: false }; const doc = { hidden: false };
globalThis.document = doc; globalThis.document = doc;
let fire = null; let fire = null;
const engine = { const engine = createFakeEngine();
frames: { postRender: { addEventListener(listener) { fire = listener; return () => { fire = null; }; } } }, engine.frames.postRender.addEventListener = (listener) => { fire = listener; return () => { fire = null; }; };
render: { request() { doc.hidden = true; queueMicrotask(() => fire?.()); } }, engine.render.request = () => { doc.hidden = true; queueMicrotask(() => fire?.()); };
};
const fresh = await renderFreshCesiumFrame(engine); const fresh = await renderFreshCesiumFrame(engine);
assert.equal(fresh, false, 'freshness rechecked after the await'); assert.equal(fresh, false, 'freshness rechecked after the await');
} finally { } 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: // Cross-feature voice contracts, driven through the real action runner:
// one result contract (engine fields → model payload → voice card), shared // one result contract (engine fields → model payload → voice card), shared
// pointing for imagery, result-set ownership between OSM and the analyst, // 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: () => build: () =>
harness({ runnerOptions: { analystMemory, layerEnabledAt } }).runner, harness({ runnerOptions: { analystMemory, layerEnabledAt } }).runner,
}); });
run.attach({ engine: { kind: 'cesium' } }); run.attach({ engine: createFakeEngine({ kind: 'cesium' }) });
const first = await run('analyst_query', { const first = await run('analyst_query', {
layers: ['flights'], layers: ['flights'],
scope: { kind: 'anywhere' }, 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); assert.equal(first.ok, true);
// The real runner of the outgoing engine is disposed by the switch. // The real runner of the outgoing engine is disposed by the switch.
run.detach(); run.detach();
run.attach({ engine: { kind: 'navara' } }); run.attach({ engine: createFakeEngine({ kind: 'navara' }) });
const followUp = await run('analyst_query', { const followUp = await run('analyst_query', {
layers: ['flights'], layers: ['flights'],
followUp: true, followUp: true,
+14 -24
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { import {
@@ -96,23 +97,16 @@ test('crop: a slow render gets one bounded fresh-frame recovery request', async
let listener = null; let listener = null;
try { try {
globalThis.document = { hidden: false }; globalThis.document = { hidden: false };
const engine = { const engine = createFakeEngine();
frames: { engine.frames.postRender.addEventListener = (next) => {
postRender: { listener = next;
addEventListener(next) { return () => {
listener = next; if (listener === next) listener = null;
return () => { };
if (listener === next) listener = null; };
}; engine.render.request = () => {
}, requests += 1;
}, if (requests === 2) queueMicrotask(() => listener?.());
},
render: {
request() {
requests += 1;
if (requests === 2) queueMicrotask(() => listener?.());
},
},
}; };
assert.equal(await renderFreshPointerFrame(engine), true); assert.equal(await renderFreshPointerFrame(engine), true);
assert.equal(requests, 2, 'only the pointer path retries once'); 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; let requests = 0;
try { try {
globalThis.document = { hidden: true }; globalThis.document = { hidden: true };
const engine = { const engine = createFakeEngine();
frames: { postRender: { addEventListener: () => () => {} } }, engine.render.request = () => {
render: { requests += 1;
request() {
requests += 1;
},
},
}; };
assert.equal(await renderFreshPointerFrame(engine), false); assert.equal(await renderFreshPointerFrame(engine), false);
assert.equal(requests, 0); 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 test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createSceneRunner, engineSwitchingRefusal } from './sceneRunner.js'; import { createSceneRunner, engineSwitchingRefusal } from './sceneRunner.js';
const engine = (kind) => ({ kind }); const engine = (kind) => createFakeEngine({ kind });
function runnerFor(log) { function runnerFor(log) {
return (scene) => { return (scene) => {
+2 -5
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createSceneVoice } from './gevRealtime.js'; import { createSceneVoice } from './gevRealtime.js';
@@ -6,11 +7,7 @@ import { createTestVoiceUi } from '../testSupport/voiceUi.mjs';
function fakeEngine(kind) { function fakeEngine(kind) {
const canvas = new EventTarget(); const canvas = new EventTarget();
canvas.getBoundingClientRect = () => ({ left: 0, top: 0 }); canvas.getBoundingClientRect = () => ({ left: 0, top: 0 });
return { return createFakeEngine({ kind, canvas, width: 0, height: 0 });
kind,
host: { canvas, size: () => ({ width: 0, height: 0 }) },
camera: { pose: () => ({}) },
};
} }
function fixture() { function fixture() {