style: format Scene controls and browser acceptance

This commit is contained in:
Sameh Khamis committed 2026-09-13 03:30:05 -07:00
1 parent cd72feb4fe
commit d42036d74f
5 files changed
+498 -103

No files matched your search

+270 -58
View File
@@ -6,123 +6,335 @@ 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 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); });
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++; }
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.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.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;
}));
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.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.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;
}));
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());
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));
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: {} }));
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.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.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.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.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)));
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));
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.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; }));
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.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));
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 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;
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') };
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(); }
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;