mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
style: format Scene controls and browser acceptance
This commit is contained in:
1 parent
cd72feb4fe
commit
d42036d74f
5 files changed
+498
-103
No files matched your search
@@ -208,5 +208,10 @@
|
|||||||
"src/ui/cockpitTrackingController.js",
|
"src/ui/cockpitTrackingController.js",
|
||||||
"src/cockpitMath.js",
|
"src/cockpitMath.js",
|
||||||
"src/cockpitUtilityLayout.js",
|
"src/cockpitUtilityLayout.js",
|
||||||
"src/cockpitVisionPolicy.js"
|
"src/cockpitVisionPolicy.js",
|
||||||
|
"src/ui/scenes.js",
|
||||||
|
"src/ui/sceneControls.js",
|
||||||
|
"src/ui/scenePresentation.js",
|
||||||
|
"src/ui/sceneControls.test.mjs",
|
||||||
|
"scripts/qa-scene-controls.mjs"
|
||||||
]
|
]
|
||||||
+270
-58
@@ -6,123 +6,335 @@ import puppeteer from 'puppeteer';
|
|||||||
|
|
||||||
const shots = path.resolve('qa-shots/scene-controls');
|
const shots = path.resolve('qa-shots/scene-controls');
|
||||||
fs.mkdirSync(shots, { recursive: true });
|
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 page = await browser.newPage();
|
||||||
const errors = [];
|
const errors = [];
|
||||||
page.on('pageerror', (error) => errors.push(error.message));
|
page.on('pageerror', (error) => errors.push(error.message));
|
||||||
let promptValue = 'QA Scene';
|
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;
|
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 {
|
try {
|
||||||
await page.setViewport({ width: 1440, height: 900 });
|
await page.setViewport({ width: 1440, height: 900 });
|
||||||
await page.goto(`${process.env.QA_BASE_URL || 'http://localhost:4173'}/?welcome=0`, { waitUntil: 'domcontentloaded' });
|
await page.goto(
|
||||||
await page.waitForFunction(() => window.__godsEyeView?.sceneDirector?._controls && document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 60000 });
|
`${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('[data-collapse-target="scene-panel"]');
|
||||||
await page.click('#scene-new-btn');
|
await page.click('#scene-new-btn');
|
||||||
await page.waitForFunction(() => document.querySelector('#scene-select option:checked')?.textContent === 'QA Scene');
|
await page.waitForFunction(
|
||||||
check('New creates and selects a persisted empty scene', await page.evaluate(() => {
|
() =>
|
||||||
const director = window.__godsEyeView.sceneDirector;
|
document.querySelector('#scene-select option:checked')?.textContent ===
|
||||||
const saved = JSON.parse(localStorage.getItem('godsEyeView.sceneProject.v2'));
|
'QA Scene',
|
||||||
return director._getSelectedScene().shots.length === 0 && !!document.querySelector('.scene-shot-empty') && saved.scenes.some((scene) => scene.title === '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.click('#scene-capture-btn');
|
||||||
await page.waitForFunction(() => document.querySelectorAll('.scene-shot-row').length === 1);
|
await page.waitForFunction(
|
||||||
check('Capture stores the actual camera and visual state', await page.evaluate(() => {
|
() => document.querySelectorAll('.scene-shot-row').length === 1,
|
||||||
const director = window.__godsEyeView.sceneDirector;
|
);
|
||||||
const shot = director._getSelectedScene().shots[0];
|
check(
|
||||||
const current = director.styleManager.getCameraState();
|
'Capture stores the actual camera and visual state',
|
||||||
return Number.isFinite(shot.camera.lat) && Math.abs(shot.camera.lat - current.lat) < 0.01 && shot.visual.style === director.styleManager.getVisualState().style;
|
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>';
|
promptValue = '<b>QA shot</b>';
|
||||||
await page.click('.scene-shot-label', { clickCount: 2 });
|
await page.click('.scene-shot-label', { clickCount: 2 });
|
||||||
await page.waitForFunction(() => document.querySelector('.scene-shot-label')?.textContent === '<b>QA shot</b>');
|
await page.waitForFunction(
|
||||||
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>'))));
|
() =>
|
||||||
|
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.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');
|
await page.click('.scene-shot-label');
|
||||||
check('Shot selection and Update use the selected shot', await page.evaluate(() => {
|
check(
|
||||||
const director = window.__godsEyeView.sceneDirector;
|
'Shot selection and Update use the selected shot',
|
||||||
const scene = director._getSelectedScene();
|
await page.evaluate(() => {
|
||||||
const count = scene.shots.length;
|
const director = window.__godsEyeView.sceneDirector;
|
||||||
document.getElementById('scene-update-shot-btn').click();
|
const scene = director._getSelectedScene();
|
||||||
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;
|
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(() => {
|
await page.evaluate(() => {
|
||||||
window.__sceneExports = [];
|
window.__sceneExports = [];
|
||||||
const create = URL.createObjectURL.bind(URL);
|
const create = URL.createObjectURL.bind(URL);
|
||||||
URL.createObjectURL = (blob) => {
|
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);
|
return create(blob);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await page.click('#scene-export-btn');
|
await page.click('#scene-export-btn');
|
||||||
const exported = await page.evaluate(async () => JSON.parse(await window.__sceneExports[0]));
|
const exported = await page.evaluate(async () =>
|
||||||
check('Export produces the current project JSON', exported.scenes.some((scene) => scene.title === 'QA Scene' && scene.shots.length === 2));
|
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
|
// Keep real camera/style playback while isolating this control check from
|
||||||
// live provider availability. Layer transactions are covered by their suites.
|
// live provider availability. Layer transactions are covered by their suites.
|
||||||
const scene = exported.scenes.find((item) => item.title === 'QA Scene');
|
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 fixture = { ...exported, scenes: [scene] };
|
||||||
const projectFile = path.join(shots, 'project.json');
|
const projectFile = path.join(shots, 'project.json');
|
||||||
fs.writeFileSync(projectFile, JSON.stringify(fixture));
|
fs.writeFileSync(projectFile, JSON.stringify(fixture));
|
||||||
const input = await page.$('#scene-import-file');
|
const input = await page.$('#scene-import-file');
|
||||||
await input.uploadFile(projectFile);
|
await input.uploadFile(projectFile);
|
||||||
await page.waitForFunction(() => document.getElementById('scene-status').textContent === 'Imported project.json');
|
await page.waitForFunction(
|
||||||
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 === ''));
|
() =>
|
||||||
|
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.click('.scene-shot-btn');
|
||||||
await page.waitForFunction(() => document.getElementById('scene-status').textContent.startsWith('Loaded:'), { timeout: 30000 });
|
await page.waitForFunction(
|
||||||
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>')));
|
() =>
|
||||||
|
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.click('#scene-start-btn');
|
||||||
await page.waitForFunction(() => window.__godsEyeView.sceneDirector.running && document.body.classList.contains('scene-playback-mode'));
|
await page.waitForFunction(
|
||||||
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));
|
() =>
|
||||||
|
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.keyboard.press('Escape');
|
||||||
await page.waitForFunction(() => !window.__godsEyeView.sceneDirector.running && !document.body.classList.contains('scene-playback-mode'));
|
await page.waitForFunction(
|
||||||
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));
|
() =>
|
||||||
|
!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');
|
await page.click('#scene-download-btn');
|
||||||
const metadata = await page.evaluate(async () => JSON.parse(await window.__sceneExports.at(-1)));
|
const metadata = await page.evaluate(async () =>
|
||||||
check('Run download contains the completed cancelled run', Boolean(metadata.endedAt && metadata.wasCancelled && Array.isArray(metadata.events)));
|
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.click('#scene-start-btn');
|
||||||
await page.waitForFunction(() => window.__godsEyeView.sceneDirector.running);
|
await page.waitForFunction(() => window.__godsEyeView.sceneDirector.running);
|
||||||
await page.$eval('#scene-stop-btn', (button) => button.click());
|
await page.$eval('#scene-stop-btn', (button) => button.click());
|
||||||
await page.waitForFunction(() => !window.__godsEyeView.sceneDirector.running);
|
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');
|
const badFile = path.join(shots, 'invalid.json');
|
||||||
fs.writeFileSync(badFile, '{invalid');
|
fs.writeFileSync(badFile, '{invalid');
|
||||||
await input.uploadFile(badFile);
|
await input.uploadFile(badFile);
|
||||||
await page.waitForFunction(() => document.getElementById('scene-status').textContent.includes('invalid JSON'));
|
await page.waitForFunction(() =>
|
||||||
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'));
|
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.screenshot({ path: path.join(shots, 'desktop.png') });
|
||||||
await page.setViewport({ width: 390, height: 844 });
|
await page.setViewport({ width: 390, height: 844 });
|
||||||
await page.screenshot({ path: path.join(shots, 'narrow.png') });
|
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.setViewport({ width: 1440, height: 900 });
|
||||||
await page.click('.scene-shot-danger');
|
await page.click('.scene-shot-danger');
|
||||||
await page.waitForFunction(() => !!document.querySelector('.scene-shot-empty'));
|
await page.waitForFunction(
|
||||||
check('Delete shot leaves the empty-state presentation', await page.evaluate(() => window.__godsEyeView.sceneDirector._getSelectedScene().shots.length === 0));
|
() => !!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.click('#scene-delete-btn');
|
||||||
await page.waitForFunction(() => !window.__godsEyeView.sceneDirector._project.scenes.some((item) => item.title === 'QA Scene'));
|
await page.waitForFunction(
|
||||||
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));
|
() =>
|
||||||
|
!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 teardown = await page.evaluate(async () => {
|
||||||
const director = window.__godsEyeView.sceneDirector;
|
const director = window.__godsEyeView.sceneDirector;
|
||||||
const controls = director._controls;
|
const controls = director._controls;
|
||||||
const previousStatus = document.getElementById('scene-status').textContent;
|
const previousStatus = document.getElementById('scene-status').textContent;
|
||||||
const project = director._project;
|
const project = director._project;
|
||||||
let finish;
|
let finish;
|
||||||
const gate = new Promise((resolve) => { finish = resolve; });
|
const gate = new Promise((resolve) => {
|
||||||
const pending = controls.run('import', { name: 'late.json', text: async () => { await gate; return '{"scenes":[]}'; } });
|
finish = resolve;
|
||||||
|
});
|
||||||
|
const pending = controls.run('import', {
|
||||||
|
name: 'late.json',
|
||||||
|
text: async () => {
|
||||||
|
await gate;
|
||||||
|
return '{"scenes":[]}';
|
||||||
|
},
|
||||||
|
});
|
||||||
const stopping = director.destroy();
|
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();
|
document.getElementById('scene-new-btn').click();
|
||||||
controls.run('capture');
|
controls.run('capture');
|
||||||
finish();
|
finish();
|
||||||
await pending;
|
await pending;
|
||||||
await stopping;
|
await stopping;
|
||||||
await director.destroy();
|
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(
|
||||||
check('Late import completion cannot replace the disposed project or status', teardown.sameProject && teardown.noLateStatus);
|
'Disposal revokes fixed and row actions before waiting for scene work',
|
||||||
check('Scene control interaction produces no uncaught browser errors', errors.length === 0);
|
teardown.stoppedSynchronously && teardown.rowsGone,
|
||||||
} finally { await browser.close(); }
|
);
|
||||||
|
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;
|
if (failures) process.exitCode = 1;
|
||||||
+45
-13
@@ -1,5 +1,12 @@
|
|||||||
/** Own Scene panel listeners and presentation through project reads and actions. */
|
/** Own Scene panel listeners and presentation through project reads and actions. */
|
||||||
import { sceneElements, renderSceneOptions, renderSceneShots, presentSceneButtons, presentSceneProgress, presentSceneRuntime } from './scenePresentation.js';
|
import {
|
||||||
|
sceneElements,
|
||||||
|
renderSceneOptions,
|
||||||
|
renderSceneShots,
|
||||||
|
presentSceneButtons,
|
||||||
|
presentSceneProgress,
|
||||||
|
presentSceneRuntime,
|
||||||
|
} from './scenePresentation.js';
|
||||||
|
|
||||||
export class SceneControls {
|
export class SceneControls {
|
||||||
constructor({ read, actions, elements = sceneElements() }) {
|
constructor({ read, actions, elements = sceneElements() }) {
|
||||||
@@ -14,11 +21,23 @@ export class SceneControls {
|
|||||||
if (!elements.select) return;
|
if (!elements.select) return;
|
||||||
this.renderSceneSelect();
|
this.renderSceneSelect();
|
||||||
this.renderShotList();
|
this.renderShotList();
|
||||||
this.listen(elements.select, 'change', () => this.run('selectScene', elements.select.value));
|
this.listen(elements.select, 'change', () =>
|
||||||
for (const [element, action] of Object.entries({ new: 'create', delete: 'deleteScene', capture: 'capture', update: 'update', next: 'next', export: 'export', download: 'download' })) {
|
this.run('selectScene', elements.select.value),
|
||||||
|
);
|
||||||
|
for (const [element, action] of Object.entries({
|
||||||
|
new: 'create',
|
||||||
|
delete: 'deleteScene',
|
||||||
|
capture: 'capture',
|
||||||
|
update: 'update',
|
||||||
|
next: 'next',
|
||||||
|
export: 'export',
|
||||||
|
download: 'download',
|
||||||
|
})) {
|
||||||
this.listen(elements[element], 'click', () => this.run(action));
|
this.listen(elements[element], 'click', () => this.run(action));
|
||||||
}
|
}
|
||||||
this.listen(elements.start, 'click', () => this.run('start', this.read().selectedSceneId));
|
this.listen(elements.start, 'click', () =>
|
||||||
|
this.run('start', this.read().selectedSceneId),
|
||||||
|
);
|
||||||
this.listen(elements.stop, 'click', () => this.run('stop', 'Stopped'));
|
this.listen(elements.stop, 'click', () => this.run('stop', 'Stopped'));
|
||||||
this.listen(elements.import, 'click', () => elements.file?.click());
|
this.listen(elements.import, 'click', () => elements.file?.click());
|
||||||
this.listen(elements.file, 'change', async () => {
|
this.listen(elements.file, 'change', async () => {
|
||||||
@@ -34,7 +53,9 @@ export class SceneControls {
|
|||||||
|
|
||||||
listen(target, type, callback, removers = this.removers) {
|
listen(target, type, callback, removers = this.removers) {
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
const listener = (event) => { if (!this.destroyed) return callback(event); };
|
const listener = (event) => {
|
||||||
|
if (!this.destroyed) return callback(event);
|
||||||
|
};
|
||||||
target.addEventListener(type, listener);
|
target.addEventListener(type, listener);
|
||||||
removers.push(() => target.removeEventListener(type, listener));
|
removers.push(() => target.removeEventListener(type, listener));
|
||||||
}
|
}
|
||||||
@@ -43,7 +64,8 @@ export class SceneControls {
|
|||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
const generation = ++this.actionGeneration;
|
const generation = ++this.actionGeneration;
|
||||||
const failed = () => {
|
const failed = () => {
|
||||||
if (!this.destroyed && generation === this.actionGeneration) this.updateStatus('Scene action failed');
|
if (!this.destroyed && generation === this.actionGeneration)
|
||||||
|
this.updateStatus('Scene action failed');
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const result = this.actions[action](...args);
|
const result = this.actions[action](...args);
|
||||||
@@ -61,16 +83,19 @@ export class SceneControls {
|
|||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
for (const remove of this.rowRemovers.splice(0)) remove();
|
for (const remove of this.rowRemovers.splice(0)) remove();
|
||||||
renderSceneShots(this.elements.shots, this.read(), {
|
renderSceneShots(this.elements.shots, this.read(), {
|
||||||
listen: (element, type, callback) => this.listen(element, type, callback, this.rowRemovers),
|
listen: (element, type, callback) =>
|
||||||
|
this.listen(element, type, callback, this.rowRemovers),
|
||||||
select: (id) => this.run('selectShot', id),
|
select: (id) => this.run('selectShot', id),
|
||||||
rename: (sceneId, shotId, title) => this.run('renameShot', sceneId, shotId, title),
|
rename: (sceneId, shotId, title) =>
|
||||||
|
this.run('renameShot', sceneId, shotId, title),
|
||||||
load: (sceneId, shotId) => this.run('load', sceneId, shotId),
|
load: (sceneId, shotId) => this.run('load', sceneId, shotId),
|
||||||
remove: (sceneId, shotId) => this.run('deleteShot', sceneId, shotId),
|
remove: (sceneId, shotId) => this.run('deleteShot', sceneId, shotId),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setButtons(running) {
|
setButtons(running) {
|
||||||
if (!this.destroyed) presentSceneButtons(this.elements, running, this.read().hasRun);
|
if (!this.destroyed)
|
||||||
|
presentSceneButtons(this.elements, running, this.read().hasRun);
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgress(progress) {
|
setProgress(progress) {
|
||||||
@@ -78,7 +103,8 @@ export class SceneControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateStatus(text) {
|
updateStatus(text) {
|
||||||
if (!this.destroyed && this.elements.status) this.elements.status.textContent = text;
|
if (!this.destroyed && this.elements.status)
|
||||||
|
this.elements.status.textContent = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRuntime(text) {
|
updateRuntime(text) {
|
||||||
@@ -96,10 +122,12 @@ export class SceneControls {
|
|||||||
this.playbackKeyRemover = null;
|
this.playbackKeyRemover = null;
|
||||||
} else if (!this.destroyed && !this.playbackKeyRemover) {
|
} else if (!this.destroyed && !this.playbackKeyRemover) {
|
||||||
const onKeyDown = (event) => {
|
const onKeyDown = (event) => {
|
||||||
if (!this.destroyed && event.key === 'Escape' && this.read().running) this.run('stop', 'Stopped (Esc)');
|
if (!this.destroyed && event.key === 'Escape' && this.read().running)
|
||||||
|
this.run('stop', 'Stopped (Esc)');
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
this.playbackKeyRemover = () => document.removeEventListener('keydown', onKeyDown);
|
this.playbackKeyRemover = () =>
|
||||||
|
document.removeEventListener('keydown', onKeyDown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +136,11 @@ export class SceneControls {
|
|||||||
this.destroyed = true;
|
this.destroyed = true;
|
||||||
this.actionGeneration++;
|
this.actionGeneration++;
|
||||||
this.setPlaybackKeyboardEnabled(false);
|
this.setPlaybackKeyboardEnabled(false);
|
||||||
for (const remove of [...this.removers.splice(0), ...this.rowRemovers.splice(0)]) remove();
|
for (const remove of [
|
||||||
|
...this.removers.splice(0),
|
||||||
|
...this.rowRemovers.splice(0),
|
||||||
|
])
|
||||||
|
remove();
|
||||||
if (this.elements.shots) this.elements.shots.textContent = '';
|
if (this.elements.shots) this.elements.shots.textContent = '';
|
||||||
presentSceneRuntime(this.elements.runtime, '');
|
presentSceneRuntime(this.elements.runtime, '');
|
||||||
this.setPlaybackActive(false);
|
this.setPlaybackActive(false);
|
||||||
|
|||||||
+141
-22
@@ -12,28 +12,116 @@ function fixture() {
|
|||||||
this._text = '';
|
this._text = '';
|
||||||
const classes = new Set();
|
const classes = new Set();
|
||||||
this.classList = {
|
this.classList = {
|
||||||
toggle(name, active) { if (active) classes.add(name); else classes.delete(name); },
|
toggle(name, active) {
|
||||||
|
if (active) classes.add(name);
|
||||||
|
else classes.delete(name);
|
||||||
|
},
|
||||||
contains: (name) => classes.has(name),
|
contains: (name) => classes.has(name),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
appendChild(element) { this.children.push(element); }
|
appendChild(element) {
|
||||||
set textContent(value) { this._text = value; this.children = []; }
|
this.children.push(element);
|
||||||
get textContent() { return this._text + this.children.map((node) => node.textContent).join(''); }
|
}
|
||||||
click() { this.dispatchEvent(new Event('click')); }
|
set textContent(value) {
|
||||||
|
this._text = value;
|
||||||
|
this.children = [];
|
||||||
|
}
|
||||||
|
get textContent() {
|
||||||
|
return (
|
||||||
|
this._text + this.children.map((node) => node.textContent).join('')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
click() {
|
||||||
|
this.dispatchEvent(new Event('click'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const saved = { document: globalThis.document, window: globalThis.window };
|
const saved = { document: globalThis.document, window: globalThis.window };
|
||||||
const document = Object.assign(new EventTarget(), { createElement: () => new Element(), body: new Element() });
|
const document = Object.assign(new EventTarget(), {
|
||||||
|
createElement: () => new Element(),
|
||||||
|
body: new Element(),
|
||||||
|
});
|
||||||
globalThis.document = document;
|
globalThis.document = document;
|
||||||
globalThis.window = { prompt: () => 'Renamed' };
|
globalThis.window = { prompt: () => 'Renamed' };
|
||||||
const elements = Object.fromEntries(['panel','select','new','delete','capture','update','shots','start','stop','next','export','import','file','download','status','progress','runtime'].map((name) => [name, new Element()]));
|
const elements = Object.fromEntries(
|
||||||
|
[
|
||||||
|
'panel',
|
||||||
|
'select',
|
||||||
|
'new',
|
||||||
|
'delete',
|
||||||
|
'capture',
|
||||||
|
'update',
|
||||||
|
'shots',
|
||||||
|
'start',
|
||||||
|
'stop',
|
||||||
|
'next',
|
||||||
|
'export',
|
||||||
|
'import',
|
||||||
|
'file',
|
||||||
|
'download',
|
||||||
|
'status',
|
||||||
|
'progress',
|
||||||
|
'runtime',
|
||||||
|
].map((name) => [name, new Element()]),
|
||||||
|
);
|
||||||
const state = {
|
const state = {
|
||||||
scenes: [{ id: 'scene-a', title: '<Scene A>', shots: [{ id: 'shot-a', title: '<Shot A>', durationSec: 4, holdSec: 0.9, visual: { style: 'normal' } }] }],
|
scenes: [
|
||||||
selectedSceneId: 'scene-a', selectedShotId: 'shot-a', running: false, hasRun: false,
|
{
|
||||||
|
id: 'scene-a',
|
||||||
|
title: '<Scene A>',
|
||||||
|
shots: [
|
||||||
|
{
|
||||||
|
id: 'shot-a',
|
||||||
|
title: '<Shot A>',
|
||||||
|
durationSec: 4,
|
||||||
|
holdSec: 0.9,
|
||||||
|
visual: { style: 'normal' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedSceneId: 'scene-a',
|
||||||
|
selectedShotId: 'shot-a',
|
||||||
|
running: false,
|
||||||
|
hasRun: false,
|
||||||
};
|
};
|
||||||
const calls = [];
|
const calls = [];
|
||||||
const actions = Object.fromEntries(['selectScene','selectShot','renameShot','create','deleteScene','capture','update','start','stop','next','export','import','download','load','deleteShot'].map((name) => [name, (...args) => { calls.push([name, ...args]); }]));
|
const actions = Object.fromEntries(
|
||||||
|
[
|
||||||
|
'selectScene',
|
||||||
|
'selectShot',
|
||||||
|
'renameShot',
|
||||||
|
'create',
|
||||||
|
'deleteScene',
|
||||||
|
'capture',
|
||||||
|
'update',
|
||||||
|
'start',
|
||||||
|
'stop',
|
||||||
|
'next',
|
||||||
|
'export',
|
||||||
|
'import',
|
||||||
|
'download',
|
||||||
|
'load',
|
||||||
|
'deleteShot',
|
||||||
|
].map((name) => [
|
||||||
|
name,
|
||||||
|
(...args) => {
|
||||||
|
calls.push([name, ...args]);
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
const owner = new SceneControls({ read: () => state, actions, elements });
|
const owner = new SceneControls({ read: () => state, actions, elements });
|
||||||
return { owner, state, actions, elements, calls, document, restore() { owner.destroy(); Object.assign(globalThis, saved); } };
|
return {
|
||||||
|
owner,
|
||||||
|
state,
|
||||||
|
actions,
|
||||||
|
elements,
|
||||||
|
calls,
|
||||||
|
document,
|
||||||
|
restore() {
|
||||||
|
owner.destroy();
|
||||||
|
Object.assign(globalThis, saved);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Scene controls render the supplied selection and running/download states', () => {
|
test('Scene controls render the supplied selection and running/download states', () => {
|
||||||
@@ -41,7 +129,10 @@ test('Scene controls render the supplied selection and running/download states',
|
|||||||
try {
|
try {
|
||||||
assert.equal(f.elements.select.children[0].textContent, '<Scene A>');
|
assert.equal(f.elements.select.children[0].textContent, '<Scene A>');
|
||||||
assert.equal(f.elements.select.value, 'scene-a');
|
assert.equal(f.elements.select.value, 'scene-a');
|
||||||
assert.equal(f.elements.shots.children[0].children[0].children[0].textContent, '<Shot A>');
|
assert.equal(
|
||||||
|
f.elements.shots.children[0].children[0].children[0].textContent,
|
||||||
|
'<Shot A>',
|
||||||
|
);
|
||||||
assert.equal(f.elements.stop.disabled, true);
|
assert.equal(f.elements.stop.disabled, true);
|
||||||
assert.equal(f.elements.download.disabled, true);
|
assert.equal(f.elements.download.disabled, true);
|
||||||
f.state.hasRun = true;
|
f.state.hasRun = true;
|
||||||
@@ -55,7 +146,9 @@ test('Scene controls render the supplied selection and running/download states',
|
|||||||
assert.equal(f.elements.progress.style.width, '100%');
|
assert.equal(f.elements.progress.style.width, '100%');
|
||||||
f.owner.setProgress(-1);
|
f.owner.setProgress(-1);
|
||||||
assert.equal(f.elements.progress.textContent, '0%');
|
assert.equal(f.elements.progress.textContent, '0%');
|
||||||
} finally { f.restore(); }
|
} finally {
|
||||||
|
f.restore();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Scene controls dispatch explicit actions and revoke replaced shot-row listeners', () => {
|
test('Scene controls dispatch explicit actions and revoke replaced shot-row listeners', () => {
|
||||||
@@ -69,19 +162,30 @@ test('Scene controls dispatch explicit actions and revoke replaced shot-row list
|
|||||||
label.click();
|
label.click();
|
||||||
label.dispatchEvent(new Event('dblclick'));
|
label.dispatchEvent(new Event('dblclick'));
|
||||||
load.click();
|
load.click();
|
||||||
assert.deepEqual(f.calls, [['capture'], ['start','scene-a'], ['selectShot','shot-a'], ['renameShot','scene-a','shot-a','Renamed'], ['load','scene-a','shot-a']]);
|
assert.deepEqual(f.calls, [
|
||||||
|
['capture'],
|
||||||
|
['start', 'scene-a'],
|
||||||
|
['selectShot', 'shot-a'],
|
||||||
|
['renameShot', 'scene-a', 'shot-a', 'Renamed'],
|
||||||
|
['load', 'scene-a', 'shot-a'],
|
||||||
|
]);
|
||||||
f.owner.renderShotList();
|
f.owner.renderShotList();
|
||||||
label.click();
|
label.click();
|
||||||
load.click();
|
load.click();
|
||||||
assert.equal(f.calls.length, 5);
|
assert.equal(f.calls.length, 5);
|
||||||
f.elements.shots.children[0].children[0].children[1].children[0].click();
|
f.elements.shots.children[0].children[0].children[1].children[0].click();
|
||||||
assert.equal(f.calls.length, 6);
|
assert.equal(f.calls.length, 6);
|
||||||
} finally { f.restore(); }
|
} finally {
|
||||||
|
f.restore();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Scene Escape and recording presentation follow playback and release on destruction', () => {
|
test('Scene Escape and recording presentation follow playback and release on destruction', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
const escape = () => f.document.dispatchEvent(Object.assign(new Event('keydown'), { key: 'Escape' }));
|
const escape = () =>
|
||||||
|
f.document.dispatchEvent(
|
||||||
|
Object.assign(new Event('keydown'), { key: 'Escape' }),
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
escape();
|
escape();
|
||||||
assert.deepEqual(f.calls, []);
|
assert.deepEqual(f.calls, []);
|
||||||
@@ -98,18 +202,26 @@ test('Scene Escape and recording presentation follow playback and release on des
|
|||||||
f.elements.capture.click();
|
f.elements.capture.click();
|
||||||
f.owner.run('capture');
|
f.owner.run('capture');
|
||||||
assert.equal(f.calls.length, 1);
|
assert.equal(f.calls.length, 1);
|
||||||
assert.equal(f.document.body.classList.contains('scene-playback-mode'), false);
|
assert.equal(
|
||||||
|
f.document.body.classList.contains('scene-playback-mode'),
|
||||||
|
false,
|
||||||
|
);
|
||||||
assert.equal(f.elements.runtime.textContent, '');
|
assert.equal(f.elements.runtime.textContent, '');
|
||||||
assert.equal(f.owner.removers.length, 0);
|
assert.equal(f.owner.removers.length, 0);
|
||||||
assert.equal(f.owner.rowRemovers.length, 0);
|
assert.equal(f.owner.rowRemovers.length, 0);
|
||||||
} finally { f.restore(); }
|
} finally {
|
||||||
|
f.restore();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a file import finishing after disposal cannot clear the retained file control', async () => {
|
test('a file import finishing after disposal cannot clear the retained file control', async () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
try {
|
try {
|
||||||
let finish;
|
let finish;
|
||||||
f.actions.import = () => new Promise((resolve) => { finish = resolve; });
|
f.actions.import = () =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
finish = resolve;
|
||||||
|
});
|
||||||
f.elements.file.files = [{ name: 'project.json' }];
|
f.elements.file.files = [{ name: 'project.json' }];
|
||||||
f.elements.file.value = 'project.json';
|
f.elements.file.value = 'project.json';
|
||||||
f.elements.file.dispatchEvent(new Event('change'));
|
f.elements.file.dispatchEvent(new Event('change'));
|
||||||
@@ -118,14 +230,19 @@ test('a file import finishing after disposal cannot clear the retained file cont
|
|||||||
await new Promise((resolve) => setImmediate(resolve));
|
await new Promise((resolve) => setImmediate(resolve));
|
||||||
assert.equal(f.elements.file.value, 'project.json');
|
assert.equal(f.elements.file.value, 'project.json');
|
||||||
assert.equal(f.elements.status.textContent, 'Ready');
|
assert.equal(f.elements.status.textContent, 'Ready');
|
||||||
} finally { f.restore(); }
|
} finally {
|
||||||
|
f.restore();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Scene action failures are visible only while that action still owns presentation', async () => {
|
test('Scene action failures are visible only while that action still owns presentation', async () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
try {
|
try {
|
||||||
let reject;
|
let reject;
|
||||||
f.actions.load = () => new Promise((_resolve, fail) => { reject = fail; });
|
f.actions.load = () =>
|
||||||
|
new Promise((_resolve, fail) => {
|
||||||
|
reject = fail;
|
||||||
|
});
|
||||||
let pending = f.owner.run('load', 'scene-a', 'shot-a');
|
let pending = f.owner.run('load', 'scene-a', 'shot-a');
|
||||||
reject(new Error('unavailable'));
|
reject(new Error('unavailable'));
|
||||||
await pending;
|
await pending;
|
||||||
@@ -141,5 +258,7 @@ test('Scene action failures are visible only while that action still owns presen
|
|||||||
reject(new Error('disposed'));
|
reject(new Error('disposed'));
|
||||||
await pending;
|
await pending;
|
||||||
assert.equal(f.elements.status.textContent, 'Current selection');
|
assert.equal(f.elements.status.textContent, 'Current selection');
|
||||||
} finally { f.restore(); }
|
} finally {
|
||||||
|
f.restore();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
@@ -1,14 +1,27 @@
|
|||||||
/** Scene panel elements and text-only project presentation. */
|
/** Scene panel elements and text-only project presentation. */
|
||||||
export function sceneElements(root = document) {
|
export function sceneElements(root = document) {
|
||||||
const ids = {
|
const ids = {
|
||||||
panel: 'scene-panel', select: 'scene-select', new: 'scene-new-btn',
|
panel: 'scene-panel',
|
||||||
delete: 'scene-delete-btn', capture: 'scene-capture-btn', update: 'scene-update-shot-btn',
|
select: 'scene-select',
|
||||||
shots: 'scene-shot-list', start: 'scene-start-btn', stop: 'scene-stop-btn',
|
new: 'scene-new-btn',
|
||||||
next: 'scene-next-btn', export: 'scene-export-btn', import: 'scene-import-btn',
|
delete: 'scene-delete-btn',
|
||||||
file: 'scene-import-file', download: 'scene-download-btn', status: 'scene-status',
|
capture: 'scene-capture-btn',
|
||||||
progress: 'scene-progress-fill', runtime: 'scene-runtime',
|
update: 'scene-update-shot-btn',
|
||||||
|
shots: 'scene-shot-list',
|
||||||
|
start: 'scene-start-btn',
|
||||||
|
stop: 'scene-stop-btn',
|
||||||
|
next: 'scene-next-btn',
|
||||||
|
export: 'scene-export-btn',
|
||||||
|
import: 'scene-import-btn',
|
||||||
|
file: 'scene-import-file',
|
||||||
|
download: 'scene-download-btn',
|
||||||
|
status: 'scene-status',
|
||||||
|
progress: 'scene-progress-fill',
|
||||||
|
runtime: 'scene-runtime',
|
||||||
};
|
};
|
||||||
return Object.fromEntries(Object.entries(ids).map(([name, id]) => [name, root.getElementById(id)]));
|
return Object.fromEntries(
|
||||||
|
Object.entries(ids).map(([name, id]) => [name, root.getElementById(id)]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderSceneOptions(element, { scenes, selectedSceneId }) {
|
export function renderSceneOptions(element, { scenes, selectedSceneId }) {
|
||||||
@@ -23,7 +36,11 @@ export function renderSceneOptions(element, { scenes, selectedSceneId }) {
|
|||||||
if (selectedSceneId) element.value = selectedSceneId;
|
if (selectedSceneId) element.value = selectedSceneId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderSceneShots(element, state, { listen, select, rename, load, remove }) {
|
export function renderSceneShots(
|
||||||
|
element,
|
||||||
|
state,
|
||||||
|
{ listen, select, rename, load, remove },
|
||||||
|
) {
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
const scene = state.scenes.find((item) => item.id === state.selectedSceneId);
|
const scene = state.scenes.find((item) => item.id === state.selectedSceneId);
|
||||||
element.textContent = '';
|
element.textContent = '';
|
||||||
@@ -74,7 +91,17 @@ export function renderSceneShots(element, state, { listen, select, rename, load,
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function presentSceneButtons(elements, running, hasRun) {
|
export function presentSceneButtons(elements, running, hasRun) {
|
||||||
for (const name of ['start', 'next', 'select', 'new', 'delete', 'capture', 'update', 'export', 'import']) {
|
for (const name of [
|
||||||
|
'start',
|
||||||
|
'next',
|
||||||
|
'select',
|
||||||
|
'new',
|
||||||
|
'delete',
|
||||||
|
'capture',
|
||||||
|
'update',
|
||||||
|
'export',
|
||||||
|
'import',
|
||||||
|
]) {
|
||||||
if (elements[name]) elements[name].disabled = running;
|
if (elements[name]) elements[name].disabled = running;
|
||||||
}
|
}
|
||||||
if (elements.stop) elements.stop.disabled = !running;
|
if (elements.stop) elements.stop.disabled = !running;
|
||||||
|
|||||||
Reference in new issue
Block a user