mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Each script launches and closes Chrome through scripts/qa/browser.mjs, waits on the application through scripts/qa/ready.mjs instead of the loading screen and fixed pauses, and reads the contract instead of Cesium where the check is not about Cesium itself.
210 lines
7.2 KiB
JavaScript
210 lines
7.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Verify imported camera directions through real rendering and camera authority handoffs. */
|
|
import { whenSettled } from './qa/ready.mjs';
|
|
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
const output = path.resolve('qa-shots/director-camera');
|
|
fs.mkdirSync(output, { recursive: true });
|
|
const browser = await launchBrowser({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
|
});
|
|
const page = await browser.newPage();
|
|
const errors = [];
|
|
page.on('pageerror', (error) => errors.push(error.message));
|
|
let failed = 0;
|
|
function check(name, passed) {
|
|
console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`);
|
|
if (!passed) failed++;
|
|
}
|
|
try {
|
|
await page.setViewport({ width: 1440, height: 900 });
|
|
await page.goto(
|
|
`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`,
|
|
{ waitUntil: 'domcontentloaded' },
|
|
);
|
|
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector, {
|
|
timeout: 60000,
|
|
});
|
|
await whenSettled(page);
|
|
const result = await page.evaluate(async () => {
|
|
const { sceneDirector: d, styleManager: style } = window.__godsEyeView;
|
|
const file = {
|
|
version: 4,
|
|
scenes: [
|
|
{
|
|
id: 'directions',
|
|
title: 'Camera directions',
|
|
anchors: [
|
|
{
|
|
id: 'start',
|
|
title: 'Austin west',
|
|
lat: 30.2672,
|
|
lon: -97.76,
|
|
alt: 6000,
|
|
altitudeReference: 'ellipsoid',
|
|
},
|
|
{
|
|
id: 'end',
|
|
title: 'Austin east',
|
|
lat: 30.2672,
|
|
lon: -97.73,
|
|
alt: 7000,
|
|
altitudeReference: 'ellipsoid',
|
|
},
|
|
],
|
|
shots: [
|
|
{
|
|
id: 'move',
|
|
title: 'Across Austin',
|
|
durationSec: 1,
|
|
holdSec: 0.2,
|
|
camera: { anchorId: 'end', heading: 10, pitch: -45, roll: 0 },
|
|
move: {
|
|
from: { anchorId: 'start', heading: 350, pitch: -45, roll: 0 },
|
|
easing: 'linear',
|
|
},
|
|
layers: {},
|
|
visual: { style: 'normal' },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await d.importProjectFile(
|
|
new File([JSON.stringify(file)], 'directions.json'),
|
|
);
|
|
const scene = d._project.scenes[0];
|
|
const shot = scene.shots[0];
|
|
const imported =
|
|
scene.anchors.length === 2 && shot.camera.anchorId === 'end';
|
|
await d.seekScene(scene.id, 0.5 / 1.2);
|
|
const middle = style.getCameraState();
|
|
const seekMidpoint =
|
|
Math.abs(middle.lon - -97.745) < 1e-6 &&
|
|
Math.abs(middle.alt - 6500) < 0.01;
|
|
await d.seekScene(scene.id, 0);
|
|
const back = style.getCameraState();
|
|
const backward = Math.abs(back.lon - -97.76) < 1e-6;
|
|
const replay = await d.replayShot(scene.id, shot.id);
|
|
const end = style.getCameraState();
|
|
const replayEnd =
|
|
replay?.started &&
|
|
Math.abs(end.lon - -97.73) < 1e-6 &&
|
|
Math.abs(end.alt - 7000) < 0.01;
|
|
d.stopScene();
|
|
await d.startScene(scene.id, { single: true, preview: false });
|
|
const completed =
|
|
!d.running && d.getPlaybackTimingState().activeTimers === 0;
|
|
const settled = Math.abs(style.getCameraState().lon - -97.73) < 1e-6;
|
|
// Updating the target creates an explicit inline pose while retaining the authored start.
|
|
d.updateSelectedShot();
|
|
const saved = JSON.parse(
|
|
localStorage.getItem('godsEyeView.sceneProject.v2'),
|
|
);
|
|
const updated =
|
|
saved.scenes[0].shots[0].camera.altitudeReference === 'ellipsoid' &&
|
|
saved.scenes[0].shots[0].move.from.anchorId === 'start';
|
|
shot.durationSec = 30;
|
|
const run = d.startScene(scene.id, { single: true, preview: false });
|
|
const deadline = Date.now() + 10000;
|
|
while (!d._cameraMotion.active && Date.now() < deadline)
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
const started = d._cameraMotion.active;
|
|
style.runImmediateNavigation('location', () => true);
|
|
await run;
|
|
const revoked =
|
|
started &&
|
|
!d.running &&
|
|
!d._cameraMotion.active &&
|
|
d.getPlaybackTimingState().activeTimers === 0;
|
|
const stoppedPose = style.getCameraState();
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
const stable =
|
|
Math.abs(style.getCameraState().lon - stoppedPose.lon) < 1e-7;
|
|
return {
|
|
imported,
|
|
seekMidpoint,
|
|
backward,
|
|
replayEnd,
|
|
completed,
|
|
settled,
|
|
updated,
|
|
revoked,
|
|
stable,
|
|
};
|
|
});
|
|
for (const [name, passed] of Object.entries(result)) check(name, passed);
|
|
await whenSettled(page);
|
|
await page.screenshot({ path: path.join(output, 'austin.png') });
|
|
await page.evaluate(() => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
const pose = d.styleManager.getCameraState();
|
|
d._setCameraView({ ...pose, heading: pose.heading + 25, pitch: -60 });
|
|
d.viewer.scene.requestRender();
|
|
});
|
|
await whenSettled(page);
|
|
await page.screenshot({ path: path.join(output, 'angle.png') });
|
|
const inputStarted = await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
const scene = d._project.scenes[0];
|
|
window.__qaCameraRun = d.startScene(scene.id, {
|
|
single: true,
|
|
preview: false,
|
|
});
|
|
const deadline = Date.now() + 10000;
|
|
while (!d._cameraMotion.active && Date.now() < deadline)
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
return d._cameraMotion.active;
|
|
});
|
|
const canvas =
|
|
(await page.$('canvas.cesium-widget-canvas')) ||
|
|
(await page.$('.cesium-widget canvas'));
|
|
const rect = await canvas.boundingBox();
|
|
await page.mouse.move(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.up();
|
|
const input =
|
|
inputStarted &&
|
|
(await page.evaluate(async () => {
|
|
await window.__qaCameraRun;
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
return !d.running && d.getPlaybackTimingState().activeTimers === 0;
|
|
}));
|
|
check('manual canvas input revokes the move and releases clocks', input);
|
|
const hold = await page.evaluate(async () => {
|
|
const { sceneDirector: d, styleManager: style } = window.__godsEyeView;
|
|
const scene = d._project.scenes[0];
|
|
const shot = scene.shots[0];
|
|
shot.durationSec = 0.2;
|
|
shot.holdSec = 30;
|
|
const run = d.startScene(scene.id, { single: true, preview: false });
|
|
const deadline = Date.now() + 10000;
|
|
while (
|
|
(!d._activeSceneTravel || !d.running || d._cameraMotion.active) &&
|
|
Date.now() < deadline
|
|
)
|
|
await new Promise((r) => setTimeout(r, 20));
|
|
const holding = d.running && !d._cameraMotion.active;
|
|
const began = performance.now();
|
|
style.runImmediateNavigation('location', () => true);
|
|
await run;
|
|
return (
|
|
holding &&
|
|
performance.now() - began < 1000 &&
|
|
d.getPlaybackTimingState().activeTimers === 0
|
|
);
|
|
});
|
|
check('navigation during an authored hold settles immediately', hold);
|
|
|
|
if (errors.length)
|
|
console.error(
|
|
errors.map((message) => message.replace(/https?:\/\/\S+/g, '[URL]')),
|
|
);
|
|
check('no uncaught browser errors', errors.length === 0);
|
|
} finally {
|
|
await closeBrowser(browser);
|
|
}
|
|
if (failed) process.exitCode = 1;
|