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'),
|
||||
);
|
||||
await page.setCacheEnabled(false);
|
||||
// Keep both options while selecting A flies the view away from B.
|
||||
await page.focus('#cctv-camera-select');
|
||||
delayCameraA = true;
|
||||
await page.select('#cctv-camera-select', 'qa-camera-a');
|
||||
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. */
|
||||
export async function whenTrafficSettled(page) {
|
||||
await whenTilesLoaded(page);
|
||||
await whenSettled(page);
|
||||
const handle = await page.waitForFunction(
|
||||
() => {
|
||||
const { engine, dataManager } = window.__godsEyeView;
|
||||
const pose = engine.camera.pose();
|
||||
const viewKey = [
|
||||
pose.cartographic.latitude,
|
||||
pose.cartographic.longitude,
|
||||
pose.cartographic.height / 10000,
|
||||
pose.heading,
|
||||
pose.pitch,
|
||||
]
|
||||
.map((value) => Number(value || 0).toFixed(7))
|
||||
.join(',');
|
||||
const stats = dataManager.layers.get('traffic').module.getStats();
|
||||
return (
|
||||
stats.roadBounds?.coverage?.viewKey === viewKey &&
|
||||
stats.count > 0 &&
|
||||
!stats.loading &&
|
||||
!stats.surfacePending
|
||||
);
|
||||
},
|
||||
{ timeout: 60000, polling: 100 },
|
||||
);
|
||||
await handle.dispose();
|
||||
let handle;
|
||||
try {
|
||||
handle = await page.waitForFunction(`(${trafficReadiness})().settled`, {
|
||||
timeout: 60000,
|
||||
polling: 100,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.name !== 'TimeoutError') throw error;
|
||||
const observed = await page.evaluate(trafficReadiness);
|
||||
throw new Error(`Readiness timeout: traffic; ${JSON.stringify(observed)}`, {
|
||||
cause: error,
|
||||
});
|
||||
} finally {
|
||||
await handle?.dispose();
|
||||
}
|
||||
}
|
||||
+106
-7
@@ -1,7 +1,12 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
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) {
|
||||
const engine = createFakeEngine({ kind: 'cesium' });
|
||||
@@ -69,7 +74,9 @@ test('traffic readiness rejects old coverage, pending debounce and partial popul
|
||||
let disposed = 0;
|
||||
await whenTrafficSettled({
|
||||
evaluate: async () => {},
|
||||
async waitForFunction(predicate) {
|
||||
async waitForFunction(expression) {
|
||||
const predicate = () =>
|
||||
typeof expression === 'function' ? expression() : eval(expression);
|
||||
if (++calls < 3) assert.equal(predicate(), true);
|
||||
else {
|
||||
assert.equal(
|
||||
@@ -100,17 +107,109 @@ test('traffic readiness rejects old coverage, pending debounce and partial popul
|
||||
assert.equal(disposed, 3);
|
||||
});
|
||||
|
||||
test('traffic loading timeouts fail the scenario', async (t) => {
|
||||
fixture(t);
|
||||
test('traffic readiness accepts the layer footprint after rounding and tiny pose drift', (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;
|
||||
await assert.rejects(
|
||||
whenTrafficSettled({
|
||||
evaluate: async () => {},
|
||||
evaluate: async (fn) => (typeof fn === 'function' ? fn() : undefined),
|
||||
async waitForFunction() {
|
||||
if (++calls === 3) throw new Error('traffic loading timeout');
|
||||
if (++calls === 3) throw timeout;
|
||||
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