mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
refactor: separate Scene controls from playback and project operations
This commit is contained in:
1 parent
8b5ad67dcb
commit
cd72feb4fe
12 files changed
+566
-222
No files matched your search
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env node
|
||||
/** Exercise installed Scene controls and real project/playback operations. */
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import puppeteer from 'puppeteer';
|
||||
|
||||
const shots = path.resolve('qa-shots/scene-controls');
|
||||
fs.mkdirSync(shots, { recursive: true });
|
||||
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', ...(process.platform === 'darwin' ? ['--use-angle=metal', '--enable-gpu'] : ['--use-gl=angle', '--use-angle=swiftshader'])] });
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', (error) => errors.push(error.message));
|
||||
let promptValue = 'QA Scene';
|
||||
page.on('dialog', async (dialog) => { await dialog.accept(dialog.type() === 'prompt' ? promptValue : undefined); });
|
||||
let failures = 0;
|
||||
function check(name, ok) { console.log(`[${ok ? 'PASS' : 'FAIL'}] ${name}`); if (!ok) 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?._controls && document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 60000 });
|
||||
await page.click('[data-collapse-target="scene-panel"]');
|
||||
await page.click('#scene-new-btn');
|
||||
await page.waitForFunction(() => document.querySelector('#scene-select option:checked')?.textContent === 'QA Scene');
|
||||
check('New creates and selects a persisted empty scene', await page.evaluate(() => {
|
||||
const director = window.__godsEyeView.sceneDirector;
|
||||
const saved = JSON.parse(localStorage.getItem('godsEyeView.sceneProject.v2'));
|
||||
return director._getSelectedScene().shots.length === 0 && !!document.querySelector('.scene-shot-empty') && saved.scenes.some((scene) => scene.title === 'QA Scene');
|
||||
}));
|
||||
await page.click('#scene-capture-btn');
|
||||
await page.waitForFunction(() => document.querySelectorAll('.scene-shot-row').length === 1);
|
||||
check('Capture stores the actual camera and visual state', await page.evaluate(() => {
|
||||
const director = window.__godsEyeView.sceneDirector;
|
||||
const shot = director._getSelectedScene().shots[0];
|
||||
const current = director.styleManager.getCameraState();
|
||||
return Number.isFinite(shot.camera.lat) && Math.abs(shot.camera.lat - current.lat) < 0.01 && shot.visual.style === director.styleManager.getVisualState().style;
|
||||
}));
|
||||
promptValue = '<b>QA shot</b>';
|
||||
await page.click('.scene-shot-label', { clickCount: 2 });
|
||||
await page.waitForFunction(() => document.querySelector('.scene-shot-label')?.textContent === '<b>QA shot</b>');
|
||||
check('Rename persists literal text without interpreting markup', await page.evaluate(() => !document.querySelector('.scene-shot-label b') && JSON.parse(localStorage.getItem('godsEyeView.sceneProject.v2')).scenes.some((scene) => scene.shots.some((shot) => shot.title === '<b>QA shot</b>'))));
|
||||
await page.click('#scene-capture-btn');
|
||||
await page.waitForFunction(() => document.querySelectorAll('.scene-shot-row').length === 2);
|
||||
await page.click('.scene-shot-label');
|
||||
check('Shot selection and Update use the selected shot', await page.evaluate(() => {
|
||||
const director = window.__godsEyeView.sceneDirector;
|
||||
const scene = director._getSelectedScene();
|
||||
const count = scene.shots.length;
|
||||
document.getElementById('scene-update-shot-btn').click();
|
||||
return director._selectedShotId === scene.shots[0].id && scene.shots.length === count && document.querySelector('.scene-shot-row.active .scene-shot-label').textContent === scene.shots[0].title;
|
||||
}));
|
||||
await page.evaluate(() => {
|
||||
window.__sceneExports = [];
|
||||
const create = URL.createObjectURL.bind(URL);
|
||||
URL.createObjectURL = (blob) => {
|
||||
if (blob.type === 'application/json') window.__sceneExports.push(blob.text());
|
||||
return create(blob);
|
||||
};
|
||||
});
|
||||
await page.click('#scene-export-btn');
|
||||
const exported = await page.evaluate(async () => JSON.parse(await window.__sceneExports[0]));
|
||||
check('Export produces the current project JSON', exported.scenes.some((scene) => scene.title === 'QA Scene' && scene.shots.length === 2));
|
||||
// Keep real camera/style playback while isolating this control check from
|
||||
// live provider availability. Layer transactions are covered by their suites.
|
||||
const scene = exported.scenes.find((item) => item.title === 'QA Scene');
|
||||
scene.shots = scene.shots.slice(0, 1).map((shot) => ({ ...shot, durationSec: 0.4, holdSec: 10, layers: {} }));
|
||||
const fixture = { ...exported, scenes: [scene] };
|
||||
const projectFile = path.join(shots, 'project.json');
|
||||
fs.writeFileSync(projectFile, JSON.stringify(fixture));
|
||||
const input = await page.$('#scene-import-file');
|
||||
await input.uploadFile(projectFile);
|
||||
await page.waitForFunction(() => document.getElementById('scene-status').textContent === 'Imported project.json');
|
||||
check('Import replaces the project and clears the file input', await page.evaluate(() => window.__godsEyeView.sceneDirector._project.scenes.length === 1 && document.querySelectorAll('.scene-shot-row').length === 1 && document.getElementById('scene-import-file').value === ''));
|
||||
await page.click('.scene-shot-btn');
|
||||
await page.waitForFunction(() => document.getElementById('scene-status').textContent.startsWith('Loaded:'), { timeout: 30000 });
|
||||
check('LOAD completes the real visual and camera operation', await page.evaluate(() => !window.__godsEyeView.sceneDirector.running && document.getElementById('scene-status').textContent.includes('<b>QA shot</b>')));
|
||||
await page.click('#scene-start-btn');
|
||||
await page.waitForFunction(() => window.__godsEyeView.sceneDirector.running && document.body.classList.contains('scene-playback-mode'));
|
||||
check('Playback enters recording presentation and disables editing', await page.evaluate(() => document.getElementById('scene-capture-btn').disabled && document.getElementById('scene-start-btn').disabled && !document.getElementById('scene-stop-btn').disabled && window.__godsEyeView.styleManager.getViewState().recording));
|
||||
await page.keyboard.press('Escape');
|
||||
await page.waitForFunction(() => !window.__godsEyeView.sceneDirector.running && !document.body.classList.contains('scene-playback-mode'));
|
||||
check('Escape ends playback and restores idle/recording controls', await page.evaluate(() => !document.getElementById('scene-start-btn').disabled && document.getElementById('scene-stop-btn').disabled && !document.getElementById('scene-download-btn').disabled && !window.__godsEyeView.styleManager.getViewState().recording));
|
||||
await page.click('#scene-download-btn');
|
||||
const metadata = await page.evaluate(async () => JSON.parse(await window.__sceneExports.at(-1)));
|
||||
check('Run download contains the completed cancelled run', Boolean(metadata.endedAt && metadata.wasCancelled && Array.isArray(metadata.events)));
|
||||
await page.click('#scene-start-btn');
|
||||
await page.waitForFunction(() => window.__godsEyeView.sceneDirector.running);
|
||||
await page.$eval('#scene-stop-btn', (button) => button.click());
|
||||
await page.waitForFunction(() => !window.__godsEyeView.sceneDirector.running);
|
||||
check('The installed Stop control also completes playback cleanup', await page.evaluate(() => !document.body.classList.contains('scene-playback-mode') && !document.getElementById('scene-start-btn').disabled));
|
||||
const badFile = path.join(shots, 'invalid.json');
|
||||
fs.writeFileSync(badFile, '{invalid');
|
||||
await input.uploadFile(badFile);
|
||||
await page.waitForFunction(() => document.getElementById('scene-status').textContent.includes('invalid JSON'));
|
||||
check('Invalid import reports failure and preserves the current project', await page.evaluate(() => window.__godsEyeView.sceneDirector._project.scenes.length === 1 && window.__godsEyeView.sceneDirector._getSelectedScene().title === 'QA Scene'));
|
||||
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') });
|
||||
check('Scene controls stay inside the narrow viewport', await page.$eval('#scene-select', (element) => { const rect = element.getBoundingClientRect(); return rect.width > 0 && rect.left >= 0 && rect.right <= innerWidth; }));
|
||||
await page.setViewport({ width: 1440, height: 900 });
|
||||
await page.click('.scene-shot-danger');
|
||||
await page.waitForFunction(() => !!document.querySelector('.scene-shot-empty'));
|
||||
check('Delete shot leaves the empty-state presentation', await page.evaluate(() => window.__godsEyeView.sceneDirector._getSelectedScene().shots.length === 0));
|
||||
await page.click('#scene-delete-btn');
|
||||
await page.waitForFunction(() => !window.__godsEyeView.sceneDirector._project.scenes.some((item) => item.title === 'QA Scene'));
|
||||
check('Deleting the final scene restores the built-in recipes', await page.evaluate(() => window.__godsEyeView.sceneDirector._project.scenes.length > 0 && document.querySelectorAll('#scene-select option').length > 0));
|
||||
const teardown = await page.evaluate(async () => {
|
||||
const director = window.__godsEyeView.sceneDirector;
|
||||
const controls = director._controls;
|
||||
const previousStatus = document.getElementById('scene-status').textContent;
|
||||
const project = director._project;
|
||||
let finish;
|
||||
const gate = new Promise((resolve) => { finish = resolve; });
|
||||
const pending = controls.run('import', { name: 'late.json', text: async () => { await gate; return '{"scenes":[]}'; } });
|
||||
const stopping = director.destroy();
|
||||
const stoppedSynchronously = controls.destroyed && controls.removers.length === 0 && controls.rowRemovers.length === 0;
|
||||
document.getElementById('scene-new-btn').click();
|
||||
controls.run('capture');
|
||||
finish();
|
||||
await pending;
|
||||
await stopping;
|
||||
await director.destroy();
|
||||
return { stoppedSynchronously, sameProject: director._project === project, noLateStatus: document.getElementById('scene-status').textContent === previousStatus, rowsGone: !document.querySelector('.scene-shot-row') };
|
||||
});
|
||||
check('Disposal revokes fixed and row actions before waiting for scene work', teardown.stoppedSynchronously && teardown.rowsGone);
|
||||
check('Late import completion cannot replace the disposed project or status', teardown.sameProject && teardown.noLateStatus);
|
||||
check('Scene control interaction produces no uncaught browser errors', errors.length === 0);
|
||||
} finally { await browser.close(); }
|
||||
if (failures) process.exitCode = 1;
|
||||
Reference in new issue
Block a user