mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Move QA scripts to the shared harness, batch 1 of 4
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.
This commit is contained in:
1 parent
731eadc34a
commit
ee697422c9
25 files changed
+587
-881
No files matched your search
+142
-158
@@ -1,8 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
// Cesium-only probes: the stock input action and DataSource counts verify Cesium input restoration.
|
||||
import { evaluateWithGeo } from './qa/geometry.mjs';
|
||||
/** Rendered acceptance for UTILITIES ▸ Draw: real clicks, real keys, real teardown. */
|
||||
import { whenSettled, whenTilesLoaded } from './qa/ready.mjs';
|
||||
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
import { chromeExecutablePath } from './qa-chrome.mjs';
|
||||
|
||||
const BASE_URL = process.env.QA_BASE_URL || 'http://localhost:4173';
|
||||
@@ -19,21 +22,14 @@ const VIEW = {
|
||||
pitch: -85,
|
||||
};
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: !HEADFUL,
|
||||
...(HEADFUL
|
||||
? {
|
||||
executablePath: chromeExecutablePath(),
|
||||
}
|
||||
: {}),
|
||||
const browser = await launchBrowser({
|
||||
headless: true,
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-background-timer-throttling',
|
||||
'--disable-renderer-backgrounding',
|
||||
...(HEADFUL ? ['--window-size=1280,860'] : []),
|
||||
...(process.platform === 'darwin'
|
||||
? ['--use-angle=metal', '--enable-gpu']
|
||||
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
||||
...(process.platform === 'darwin' ? ['--enable-gpu'] : []),
|
||||
],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
@@ -68,7 +64,6 @@ const shot = (name) =>
|
||||
type: 'jpeg',
|
||||
quality: 82,
|
||||
});
|
||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/** A clip of the globe canvas, clear of the panels on every side. */
|
||||
const CANVAS_CLIP = { x: 340, y: 200, width: 560, height: 380 };
|
||||
@@ -163,7 +158,7 @@ async function pressFinish() {
|
||||
/** One click on the globe canvas, the way a person makes one. */
|
||||
async function clickWorld(x, y, options = {}) {
|
||||
await page.mouse.click(x, y, options);
|
||||
await wait(options.settle ?? 140);
|
||||
await whenSettled(page);
|
||||
}
|
||||
|
||||
const drawState = () =>
|
||||
@@ -211,50 +206,34 @@ try {
|
||||
{ waitUntil: 'domcontentloaded' },
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__gevDrawTool &&
|
||||
window.__gevAnnotations &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
() => window.__gevDrawTool && window.__gevAnnotations,
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await page.evaluate((view) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.scene.tweens?.removeAll?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: (view.heading * Math.PI) / 180,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
}, VIEW);
|
||||
const settled = await page
|
||||
.waitForFunction(
|
||||
() => {
|
||||
const primitives = window.__godsEyeView.viewer.scene.primitives;
|
||||
for (let index = 0; index < primitives.length; index++) {
|
||||
const primitive = primitives.get(index);
|
||||
if (
|
||||
typeof primitive.tilesLoaded === 'boolean' &&
|
||||
!primitive.tilesLoaded
|
||||
)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
{ timeout: 90_000 },
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
await whenSettled(page);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, view) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: (view.heading * Math.PI) / 180,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
},
|
||||
VIEW,
|
||||
);
|
||||
const settled = await whenTilesLoaded(page, { timeoutMs: 90_000 }).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
check('visible tile content settles before drawing', settled);
|
||||
|
||||
// UTILITIES starts collapsed on a first run: open it the way a user does.
|
||||
@@ -321,9 +300,9 @@ try {
|
||||
const before = await drawState();
|
||||
for (let cycle = 0; cycle < 3; cycle += 1) {
|
||||
await page.click('#draw-toggle');
|
||||
await wait(120);
|
||||
await whenSettled(page);
|
||||
await page.click('#draw-toggle');
|
||||
await wait(120);
|
||||
await whenSettled(page);
|
||||
}
|
||||
const after = await drawState();
|
||||
check(
|
||||
@@ -379,7 +358,7 @@ try {
|
||||
await clickWorld(600, 470);
|
||||
const beforeBackspace = (await drawState()).vertices;
|
||||
await page.keyboard.press('Backspace');
|
||||
await wait(120);
|
||||
await whenSettled(page);
|
||||
const afterBackspace = (await drawState()).vertices;
|
||||
check(
|
||||
'Backspace removes the last vertex only',
|
||||
@@ -415,7 +394,7 @@ try {
|
||||
);
|
||||
await page.mouse.click(410, 500, { count: 2 });
|
||||
const areaLanded = await waitForMarks(3);
|
||||
await wait(900);
|
||||
await whenSettled(page);
|
||||
const afterDoubleClick = await page.evaluate(() =>
|
||||
window.__gevAnnotations.count(),
|
||||
);
|
||||
@@ -440,7 +419,7 @@ try {
|
||||
await page.click(rowControl(areaId, 'name'));
|
||||
await page.keyboard.type('Civic Center block');
|
||||
await page.keyboard.press('Enter');
|
||||
await wait(300);
|
||||
await whenSettled(page);
|
||||
const areaMark = (await marks()).find(
|
||||
(mark) => mark.label === 'Civic Center block',
|
||||
);
|
||||
@@ -453,7 +432,7 @@ try {
|
||||
'naming a shape keeps Draw on and the shape in progress untouched',
|
||||
(await drawState()).active === true,
|
||||
);
|
||||
await wait(1200);
|
||||
await whenSettled(page);
|
||||
const amberAfter = await amberPixels();
|
||||
check(
|
||||
'the finished area actually PAINTS — amber pixels appear on this surface',
|
||||
@@ -512,32 +491,31 @@ try {
|
||||
// Fly-to frames the line by its whole path: once the flight settles, the
|
||||
// middle of the line sits near the middle of the view.
|
||||
await page.click(rowControl(ids[1], 'fly'));
|
||||
await page
|
||||
.waitForFunction(() => !window.__godsEyeView.viewer.camera._currentFlight, {
|
||||
timeout: 15_000,
|
||||
})
|
||||
.catch(() => {});
|
||||
await wait(300);
|
||||
const framed = await page.evaluate((id) => {
|
||||
const { viewer } = window.__godsEyeView;
|
||||
// No Cesium global in the page: borrow Cartesian3 from a live object.
|
||||
const Cartesian3 = viewer.camera.positionWC.constructor;
|
||||
const mark = window.__gevAnnotations.list().find((m) => m.id === id);
|
||||
const lons = mark.path.map((p) => p.lon);
|
||||
const lats = mark.path.map((p) => p.lat);
|
||||
const middle = Cartesian3.fromDegrees(
|
||||
(Math.min(...lons) + Math.max(...lons)) / 2,
|
||||
(Math.min(...lats) + Math.max(...lats)) / 2,
|
||||
);
|
||||
const at = viewer.scene.cartesianToCanvasCoordinates(middle);
|
||||
const canvas = viewer.scene.canvas;
|
||||
return (
|
||||
at && {
|
||||
dx: Math.abs(at.x - canvas.clientWidth / 2) / canvas.clientWidth,
|
||||
dy: Math.abs(at.y - canvas.clientHeight / 2) / canvas.clientHeight,
|
||||
}
|
||||
);
|
||||
}, ids[1]);
|
||||
await whenSettled(page).catch(() => {});
|
||||
await whenSettled(page);
|
||||
const framed = await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, id) => {
|
||||
// No Cesium global in the page: borrow Cartesian3 from a live object.
|
||||
const Cartesian3 = qaGeo.cartesian3;
|
||||
const mark = window.__gevAnnotations.list().find((m) => m.id === id);
|
||||
const lons = mark.path.map((p) => p.lon);
|
||||
const lats = mark.path.map((p) => p.lat);
|
||||
const middle = Cartesian3.fromDegrees(
|
||||
(Math.min(...lons) + Math.max(...lons)) / 2,
|
||||
(Math.min(...lats) + Math.max(...lats)) / 2,
|
||||
);
|
||||
const at = window.__godsEyeView.engine.projector.worldToWindow(middle);
|
||||
const canvas = window.__godsEyeView.engine.host.canvas;
|
||||
return (
|
||||
at && {
|
||||
dx: Math.abs(at.x - canvas.clientWidth / 2) / canvas.clientWidth,
|
||||
dy: Math.abs(at.y - canvas.clientHeight / 2) / canvas.clientHeight,
|
||||
}
|
||||
);
|
||||
},
|
||||
ids[1],
|
||||
);
|
||||
check(
|
||||
'fly to frames that shape: its middle is near the middle of the view',
|
||||
Boolean(framed) && framed.dx < 0.2 && framed.dy < 0.2,
|
||||
@@ -550,11 +528,11 @@ try {
|
||||
await page.click(rowControl(lineId, 'swatch'));
|
||||
await page.click(rowControl(lineId, 'color-amber'));
|
||||
await markReaches(lineId, { color: 'amber', alpha: 1 });
|
||||
await wait(800);
|
||||
await whenSettled(page);
|
||||
const shown = await canvasShot();
|
||||
await page.click(rowControl(lineId, 'toggle'));
|
||||
await markReaches(lineId, { hidden: true, alpha: 0 });
|
||||
await wait(800);
|
||||
await whenSettled(page);
|
||||
const hiddenShot = await canvasShot();
|
||||
// Amber the shown line had and the hidden one lost; amber that appears
|
||||
// only once it is hidden is noise, and must stay small beside it.
|
||||
@@ -566,7 +544,7 @@ try {
|
||||
`${gone} amber pixels went with the line, ${noise} appeared`,
|
||||
);
|
||||
await page.click(rowControl(lineId, 'toggle'));
|
||||
await wait(1200);
|
||||
await whenSettled(page);
|
||||
const back = await amberOnlyIn(await canvasShot(), hiddenShot);
|
||||
check(
|
||||
'and paints again when shown',
|
||||
@@ -581,7 +559,7 @@ try {
|
||||
await clickWorld(560, 340);
|
||||
check('a new shape is in progress', (await drawState()).vertices === 2);
|
||||
await page.keyboard.press('Escape');
|
||||
await wait(150);
|
||||
await whenSettled(page);
|
||||
const afterFirstEscape = await drawState();
|
||||
check(
|
||||
'Escape cancels the shape in progress and stays in draw mode',
|
||||
@@ -589,7 +567,7 @@ try {
|
||||
`vertices ${afterFirstEscape.vertices}, active ${afterFirstEscape.active}`,
|
||||
);
|
||||
await page.keyboard.press('Escape');
|
||||
await wait(150);
|
||||
await whenSettled(page);
|
||||
const afterSecondEscape = await drawState();
|
||||
check(
|
||||
'a second Escape leaves draw mode and gives the pointer back',
|
||||
@@ -667,7 +645,7 @@ try {
|
||||
);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.keyboard.press('Escape');
|
||||
await wait(150);
|
||||
await whenSettled(page);
|
||||
}
|
||||
|
||||
// ── what a reload actually does ─────────────────────────────────────────
|
||||
@@ -675,12 +653,10 @@ try {
|
||||
window.__gevAnnotations.count(),
|
||||
);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
window.__gevAnnotations &&
|
||||
document.getElementById('loading-screen')?.classList.contains('hidden'),
|
||||
{ timeout: 90_000 },
|
||||
);
|
||||
await page.waitForFunction(() => window.__gevAnnotations, {
|
||||
timeout: 90_000,
|
||||
});
|
||||
await whenSettled(page);
|
||||
const afterReload = await page.evaluate(() =>
|
||||
window.__gevAnnotations.count(),
|
||||
);
|
||||
@@ -722,25 +698,28 @@ try {
|
||||
// flight left a low, near-horizontal view there) with a boot flight that can
|
||||
// land after a setView: set the view until it holds.
|
||||
for (let attempt = 0; attempt < 6; attempt += 1) {
|
||||
await page.evaluate((view) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: 0,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
}, VIEW);
|
||||
await wait(2500);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, view) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (view.lon * Math.PI) / 180,
|
||||
latitude: (view.lat * Math.PI) / 180,
|
||||
height: view.height,
|
||||
}),
|
||||
orientation: {
|
||||
heading: 0,
|
||||
pitch: (view.pitch * Math.PI) / 180,
|
||||
roll: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
VIEW,
|
||||
);
|
||||
await whenSettled(page);
|
||||
const held = await page.evaluate((view) => {
|
||||
const carto = window.__godsEyeView.viewer.camera.positionCartographic;
|
||||
const carto = window.__godsEyeView.engine.camera.pose().cartographic;
|
||||
return (
|
||||
Math.abs((carto.latitude * 180) / Math.PI - view.lat) < 0.01 &&
|
||||
Math.abs((carto.longitude * 180) / Math.PI - view.lon) < 0.01 &&
|
||||
@@ -767,7 +746,7 @@ try {
|
||||
const beforeButtonEnter = await drawState();
|
||||
await page.focus('#draw-clear');
|
||||
await page.keyboard.press('Enter');
|
||||
await wait(600);
|
||||
await whenSettled(page);
|
||||
const afterButtonEnter = await drawState();
|
||||
check(
|
||||
'Enter on a keyboard-focused button activates the button, it does not finish the shape',
|
||||
@@ -790,7 +769,7 @@ try {
|
||||
await waitForMarks(1),
|
||||
);
|
||||
await page.click('#draw-clear');
|
||||
await wait(400);
|
||||
await whenSettled(page);
|
||||
const cleared = await drawState();
|
||||
check(
|
||||
'Clear removes every mark from the board',
|
||||
@@ -815,7 +794,7 @@ try {
|
||||
`${degenerateVertices} vertices`,
|
||||
);
|
||||
await pressFinish();
|
||||
await wait(600);
|
||||
await whenSettled(page);
|
||||
const degenerate = await drawState();
|
||||
check(
|
||||
'three points in a line are refused with a reason, not placed',
|
||||
@@ -824,20 +803,19 @@ try {
|
||||
);
|
||||
|
||||
// ── a shape drawn across the antimeridian lands where it was drawn ──────
|
||||
await page.evaluate(() => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
await evaluateWithGeo(page, async (qaGeo) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (179.9995 * Math.PI) / 180,
|
||||
latitude: 0,
|
||||
height: 3000,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-88 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
window.__godsEyeView.engine.render.request();
|
||||
});
|
||||
await wait(2500);
|
||||
await whenSettled(page);
|
||||
await page.evaluate(() => {
|
||||
const tool = window.__gevDrawTool;
|
||||
tool.cancel();
|
||||
@@ -865,7 +843,7 @@ try {
|
||||
await page.evaluate(() => window.__gevAnnotations.clear());
|
||||
|
||||
await page.click('#draw-toggle');
|
||||
await wait(200);
|
||||
await whenSettled(page);
|
||||
const finalState = await drawState();
|
||||
check(
|
||||
'leaving draw mode gives back the pointer, the handler and the double-click',
|
||||
@@ -880,7 +858,7 @@ try {
|
||||
() => window.__godsEyeView.viewer.dataSources.length,
|
||||
);
|
||||
await page.click('#draw-toggle'); // destroy from an ACTIVE session: the harder case
|
||||
await wait(200);
|
||||
await whenSettled(page);
|
||||
const destroyed = await page.evaluate(async () => {
|
||||
const tool = window.__gevDrawTool;
|
||||
const wasActive = tool.active;
|
||||
@@ -936,7 +914,7 @@ try {
|
||||
errors.join(' | '),
|
||||
);
|
||||
} finally {
|
||||
await browser.close();
|
||||
await closeBrowser(browser);
|
||||
}
|
||||
console.log(`RESULT: ${failures} failures`);
|
||||
process.exitCode = failures ? 1 : 0;
|
||||
@@ -975,20 +953,23 @@ async function findClickableTarget() {
|
||||
.catch(() => null);
|
||||
|
||||
if (camera) {
|
||||
await page.evaluate((spot) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (spot.lon * Math.PI) / 180,
|
||||
latitude: (spot.lat * Math.PI) / 180,
|
||||
height: 600,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-80 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
}, camera);
|
||||
await wait(4000);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, spot) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (spot.lon * Math.PI) / 180,
|
||||
latitude: (spot.lat * Math.PI) / 180,
|
||||
height: 600,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-80 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
},
|
||||
camera,
|
||||
);
|
||||
await whenSettled(page);
|
||||
const spot = await onScreen(['cctv']);
|
||||
if (spot) return { ...spot, kind: 'cctv', layer: 'CCTV camera' };
|
||||
}
|
||||
@@ -1021,20 +1002,23 @@ async function findClickableTarget() {
|
||||
})
|
||||
.catch(() => null);
|
||||
if (!site) return null;
|
||||
await page.evaluate((spot) => {
|
||||
const viewer = window.__godsEyeView.viewer;
|
||||
viewer.camera.cancelFlight?.();
|
||||
viewer.camera.setView({
|
||||
destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({
|
||||
longitude: (spot.lon * Math.PI) / 180,
|
||||
latitude: (spot.lat * Math.PI) / 180,
|
||||
height: 2500,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-88 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
}, site);
|
||||
await wait(4000);
|
||||
await evaluateWithGeo(
|
||||
page,
|
||||
async (qaGeo, spot) => {
|
||||
window.__godsEyeView.engine.camera.cancelFlight?.();
|
||||
window.__godsEyeView.engine.camera.setView({
|
||||
destination: qaGeo.cartographic.toCartesian({
|
||||
longitude: (spot.lon * Math.PI) / 180,
|
||||
latitude: (spot.lat * Math.PI) / 180,
|
||||
height: 2500,
|
||||
}),
|
||||
orientation: { heading: 0, pitch: (-88 * Math.PI) / 180, roll: 0 },
|
||||
});
|
||||
window.__godsEyeView.engine.render.request();
|
||||
},
|
||||
site,
|
||||
);
|
||||
await whenSettled(page);
|
||||
const spot = await onScreen([
|
||||
'local-datacenters-tips',
|
||||
'local-datacenters-pins',
|
||||
|
||||
Reference in new issue
Block a user