#!/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;