Measure engine coupling, QA harness use and contract coverage

Counters that may only fall, each with a committed baseline and a test
that fails on growth:

- report-engine-coupling.mjs gains layer modules that take the viewer
  in attachRenderer (33), layer folders with their own "view being
  restored" flag (9), engine.surface calls outside src/globe (41) and
  layer ids named in the engine switch (3).
- report-qa-harness.mjs counts QA scripts that launch Chrome themselves
  (98), wait with fixed sleeps (117), take the hidden loading screen as
  readiness (9), read Cesium-only objects (75) or define their own
  waitFor helpers (19).
- withCallLog records every contract member the contract suite calls on
  the fake engine; contractCoverage.test.mjs fails when a member outside
  contract-uncalled-baseline.json goes uncalled, and when a listed one
  is called now, so the list of 12 only shrinks.
This commit is contained in:
German Khokhlov committed 2026-11-10 14:23:29 +04:00
1 parent ba04ceaa32
commit 9c2a2d6951
11 files changed
+842 -3

No files matched your search

+12
View File
@@ -604,6 +604,18 @@ checks. Test ownership and repeated destruction as well as visible output.
The browser harness is `scripts/qa-contract-suite.mjs`; use
`--only=ground:,imagery:` for ground and imagery pixel checks.
Three reports keep migration debt from growing: `scripts/report-engine-coupling.mjs`
counts application coupling, `scripts/report-qa-harness.mjs` counts browser QA
duplication, and `src/globe/testing/contractCoverage.test.mjs` reports contract
members the fake-engine suite leaves uncalled. After removing coupling or moving
QA work to `scripts/qa-strict-harness.mjs` helpers, run the matching report with
`--write-baseline` and keep only decreases in its baseline. After adding behavioral
contract checks, remove the newly exercised names from
`src/globe/testing/contract-uncalled-baseline.json`; keep it sorted, and remove
members that no longer exist too. Run the matching tests with
`node --import=./build/registerVue.js --test` followed by their paths; the numeric
baselines reject growth, and the contract baseline also rejects stale entries.
The application currently registers two kinds: adding another still requires
registration in `ENGINE_KINDS`, scene construction/choice and the corresponding
QA harnesses. `assertGlobeEngine` currently validates that registration. The
+5 -2
View File
@@ -1,5 +1,9 @@
{
"metrics": {
"attachRendererWithViewer": 33,
"restoreFlagCopies": 9,
"surfaceCallsOutsideGlobe": 41,
"engineSwitchLayerIds": 3,
"cesiumLayerRendererLines": 0,
"navaraLayerRendererLines": 0,
"cesiumImportFiles": 3,
@@ -11,6 +15,5 @@
"cesiumSdkLoads": 3,
"screenSpaceHandlerFiles": 2,
"engineNameLines": 21
},
"measuredAt": "Orbital cockpit and bridge mesh sampling"
}
}
+82
View File
@@ -39,3 +39,85 @@ test('a metric that grows is reported, one that falls is not', () => {
['viewerLines'],
);
});
test('renderer attachment counts definitions once per module, not calls', () => {
const { measure } = ENGINE_COUPLING_METRICS.find(
({ id }) => id === 'attachRendererWithViewer',
);
assert.equal(
measure([
{
text: 'attachRenderer(viewer, options = {}) {}\nattachRenderer(scene) {}',
},
{ text: 'function attachRenderer(\n _viewer, options\n) {}' },
{ text: 'const attachRenderer = (nextViewer) => {};' },
{
text: 'attachRenderer(_scene); layer.attachRenderer(viewer, options);',
},
{ text: 'attachRenderer(engine) {}' },
{ text: 'attachRenderer(viewerState) {}' },
]),
3,
);
});
test('restore flags count layer directories and surface calls exclude globe', () => {
const measure = (id, files) =>
ENGINE_COUPLING_METRICS.find((metric) => metric.id === id).measure(files);
assert.equal(
measure('restoreFlagCopies', [
{
file: 'src/layers/traffic/lifecycle.js',
text: 'state._restoringViewport = true;',
},
{
file: 'src/layers/traffic/viewport.js',
text: 'state._restoringViewport',
},
{ file: 'src/layers/alpr/index.js', text: 'let restoringView = false;' },
{
file: 'src/layers/streetLevel/index.js',
text: 'let waitingForView = false;',
},
{ file: 'src/app/view.js', text: 'let restoringView = false;' },
]),
3,
);
const text = [
'terrainHeight',
'sampleHeight',
'sampleHeightDetailed',
'sampleTerrainMostDetailed',
'clampToHeight',
'raycast',
]
.map((name) => `engine.surface.${name}(point);`)
.join('\n');
assert.equal(
measure('surfaceCallsOutsideGlobe', [
{ file: 'src/globe/navara/surface.js', text },
{ file: 'src/layers/traffic/viewport.js', text },
{
file: 'src/app/view.js',
text: 'engine.surface?.raycast?.(ray); surface.tilesLoaded();',
},
]),
7,
);
});
test('engine switch layer ids count lines and follow the catalog', () => {
const { measure } = ENGINE_COUPLING_METRICS.find(
({ id }) => id === 'engineSwitchLayerIds',
);
assert.equal(
measure([
{
file: 'src/standalone/engineSwitch.js',
text: "get('directions'); get('satellites');\nget('traffic');\nget('not-a-layer');",
},
{ file: 'src/app/view.js', text: "get('directions');" },
]),
2,
);
});
+9
View File
@@ -0,0 +1,9 @@
{
"metrics": {
"ownLaunch": 98,
"fixedSleeps": 117,
"loadingScreenReadiness": 9,
"cesiumOnlyReads": 75,
"ownWaitHelpers": 19
}
}
+100
View File
@@ -0,0 +1,100 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
QA_HARNESS_METRICS,
harnessGrowth,
measureQaHarness,
readBaseline,
readQaSources,
} from './report-qa-harness.mjs';
test('QA harness duplication does not grow past the baseline', () => {
assert.deepEqual(
harnessGrowth(measureQaHarness(), readBaseline().metrics),
[],
'QA harness duplication grew; use scripts/qa-strict-harness.mjs helpers for browser launch, engine readiness and camera waits instead.',
);
});
test('the baseline names every metric', () => {
assert.deepEqual(
Object.keys(readBaseline().metrics).sort(),
QA_HARNESS_METRICS.map(({ id }) => id).sort(),
);
});
test('a metric that grows is reported, one that falls is not', () => {
const baseline = Object.fromEntries(
QA_HARNESS_METRICS.map(({ id }) => [id, 10]),
);
assert.deepEqual(
harnessGrowth({ ...baseline, ownLaunch: 11, fixedSleeps: 3 }, baseline).map(
({ id }) => id,
),
['ownLaunch'],
);
assert.equal(harnessGrowth(baseline, {}).length, QA_HARNESS_METRICS.length);
});
test('QA sources include the harness and exclude tests and other reports', () => {
const names = readQaSources().map(({ file }) => file);
assert.ok(names.includes('scripts/qa-strict-harness.mjs'));
assert.ok(!names.some((name) => name.endsWith('.test.mjs')));
assert.ok(!names.includes('scripts/report-qa-harness.mjs'));
});
test('metrics count scripts once and distinguish helpers from their callers', () => {
const measure = (id, ...sources) =>
QA_HARNESS_METRICS.find((metric) => metric.id === id).measure(
sources.map((text) => ({ text })),
);
assert.equal(
measure(
'ownLaunch',
'puppeteer.launch({}); puppeteer.launch({});',
'launchBrowser();',
),
1,
);
assert.equal(
measure(
'fixedSleeps',
'const sleep = (ms) => new Promise((r) => setTimeout(r, ms));',
'await new Promise((resolve) => { setTimeout(resolve, 1000); });',
'await new Promise((resolve) => setTimeout(resolve));',
'new Promise((resolve) => requestAnimationFrame(resolve));',
'setTimeout(() => reject(new Error()), 1000);',
),
3,
);
assert.equal(
measure(
'loadingScreenReadiness',
"page.waitForSelector('#loading-screen');",
"page.waitForSelector('#loading-screen'); await waitForEngine(page);",
"document.querySelector('#loading-screen');",
),
1,
);
assert.equal(
measure(
'cesiumOnlyReads',
'window.__godsEyeView?.viewer; window.__godsEyeView.tileset;',
'const { scene } = window.__godsEyeView;',
'const app = window.__godsEyeView; app.viewer;',
'window.__godsEyeView.sceneDirector;',
'window.__godsEyeView.engine;',
),
3,
);
assert.equal(
measure(
'ownWaitHelpers',
'async function waitForReady() {}',
'const waitForReady = async (page) => page.ready();',
'await waitForEngine(page);',
"import { waitForEngine } from './qa-strict-harness.mjs';",
),
2,
);
});
+66 -1
View File
@@ -16,6 +16,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { APPLICATION_LAYER_DIRECTORY } from '../src/app/layers/directory.js';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
export const BASELINE_PATH = path.join(
@@ -37,12 +38,72 @@ const countLines = (text, pattern) =>
lines(text).filter((line) => pattern.test(line)).length;
const countMatches = (text, pattern) =>
(text.match(new RegExp(pattern.source, 'g')) ?? []).length;
const layerIds = new Set(APPLICATION_LAYER_DIRECTORY.map(({ id }) => id));
/**
* The metrics, in report order. `measure(files)` gets every production
* source as `{ file, text }` (file relative to the repository, with `/`).
*/
export const ENGINE_COUPLING_METRICS = Object.freeze([
{
id: 'attachRendererWithViewer',
label: 'Modules defining attachRenderer with a viewer or scene parameter',
measure: (files) =>
files.filter(({ text }) =>
/\battachRenderer\s*(?:(?:=|:)\s*(?:async\s+)?(?:function\s*)?)?\(\s*(?:viewer|_viewer|nextViewer|_scene|scene)\s*(?:,[^)]*|=[^)]*)?\)\s*(?:\{|=>)/.test(
text,
),
).length,
},
{
id: 'restoreFlagCopies',
label: 'Layer directories keeping their own view-restore flag',
measure: (files) =>
new Set(
files
.filter(
({ file, text }) =>
file.startsWith('src/layers/') &&
/\b(?:_restoringViewport|restoringView|waitingForView)\b/.test(
text,
),
)
.map(({ file }) => file.split('/')[2]),
).size,
},
{
id: 'surfaceCallsOutsideGlobe',
label: 'Surface sampling calls outside src/globe',
measure: (files) =>
files
.filter(({ file }) => !inGlobe(file))
.reduce(
(sum, { text }) =>
sum +
countMatches(
text,
/\bsurface\??\.\s*(?:terrainHeight|sampleHeight|sampleHeightDetailed|sampleTerrainMostDetailed|clampToHeight|raycast)\s*(?:\?\.)?\(/,
),
0,
),
},
{
id: 'engineSwitchLayerIds',
label: 'Lines naming catalog layer ids in src/standalone/engineSwitch.js',
measure: (files) =>
files
.filter(({ file }) => file === 'src/standalone/engineSwitch.js')
.reduce(
(sum, { text }) =>
sum +
lines(text).filter((line) =>
[...line.matchAll(/(['"])([^'"\n]+)\1/g)].some((match) =>
layerIds.has(match[2]),
),
).length,
0,
),
},
{
id: 'cesiumLayerRendererLines',
label: 'Lines in src/layers/**/render/cesium*',
@@ -170,7 +231,11 @@ export function readProductionSources(root = ROOT) {
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
const absolute = path.join(directory, entry.name);
if (entry.isDirectory()) visit(absolute);
else if (entry.isFile() && /\.(?:js|vue)$/.test(entry.name))
else if (
entry.isFile() &&
/\.(?:js|vue)$/.test(entry.name) &&
!/\.test\./.test(entry.name)
)
files.push({
file: path.relative(root, absolute).split(path.sep).join('/'),
text: fs.readFileSync(absolute, 'utf8'),
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env node
/**
* Browser QA still doing work owned by scripts/qa-strict-harness.mjs.
* Each metric counts scripts, so repeated uses in one script count once.
*
* node scripts/report-qa-harness.mjs
* node scripts/report-qa-harness.mjs --json
* node scripts/report-qa-harness.mjs --write-baseline
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
const ROOT = fileURLToPath(new URL('..', import.meta.url));
export const BASELINE_PATH = path.join(
ROOT,
'scripts',
'qa-harness-baseline.json',
);
const readsCesiumState = (text) => {
const names = [
'__godsEyeView',
...[
...text.matchAll(
/\b(?:const|let|var)\s+(\w+)\s*=\s*(?:window\.)?__godsEyeView\s*[;,]/g,
),
].map((match) => match[1]),
];
return names.some((name) =>
new RegExp(
`\\b${name}\\s*\\??\\.\\s*(?:viewer|tileset|scene)\\b|\\{[^{}]*\\b(?:viewer|tileset|scene)\\b[^{}]*\\}\\s*=\\s*(?:window\\.)?${name}\\b`,
).test(text),
);
};
export const QA_HARNESS_METRICS = Object.freeze([
{
id: 'ownLaunch',
label: 'Scripts launching puppeteer themselves',
measure: (files) =>
files.filter(({ text }) => /\bpuppeteer\.launch\s*\(/.test(text)).length,
},
{
id: 'fixedSleeps',
label: 'Scripts with setTimeout-based pauses',
measure: (files) =>
files.filter(({ text }) =>
/new\s+Promise\s*\(\s*\(?\s*(\w+)\s*\)?\s*=>\s*(?:\{\s*(?:return\s+)?)?setTimeout\s*\(\s*\1\s*[,)]/.test(
text,
),
).length,
},
{
id: 'loadingScreenReadiness',
label: 'Scripts waiting for #loading-screen without waitForEngine',
measure: (files) =>
files.filter(
({ text }) =>
/\bwaitFor(?:Selector|Function)\s*\([^;]*#loading-screen\b/.test(
text,
) && !/\bwaitForEngine\s*\(/.test(text),
).length,
},
{
id: 'cesiumOnlyReads',
label: 'Scripts reading __godsEyeView viewer, tileset or scene',
measure: (files) =>
files.filter(({ text }) => readsCesiumState(text)).length,
},
{
id: 'ownWaitHelpers',
label: 'Scripts defining their own waitFor functions',
measure: (files) =>
files.filter(({ text }) =>
/\bfunction\s+waitFor\w*\s*\(|\b(?:const|let|var)\s+waitFor\w*\s*=\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*=>|\w+\s*=>)/.test(
text,
),
).length,
},
]);
/** QA scripts, including the shared harness itself, but excluding tests. */
export function readQaSources(root = ROOT) {
return fs
.readdirSync(path.join(root, 'scripts'))
.filter((name) => /^qa-.*\.mjs$/.test(name) && !name.endsWith('.test.mjs'))
.sort()
.map((name) => ({
file: `scripts/${name}`,
text: fs.readFileSync(path.join(root, 'scripts', name), 'utf8'),
}));
}
export function measureQaHarness(root = ROOT) {
const files = readQaSources(root);
return Object.fromEntries(
QA_HARNESS_METRICS.map(({ id, measure }) => [id, measure(files)]),
);
}
export function harnessGrowth(measured, baseline) {
return QA_HARNESS_METRICS.filter(
({ id }) => !(measured[id] <= (baseline[id] ?? -Infinity)),
).map(({ id, label }) => ({
id,
label,
measured: measured[id],
baseline: baseline[id],
}));
}
export function readBaseline(file = BASELINE_PATH) {
return JSON.parse(fs.readFileSync(file, 'utf8'));
}
function main(args) {
const measured = measureQaHarness();
if (args.includes('--write-baseline')) {
const note = args.find((arg) => arg.startsWith('--note='))?.slice(7);
fs.writeFileSync(
BASELINE_PATH,
`${JSON.stringify({ metrics: measured, ...(note ? { measuredAt: note } : {}) }, null, 2)}\n`,
);
}
if (args.includes('--json')) {
console.log(JSON.stringify(measured, null, 2));
return;
}
const baseline = fs.existsSync(BASELINE_PATH) ? readBaseline().metrics : {};
for (const { id, label } of QA_HARNESS_METRICS) {
const was = baseline[id];
const change =
was === undefined
? ''
: ` (baseline ${was}, ${measured[id] - was >= 0 ? '+' : ''}${measured[id] - was})`;
console.log(`${String(measured[id]).padStart(7)} ${label}${change}`);
}
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href)
main(process.argv.slice(2));
+9
View File
@@ -1075,6 +1075,15 @@ const SHAPE = {
},
};
/** Required capability members checked by assertGlobeEngine. */
export function engineContractMembers() {
return Object.entries(SHAPE)
.flatMap(([namespace, members]) =>
Object.keys(members).map((member) => `${namespace}.${member}`),
)
.sort();
}
const isNameList = (value) =>
Array.isArray(value) &&
value.length > 0 &&
+184
View File
@@ -0,0 +1,184 @@
import { engineContractMembers, isEngineEvent } from '../contract.js';
// Returned handles keep the names of their contract typedefs.
const HANDLE_TYPES = {
MoverSet: {
creators: ['movers.createSet'],
members: [
'set',
'setCount',
'count',
'commit',
'ready',
'failed',
'destroy',
],
},
Line: {
creators: ['lines.createLine'],
members: ['setPositions', 'setColor', 'setModelMatrix', 'show', 'destroy'],
},
LineSegments: {
creators: ['lines.createSegments'],
members: ['replace', 'commit', 'show', 'destroy'],
},
GroundSet: {
creators: ['ground.createSet'],
members: [
'id',
'setReveal',
'replace',
'commit',
'show',
'shown',
'count',
'ready',
'destroy',
],
},
GroundPulseField: {
creators: ['ground.createPulseField'],
members: ['id', 'draw', 'move', 'destroy', 'isDestroyed'],
},
ImageryLayer: {
creators: ['imagery.createLayer'],
members: [
'id',
'setSource',
'source',
'staging',
'setDetail',
'detail',
'raise',
'setAlpha',
'alpha',
'show',
'shown',
'setSplit',
'split',
'ready',
'destroy',
],
},
GlobeMesh: {
creators: ['meshes.createMesh'],
members: ['id', 'update', 'show', 'destroy'],
},
FollowHandle: {
creators: ['camera.follow', 'camera.following'],
members: ['stop', 'retarget', 'target', 'native', 'label', 'name'],
},
PointerClaim: {
creators: ['input.acquirePointer'],
members: ['release'],
},
EngineEvent: {
creators: [],
members: ['addEventListener', 'removeEventListener'],
},
};
/** Members tracked by the suite, including handles returned by the engine. */
export function contractCallMembers() {
return [
...engineContractMembers(),
...Object.entries(HANDLE_TYPES).flatMap(([type, { members }]) =>
members.map((member) => `${type}.${member}`),
),
].sort();
}
/** Log contract calls and value reads without changing the engine's receiver. */
export function withCallLog(engine) {
const called = new Set();
const members = new Set(contractCallMembers());
const namespaces = new Set(
engineContractMembers().map((name) => name.split('.')[0]),
);
const returnedTypes = new Map(
Object.entries(HANDLE_TYPES).flatMap(([type, { creators }]) =>
creators.map((name) => [name, type]),
),
);
const proxies = new WeakMap();
const originals = new WeakMap();
const unwrap = (value) => originals.get(value) ?? value;
const unwrapExclusions = (exclude) =>
Array.isArray(exclude) && exclude.some((value) => originals.has(value))
? exclude.map(unwrap)
: exclude;
const wrap = (target, prefix, followEvent = false) => {
if (target == null || typeof target !== 'object') return target;
let cached = proxies.get(target);
if (!cached) proxies.set(target, (cached = new Map()));
const key = followEvent ? 'camera.followChanged' : prefix;
if (cached.has(key)) return cached.get(key);
const methods = new Map();
const listeners = new WeakMap();
const proxy = new Proxy(target, {
get(target, property) {
const value = Reflect.get(target, property, target);
if (typeof property !== 'string') return value;
if (!prefix && namespaces.has(property)) return wrap(value, property);
const name = `${prefix}.${property}`;
if (typeof value !== 'function') {
if (members.has(name)) called.add(name);
return isEngineEvent(value)
? wrap(value, 'EngineEvent', name === 'camera.followChanged')
: value;
}
if (methods.get(property)?.original === value)
return methods.get(property).wrapped;
const wrapped = function (...args) {
if (members.has(name)) called.add(name);
if (
followEvent &&
(property === 'addEventListener' ||
property === 'removeEventListener') &&
typeof args[0] === 'function'
) {
const listener = args[0];
if (!listeners.has(listener))
listeners.set(listener, (handle) =>
listener(wrap(handle, 'FollowHandle')),
);
args[0] = listeners.get(listener);
}
if (name === 'input.on' && args[2]?.pointer) {
args[2] = { ...args[2], pointer: unwrap(args[2].pointer) };
}
// Surface adapters identify excluded resources by their native handles.
if (
name === 'surface.sampleHeight' ||
name === 'surface.clampToHeight'
) {
if (args.length > 1) args[1] = unwrapExclusions(args[1]);
} else if (
name === 'surface.sampleHeightDetailed' ||
name === 'surface.raycast'
) {
const index = name === 'surface.raycast' ? 0 : 1;
const options = args[index];
const exclude = unwrapExclusions(options?.exclude);
if (exclude !== options?.exclude)
args[index] = { ...options, exclude };
}
const result = Reflect.apply(value, target, args);
const type = returnedTypes.get(name);
return type ? wrap(result, type) : result;
};
methods.set(property, { original: value, wrapped });
return wrapped;
},
set(target, property, value) {
return Reflect.set(target, property, unwrap(value), target);
},
});
cached.set(key, proxy);
originals.set(proxy, target);
return proxy;
};
return { engine: wrap(engine, ''), called: () => [...called].sort() };
}
@@ -0,0 +1,14 @@
[
"FollowHandle.label",
"FollowHandle.name",
"GlobeMesh.id",
"GroundPulseField.id",
"Line.setColor",
"Line.setModelMatrix",
"Line.show",
"MoverSet.failed",
"maps.terrainRevision",
"render.maxTextureSize",
"surface.raycast",
"surface.sampleTerrainMostDetailed"
]
+219
View File
@@ -0,0 +1,219 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import test from 'node:test';
import { createEngineEvent, engineContractMembers } from '../contract.js';
import { contractCallMembers, withCallLog } from './callLog.js';
import { createContractSuite } from './contractSuite.mjs';
import { createFakeEngine } from './fakeEngine.js';
test('the contract suite does not leave new members uncalled', async (t) => {
const fake = createFakeEngine();
const { engine, called } = withCallLog(fake);
const checks = createContractSuite(engine, {
waitFrames: async (count) => fake.fake.frames(count),
pointer: {
click: async (x, y) => fake.fake.input('click', { position: { x, y } }),
move: async (x, y) =>
fake.fake.input('move', {
startPosition: { x: x - 1, y },
endPosition: { x, y },
}),
},
mapSources: {
imagery: [{ kind: 'osm' }, { kind: 'esri-imagery' }],
photoreal: { kind: 'photoreal' },
},
});
try {
for (const check of checks) await t.test(check.name, () => check.run());
const baseline = JSON.parse(
fs.readFileSync(
new URL('./contract-uncalled-baseline.json', import.meta.url),
'utf8',
),
);
const seen = new Set(called());
const uncalled = contractCallMembers().filter((name) => !seen.has(name));
assert.deepEqual(
baseline,
[...new Set(baseline)].sort(),
'Keep contract-uncalled-baseline.json sorted and remove duplicate names.',
);
assert.deepEqual(
uncalled.filter((name) => !baseline.includes(name)),
[],
'Contract members lost coverage; add behavioral checks to createContractSuite that exercise them instead of growing contract-uncalled-baseline.json.',
);
assert.deepEqual(
baseline.filter((name) => !uncalled.includes(name)),
[],
'Contract members are now exercised or no longer exist; remove these names from contract-uncalled-baseline.json to lower the baseline.',
);
} finally {
fake.destroy();
}
});
test('call logging keeps receivers, arguments, results and function identity', async () => {
const result = { height: 5 };
const promise = Promise.resolve(result);
const camera = {
flyTo(destination) {
assert.equal(this, camera);
return destination;
},
};
const maps = {
setTerrain() {
assert.equal(this, maps);
return promise;
},
};
const { engine, called } = withCallLog({ camera, maps });
const flyTo = engine.camera.flyTo;
assert.equal(flyTo, engine.camera.flyTo);
assert.deepEqual(called(), [], 'shape inspection must not count as a call');
assert.equal(flyTo(result), result);
assert.equal(engine.maps.setTerrain(), promise);
assert.equal(await promise, result);
assert.deepEqual(called(), ['camera.flyTo', 'maps.setTerrain']);
called().push('not a member');
assert.deepEqual(called(), ['camera.flyTo', 'maps.setTerrain']);
});
test('returned handles log under their contract type and retain receivers', () => {
const handle = {
set() {
assert.equal(this, handle);
return 42;
},
};
const { engine, called } = withCallLog({
movers: { createSet: () => handle },
});
const set = engine.movers.createSet();
assert.equal(set, engine.movers.createSet());
assert.equal(set.set(), 42);
assert.deepEqual(called(), ['MoverSet.set', 'movers.createSet']);
});
test('getter reads and setters use the original receiver', () => {
let revision = 7;
const maps = {
get terrainRevision() {
assert.equal(this, maps);
return revision;
},
set terrainRevision(value) {
assert.equal(this, maps);
revision = value;
},
};
const { engine, called } = withCallLog({ maps });
assert.deepEqual(called(), []);
assert.equal(engine.maps.terrainRevision, 7);
engine.maps.terrainRevision = 8;
assert.equal(engine.maps.terrainRevision, 8);
assert.deepEqual(called(), ['maps.terrainRevision']);
});
test('surface exclusions reach the engine as its original handles', () => {
const handle = { set() {} };
const point = {};
const surface = {
sampleHeight(position, exclude) {
assert.equal(position, point);
assert.equal(exclude[0], handle);
return 12;
},
sampleHeightDetailed(position, { exclude }) {
assert.equal(position, point);
assert.equal(exclude[0], handle);
return { height: 12, source: 'terrain' };
},
clampToHeight(position, exclude) {
assert.equal(exclude[0], handle);
return position;
},
raycast({ exclude }) {
assert.equal(exclude[0], handle);
return undefined;
},
};
const { engine } = withCallLog({
movers: { createSet: () => handle },
surface,
});
const set = engine.movers.createSet();
const exclude = [set];
assert.equal(engine.surface.sampleHeight(point, exclude), 12);
assert.deepEqual(engine.surface.sampleHeightDetailed(point, { exclude }), {
height: 12,
source: 'terrain',
});
assert.equal(engine.surface.clampToHeight(point, exclude), point);
assert.equal(engine.surface.raycast({ exclude }), undefined);
assert.equal(exclude[0], set, 'caller-owned exclusions must stay unchanged');
});
test('unknown members remain undefined or throw the original error', () => {
const error = new Error('unavailable');
const camera = {
get unknown() {
throw error;
},
};
const { engine, called } = withCallLog({ camera });
assert.equal(engine.missing, undefined);
assert.equal(engine.camera.missing, camera.missing);
assert.throws(() => engine.camera.missing(), TypeError);
assert.throws(
() => engine.camera.unknown,
(caught) => caught === error,
);
assert.deepEqual(called(), []);
});
test('follow events retain handle identity, duplicate listeners and removers', () => {
const followChanged = createEngineEvent();
const handle = { stop() {}, retarget() {}, target() {} };
const camera = {
followChanged,
follow() {
followChanged.raiseEvent(handle);
return handle;
},
following: () => handle,
};
const { engine } = withCallLog({ camera });
const seen = [];
const listener = (value) => seen.push(value);
const remove = engine.camera.followChanged.addEventListener(listener);
engine.camera.followChanged.addEventListener(listener);
const wrapped = engine.camera.follow();
assert.equal(engine.camera.following(), wrapped);
assert.deepEqual(seen, [wrapped]);
assert.equal(engine.camera.followChanged.removeEventListener(listener), true);
remove();
engine.camera.follow();
assert.equal(seen.length, 1);
});
test('contract inventory is sorted, unique and includes required capabilities and handles', () => {
const members = contractCallMembers();
assert.deepEqual(members, [...new Set(members)].sort());
for (const name of [
'camera.flyTo',
'maps.terrainRevision',
'FollowHandle.retarget',
'Line.setPositions',
'GroundSet.replace',
'ImageryLayer.setAlpha',
'GlobeMesh.update',
])
assert.ok(members.includes(name), name);
const names = engineContractMembers();
assert.ok(names.every((name) => members.includes(name)));
names.length = 0;
assert.ok(engineContractMembers().length > 0);
});