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.
355 lines
11 KiB
JavaScript
355 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
// Cesium-only probes: native primitive readiness and entity counts detect Cesium resource leaks.
|
|
/** Synthetic, self-authored assets exercise import, rendering and pack resource lifetime. */
|
|
import { whenSettled, whenTilesLoaded, until } from './qa/ready.mjs';
|
|
import { launchBrowser, closeBrowser } from './qa/browser.mjs';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import sharp from 'sharp';
|
|
const output = path.resolve('qa-shots/director-packs');
|
|
fs.mkdirSync(output, { recursive: true });
|
|
const png = await sharp({
|
|
create: {
|
|
width: 64,
|
|
height: 64,
|
|
channels: 4,
|
|
background: { r: 255, g: 80, b: 30, alpha: 0.65 },
|
|
},
|
|
})
|
|
.png()
|
|
.toBuffer();
|
|
const wav = Buffer.alloc(44 + 16000);
|
|
wav.write('RIFF');
|
|
wav.writeUInt32LE(wav.length - 8, 4);
|
|
wav.write('WAVEfmt ', 8);
|
|
wav.writeUInt32LE(16, 16);
|
|
wav.writeUInt16LE(1, 20);
|
|
wav.writeUInt16LE(1, 22);
|
|
wav.writeUInt32LE(8000, 24);
|
|
wav.writeUInt32LE(16000, 28);
|
|
wav.writeUInt16LE(2, 32);
|
|
wav.writeUInt16LE(16, 34);
|
|
wav.write('data', 36);
|
|
wav.writeUInt32LE(16000, 40);
|
|
const geojson = {
|
|
type: 'FeatureCollection',
|
|
features: [
|
|
{
|
|
type: 'Feature',
|
|
id: 'point',
|
|
geometry: { type: 'Point', coordinates: [-97.742, 30.27, 450] },
|
|
},
|
|
{
|
|
type: 'Feature',
|
|
id: 'line',
|
|
geometry: {
|
|
type: 'LineString',
|
|
coordinates: [
|
|
[-97.745, 30.268, 400],
|
|
[-97.735, 30.268, 400],
|
|
],
|
|
},
|
|
},
|
|
{
|
|
type: 'Feature',
|
|
id: 'polygon',
|
|
geometry: {
|
|
type: 'Polygon',
|
|
coordinates: [
|
|
[
|
|
[-97.746, 30.265, 300],
|
|
[-97.74, 30.265, 300],
|
|
[-97.74, 30.267, 300],
|
|
[-97.746, 30.265, 300],
|
|
],
|
|
],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
const browser = await launchBrowser({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
|
});
|
|
const page = await browser.newPage();
|
|
const errors = [],
|
|
requests = [];
|
|
let pending,
|
|
failures = 0;
|
|
const check = (name, passed) => {
|
|
console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`);
|
|
if (!passed) failures++;
|
|
};
|
|
page.on('pageerror', (e) => errors.push(e.message));
|
|
|
|
await page.setRequestInterception(true);
|
|
page.on('request', (request) => {
|
|
const url = new URL(request.url());
|
|
if (!url.pathname.startsWith('/scene-assets/'))
|
|
return void request.continue();
|
|
requests.push(url.pathname);
|
|
if (url.pathname.endsWith('/slow.json')) {
|
|
pending = request;
|
|
return;
|
|
}
|
|
const reply = url.pathname.endsWith('/outline.json')
|
|
? { contentType: 'application/geo+json', body: JSON.stringify(geojson) }
|
|
: url.pathname.endsWith('/image.png')
|
|
? { contentType: 'image/png', body: png }
|
|
: url.pathname.endsWith('/audio.wav')
|
|
? { contentType: 'audio/wav', body: wav }
|
|
: { status: 404, body: 'missing' };
|
|
void request.respond(reply).catch(() => {});
|
|
});
|
|
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);
|
|
await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
const manifest = (id, format, file, placement) => ({
|
|
id,
|
|
version: 1,
|
|
format,
|
|
source: { adapter: 'assets', path: `qa/${file}` },
|
|
attribution: { text: 'Synthetic QA fixture', license: 'CC0-1.0' },
|
|
placement,
|
|
});
|
|
const camera = {
|
|
lat: 30.2672,
|
|
lon: -97.742,
|
|
alt: 4000,
|
|
heading: 0,
|
|
pitch: -90,
|
|
roll: 0,
|
|
};
|
|
const project = {
|
|
version: 5,
|
|
scenes: [
|
|
{
|
|
id: 'packs',
|
|
title: 'Data packs QA',
|
|
anchors: [
|
|
{
|
|
id: 'media',
|
|
lat: 30.269,
|
|
lon: -97.737,
|
|
alt: 450,
|
|
altitudeReference: 'ellipsoid',
|
|
},
|
|
],
|
|
dataPacks: [
|
|
manifest('outline', 'geojson', 'outline.json', {
|
|
altitudeReference: 'ellipsoid',
|
|
}),
|
|
manifest('image', 'image', 'image.png', {
|
|
bounds: [-97.738, 30.264, -97.732, 30.267],
|
|
height: 300,
|
|
altitudeReference: 'ellipsoid',
|
|
}),
|
|
manifest('media', 'media', 'audio.wav', { anchorId: 'media' }),
|
|
manifest('slow', 'geojson', 'slow.json', {
|
|
altitudeReference: 'ellipsoid',
|
|
}),
|
|
manifest('missing', 'geojson', 'missing.json', {
|
|
altitudeReference: 'ellipsoid',
|
|
}),
|
|
],
|
|
shots: ['all', 'empty', 'slow', 'missing'].map((id) => ({
|
|
id,
|
|
title: id,
|
|
camera,
|
|
durationSec: 0.3,
|
|
holdSec: 0.2,
|
|
visual: { style: 'normal', mapStack: 'photoreal' },
|
|
layers: {},
|
|
dataPackIds:
|
|
id === 'all'
|
|
? ['outline', 'image', 'media']
|
|
: id === 'empty'
|
|
? []
|
|
: [id],
|
|
})),
|
|
},
|
|
],
|
|
};
|
|
await d.importProjectFile(
|
|
new File([JSON.stringify(project)], 'packs.json'),
|
|
);
|
|
window.__packBaseline = d.viewer.entities.values.length;
|
|
window.__packPrimitiveBaseline = d.viewer.scene.primitives.length;
|
|
window.__packRevoked = [];
|
|
const revoke = URL.revokeObjectURL.bind(URL);
|
|
URL.revokeObjectURL = (url) => {
|
|
window.__packRevoked.push(url);
|
|
revoke(url);
|
|
};
|
|
});
|
|
check('import does not acquire unselected assets', requests.length === 0);
|
|
const loaded = await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
const result = await d.loadShot('packs', 'all', { flyDuration: 0.3 });
|
|
window.__packMaterials = Array.from(
|
|
{
|
|
length:
|
|
d.viewer.scene.primitives.length - window.__packPrimitiveBaseline,
|
|
},
|
|
(_, i) =>
|
|
d.viewer.scene.primitives.get(window.__packPrimitiveBaseline + i)
|
|
.appearance?.material,
|
|
).filter(Boolean);
|
|
return {
|
|
started: result?.started,
|
|
state: d.getDataPackState(),
|
|
count: d.viewer.entities.values.length - window.__packBaseline,
|
|
primitives:
|
|
d.viewer.scene.primitives.length - window.__packPrimitiveBaseline,
|
|
cards: document.querySelectorAll('[data-director-pack]').length,
|
|
paused: document.querySelector('[data-director-pack] audio')?.paused,
|
|
};
|
|
});
|
|
check(
|
|
'GeoJSON, PNG and media load only through registered sources',
|
|
loaded.started && loaded.state.count === 3 && requests.length === 3,
|
|
);
|
|
check(
|
|
'geometry and image batches plus anchored markers render separately',
|
|
loaded.count === 2 && loaded.primitives === 3,
|
|
);
|
|
check(
|
|
'attribution is visible and media does not autoplay',
|
|
loaded.cards === 3 && loaded.paused === true,
|
|
);
|
|
await page.waitForFunction(
|
|
() => window.__godsEyeView.mapStackController.getActiveId() === 'photoreal',
|
|
{ timeout: 15000 },
|
|
);
|
|
await whenSettled(page);
|
|
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
|
const evidence = await page.evaluate(() => ({
|
|
mapStack: window.__godsEyeView.mapStackController.getActiveId(),
|
|
tilesSettled: window.__godsEyeView.engine.surface.tilesLoaded() === true,
|
|
}));
|
|
fs.writeFileSync(
|
|
path.join(output, 'report.json'),
|
|
JSON.stringify(evidence, null, 2),
|
|
);
|
|
check(
|
|
'static geometry is ready before visual acceptance',
|
|
await page.evaluate(() => {
|
|
const primitives = window.__godsEyeView.viewer.scene.primitives;
|
|
return Array.from(
|
|
{ length: primitives.length - window.__packPrimitiveBaseline },
|
|
(_, i) => primitives.get(window.__packPrimitiveBaseline + i),
|
|
).every((p) => p.ready !== false);
|
|
}),
|
|
);
|
|
await page.screenshot({ path: path.join(output, 'packs.png') });
|
|
await page.evaluate(() => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
d._setCameraView({
|
|
...d.styleManager.getCameraState(),
|
|
heading: 30,
|
|
pitch: -85,
|
|
});
|
|
});
|
|
await whenTilesLoaded(page, { timeoutMs: 60000 });
|
|
await whenSettled(page);
|
|
await page.screenshot({ path: path.join(output, 'angle.png') });
|
|
const life = await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
const media = document.querySelector('[data-director-pack] audio');
|
|
await media.play();
|
|
const played = !media.paused;
|
|
await d.loadShot('packs', 'empty', { flyDuration: 0.3 });
|
|
const cleared =
|
|
d.getDataPackState().count === 0 &&
|
|
!document.querySelector('[data-director-packs]') &&
|
|
d.viewer.entities.values.length === window.__packBaseline &&
|
|
d.viewer.scene.primitives.length === window.__packPrimitiveBaseline &&
|
|
media.paused &&
|
|
!media.getAttribute('src') &&
|
|
window.__packRevoked.length === 2 &&
|
|
window.__packMaterials.length === 1 &&
|
|
window.__packMaterials.every((material) => material.isDestroyed());
|
|
await d.seekScene('packs', 0.1);
|
|
const seek = d.getDataPackState().count === 3;
|
|
d.stopScene();
|
|
const stopped =
|
|
d.getDataPackState().count === 0 &&
|
|
d.viewer.entities.values.length === window.__packBaseline;
|
|
const missing = await d.loadShot('packs', 'missing', { flyDuration: 0.3 });
|
|
return {
|
|
played,
|
|
cleared,
|
|
seek,
|
|
stopped,
|
|
missing: missing?.started === false && d.getDataPackState().count === 0,
|
|
};
|
|
});
|
|
for (const [name, passed] of Object.entries(life)) check(name, passed);
|
|
await page.evaluate(() => {
|
|
window.__pendingPack = window.__godsEyeView.sceneDirector.loadShot(
|
|
'packs',
|
|
'slow',
|
|
);
|
|
});
|
|
await until(() => pending, {
|
|
timeoutMs: 15000,
|
|
label: 'pending pack transport',
|
|
});
|
|
check('pending transport reached', !!pending);
|
|
const cancelled = await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
const start = performance.now();
|
|
d.stopScene();
|
|
await window.__pendingPack;
|
|
return performance.now() - start < 1000 && d.getDataPackState().count === 0;
|
|
});
|
|
check('Stop promptly settles a pending transport', cancelled);
|
|
if (pending)
|
|
await pending
|
|
.respond({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify(geojson),
|
|
})
|
|
.catch(() => {});
|
|
check(
|
|
'late response cannot reintroduce entities',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.viewer.entities.values.length ===
|
|
window.__packBaseline &&
|
|
window.__godsEyeView.viewer.scene.primitives.length ===
|
|
window.__packPrimitiveBaseline,
|
|
),
|
|
);
|
|
await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
await d.loadShot('packs', 'all', { flyDuration: 0.3 });
|
|
await d.importProjectFile(
|
|
new File([JSON.stringify({ version: 5, scenes: [] })], 'empty.json'),
|
|
);
|
|
});
|
|
check(
|
|
'replacement import clears every pack resource',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.sceneDirector.getDataPackState().count === 0 &&
|
|
!document.querySelector('[data-director-packs]'),
|
|
),
|
|
);
|
|
check('no uncaught browser errors', errors.length === 0);
|
|
if (errors.length)
|
|
console.error(errors.map((e) => e.replace(/https?:\/\/\S+/g, '[URL]')));
|
|
} finally {
|
|
await closeBrowser(browser);
|
|
}
|
|
if (failures) process.exitCode = 1;
|