// Real Chromium smoke test; Node built-ins only. No paid model calls. import {spawn} from 'node:child_process'; import {mkdtemp, mkdir, writeFile, rm} from 'node:fs/promises'; import {tmpdir} from 'node:os'; import path from 'node:path'; const port=18765, debugPort=18766; const pause=ms=>new Promise(r=>setTimeout(r,ms)); const output=path.resolve('service-data/ui-preview');await mkdir(output,{recursive:true}); const profile=await mkdtemp(path.join(tmpdir(),'vba-browser-')); const fixtureRoot=await mkdtemp(path.join(tmpdir(),'vba-ui-data-')); let browser,server,ws; const errors=[]; async function until(fn, milliseconds=15000){const start=Date.now();while(Date.now()-start (await fetch(`http://127.0.0.1:${port}/healthz`)).ok); browser=spawn(process.env.BROWSER_PATH||'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe',[ '--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check', `--user-data-dir=${profile}`,`--remote-debugging-port=${debugPort}`,'about:blank'],{windowsHide:true,stdio:'ignore'}); const tabs=await until(async()=>await (await fetch(`http://127.0.0.1:${debugPort}/json`)).json()); ws=new WebSocket(tabs.find(t=>t.type==='page').webSocketDebuggerUrl); await new Promise((resolve,reject)=>{ws.onopen=resolve;ws.onerror=reject;}); let next=0;const pending=new Map(); ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.id){const p=pending.get(m.id);pending.delete(m.id);m.error?p.reject(new Error(m.error.message)):p.resolve(m.result);} if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.text); if(m.method==='Log.entryAdded'&&m.params.entry.level==='error')errors.push(m.params.entry.text);}; const send=(method,params={})=>new Promise((resolve,reject)=>{const id=++next;pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params}));}); const evaluate=async expression=>{const r=await send('Runtime.evaluate',{expression,returnByValue:true,awaitPromise:true});if(r.exceptionDetails)throw new Error(r.exceptionDetails.text);return r.result.value;}; await send('Runtime.enable');await send('Log.enable');await send('Page.enable'); await send('Emulation.setDeviceMetricsOverride',{width:1440,height:1200,deviceScaleFactor:1,mobile:false}); await send('Page.navigate',{url:`http://127.0.0.1:${port}/`}); await until(()=>evaluate("document.readyState === 'complete' && typeof render === 'function'")); await evaluate("document.getElementById('token').value='ui_fixture_"+'x'.repeat(40)+"';document.getElementById('login-form').dispatchEvent(new Event('submit',{bubbles:true,cancelable:true}));"); await until(()=>evaluate("document.getElementById('token-count').textContent==='35.300'")); const state=await evaluate("({workers:document.querySelectorAll('.worker').length,cases:document.querySelectorAll('#case-rows tr').length,active:document.getElementById('active-count').textContent,correction:document.getElementById('correction-count').textContent,overflow:document.documentElement.scrollWidth>innerWidth,stored:localStorage.length,progress:document.querySelector('.worker [role=progressbar]').getAttribute('aria-valuenow')})"); if(state.workers!==1||state.cases!==1||state.active!=='1'||state.stored!==0||state.overflow||state.progress!=='57')throw new Error(JSON.stringify(state)); const categories=await evaluate("Array.from(document.querySelectorAll('#legend li')).map(e=>e.textContent)"); for(const name of ['Fallaufbau','Fallplanung','Hypothesen','Untersuchung','Diagnose','Codingauftrag','Korrekturen']) { if(!categories.some(c=>c.includes(name)))throw new Error(`Missing phase in pie legend: ${name}`); } await writeFile(path.join(output,'desktop.png'),Buffer.from((await send('Page.captureScreenshot',{format:'png'})).data,'base64')); await send('Emulation.setDeviceMetricsOverride',{width:390,height:1200,deviceScaleFactor:1,mobile:true});await pause(200); if(await evaluate('document.documentElement.scrollWidth>innerWidth'))throw new Error('Mobile horizontal overflow'); await writeFile(path.join(output,'mobile.png'),Buffer.from((await send('Page.captureScreenshot',{format:'png'})).data,'base64')); await evaluate("document.getElementById('project-filter').value='viano-core';document.getElementById('project-filter').dispatchEvent(new Event('change')); "); if(await evaluate("document.getElementById('token-count').textContent")!=='35.300')throw new Error('Project aggregation differs'); await evaluate("location.hash='#results'"); await until(()=>evaluate("!document.getElementById('results').hidden&&document.querySelector('#result-rows a')")); await evaluate("document.querySelector('#result-rows a').click()"); await until(()=>evaluate("document.getElementById('case-detail').textContent.includes('SQL-Vorlage mit Platzhaltern')")); if(!await evaluate("document.getElementById('case-detail').textContent.includes('Passende Spawndaten ermitteln')"))throw new Error('Human assignment absent'); if(await evaluate('document.documentElement.scrollWidth>innerWidth'))throw new Error('Mobile detail overflow'); await writeFile(path.join(output,'case-mobile.png'),Buffer.from((await send('Page.captureScreenshot',{format:'png'})).data,'base64')); await send('Emulation.setDeviceMetricsOverride',{width:1440,height:1200,deviceScaleFactor:1,mobile:false});await pause(200); await writeFile(path.join(output,'case-desktop.png'),Buffer.from((await send('Page.captureScreenshot',{format:'png'})).data,'base64')); await evaluate("location.hash='#overview'");await pause(100); await evaluate("document.getElementById('kind').value='case_return';document.getElementById('kind').dispatchEvent(new Event('change'));"); if(!await evaluate("document.getElementById('new-files').hidden&&!document.getElementById('return-files').hidden"))throw new Error('Return form not selected'); await evaluate("document.getElementById('logout').click()"); if(!await evaluate("document.getElementById('content').hidden&&!document.getElementById('login').hidden&&document.getElementById('token-count').textContent==='—'"))throw new Error('Logout retained visible case data'); if(!await evaluate("document.getElementById('case-detail').childElementCount===0&&document.getElementById('result-rows').childElementCount===0"))throw new Error('Logout retained detail data'); if(errors.filter(e=>!e.includes('404')).length)throw new Error(errors.join('\n')); console.log(JSON.stringify({status:'PASS',...state,console_errors:errors.filter(e=>!e.includes('404'))})); } finally { ws?.close();browser?.kill();server?.kill();await pause(400); for(const [folder,prefix] of [[profile,'vba-browser-'],[fixtureRoot,'vba-ui-data-']]) { const target=path.resolve(folder),relative=path.relative(path.resolve(tmpdir()),target); if(!relative||relative.startsWith('..')||path.isAbsolute(relative)||!path.basename(target).startsWith(prefix))throw new Error('Unsafe temporary cleanup'); await rm(target,{recursive:true,force:true,maxRetries:5,retryDelay:200}); } }