mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'german/refactor-e1-readiness-fixes' into german/navara-refactor
This commit is contained in:
commit
facc9541ab
11 files changed
+291
-42
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { clickUntilSelected, whenPickable, whenSettled } from './qa/ready.mjs';
|
||||
import { clickUntilSelected, whenFramesDrawn } from './qa/ready.mjs';
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import sharp from 'sharp';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -213,7 +213,7 @@ await strict.run({}, async (b) => {
|
||||
const deadline = Date.now() + 20000;
|
||||
let on = await capture(true, layerId);
|
||||
while (orangeOf(on) <= orangeOf(off) + 5 && Date.now() < deadline) {
|
||||
await whenSettled(p);
|
||||
await whenFramesDrawn(p);
|
||||
on = await sample();
|
||||
}
|
||||
return on;
|
||||
@@ -328,7 +328,7 @@ await strict.run({}, async (b) => {
|
||||
{ timeout: 15000 },
|
||||
visible,
|
||||
);
|
||||
const click = async (lon, lat, predicate) => {
|
||||
const click = async (lon, lat) => {
|
||||
const { x, y } = await p.evaluate(
|
||||
(world) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
@@ -339,7 +339,6 @@ await strict.run({}, async (b) => {
|
||||
},
|
||||
fromDegrees(lon, lat),
|
||||
);
|
||||
if (predicate) await whenPickable(p, { x, y }, predicate);
|
||||
await p.mouse.click(x, y);
|
||||
};
|
||||
for (const kind of ['navara', 'cesium', 'navara']) {
|
||||
@@ -400,11 +399,26 @@ await strict.run({}, async (b) => {
|
||||
await p.waitForFunction(
|
||||
() => window.qaVesselEntry.module.qaDrawn()?.chevrons[0]?.show,
|
||||
);
|
||||
const vesselPicked = (picked) => picked?.id === '123456789';
|
||||
const selectVessel = () =>
|
||||
clickUntilSelected(
|
||||
p,
|
||||
() => click(-79.38, 43.65, vesselPicked),
|
||||
async () => {
|
||||
const { x, y } = await p.evaluate(() => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
const vessel = window.qaVesselEntry.module
|
||||
.qaDrawn()
|
||||
.chevrons.find((row) => row.id === '123456789' && row.show);
|
||||
const point =
|
||||
vessel && engine.projector.worldToWindow(vessel.position);
|
||||
if (!point) throw Error('the vessel is not in view');
|
||||
const box = engine.host.canvas.getBoundingClientRect();
|
||||
return { x: box.left + point.x, y: box.top + point.y };
|
||||
});
|
||||
// Vessels also resolve painted-card hits in their click handler;
|
||||
// picker.pick cannot see those. Retry at the live anchor, including
|
||||
// its sea datum, and verify selection through the real click path.
|
||||
await p.mouse.click(x, y);
|
||||
},
|
||||
() => window.qaVesselEntry.module.getSelectedInfo()?.mmsi === '123456789',
|
||||
);
|
||||
// Click-picking on the running engine: clear the selection with a click on
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { whenFramesDrawn, whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
// Recent imagery on a keyless preview, starting on Navara: the NASA catalog
|
||||
// and imagery tiles are fixtures, and the image, A/B, swap, divider, opacity,
|
||||
// basemap, toggle, failure, box-drag and zoom-to-fit checks read the rendered
|
||||
@@ -206,7 +206,10 @@ try {
|
||||
await whenSettled(page);
|
||||
const capture = async (name) => {
|
||||
await page.evaluate(() => window.__godsEyeView.requestRender('imagery-qa'));
|
||||
await whenSettled(page);
|
||||
// The box tool holds inputs until mouseup; this capture only needs its drawing.
|
||||
await whenFramesDrawn(page);
|
||||
await whenTilesLoaded(page, { timeoutMs: 90000 });
|
||||
await whenFramesDrawn(page);
|
||||
const buffer = await page.screenshot();
|
||||
await fs.writeFile(output.replace(/\.json$/, `-${name}.png`), buffer);
|
||||
const pixels = await sharp(buffer)
|
||||
|
||||
@@ -110,19 +110,7 @@ await strict.run({}, async (browser) => {
|
||||
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
|
||||
await waitForEngine(page, 'navara');
|
||||
// Cockpit mode deliberately holds camera inputs throughout these handoffs.
|
||||
const attached = async (kind) => {
|
||||
await page.waitForFunction(
|
||||
(kind) =>
|
||||
window.__godsEyeView?.engine?.kind === kind &&
|
||||
document.querySelector('#globe-engine')?.value === kind &&
|
||||
!document.querySelector('#globe-engine:is([disabled], [aria-busy="true"])'),
|
||||
{ timeout: 90000 },
|
||||
kind,
|
||||
);
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
};
|
||||
const attached = (kind) => waitForEngine(page, kind, { timeout: 90000 });
|
||||
await page.evaluate(
|
||||
() => window.__godsEyeView.styleManager.initialRestorePromise,
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { whenFramesDrawn, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import sharp from 'sharp';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
@@ -90,7 +90,9 @@ await strict.run({}, async (b) => {
|
||||
});
|
||||
});
|
||||
const sample = async () => {
|
||||
await whenSettled(p);
|
||||
await whenFramesDrawn(p);
|
||||
await whenTilesLoaded(p);
|
||||
await whenFramesDrawn(p);
|
||||
return sharp(await p.screenshot())
|
||||
.extract({ left: 490, top: 370, width: 25, height: 25 })
|
||||
.removeAlpha()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { whenFramesDrawn } from './qa/ready.mjs';
|
||||
import sharp from 'sharp';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
@@ -152,7 +152,7 @@ await strict.run({}, async (b) => {
|
||||
const deadline = Date.now() + 20000;
|
||||
let on = await sample();
|
||||
while (!test(on, off) && Date.now() < deadline) {
|
||||
await whenSettled(p);
|
||||
await whenFramesDrawn(p);
|
||||
on = await sample();
|
||||
}
|
||||
return on;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { whenFramesDrawn, whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { trafficFixtureResponse } from './traffic-fixtures.mjs';
|
||||
import sharp from 'sharp';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -157,7 +157,7 @@ await strict.run({}, async (b) => {
|
||||
'traffic did not settle with 12 vehicles in view: ' +
|
||||
JSON.stringify({ loading, inView: rows.length }),
|
||||
);
|
||||
await whenSettled(p);
|
||||
await whenFramesDrawn(p);
|
||||
}
|
||||
};
|
||||
// The population once roads, heights and dots have settled. Source counts
|
||||
@@ -165,6 +165,8 @@ await strict.run({}, async (b) => {
|
||||
// attached road heights are sampled after drawn frames on either engine.
|
||||
const settledPopulation = async () => {
|
||||
await whenSettled(p);
|
||||
await whenTilesLoaded(p);
|
||||
await whenFramesDrawn(p);
|
||||
await settled();
|
||||
return p.evaluate(() => window.qaTraffic.getStats().count);
|
||||
};
|
||||
@@ -252,7 +254,7 @@ await strict.run({}, async (b) => {
|
||||
origin: 'user',
|
||||
}),
|
||||
);
|
||||
await whenSettled(p);
|
||||
await whenFramesDrawn(p);
|
||||
const hidden = await raw();
|
||||
const { width, height } = shown.info;
|
||||
const scale = width / 1024;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { whenFramesDrawn, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import sharp from 'sharp';
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -183,7 +183,37 @@ await strict.run({}, async (b) => {
|
||||
time,
|
||||
);
|
||||
const sample = async () => {
|
||||
await whenSettled(p);
|
||||
const handle = await p.waitForFunction(
|
||||
(ids) => {
|
||||
const { engine, dataManager } = window.__godsEyeView;
|
||||
engine.render.request('qa-weather-imagery');
|
||||
const active = ids.filter((id) => dataManager.isEnabled(id));
|
||||
return (
|
||||
active.length > 0 &&
|
||||
active.every((id) => {
|
||||
const state = dataManager.layers.get(id).module.getDiagnostics();
|
||||
return (
|
||||
state.time && !state.loading && !state.hidden && !state.error
|
||||
);
|
||||
}) &&
|
||||
engine.imagery
|
||||
.list()
|
||||
.filter(
|
||||
(layer) =>
|
||||
layer.id.startsWith('weather-') &&
|
||||
layer.shown &&
|
||||
layer.alpha > 0,
|
||||
).length >= active.length
|
||||
);
|
||||
},
|
||||
{ polling: 100, timeout: 50000 },
|
||||
ids,
|
||||
);
|
||||
await handle.dispose();
|
||||
// A staged frame can be current before the newly shown layer has painted.
|
||||
await whenFramesDrawn(p);
|
||||
await whenTilesLoaded(p);
|
||||
await whenFramesDrawn(p);
|
||||
const crop = await sharp(await p.screenshot())
|
||||
.extract({ left: 535, top: 388, width: 12, height: 12 })
|
||||
.removeAlpha()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { whenFramesDrawn } from './qa/ready.mjs';
|
||||
import assert from 'node:assert/strict';
|
||||
import { fromDegrees } from '../src/geo/cartesian3.js';
|
||||
import { fromCartesian } from '../src/geo/cartographic.js';
|
||||
@@ -162,7 +162,7 @@ await strict.run({}, async (b) => {
|
||||
(badge) => badge.picked?.id === `wildlife:${badge.id}`,
|
||||
);
|
||||
if (ok || Date.now() > deadline) return drawn;
|
||||
await whenSettled(p);
|
||||
await whenFramesDrawn(p);
|
||||
}
|
||||
};
|
||||
const degrees = (position) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { whenSettled } from './qa/ready.mjs';
|
||||
import { whenFramesDrawn } from './qa/ready.mjs';
|
||||
import sharp from 'sharp';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
@@ -96,7 +96,7 @@ await strict.run({}, async (b) => {
|
||||
window.qaWind.getRowControls().summary.reading?.speed === 10,
|
||||
{ timeout: 90000 },
|
||||
);
|
||||
await whenSettled(p);
|
||||
await whenFramesDrawn(p);
|
||||
const check = await p.evaluate(() => {
|
||||
const { dataManager } = window.__godsEyeView;
|
||||
const mod = dataManager.layers.get('wind').module;
|
||||
@@ -162,7 +162,7 @@ await strict.run({}, async (b) => {
|
||||
const frozen = await p.evaluate(
|
||||
() => window.qaWind.getDiagnostics().gpu.phaseSeconds,
|
||||
);
|
||||
await whenSettled(p);
|
||||
await whenFramesDrawn(p);
|
||||
await strict.check(
|
||||
p,
|
||||
'wind',
|
||||
|
||||
+57
-7
@@ -1,6 +1,6 @@
|
||||
import { createSettledState } from '../../src/app/qaProbe.js';
|
||||
|
||||
/** A CPU-ready pick can precede the GPU hit delivered for a real click. */
|
||||
/** Verify real click selection, including targets the CPU picker cannot answer. */
|
||||
export async function clickUntilSelected(page, click, selected) {
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
await click();
|
||||
@@ -72,7 +72,7 @@ export async function whenSettled(
|
||||
});
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Readiness timeout: engineStarted, initialViewRestored, frameDrawn, noFlight, cameraInputsEnabled, cameraPoseStable',
|
||||
'Readiness timeout: engineStarted, initialViewRestored, frameDrawn, noFlight, cameraInputsEnabled, cameraPoseStable; observed: inputsEnabled=unknown, following=unknown, cockpitActive=unknown, lastPoseDelta=null',
|
||||
);
|
||||
}
|
||||
return page.evaluate(
|
||||
@@ -85,6 +85,33 @@ export function waitForSettled(createState, { frames, timeoutMs }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { engine } = window.__godsEyeView;
|
||||
const state = createState(engine);
|
||||
let previousPose = null;
|
||||
let lastPoseDelta = null;
|
||||
const statusOf = () => {
|
||||
const status = state.settled({ frames });
|
||||
const started = status.conditions.engineStarted;
|
||||
const following = started && Boolean(engine.camera.following());
|
||||
const cockpitActive = Boolean(
|
||||
window.__godsEyeView?.styleManager?.getCockpitState?.()?.active,
|
||||
);
|
||||
status.observed = {
|
||||
inputsEnabled: status.conditions.cameraInputsEnabled,
|
||||
following,
|
||||
cockpitActive,
|
||||
lastPoseDelta,
|
||||
};
|
||||
// Only a live camera owner excuses motion or held inputs, never a scenario flag.
|
||||
if (following || cockpitActive) {
|
||||
status.conditions.cameraInputsEnabled = true;
|
||||
status.conditions.cameraPoseStable = true;
|
||||
}
|
||||
status.conditions.frameDrawn = status.framesDrawn >= frames;
|
||||
status.unmet = Object.keys(status.conditions).filter(
|
||||
(key) => !status.conditions[key],
|
||||
);
|
||||
status.settled = !status.unmet.length;
|
||||
return status;
|
||||
};
|
||||
let finished = false;
|
||||
let remove = () => {};
|
||||
const finish = (error, result) => {
|
||||
@@ -97,19 +124,37 @@ export function waitForSettled(createState, { frames, timeoutMs }) {
|
||||
else resolve(result);
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
const status = state.settled({ frames });
|
||||
const status = statusOf();
|
||||
if (window.__godsEyeView?.engine !== engine)
|
||||
status.unmet.push('engineUnchanged');
|
||||
finish(
|
||||
new Error(
|
||||
`Readiness timeout: ${status.unmet.join(', ')}${status.restoreError ? ': ' + status.restoreError : ''}`,
|
||||
`Readiness timeout: ${status.unmet.join(', ')}${status.restoreError ? ': ' + status.restoreError : ''}; observed: ${JSON.stringify(status.observed)}`,
|
||||
),
|
||||
);
|
||||
}, timeoutMs);
|
||||
remove = engine.frames.postRender.addEventListener(() => {
|
||||
const status = state.settled({ frames });
|
||||
if (window.__godsEyeView?.engine !== engine)
|
||||
return finish(new Error('Readiness interrupted: engineChanged'));
|
||||
if (!engine.isDestroyed()) {
|
||||
const pose = engine.camera.pose();
|
||||
const values = ['position', 'direction', 'up'].flatMap((key) =>
|
||||
['x', 'y', 'z'].map((axis) => pose[key][axis]),
|
||||
);
|
||||
values.push(pose.fovy, pose.aspectRatio);
|
||||
if (previousPose) {
|
||||
const delta = values.map((value, i) =>
|
||||
Math.abs(value - previousPose[i]),
|
||||
);
|
||||
lastPoseDelta = {
|
||||
positionM: Math.hypot(...delta.slice(0, 3)),
|
||||
orientation: Math.max(...delta.slice(3, 9)),
|
||||
projection: Math.max(...delta.slice(9)),
|
||||
};
|
||||
}
|
||||
previousPose = values;
|
||||
}
|
||||
const status = statusOf();
|
||||
if (status.settled) finish(null, status);
|
||||
else
|
||||
queueMicrotask(() => {
|
||||
@@ -120,7 +165,7 @@ export function waitForSettled(createState, { frames, timeoutMs }) {
|
||||
engine.render.request('qa-readiness');
|
||||
});
|
||||
}
|
||||
/** Wait for drawn frames when the scenario deliberately keeps a follow or orbit moving. */
|
||||
/** Wait for drawing independently of camera rest or input ownership. */
|
||||
export async function whenFramesDrawn(
|
||||
page,
|
||||
{ frames = 3, timeoutMs = 30000 } = {},
|
||||
@@ -151,7 +196,12 @@ export async function whenFramesDrawn(
|
||||
/** Tile readiness is independent of camera rest, including a keyless globe. */
|
||||
export async function whenTilesLoaded(page, { timeoutMs = 30000 } = {}) {
|
||||
const handle = await page.waitForFunction(
|
||||
() => window.__godsEyeView?.engine?.surface.tilesLoaded(),
|
||||
() => {
|
||||
const engine = window.__godsEyeView?.engine;
|
||||
if (!engine || engine.isDestroyed()) return false;
|
||||
engine.render.request('qa-tile-readiness');
|
||||
return engine.surface.tilesLoaded();
|
||||
},
|
||||
{ timeout: timeoutMs, polling: 100 },
|
||||
);
|
||||
await handle.dispose();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
|
||||
import { createSettledState } from '../../src/app/qaProbe.js';
|
||||
import {
|
||||
waitForSettled,
|
||||
whenFramesDrawn,
|
||||
whenTilesLoaded,
|
||||
whenUiSettled,
|
||||
@@ -9,6 +11,164 @@ import {
|
||||
until,
|
||||
} from './ready.mjs';
|
||||
|
||||
function pageEngine(t) {
|
||||
const engine = createFakeEngine();
|
||||
const previous = globalThis.window;
|
||||
const app = {
|
||||
engine,
|
||||
styleManager: {
|
||||
initialRestorePromise: Promise.resolve(),
|
||||
getCockpitState: () => ({ active: false }),
|
||||
},
|
||||
};
|
||||
globalThis.window = { __godsEyeView: app };
|
||||
t.after(() => {
|
||||
if (previous === undefined) delete globalThis.window;
|
||||
else globalThis.window = previous;
|
||||
engine.destroy();
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
test('cockpit and follow readiness allow held inputs and moving poses after drawn frames', async (t) => {
|
||||
for (const owner of ['cockpit', 'follow']) {
|
||||
await t.test(owner, async (t) => {
|
||||
const { engine, styleManager } = pageEngine(t);
|
||||
engine.input.suspendCameraInputs();
|
||||
let drawn = 0;
|
||||
if (owner === 'cockpit')
|
||||
styleManager.getCockpitState = () => ({ active: true });
|
||||
else
|
||||
engine.camera.follow({
|
||||
position: () => ({ x: 6378137, y: drawn * 10, z: 0 }),
|
||||
viewFrom: { x: 0, y: -100, z: 100 },
|
||||
});
|
||||
t.mock.method(engine.render, 'request', () =>
|
||||
setImmediate(() => {
|
||||
engine.fake.setPose({ heading: ++drawn / 100 });
|
||||
engine.fake.frame();
|
||||
}),
|
||||
);
|
||||
const status = await waitForSettled(createSettledState, {
|
||||
frames: 3,
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
assert.equal(status.settled, true);
|
||||
assert.equal(status.framesDrawn, 3);
|
||||
assert.equal(status.observed.inputsEnabled, false);
|
||||
assert.equal(status.observed.cockpitActive, owner === 'cockpit');
|
||||
assert.equal(status.observed.following, owner === 'follow');
|
||||
assert.ok(status.observed.lastPoseDelta.orientation > 0);
|
||||
assert.equal(engine.frames.postRender.numberOfListeners, 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('camera owners do not excuse restoration, flights, missing frames or destruction', async (t) => {
|
||||
for (const unmet of [
|
||||
'initialViewRestored',
|
||||
'noFlight',
|
||||
'frameDrawn',
|
||||
'engineStarted',
|
||||
]) {
|
||||
await t.test(unmet, async (t) => {
|
||||
const { engine, styleManager } = pageEngine(t);
|
||||
styleManager.getCockpitState = () => ({ active: true });
|
||||
engine.input.suspendCameraInputs();
|
||||
if (unmet === 'initialViewRestored')
|
||||
styleManager.initialRestorePromise = new Promise(() => {});
|
||||
if (unmet === 'noFlight')
|
||||
engine.camera.flyTo({
|
||||
destination: { x: 7000000, y: 0, z: 0 },
|
||||
duration: 1,
|
||||
});
|
||||
t.mock.method(engine.render, 'request', () => {});
|
||||
const pending = waitForSettled(createSettledState, {
|
||||
frames: 3,
|
||||
timeoutMs: 10,
|
||||
});
|
||||
await Promise.resolve();
|
||||
if (unmet === 'engineStarted') {
|
||||
engine.destroy();
|
||||
t.mock.method(engine.camera, 'pose', () => {
|
||||
throw Error('released camera');
|
||||
});
|
||||
} else engine.fake.frames(unmet === 'frameDrawn' ? 2 : 4);
|
||||
await assert.rejects(pending, (error) => {
|
||||
assert.match(error.message, new RegExp(`Readiness timeout: ${unmet}`));
|
||||
assert.match(error.message, /"cockpitActive":true/);
|
||||
return true;
|
||||
});
|
||||
assert.equal(engine.frames.postRender.numberOfListeners, 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('unowned input locks and moving cameras time out with observed state', async (t) => {
|
||||
for (const moving of [false, true]) {
|
||||
await t.test(moving ? 'moving' : 'locked', async (t) => {
|
||||
const { engine } = pageEngine(t);
|
||||
if (!moving) engine.input.suspendCameraInputs();
|
||||
t.mock.method(engine.render, 'request', () => {});
|
||||
const pending = waitForSettled(createSettledState, {
|
||||
frames: 3,
|
||||
timeoutMs: 10,
|
||||
});
|
||||
await Promise.resolve();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (moving)
|
||||
engine.fake.setPose({ position: { x: 7000000 + i, y: 0, z: 0 } });
|
||||
engine.fake.frame();
|
||||
}
|
||||
await assert.rejects(pending, (error) => {
|
||||
assert.match(error.message, /cameraPoseStable/);
|
||||
if (!moving) assert.match(error.message, /cameraInputsEnabled/);
|
||||
const observed = JSON.parse(error.message.split('; observed: ')[1]);
|
||||
assert.deepEqual(observed, {
|
||||
inputsEnabled: moving,
|
||||
following: false,
|
||||
cockpitActive: false,
|
||||
lastPoseDelta: {
|
||||
positionM: moving ? 1 : 0,
|
||||
orientation: 0,
|
||||
projection: 0,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('leaving a camera owner restores strict input and pose requirements', async (t) => {
|
||||
for (const owner of ['cockpit', 'follow']) {
|
||||
await t.test(owner, async (t) => {
|
||||
const { engine, styleManager } = pageEngine(t);
|
||||
engine.input.suspendCameraInputs();
|
||||
let active = owner === 'cockpit';
|
||||
styleManager.getCockpitState = () => ({ active });
|
||||
const follow =
|
||||
owner === 'follow'
|
||||
? engine.camera.follow({
|
||||
position: () => ({ x: 6378137, y: 0, z: 0 }),
|
||||
viewFrom: { x: 0, y: -100, z: 100 },
|
||||
})
|
||||
: null;
|
||||
t.mock.method(engine.render, 'request', () => {});
|
||||
const pending = waitForSettled(createSettledState, {
|
||||
frames: 3,
|
||||
timeoutMs: 10,
|
||||
});
|
||||
await Promise.resolve();
|
||||
engine.fake.frames(2);
|
||||
active = false;
|
||||
follow?.stop();
|
||||
engine.fake.frames(4);
|
||||
await assert.rejects(pending, /cameraInputsEnabled, cameraPoseStable/);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('frame readiness advances a request-mode engine and removes its listener', async (t) => {
|
||||
const engine = createFakeEngine();
|
||||
const previous = globalThis.window;
|
||||
|
||||
Reference in new issue
Block a user