mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
483 lines
15 KiB
JavaScript
483 lines
15 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Sharing/authoring acceptance with local synthetic assets and real installed controls. */
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import puppeteer from 'puppeteer';
|
|
const output = path.resolve('qa-shots/director-sharing');
|
|
fs.mkdirSync(output, { recursive: true });
|
|
const geometry = {
|
|
type: 'FeatureCollection',
|
|
features: [
|
|
{
|
|
type: 'Feature',
|
|
id: 'point',
|
|
geometry: { type: 'Point', coordinates: [-97.742, 30.2672, 500] },
|
|
},
|
|
],
|
|
};
|
|
fs.writeFileSync(path.join(output, 'data.geojson'), JSON.stringify(geometry));
|
|
const camera = {
|
|
lat: 30.2672,
|
|
lon: -97.742,
|
|
alt: 4000,
|
|
heading: 0,
|
|
pitch: -90,
|
|
roll: 0,
|
|
};
|
|
const project = {
|
|
version: 6,
|
|
scenes: [
|
|
{
|
|
id: 'share',
|
|
title: 'Sharing fixture',
|
|
dataPacks: [
|
|
{
|
|
id: 'data',
|
|
version: 1,
|
|
format: 'geojson',
|
|
source: { adapter: 'assets', path: 'example/data.geojson' },
|
|
attribution: { text: 'Synthetic <b>fixture</b>', license: 'CC0-1.0' },
|
|
placement: { altitudeReference: 'ellipsoid' },
|
|
},
|
|
],
|
|
shots: [
|
|
{
|
|
id: 'shot',
|
|
title: 'First',
|
|
camera,
|
|
durationSec: 0.3,
|
|
holdSec: 0,
|
|
visual: { style: 'normal', mapStack: 'photoreal' },
|
|
layers: {},
|
|
dataPackIds: ['data'],
|
|
interactions: [
|
|
{
|
|
id: 'info',
|
|
label: 'Read fixture',
|
|
target: { packId: 'data', featureId: 'point' },
|
|
action: { type: 'card', text: 'Synthetic scene note' },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{ id: 'second', title: 'Other scene', shots: [] },
|
|
],
|
|
};
|
|
const file = path.join(output, 'project.json');
|
|
fs.writeFileSync(file, JSON.stringify(project));
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-dev-shm-usage'],
|
|
});
|
|
const page = await browser.newPage();
|
|
let failures = 0;
|
|
const errors = [],
|
|
requests = [];
|
|
const check = (name, ok) => {
|
|
console.log(`[${ok ? 'PASS' : 'FAIL'}] ${name}`);
|
|
if (!ok) failures++;
|
|
};
|
|
page.on('pageerror', (e) => errors.push(e.message));
|
|
page.on('request', (r) => {
|
|
if (new URL(r.url()).pathname.startsWith('/scene-assets/'))
|
|
requests.push(r.url());
|
|
});
|
|
async function click(label) {
|
|
const handle = await page.evaluateHandle(
|
|
(label) =>
|
|
[...document.querySelectorAll('button')].find(
|
|
(b) => b.textContent === label,
|
|
),
|
|
label,
|
|
);
|
|
await handle.asElement().click();
|
|
await handle.dispose();
|
|
}
|
|
async function value(label, text) {
|
|
await page.$eval(
|
|
`[aria-label="${label}"]`,
|
|
(node, text) => {
|
|
node.value = text;
|
|
node.dispatchEvent(new Event('input', { bubbles: true }));
|
|
},
|
|
text,
|
|
);
|
|
}
|
|
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.click('[data-collapse-target="scene-panel"]');
|
|
const before = await page.evaluate(() =>
|
|
JSON.stringify(window.__godsEyeView.sceneDirector._project),
|
|
);
|
|
const input = await page.$('#scene-import-file');
|
|
await input.uploadFile(file);
|
|
await page.waitForSelector('[data-director-apply-import]');
|
|
check(
|
|
'preview parses metadata without acquiring assets or replacing the current project',
|
|
requests.length === 0 &&
|
|
(await page.evaluate(
|
|
(before) =>
|
|
JSON.stringify(window.__godsEyeView.sceneDirector._project) ===
|
|
before,
|
|
before,
|
|
)),
|
|
);
|
|
check(
|
|
'preview renders author attribution as literal text',
|
|
await page.evaluate(
|
|
() =>
|
|
document
|
|
.querySelector('[data-director-dialog]')
|
|
.textContent.includes('Synthetic <b>fixture</b>') &&
|
|
!document.querySelector('[data-director-dialog] b'),
|
|
),
|
|
);
|
|
await page.keyboard.press('Escape');
|
|
check(
|
|
'Escape cancels preview without altering saved work',
|
|
await page.evaluate(
|
|
(before) =>
|
|
!document.querySelector('[data-director-dialog]') &&
|
|
JSON.stringify(window.__godsEyeView.sceneDirector._project) === before,
|
|
before,
|
|
),
|
|
);
|
|
await input.uploadFile(file);
|
|
await page.waitForSelector('[data-director-apply-import]');
|
|
await page.click('[data-director-apply-import]');
|
|
await page.waitForFunction(
|
|
() => !document.querySelector('[data-director-dialog]'),
|
|
);
|
|
check(
|
|
'Apply import replaces metadata but does not start playback or fetch a pack',
|
|
requests.length === 0 &&
|
|
(await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.sceneDirector._project.scenes.length === 2 &&
|
|
!window.__godsEyeView.sceneDirector.running,
|
|
)),
|
|
);
|
|
await click('EDIT DETAILS');
|
|
await click('Capture camera as anchor');
|
|
await page.waitForFunction(() =>
|
|
document
|
|
.querySelector('[data-director-dialog] [role=status]')
|
|
.textContent.includes('Anchor added'),
|
|
);
|
|
await click('Set move start to current camera');
|
|
await page.waitForFunction(() =>
|
|
document
|
|
.querySelector('[data-director-dialog] [role=status]')
|
|
.textContent.includes('Move added'),
|
|
);
|
|
const draft = await page.$eval(
|
|
'[aria-label="Shot camera, timing, packs and actions"]',
|
|
(n) => n.value,
|
|
);
|
|
const bad = JSON.parse(draft);
|
|
bad.camera.lat = 91;
|
|
await value('Shot camera, timing, packs and actions', JSON.stringify(bad));
|
|
const saved = await page.evaluate(() =>
|
|
localStorage.getItem('godsEyeView.sceneProject.v2'),
|
|
);
|
|
await page.click('[data-director-apply-details]');
|
|
await page.waitForFunction(() =>
|
|
document
|
|
.querySelector('[data-director-dialog] [role=status]')
|
|
.textContent.includes('camera.lat'),
|
|
);
|
|
check(
|
|
'invalid draft preserves saved bytes and existing authored content',
|
|
await page.evaluate(
|
|
(saved) => localStorage.getItem('godsEyeView.sceneProject.v2') === saved,
|
|
saved,
|
|
),
|
|
);
|
|
await value('Shot camera, timing, packs and actions', draft);
|
|
await page.click('[data-director-apply-details]');
|
|
await page.waitForFunction(
|
|
() => !document.querySelector('[data-director-dialog]'),
|
|
);
|
|
check(
|
|
'author controls preserve selected shot, pack attribution and action definitions',
|
|
await page.evaluate(() => {
|
|
const d = window.__godsEyeView.sceneDirector,
|
|
s = d._getSelectedScene();
|
|
return (
|
|
d._selectedShotId === 'shot' &&
|
|
s.anchors.length === 1 &&
|
|
s.shots[0].move.easing === 'cubic-in-out' &&
|
|
s.shots[0].interactions.length === 1 &&
|
|
s.dataPacks[0].attribution.license === 'CC0-1.0'
|
|
);
|
|
}),
|
|
);
|
|
await page.evaluate(() => {
|
|
window.__shareExports = [];
|
|
const create = URL.createObjectURL.bind(URL);
|
|
URL.createObjectURL = (blob) => {
|
|
if (blob.type === 'application/json')
|
|
window.__shareExports.push(blob.text());
|
|
return create(blob);
|
|
};
|
|
});
|
|
await click('SHARE SCENE');
|
|
await click('Download scene JSON');
|
|
const exported = await page.evaluate(async () =>
|
|
JSON.parse(await window.__shareExports.at(-1)),
|
|
);
|
|
check(
|
|
'scene JSON exports only the selected scene and keeps its credit',
|
|
exported.scenes.length === 1 &&
|
|
exported.scenes[0].dataPacks[0].attribution.license === 'CC0-1.0',
|
|
);
|
|
await click('Download asset bundle');
|
|
await page.waitForFunction(() =>
|
|
document
|
|
.querySelector('[data-director-dialog] [role=status]')
|
|
.textContent.includes('Could not complete'),
|
|
);
|
|
check(
|
|
'missing explicit files produce no partial download or network fallback',
|
|
requests.length === 0 &&
|
|
(await page.evaluate(() => window.__shareExports.length === 1)),
|
|
);
|
|
const files = await page.$('[aria-label="Choose data-pack files"]');
|
|
await files.uploadFile(path.join(output, 'data.geojson'));
|
|
await click('Download asset bundle');
|
|
await page.waitForFunction(
|
|
() =>
|
|
document.querySelector('[data-director-dialog] [role=status]')
|
|
.textContent === 'Asset bundle downloaded',
|
|
);
|
|
const bundle = await page.evaluate(
|
|
async () => await window.__shareExports.at(-1),
|
|
);
|
|
const bundleFile = path.join(output, 'scene.gevbundle.json');
|
|
fs.writeFileSync(bundleFile, bundle);
|
|
check(
|
|
'bundle contains the selected bytes and preserves attribution',
|
|
JSON.parse(bundle).assets.length === 1 &&
|
|
JSON.parse(bundle).project.scenes[0].dataPacks[0].attribution.license ===
|
|
'CC0-1.0',
|
|
);
|
|
await page.keyboard.press('Escape');
|
|
await input.uploadFile(bundleFile);
|
|
await page.waitForSelector('[data-director-apply-import]');
|
|
check(
|
|
'bundle preview verifies bytes and explains session-only storage',
|
|
await page.evaluate(
|
|
() =>
|
|
document
|
|
.querySelector('[data-director-dialog]')
|
|
.textContent.includes('bundled bytes verified') &&
|
|
window.__godsEyeView.sceneDirector.getSharingState().assets.count === 0,
|
|
),
|
|
);
|
|
await page.screenshot({ path: path.join(output, 'preview.png') });
|
|
await page.click('[data-director-apply-import]');
|
|
await page.waitForFunction(
|
|
() => !document.querySelector('[data-director-dialog]'),
|
|
);
|
|
await page.evaluate(() =>
|
|
window.__godsEyeView.sceneDirector.loadShot('share', 'shot', {
|
|
flyDuration: 0.3,
|
|
}),
|
|
);
|
|
check(
|
|
'imported bundle renders and enables feature actions with zero asset requests',
|
|
requests.length === 0 &&
|
|
(await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.sceneDirector.getDataPackState().count === 1 &&
|
|
window.__godsEyeView.sceneDirector.getInteractionState().count === 1,
|
|
)),
|
|
);
|
|
await page.waitForFunction(
|
|
() => window.__godsEyeView.tileset?.tilesLoaded === true,
|
|
{ timeout: 60000 },
|
|
);
|
|
await page.screenshot({ path: path.join(output, 'rendered.png') });
|
|
fs.writeFileSync(
|
|
path.join(output, 'report.json'),
|
|
JSON.stringify(
|
|
await page.evaluate(() => ({
|
|
tilesSettled: window.__godsEyeView.tileset?.tilesLoaded === true,
|
|
})),
|
|
),
|
|
);
|
|
await page.evaluate(() => window.__godsEyeView.sceneDirector.stopScene());
|
|
check(
|
|
'Stop releases rendered resources while retaining reusable project bytes',
|
|
await page.evaluate(() => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
return (
|
|
d.getDataPackState().count === 0 &&
|
|
d.getSharingState().assets.count === 1
|
|
);
|
|
}),
|
|
);
|
|
await page.evaluate(() =>
|
|
window.__godsEyeView.sceneDirector.seekScene('share', 0),
|
|
);
|
|
check(
|
|
'seek reloads the bundle without a network fallback',
|
|
requests.length === 0 &&
|
|
(await page.evaluate(
|
|
() => window.__godsEyeView.sceneDirector.getDataPackState().count === 1,
|
|
)),
|
|
);
|
|
await click('EDIT DETAILS');
|
|
await page.evaluate(() => {
|
|
window.__godsEyeView.sceneDirector._project.scenes[0].title = 'Newer edit';
|
|
});
|
|
await page.click('[data-director-apply-details]');
|
|
check(
|
|
'stale author draft cannot overwrite a newer edit',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.sceneDirector._project.scenes[0].title ===
|
|
'Newer edit',
|
|
),
|
|
);
|
|
await page.keyboard.press('Escape');
|
|
await click('SHARE SCENE');
|
|
await page.setViewport({ width: 390, height: 844 });
|
|
check(
|
|
'modal remains within a narrow viewport',
|
|
await page.$eval('[data-director-dialog]', (n) => {
|
|
const r = n.getBoundingClientRect();
|
|
return (
|
|
r.left >= 0 &&
|
|
r.right <= innerWidth &&
|
|
r.top >= 0 &&
|
|
r.bottom <= innerHeight
|
|
);
|
|
}),
|
|
);
|
|
await page.screenshot({ path: path.join(output, 'narrow.png') });
|
|
await page.keyboard.press('Escape');
|
|
const pending = await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
let finish;
|
|
const work = d._sharing.preview({
|
|
name: 'pending.json',
|
|
text: () => new Promise((r) => (finish = r)),
|
|
});
|
|
d._sharing.close();
|
|
await work;
|
|
finish(JSON.stringify({ version: 6, scenes: [] }));
|
|
return !d.getSharingState().open;
|
|
});
|
|
check(
|
|
'cancel settles a pending file read and suppresses its late completion',
|
|
pending,
|
|
);
|
|
const cancelledApply = await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
const before = JSON.stringify(d._project);
|
|
const saved = JSON.stringify({ ...localStorage });
|
|
let finish;
|
|
const pendingWork = new Promise((resolve) => (finish = resolve));
|
|
d._pendingWork.add(pendingWork);
|
|
const owner = new AbortController();
|
|
const work = d.importProjectFile(
|
|
{ name: 'cancelled.json' },
|
|
{
|
|
prepared: { project: { version: 6, scenes: [] }, assets: new Map() },
|
|
expectedProject: JSON.stringify(d._project, null, 2),
|
|
signal: owner.signal,
|
|
},
|
|
);
|
|
owner.abort();
|
|
finish();
|
|
const applied = await work;
|
|
d._pendingWork.delete(pendingWork);
|
|
return (
|
|
applied === false &&
|
|
JSON.stringify(d._project) === before &&
|
|
JSON.stringify({ ...localStorage }) === saved &&
|
|
d.getSharingState().assets.count === 1
|
|
);
|
|
});
|
|
check(
|
|
'cancelling Apply during pending scene cleanup preserves saved project and bundle bytes',
|
|
cancelledApply,
|
|
);
|
|
await click('EDIT DETAILS');
|
|
await page.evaluate(() => {
|
|
const fields = [
|
|
...document.querySelectorAll('[data-director-dialog] textarea'),
|
|
];
|
|
const scene = JSON.parse(fields[0].value),
|
|
shot = JSON.parse(fields[1].value);
|
|
scene.dataPacks = [];
|
|
shot.dataPackIds = [];
|
|
shot.interactions = [];
|
|
fields[0].value = JSON.stringify(scene);
|
|
fields[1].value = JSON.stringify(shot);
|
|
});
|
|
await page.click('[data-director-apply-details]');
|
|
await page.waitForFunction(
|
|
() => !document.querySelector('[data-director-dialog]'),
|
|
);
|
|
check(
|
|
'removing a pack through authoring releases its retained bytes',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.sceneDirector.getSharingState().assets.count === 0,
|
|
),
|
|
);
|
|
await page.evaluate(
|
|
(text) =>
|
|
window.__godsEyeView.sceneDirector.importProjectFile(
|
|
new File([text], 'again.gevbundle.json'),
|
|
),
|
|
bundle,
|
|
);
|
|
await page.evaluate(() =>
|
|
window.__godsEyeView.sceneDirector.importProjectFile(
|
|
new File([JSON.stringify({ version: 6, scenes: [] })], 'empty.json'),
|
|
),
|
|
);
|
|
check(
|
|
'replacement releases stored bundle bytes',
|
|
await page.evaluate(
|
|
() =>
|
|
window.__godsEyeView.sceneDirector.getSharingState().assets.count === 0,
|
|
),
|
|
);
|
|
await page.evaluate(async () => {
|
|
const d = window.__godsEyeView.sceneDirector;
|
|
void d._sharing.preview({
|
|
name: 'pending.json',
|
|
text: () => new Promise(() => {}),
|
|
});
|
|
await d.destroy();
|
|
});
|
|
check(
|
|
'teardown removes pending dialogs and authoring controls',
|
|
await page.evaluate(
|
|
() =>
|
|
!document.querySelector('[data-director-dialog]') &&
|
|
!document.querySelector('[data-director-authoring]') &&
|
|
!window.__godsEyeView.sceneDirector.getSharingState().open,
|
|
),
|
|
);
|
|
check('no uncaught browser errors', errors.length === 0);
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
process.exitCode = failures ? 1 : 0;
|