mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 13:52:19 +00:00
Merge branch 'german/refactor-e12-script-regressions-2' into german/navara-refactor
This commit is contained in:
commit
ab41c765cc
4 files changed
+271
-31
No files matched your search
@@ -279,6 +279,8 @@ try {
|
|||||||
.classList.contains('has-frame'),
|
.classList.contains('has-frame'),
|
||||||
);
|
);
|
||||||
await page.setCacheEnabled(false);
|
await page.setCacheEnabled(false);
|
||||||
|
// Keep both options while selecting A flies the view away from B.
|
||||||
|
await page.focus('#cctv-camera-select');
|
||||||
delayCameraA = true;
|
delayCameraA = true;
|
||||||
await page.select('#cctv-camera-select', 'qa-camera-a');
|
await page.select('#cctv-camera-select', 'qa-camera-a');
|
||||||
await page.waitForFunction(
|
await page.waitForFunction(
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import '../src/testSupport/registerDom.mjs';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import { createApp, nextTick } from 'vue';
|
||||||
|
import { CctvControls } from '../src/ui/cctvControls.js';
|
||||||
|
import { useCctvStore } from '../src/ui/stores/cctv.js';
|
||||||
|
import CctvPanel from '../src/ui/views/panels/CctvPanel.vue';
|
||||||
|
|
||||||
|
const source = readFileSync(
|
||||||
|
new URL('./qa-camera-controls.mjs', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
test('the delayed-frame scenario keeps camera B selectable after the flight to A', async (t) => {
|
||||||
|
document.body.replaceChildren();
|
||||||
|
const host = document.body.appendChild(document.createElement('div'));
|
||||||
|
const pinia = createPinia();
|
||||||
|
const app = createApp(CctvPanel).use(pinia);
|
||||||
|
app.mount(host);
|
||||||
|
const select = document.getElementById('cctv-camera-select');
|
||||||
|
const controls = new CctvControls({
|
||||||
|
elements: {
|
||||||
|
_cctvPanel: document.getElementById('cctv-panel'),
|
||||||
|
_cctvSelect: select,
|
||||||
|
},
|
||||||
|
cctv: {},
|
||||||
|
actions: {
|
||||||
|
isEnabled: () => true,
|
||||||
|
setPanelCollapsed() {},
|
||||||
|
syncViewport() {},
|
||||||
|
},
|
||||||
|
store: useCctvStore(pinia),
|
||||||
|
});
|
||||||
|
t.after(() => {
|
||||||
|
controls.destroy();
|
||||||
|
app.unmount();
|
||||||
|
});
|
||||||
|
const [a, b] = ['a', 'b'].map((id) => ({
|
||||||
|
id: `qa-camera-${id}`,
|
||||||
|
name: id,
|
||||||
|
city: 'Austin',
|
||||||
|
}));
|
||||||
|
const render = async (activeCamera, cameraIndex) => {
|
||||||
|
controls._renderCctvState({
|
||||||
|
enabled: true,
|
||||||
|
count: 2,
|
||||||
|
activeCameraId: activeCamera.id,
|
||||||
|
activeCamera,
|
||||||
|
cameraIndex,
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
};
|
||||||
|
await render(b, [a, b]);
|
||||||
|
const prepare = source.slice(
|
||||||
|
source.indexOf(' await page.setCacheEnabled(false);'),
|
||||||
|
source.indexOf(' delayCameraA = true;'),
|
||||||
|
);
|
||||||
|
await new Function('page', `return (async () => { ${prepare} })();`)({
|
||||||
|
setCacheEnabled: async () => {},
|
||||||
|
focus: async (selector) => document.querySelector(selector).focus(),
|
||||||
|
});
|
||||||
|
await render(a, [a]);
|
||||||
|
assert.ok([...select.options].some((option) => option.value === b.id));
|
||||||
|
select.value = b.id;
|
||||||
|
assert.equal(select.value, b.id);
|
||||||
|
select.blur();
|
||||||
|
await nextTick();
|
||||||
|
assert.ok(
|
||||||
|
![...select.options].some((option) => option.value === b.id),
|
||||||
|
'an unfocused picker drops B once the flight takes it out of view',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('camera B fixtures respond while camera A frames remain held', async () => {
|
||||||
|
let route;
|
||||||
|
const delayedFrames = new Set();
|
||||||
|
const routing = source.slice(
|
||||||
|
source.indexOf(" page.on('request',"),
|
||||||
|
source.indexOf(' await page.goto('),
|
||||||
|
);
|
||||||
|
new Function('page', 'origin', 'delayCameraA', 'delayedFrames', routing)(
|
||||||
|
{ on: (event, handler) => (route = handler) },
|
||||||
|
'http://localhost:4321',
|
||||||
|
true,
|
||||||
|
delayedFrames,
|
||||||
|
);
|
||||||
|
const replies = [];
|
||||||
|
for (const id of ['a', 'b']) {
|
||||||
|
route({
|
||||||
|
url: () => `http://localhost:4321/api/cctv/frame/qa-camera-${id}?ts=1`,
|
||||||
|
respond: async (response) => replies.push({ id, response }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
assert.equal(delayedFrames.size, 1);
|
||||||
|
assert.deepEqual(
|
||||||
|
replies.map(({ id }) => id),
|
||||||
|
['b'],
|
||||||
|
);
|
||||||
|
assert.match(replies[0].response.body, /QA CAMERA B/);
|
||||||
|
await Promise.all([...delayedFrames].map((respond) => respond()));
|
||||||
|
assert.deepEqual(
|
||||||
|
replies.map(({ id }) => id),
|
||||||
|
['b', 'a'],
|
||||||
|
);
|
||||||
|
});
|
||||||
+56
-24
@@ -38,32 +38,64 @@ export function moveTrafficCamera(page, view, seconds) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Compare the layer's camera-event sample with the view currently on screen. */
|
||||||
|
export function trafficReadiness() {
|
||||||
|
const { engine, dataManager } = window.__godsEyeView;
|
||||||
|
const pose = engine.camera.pose();
|
||||||
|
const view = [
|
||||||
|
pose.cartographic.latitude,
|
||||||
|
pose.cartographic.longitude,
|
||||||
|
pose.cartographic.height / 10000,
|
||||||
|
pose.heading,
|
||||||
|
pose.pitch,
|
||||||
|
];
|
||||||
|
const stats = dataManager.layers.get('traffic').module.getStats();
|
||||||
|
const coverageViewKey = stats.roadBounds?.coverage?.viewKey ?? null;
|
||||||
|
const coverage = coverageViewKey?.split(',').map(Number);
|
||||||
|
// Allow rounding and sub-metre settling, not another altitude or camera view.
|
||||||
|
// Angles are radians; scaled height has a one-metre tolerance.
|
||||||
|
const tolerances = [1e-7, 1e-7, 1e-4, 1e-6, 1e-6];
|
||||||
|
const viewMatches = Boolean(
|
||||||
|
coverage?.length === view.length &&
|
||||||
|
view.every((value, i) => {
|
||||||
|
if (!Number.isFinite(value) || !Number.isFinite(coverage[i]))
|
||||||
|
return false;
|
||||||
|
const delta = value - coverage[i];
|
||||||
|
const distance =
|
||||||
|
i === 1 || i === 3
|
||||||
|
? Math.abs(Math.atan2(Math.sin(delta), Math.cos(delta)))
|
||||||
|
: Math.abs(delta);
|
||||||
|
return distance <= tolerances[i];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
viewKey: view.map((value) => Number(value).toFixed(7)).join(','),
|
||||||
|
coverageViewKey,
|
||||||
|
count: stats.count,
|
||||||
|
loading: stats.loading,
|
||||||
|
surfacePending: stats.surfacePending,
|
||||||
|
settled:
|
||||||
|
viewMatches && stats.count > 0 && !stats.loading && !stats.surfacePending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** A quiet loader can still be waiting for its camera-change debounce. */
|
/** A quiet loader can still be waiting for its camera-change debounce. */
|
||||||
export async function whenTrafficSettled(page) {
|
export async function whenTrafficSettled(page) {
|
||||||
await whenTilesLoaded(page);
|
await whenTilesLoaded(page);
|
||||||
await whenSettled(page);
|
await whenSettled(page);
|
||||||
const handle = await page.waitForFunction(
|
let handle;
|
||||||
() => {
|
try {
|
||||||
const { engine, dataManager } = window.__godsEyeView;
|
handle = await page.waitForFunction(`(${trafficReadiness})().settled`, {
|
||||||
const pose = engine.camera.pose();
|
timeout: 60000,
|
||||||
const viewKey = [
|
polling: 100,
|
||||||
pose.cartographic.latitude,
|
});
|
||||||
pose.cartographic.longitude,
|
} catch (error) {
|
||||||
pose.cartographic.height / 10000,
|
if (error.name !== 'TimeoutError') throw error;
|
||||||
pose.heading,
|
const observed = await page.evaluate(trafficReadiness);
|
||||||
pose.pitch,
|
throw new Error(`Readiness timeout: traffic; ${JSON.stringify(observed)}`, {
|
||||||
]
|
cause: error,
|
||||||
.map((value) => Number(value || 0).toFixed(7))
|
});
|
||||||
.join(',');
|
} finally {
|
||||||
const stats = dataManager.layers.get('traffic').module.getStats();
|
await handle?.dispose();
|
||||||
return (
|
}
|
||||||
stats.roadBounds?.coverage?.viewKey === viewKey &&
|
|
||||||
stats.count > 0 &&
|
|
||||||
!stats.loading &&
|
|
||||||
!stats.surfacePending
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ timeout: 60000, polling: 100 },
|
|
||||||
);
|
|
||||||
await handle.dispose();
|
|
||||||
}
|
}
|
||||||
+106
-7
@@ -1,7 +1,12 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
|
import { createFakeEngine } from '../../src/globe/testing/fakeEngine.js';
|
||||||
import { moveTrafficCamera, whenTrafficSettled } from './traffic.mjs';
|
import { createReticleFootprint } from '../../src/layers/traffic/footprint.js';
|
||||||
|
import {
|
||||||
|
moveTrafficCamera,
|
||||||
|
trafficReadiness,
|
||||||
|
whenTrafficSettled,
|
||||||
|
} from './traffic.mjs';
|
||||||
|
|
||||||
function fixture(t) {
|
function fixture(t) {
|
||||||
const engine = createFakeEngine({ kind: 'cesium' });
|
const engine = createFakeEngine({ kind: 'cesium' });
|
||||||
@@ -69,7 +74,9 @@ test('traffic readiness rejects old coverage, pending debounce and partial popul
|
|||||||
let disposed = 0;
|
let disposed = 0;
|
||||||
await whenTrafficSettled({
|
await whenTrafficSettled({
|
||||||
evaluate: async () => {},
|
evaluate: async () => {},
|
||||||
async waitForFunction(predicate) {
|
async waitForFunction(expression) {
|
||||||
|
const predicate = () =>
|
||||||
|
typeof expression === 'function' ? expression() : eval(expression);
|
||||||
if (++calls < 3) assert.equal(predicate(), true);
|
if (++calls < 3) assert.equal(predicate(), true);
|
||||||
else {
|
else {
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -100,17 +107,109 @@ test('traffic readiness rejects old coverage, pending debounce and partial popul
|
|||||||
assert.equal(disposed, 3);
|
assert.equal(disposed, 3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('traffic loading timeouts fail the scenario', async (t) => {
|
test('traffic readiness accepts the layer footprint after rounding and tiny pose drift', (t) => {
|
||||||
fixture(t);
|
const app = fixture(t);
|
||||||
|
app.engine.fake.setPose({
|
||||||
|
latitude: (30.2672 * Math.PI) / 180,
|
||||||
|
longitude: (-97.7431 * Math.PI) / 180,
|
||||||
|
height: 300,
|
||||||
|
heading: 2 * Math.PI - 1e-8,
|
||||||
|
pitch: -Math.PI / 12,
|
||||||
|
});
|
||||||
|
const stats = {
|
||||||
|
count: 100,
|
||||||
|
loading: false,
|
||||||
|
surfacePending: 0,
|
||||||
|
roadBounds: createReticleFootprint()(app.engine, 300),
|
||||||
|
};
|
||||||
|
app.dataManager = {
|
||||||
|
layers: new Map([['traffic', { module: { getStats: () => stats } }]]),
|
||||||
|
};
|
||||||
|
assert.equal(trafficReadiness().settled, true);
|
||||||
|
app.engine.fake.setPose({
|
||||||
|
latitude: (30.2672 * Math.PI) / 180 + 2e-8,
|
||||||
|
longitude: (-97.7431 * Math.PI) / 180 - 2e-8,
|
||||||
|
height: 300.1,
|
||||||
|
heading: 1e-8,
|
||||||
|
pitch: -Math.PI / 12 + 2e-8,
|
||||||
|
});
|
||||||
|
const observed = trafficReadiness();
|
||||||
|
assert.notEqual(observed.viewKey, observed.coverageViewKey);
|
||||||
|
assert.equal(observed.settled, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('traffic readiness rejects changed views and malformed coverage keys', (t) => {
|
||||||
|
const app = fixture(t);
|
||||||
|
const stats = {
|
||||||
|
count: 100,
|
||||||
|
loading: false,
|
||||||
|
surfacePending: 0,
|
||||||
|
roadBounds: { coverage: {} },
|
||||||
|
};
|
||||||
|
app.dataManager = {
|
||||||
|
layers: new Map([['traffic', { module: { getStats: () => stats } }]]),
|
||||||
|
};
|
||||||
|
for (const key of [
|
||||||
|
null,
|
||||||
|
'old view',
|
||||||
|
'0,0,0.1,0',
|
||||||
|
'0,0,0.1,0,NaN',
|
||||||
|
'0,0,Infinity,0,-0.2617994',
|
||||||
|
'0.01,0,0.1,0,-0.2617994',
|
||||||
|
'0,0.01,0.1,0,-0.2617994',
|
||||||
|
'0,0,0.3,0,-0.2617994',
|
||||||
|
'0,0,0.1,0.01,-0.2617994',
|
||||||
|
'0,0,0.1,0,-0.7853982',
|
||||||
|
]) {
|
||||||
|
stats.roadBounds.coverage.viewKey = key;
|
||||||
|
assert.equal(trafficReadiness().settled, false, String(key));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('traffic loading timeouts include both keys and pending work', async (t) => {
|
||||||
|
const app = fixture(t);
|
||||||
|
const coverageViewKey = '0.01,0,0.1,0,-0.2617994';
|
||||||
|
app.dataManager = {
|
||||||
|
layers: new Map([
|
||||||
|
[
|
||||||
|
'traffic',
|
||||||
|
{
|
||||||
|
module: {
|
||||||
|
getStats: () => ({
|
||||||
|
count: 12,
|
||||||
|
loading: true,
|
||||||
|
surfacePending: 2,
|
||||||
|
roadBounds: { coverage: { viewKey: coverageViewKey } },
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
const timeout = new Error('traffic loading timeout');
|
||||||
|
timeout.name = 'TimeoutError';
|
||||||
let calls = 0;
|
let calls = 0;
|
||||||
await assert.rejects(
|
await assert.rejects(
|
||||||
whenTrafficSettled({
|
whenTrafficSettled({
|
||||||
evaluate: async () => {},
|
evaluate: async (fn) => (typeof fn === 'function' ? fn() : undefined),
|
||||||
async waitForFunction() {
|
async waitForFunction() {
|
||||||
if (++calls === 3) throw new Error('traffic loading timeout');
|
if (++calls === 3) throw timeout;
|
||||||
return { dispose: async () => {} };
|
return { dispose: async () => {} };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
/traffic loading timeout/,
|
(error) => {
|
||||||
|
assert.equal(error.cause, timeout);
|
||||||
|
assert.match(error.message, /Readiness timeout: traffic/);
|
||||||
|
assert.ok(
|
||||||
|
error.message.includes(`"coverageViewKey":"${coverageViewKey}"`),
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
error.message.includes(
|
||||||
|
'"viewKey":"0.0000000,0.0000000,0.1000000,0.0000000,-0.2617994"',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.match(error.message, /"loading":true,"surfacePending":2/);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user