Prepare community integration base for the weather draft

This commit is contained in:
Bilawal Sidhu committed 2026-09-15 22:31:01 -05:00
commit fa69852323
48 files changed
+5028 -900

No files matched your search

+12 -1
View File
@@ -735,5 +735,16 @@
"src/sources/overpassFeatures.test.mjs",
"src/data/cctvHlsStream.test.mjs",
"src/layers/cctv/videoPlayback.test.mjs",
"src/ui/cctvVideo.test.mjs"
"src/ui/cctvVideo.test.mjs",
"src/director/clock.test.mjs",
"src/director/timeline.test.mjs",
"src/scenes/packs/registry.test.mjs",
"scripts/qa-director-timing.mjs",
"src/director/document.test.mjs",
"src/director/camera.test.mjs",
"src/scenes/cameraMotion.test.mjs",
"scripts/qa-director-camera.mjs",
"src/director/packs/packs.test.mjs",
"scripts/qa-director-packs.mjs",
"src/scenes/dataPacks/presentation.test.mjs"
]
+58 -4
View File
@@ -623,7 +623,19 @@
},
"director": {
"exports": ["./director"],
"modules": ["src/director/playback.js"],
"modules": [
"src/director/playback.js",
"src/director/clock.js",
"src/director/timeline.js",
"src/director/document.js",
"src/director/documentFields.js",
"src/director/cameraDocument.js",
"src/director/camera.js",
"src/director/packs/source.js",
"src/director/packs/session.js",
"src/director/packs/geojson.js",
"src/director/packs/manifest.js"
],
"external": []
},
"scenes": {
@@ -644,7 +656,26 @@
"src/ui/scenes.js",
"src/ui/sceneControls.js",
"src/ui/scenePresentation.js",
"src/contextModePolicy.js"
"src/contextModePolicy.js",
"src/director/clock.js",
"src/director/timeline.js",
"src/scenes/shotPresentation.js",
"src/scenes/packs/registry.js",
"src/scenes/packs/defaults.js",
"src/scenes/packs/nepal.js",
"src/director/document.js",
"src/scenes/project.js",
"src/director/documentFields.js",
"src/director/cameraDocument.js",
"src/director/camera.js",
"src/scenes/cameraMotion.js",
"src/director/packs/source.js",
"src/director/packs/session.js",
"src/director/packs/geojson.js",
"src/director/packs/manifest.js",
"src/scenes/dataPacks/controller.js",
"src/scenes/dataPacks/presentation.js",
"src/scenes/dataPacks/geometry.js"
],
"external": ["cesium"]
},
@@ -1757,7 +1788,26 @@
"src/voice/realtimeFacade.js",
"src/voice/realtimeRadio.js",
"src/voice/realtimeTurns.js",
"src/voice/realtimeConnection.js"
"src/voice/realtimeConnection.js",
"src/director/clock.js",
"src/director/timeline.js",
"src/scenes/shotPresentation.js",
"src/scenes/packs/registry.js",
"src/scenes/packs/defaults.js",
"src/scenes/packs/nepal.js",
"src/director/document.js",
"src/scenes/project.js",
"src/director/documentFields.js",
"src/director/cameraDocument.js",
"src/director/camera.js",
"src/scenes/cameraMotion.js",
"src/director/packs/source.js",
"src/director/packs/session.js",
"src/director/packs/geojson.js",
"src/director/packs/manifest.js",
"src/scenes/dataPacks/controller.js",
"src/scenes/dataPacks/presentation.js",
"src/scenes/dataPacks/geometry.js"
],
"external": [
"hls.js",
@@ -2230,7 +2280,11 @@
},
"ui-navigation": {
"exports": ["./ui/navigation"],
"modules": ["src/navigationPolicy.js", "src/ui/navigationController.js"],
"modules": [
"src/navigationPolicy.js",
"src/ui/navigationController.js",
"src/app/stateChannel.js"
],
"external": ["cesium"]
},
"ui-share-restoration": {
+210
View File
@@ -0,0 +1,210 @@
#!/usr/bin/env node
/** Verify imported camera directions through real rendering and camera authority handoffs. */
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
const output = path.resolve('qa-shots/director-camera');
fs.mkdirSync(output, { recursive: true });
const browser = await puppeteer.launch({
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 &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 60000 },
);
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 new Promise((r) => setTimeout(r, 6000));
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 new Promise((r) => setTimeout(r, 3000));
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 browser.close();
}
if (failed) process.exitCode = 1;
+359
View File
@@ -0,0 +1,359 @@
#!/usr/bin/env node
/** Synthetic, self-authored assets exercise import, rendering and pack resource lifetime. */
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
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 puppeteer.launch({
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 &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 60000 },
);
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 new Promise((r) => setTimeout(r, 5000));
await page.waitForFunction(
() => window.__godsEyeView.tileset?.tilesLoaded === true,
{ timeout: 60000 },
);
const evidence = await page.evaluate(() => ({
mapStack: window.__godsEyeView.mapStackController.getActiveId(),
tilesSettled: window.__godsEyeView.tileset?.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 page.waitForFunction(
() => window.__godsEyeView.tileset?.tilesLoaded === true,
{ timeout: 60000 },
);
await new Promise((r) => setTimeout(r, 2000));
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',
);
});
const deadline = Date.now() + 15000;
while (!pending && Date.now() < deadline)
await new Promise((r) => setTimeout(r, 50));
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 browser.close();
}
if (failures) process.exitCode = 1;
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env node
/** Exercise scene-clock ownership and authored seek behavior in the composed browser app. */
import fs from 'node:fs';
import path from 'node:path';
import puppeteer from 'puppeteer';
const shots = path.resolve('qa-shots/director-timing');
fs.mkdirSync(shots, { recursive: true });
const browser = await puppeteer.launch({
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 failures = 0;
function check(name, passed) {
console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`);
if (!passed) failures++;
}
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 &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 60000 },
);
const result = await page.evaluate(async () => {
const director = window.__godsEyeView.sceneDirector;
const camera = director.styleManager.getCameraState();
const visual = director.styleManager.getVisualState();
const scene = {
id: 'timing-qa',
title: 'Timing QA',
shots: [0, 1].map((index) => ({
id: `timing-${index}`,
title: `Timing ${index}`,
durationSec: 0.4,
holdSec: 0.3,
camera: { ...camera, lon: camera.lon + index * 0.002 },
visual,
layers: {},
})),
};
director._project.scenes.push(scene);
const load = await director.loadShot(scene.id, scene.shots[0].id, {
flyDuration: 0.3,
});
const forward = await director.seekScene(scene.id, 0.9);
const forwardSnapshot = director.getPlaybackTimingState().snapshot;
const backward = await director.seekScene(scene.id, 0.1);
const backwardSnapshot = director.getPlaybackTimingState().snapshot;
const replay = await director.replayShot(scene.id, scene.shots[1].id);
director.stopScene();
await director.startScene(scene.id, { single: true, preview: false });
const completed =
!director.running && director.getPlaybackTimingState().activeTimers === 0;
scene.shots[0].holdSec = 60;
const run = director.startScene(scene.id, { single: true, preview: false });
const deadline = Date.now() + 10000;
while (
director.getPlaybackTimingState().activeTimers < 3 &&
Date.now() < deadline
)
await new Promise((resolve) => setTimeout(resolve, 25));
const holding = director.getPlaybackTimingState().activeTimers >= 3;
const stoppedAt = performance.now();
director.stopScene('QA Stop during hold');
await run;
const stoppedPromptly = performance.now() - stoppedAt < 1000;
const noTimers = director.getPlaybackTimingState().activeTimers === 0;
director._project.scenes = director._project.scenes.filter(
({ id }) => id !== scene.id,
);
return {
loaded: load?.started,
forward: forward && forwardSnapshot.shotId === 'timing-1',
backward: backward && backwardSnapshot.shotId === 'timing-0',
replay: replay?.started,
completed,
holding,
stoppedPromptly,
noTimers,
};
});
for (const [name, passed] of Object.entries(result)) check(name, passed);
const content = await page.evaluate(async () => {
const d = window.__godsEyeView.sceneDirector;
const scene = d._project.scenes.find(({ title }) =>
/Nepal Flood Incident/i.test(title),
);
if (!scene) return { exists: false };
const shot =
scene.shots.find(({ title }) => /Regional Approach/i.test(title)) ||
scene.shots[0];
const loaded = await d.loadShot(scene.id, shot.id, { flyDuration: 0.3 });
return {
exists: true,
shots: scene.shots.length,
loaded: loaded?.started === true,
};
});
check(
'existing Nepal scene and authored shots load',
content.exists && content.shots >= 25 && content.loaded,
);
await new Promise((resolve) => setTimeout(resolve, 2500));
await page.screenshot({ path: path.join(shots, 'nepal-regional.png') });
await page.evaluate(() => {
const g = window.__godsEyeView;
g.sceneDirector.stopScene();
const pose = g.styleManager.getCameraState();
g.sceneDirector._setCameraView({
...pose,
heading: pose.heading + 45,
pitch: -55,
});
});
await new Promise((resolve) => setTimeout(resolve, 1000));
await page.screenshot({ path: path.join(shots, 'nepal-angle.png') });
check(
'no pending clocks after Stop',
await page.evaluate(
() =>
window.__godsEyeView.sceneDirector.getPlaybackTimingState()
.activeTimers === 0,
),
);
check('no browser exceptions', errors.length === 0);
} finally {
await browser.close();
}
if (failures) process.exitCode = 1;
+34
View File
@@ -274,6 +274,40 @@ try {
'QA Scene',
),
);
const savedBefore = await page.evaluate(() =>
localStorage.getItem('godsEyeView.sceneProject.v2'),
);
const futureFile = path.join(shots, 'future.json');
fs.writeFileSync(futureFile, JSON.stringify({ version: 99, scenes: [] }));
await input.uploadFile(futureFile);
await page.waitForFunction(() =>
document.getElementById('scene-status').textContent.includes('$.version'),
);
check(
'Unsupported versions leave authored state and saved bytes unchanged',
await page.evaluate(
(saved) =>
localStorage.getItem('godsEyeView.sceneProject.v2') === saved &&
window.__godsEyeView.sceneDirector._getSelectedScene().title ===
'QA Scene',
savedBefore,
),
);
const malformed = structuredClone(fixture);
malformed.scenes[0].shots[0].camera.lat = 91;
const malformedFile = path.join(shots, 'malformed.json');
fs.writeFileSync(malformedFile, JSON.stringify(malformed));
await input.uploadFile(malformedFile);
await page.waitForFunction(() =>
document.getElementById('scene-status').textContent.includes('camera.lat'),
);
check(
'Invalid camera field identifies its path without replacing the project',
await page.evaluate(
(saved) => localStorage.getItem('godsEyeView.sceneProject.v2') === saved,
savedBefore,
),
);
await page.screenshot({ path: path.join(shots, 'desktop.png') });
await page.setViewport({ width: 390, height: 844 });
await page.screenshot({ path: path.join(shots, 'narrow.png') });