#!/usr/bin/env node /** Rendered acceptance for style transitions and effect-state ownership. */ 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; function check(name, passed) { console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`); if (!passed) failures++; } 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?._visualEffects && document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 60_000 }, ); await page.evaluate(() => { const viewer = window.__godsEyeView.viewer; viewer.camera.cancelFlight?.(); viewer.scene.tweens?.removeAll?.(); viewer.camera.setView({ destination: viewer.scene.globe.ellipsoid.cartographicToCartesian({ longitude: (-97.7431 * Math.PI) / 180, latitude: (30.2568 * Math.PI) / 180, height: 1200, }), orientation: { heading: 0, pitch: -0.85, roll: 0 }, }); viewer.scene.requestRender(); }); await page.evaluate( () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)), ), ); const settled = await page .waitForFunction( () => { const primitives = window.__godsEyeView.viewer.scene.primitives; for (let i = 0; i < primitives.length; i++) { const primitive = primitives.get(i); if ( typeof primitive.tilesLoaded === 'boolean' && !primitive.tilesLoaded ) return false; } return true; }, { timeout: 60_000 }, ) .then( () => true, () => false, ); check('visible tile content settles before visual captures', settled); fs.mkdirSync('qa-shots/visual-effects', { recursive: true }); for (const style of [ 'normal', 'retro', 'surveillance', 'thermal', 'anime', 'noir', 'snow', 'normal', ]) { await page.evaluate( (name) => window.__godsEyeView.styleManager.setStyle(name), style, ); await page.waitForFunction( () => window.__godsEyeView.styleManager.transitions.size === 0, { timeout: 10_000 }, ); const state = await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; const effects = manager._visualEffects; const visible = Object.entries(effects.stages) .filter(([, stage]) => stage.enabled) .map(([name]) => name); const animated = Object.values(effects.stages).some( (stage) => stage.enabled && stage.uniforms.time !== undefined && stage.uniforms.intensity > 0.001, ); return { active: manager.activeStyle, visible, clock: effects.frameId !== null, animated, }; }); check( `${style}: only the selected style remains visible`, state.active === style && JSON.stringify(state.visible) === JSON.stringify(style === 'normal' ? [] : [style]), ); check( `${style}: the animation clock follows visible animated stages`, state.clock === state.animated, ); if (['normal', 'surveillance', 'thermal'].includes(style)) { await page.screenshot({ path: `qa-shots/visual-effects/${style}.png` }); if (style === 'surveillance') await page.evaluate(() => window.__godsEyeView.viewer.camera.lookRight(0.2), ); } } await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; manager.setStyle('noir'); manager.setStyle('retro'); manager.setStyle('normal'); }); await page.waitForFunction( () => window.__godsEyeView.styleManager.transitions.size === 0, { timeout: 10_000 }, ); check( 'rapid switches settle to Normal without an old style or clock', await page.evaluate(() => { const effects = window.__godsEyeView.styleManager._visualEffects; return ( effects.frameId === null && Object.values(effects.stages).every((stage) => !stage.enabled) ); }), ); check( 'bloom actions, effect state and saved snapshot agree', await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; const states = []; const unsubscribe = manager.subscribeShareState((value) => states.push(value)); const result = manager.setBloom({ enabled: true, intensityPct: 98 }); unsubscribe(); const published = states.at(-1); const snapshot = manager.getVisualState(); return ( states[0].initial && states.length > 1 && Object.isFrozen(published.state.options) && published.state.bloomEnabled && published.state.options.bloomIntensity === 98 && result.ok && result.bloom.intensityPct === 98 && snapshot.bloom.intensity === 98 && manager._visualEffects.bloomIntensity === 98 && manager._bloomStage.enabled ); }), ); check( 'sharpen actions, effect state and saved snapshot agree', await page.evaluate(() => { const manager = window.__godsEyeView.styleManager; const result = manager.setSharpen({ enabled: true, intensityPct: 61 }); return ( result.ok && result.sharpen.intensityPct === 61 && manager.getVisualState().sharpen.intensity === 61 && manager._visualEffects.sharpenIntensity === 0.61 && Math.abs(manager._sharpenStage.uniforms.amount - 1.32) < 1e-9 ); }), ); await page.setViewport({ width: 620, height: 900 }); await page.evaluate(() => window.__godsEyeView.styleManager.setPanelCollapsed('pp-toggles', false, { persist: false, syncShare: false, }), ); await page.screenshot({ path: 'qa-shots/visual-effects/narrow.png' }); const teardown = await page.evaluate(async () => { const manager = window.__godsEyeView.styleManager; const effects = manager._visualEffects; manager.setStyle('retro'); const owned = [...Object.values(effects.stages), effects.sharpenStage]; const collection = manager.viewer.scene.postProcessStages; const previousBloom = effects.previousBloom; const pending = manager.dispose(); const stoppedBeforeAwait = effects.stopped && effects.frameId === null && effects.transitions.size === 0; await pending; return { stoppedBeforeAwait, removed: owned.every((stage) => !collection.contains(stage)), restored: effects.bloomStage.enabled === previousBloom.enabled && Object.entries(previousBloom.uniforms).every( ([name, value]) => effects.bloomStage.uniforms[name] === value, ), }; }); check( 'UI teardown stops effect animation before its first await', teardown.stoppedBeforeAwait, ); check( 'UI teardown removes owned stages and restores borrowed bloom', teardown.removed && teardown.restored, ); check( 'effects do not introduce uncaught browser errors', errors.length === 0, ); if (errors.length) console.log(JSON.stringify(errors)); } finally { await browser.close(); } console.log(`RESULT: ${failures} failures`); process.exitCode = failures ? 1 : 0;