Files
Sameh Khamis 3c3f8e3e67 feat(display): answer the Display controls through the store with UiSegmented
The Display panel, the dock's style buttons and the clean-view exit call
the display store's actions, and DisplayBindings connects their handlers
to it, replacing the bindDisplayControls listeners. The detection
allocation and 3D model coverage choices render through a new kit
UiSegmented, which takes v-model, options, label,
stacked and disabled, and adds a keyboard policy, item classes and
per-option attributes.

Shell element fields and facade accessors that only the old listeners
read are removed. The package boundaries now own UiSegmented and the
UiPanel kit files, and the format scope drops the deleted test.
2026-11-01 09:56:47 -08:00

286 lines
9.8 KiB
JavaScript

#!/usr/bin/env node
/** Browser acceptance for application shortcuts and generated parameter controls. */
import fs from 'node:fs';
import puppeteer from 'puppeteer';
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 failures = 0;
const check = (name, passed) => {
console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`);
if (!passed) failures += 1;
};
try {
await page.setViewport({ width: 1440, height: 900 });
const url = new URL(process.env.QA_BASE_URL || 'http://127.0.0.1:4173');
url.searchParams.set('welcome', '0');
await page.goto(url.href, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() => window.__godsEyeView?.styleManager?._applicationShortcuts,
{ timeout: 60_000 },
);
await page.waitForFunction(
() => {
const cover = document.getElementById('loading-screen');
return (
cover?.classList.contains('hidden') &&
Number(getComputedStyle(cover).opacity) === 0
);
},
{ timeout: 60_000 },
);
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
manager.setStyle('normal');
document.activeElement?.blur();
});
await page.keyboard.press('2');
check(
'native number shortcut selects CRT',
await page.evaluate(
() => window.__godsEyeView.styleManager.activeStyle === 'retro',
),
);
const initial = await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
manager.setPanelCollapsed('pp-toggles', false, {
persist: false,
syncShare: false,
});
manager._presentPanel(manager._sliderPanel, { collapsed: false });
const slider = manager._sliderContainer.querySelector('input');
window.__qaOldSlider = slider;
const name = 'pixelation';
window.__qaParameter = {
name,
before: manager.stages.retro.uniforms[name],
};
slider.focus();
return {
before: Number(slider.value),
step: Number(slider.step),
label: slider.getAttribute('aria-label'),
};
});
check(
'generated range has an accessible label and a numeric step',
initial.label === 'Pixelation' && initial.step > 0,
);
await page.keyboard.press('ArrowRight');
const changed = await page.evaluate(() => {
const slider = window.__qaOldSlider;
const manager = window.__godsEyeView.styleManager;
return {
value: Number(slider.value),
readout: slider.nextElementSibling.textContent,
uniform: manager.stages.retro.uniforms[window.__qaParameter.name],
};
});
check(
'native range input changes the shader and formatted readout',
changed.value > initial.before &&
changed.uniform === changed.value &&
Number(changed.readout) === changed.value,
);
await page.keyboard.press('3');
check(
'focused range retains native input instead of changing style',
await page.evaluate(
() => window.__godsEyeView.styleManager.activeStyle === 'retro',
),
);
check(
'replacing a style revokes its detached slider',
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
const before = manager.stages.retro.uniforms[window.__qaParameter.name];
manager.setStyle('thermal');
window.__qaOldSlider.value = '0';
window.__qaOldSlider.dispatchEvent(new Event('input', { bubbles: true }));
return (
manager.stages.retro.uniforms[window.__qaParameter.name] === before
);
}),
);
check(
'normal style clears rows; reopening restores current shader values',
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
manager.setStyle('normal');
const cleared = manager._sliderContainer.children.length === 0;
manager.setStyle('retro');
return (
cleared &&
Number(manager._sliderContainer.querySelector('input').value) ===
manager.stages.retro.uniforms[window.__qaParameter.name]
);
}),
);
const hudBefore = await page.evaluate(() => {
document.getElementById('hud-layout-select').focus();
return window.__godsEyeView.styleManager.hud.visible;
});
await page.keyboard.press('h');
check(
'HUD select keeps its native typing and selection',
await page.evaluate(
(visible) =>
document.activeElement?.id === 'hud-layout-select' &&
window.__godsEyeView.styleManager.hud.visible === visible,
hudBefore,
),
);
const displayBefore = await page.evaluate(async () => {
const manager = window.__godsEyeView.styleManager;
manager._setBloomEnabled(true);
// The Display view shows the slider row on its next render.
await new Promise((resolve) => requestAnimationFrame(resolve));
manager._bloomSlider.value = '80';
manager._bloomSlider.dispatchEvent(new Event('input', { bubbles: true }));
manager._bloomSlider.focus();
return {
value: Number(manager._bloomSlider.value),
contrast: manager._bloomStage.uniforms.contrast,
};
});
await page.keyboard.press('ArrowRight');
check(
'native Display slider updates effect state',
await page.evaluate((before) => {
const manager = window.__godsEyeView.styleManager;
return (
Number(manager._bloomSlider.value) > before.value &&
manager._bloomStage.uniforms.contrast < before.contrast
);
}, displayBefore),
);
check(
'Display buttons toggle current state once',
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
const before = manager.bloomEnabled;
manager._bloomBtn.click();
const changed = manager.bloomEnabled !== before;
manager._bloomBtn.click();
return changed && manager.bloomEnabled === before;
}),
);
check(
'Display rebind removes old listeners',
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
manager._initUI();
const before = manager.bloomEnabled;
manager._bloomBtn.click();
return manager.bloomEnabled !== before;
}),
);
const sliders = await page.evaluate(() => {
const ordinary = [
...document.querySelectorAll('input[type="range"]'),
].filter(
(input) => !input.matches('#radio-tuner-slider, .bhote-event-range'),
);
return {
count: ordinary.length,
shared: ordinary.every((input) =>
input.classList.contains('gev-quantitative-slider'),
),
surface: ordinary.every((input) => {
const style = getComputedStyle(input);
return (
style.height === '18px' &&
style.appearance === 'none' &&
style.padding === '0px' &&
style.borderWidth === '0px' &&
(!input.disabled ||
(style.opacity === '0.42' && style.cursor === 'not-allowed'))
);
}),
tunerSeparate: !document
.getElementById('radio-tuner-slider')
.classList.contains('gev-quantitative-slider'),
};
});
check(
'ordinary sliders share one native surface',
sliders.count > 10 && sliders.shared && sliders.surface,
);
check('radio tuning keeps its specialized control', sliders.tunerSeparate);
fs.mkdirSync('qa-shots/visual-input', { recursive: true });
await page.screenshot({ path: 'qa-shots/visual-input/desktop.png' });
await page.setViewport({ width: 620, height: 900 });
await page.waitForFunction(
() =>
document.getElementById('left-panel-stack')?.dataset.layoutMode ===
'mobile',
);
await page.evaluate(() => {
const slider =
window.__godsEyeView.styleManager._sliderContainer.querySelector('input');
slider.focus();
slider.scrollIntoView({ block: 'center', behavior: 'instant' });
});
await page.screenshot({ path: 'qa-shots/visual-input/narrow.png' });
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
window.__qaCurrentSlider = manager._sliderContainer.querySelector('input');
manager._applicationShortcuts.destroy();
manager._styleParameters.destroy();
document.activeElement?.blur();
});
await page.keyboard.press('3');
check(
'destroyed application shortcuts cannot change the selected style',
await page.evaluate(
() => window.__godsEyeView.styleManager.activeStyle === 'retro',
),
);
check(
'destroyed parameter controls cannot write through a retained element',
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
const before = manager.stages.retro.uniforms[window.__qaParameter.name];
window.__qaCurrentSlider.value = '0';
window.__qaCurrentSlider.dispatchEvent(
new Event('input', { bubbles: true }),
);
return (
manager._sliderContainer.children.length === 0 &&
manager.stages.retro.uniforms[window.__qaParameter.name] === before
);
}),
);
check(
'destroyed Display controls cannot change settings',
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
manager._displayBindings._detachDisplayControls();
const before = manager.bloomEnabled;
const contrast = manager._bloomStage.uniforms.contrast;
manager._bloomBtn.click();
manager._bloomSlider.value = '15';
manager._bloomSlider.dispatchEvent(new Event('input', { bubbles: true }));
return (
manager.bloomEnabled === before &&
manager._bloomStage.uniforms.contrast === contrast
);
}),
);
check('runtime has no uncaught browser errors', errors.length === 0);
} finally {
await browser.close();
}
console.log(`RESULT: ${failures} failures`);
process.exitCode = failures ? 1 : 0;