// Local-only end-to-end smoke test. Requires a built gateway and Chrome/Edge. // Run: go build -o .cache/gateway-smoke.exe ./cmd/gateway // node scripts/browser-smoke.cjs const fs=require('fs'),path=require('path'),net=require('net'),{spawn}=require('child_process'); const wait=ms=>new Promise(r=>setTimeout(r,ms)); async function port(){const s=net.createServer();await new Promise(r=>s.listen(0,'127.0.0.1',r));const p=s.address().port;await new Promise(r=>s.close(r));return p} async function until(fn,label){for(let i=0;i<100;i++){try{const value=await fn();if(value)return value}catch{}await wait(100)}throw Error('Timeout: '+label)} let gateway,browser,ws,browserLog='';const errors=[];const pending=new Map();let sequence=0; function call(method,params={}){return new Promise((resolve,reject)=>{const id=++sequence;pending.set(id,{resolve,reject});ws.send(JSON.stringify({id,method,params}))})} async function evaluate(expression){const r=await call('Runtime.evaluate',{expression,awaitPromise:true,returnByValue:true});if(r.exceptionDetails)throw Error(r.exceptionDetails.text+': '+JSON.stringify(r.exceptionDetails.exception));return r.result?.value} (async()=>{ const httpPort=await port(),debugPort=await port();const dir=fs.mkdtempSync(path.resolve('.cache/ui-smoke-')); const cfg=JSON.parse(fs.readFileSync('config.example.json','utf8'));cfg.server.listen='127.0.0.1:'+httpPort;cfg.server.session_secret='';cfg.server.admin_password_hash=''; cfg.outbounds=cfg.outbounds.filter(o=>['discord','webhook'].includes(o.provider));cfg.ingress.mail=[];cfg.ingress.discord={enabled:false}; fs.writeFileSync(path.join(dir,'config.json'),JSON.stringify(cfg)); gateway=spawn(path.resolve('.cache/gateway-smoke.exe'),['-config',path.join(dir,'config.json')],{windowsHide:true,env:{...process.env,GATEWAY_ADMIN_PASSWORD:'local-smoke-password'},stdio:'ignore'}); const base='http://127.0.0.1:'+httpPort; await until(async()=>{const r=await fetch(base+'/readyz');return r.ok},'gateway readiness'); const executable=process.env.BROWSER_PATH||['C:/Program Files/Google/Chrome/Application/chrome.exe','C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe'].find(p=>fs.existsSync(p));if(!executable)throw Error('Set BROWSER_PATH'); browser=spawn(executable,['--headless=new','--disable-gpu','--no-first-run','--no-default-browser-check','--remote-debugging-port='+debugPort,'--user-data-dir='+path.join(dir,'browser'),'about:blank'],{windowsHide:true,stdio:['ignore','ignore','pipe']}); browser.stderr.on('data',b=>{browserLog=(browserLog+b).slice(-4000)}); const pages=await until(async()=>{const r=await fetch('http://127.0.0.1:'+debugPort+'/json/list');const j=await r.json();return j.find(p=>p.type==='page')},'browser'); ws=new WebSocket(pages.webSocketDebuggerUrl);await new Promise((resolve,reject)=>{ws.onopen=resolve;ws.onerror=reject}); ws.onmessage=e=>{const m=JSON.parse(e.data);if(m.id){const p=pending.get(m.id);if(p){pending.delete(m.id);m.error?p.reject(Error(m.error.message)):p.resolve(m.result)}}else if(m.method==='Runtime.exceptionThrown'){errors.push(m.params.exceptionDetails.text)}else if(m.method==='Log.entryAdded'&&m.params.entry.level==='error'){errors.push(m.params.entry.text)}}; await call('Runtime.enable');await call('Log.enable');await call('Page.enable'); await call('Page.navigate',{url:base+'/login'});await until(()=>evaluate("!!document.querySelector('[name=username]')"),'login form'); await evaluate("document.querySelector('[name=username]').value='admin';document.querySelector('[name=password]').value='local-smoke-password';document.querySelector('form').requestSubmit()"); await until(()=>evaluate("typeof cfg!=='undefined'&&cfg!==null"),'loaded admin config'); await evaluate(`showPage('outbounds');document.querySelector('#addOutbound').click();var card=document.querySelector('[data-output="2"]');var select=card.querySelector('[data-field=provider]');select.value='smtp';select.dispatchEvent(new Event('input'));`); await evaluate(`var card=document.querySelector('[data-output="2"]');for(var [key,value] of Object.entries({smtp_host:'smtp.example.invalid',from:'sender@example.org',to:'receiver@example.org'})){var el=card.querySelector('[data-field='+key+']');el.value=value;el.dispatchEvent(new Event('input'))}showPage('ingress');document.querySelector('#addMail').click();`); await evaluate(`showPage('mappings');document.querySelector('#addMapping').click();document.querySelector('#mName').value='Browser smoke';document.querySelector('#mId').value='browser-smoke';document.querySelector('#mSource').value='webhook';document.querySelector('#mTarget').value='smtp';document.querySelector('#mTarget').dispatchEvent(new Event('change'));document.querySelector('#mOutbound').value=cfg.outbounds[2].id;document.querySelector('#pSource').value='webhook';document.querySelector('#runPreview').click();`); await until(()=>evaluate("document.querySelector('#previewOut').textContent.includes('Rauchentwicklung')"),'SMTP preview'); await evaluate("document.querySelector('#applyMapping').click();document.querySelector('#saveAll').click()"); await until(()=>evaluate("dirty===false&&cfg.mappings.some(m=>m.id==='browser-smoke')"),'save configuration with CSRF'); const accepted=await fetch(base+'/in/webhook/default',{method:'POST',headers:{Authorization:'Bearer replace-me','Content-Type':'application/json','Idempotency-Key':'browser-smoke'},body:JSON.stringify({title:'Test',message:'Smoke test'})});if(accepted.status!==202)throw Error('Ingress: '+accepted.status+' '+await accepted.text()); await until(async()=>{await evaluate("showPage('history');loadHistory()");return evaluate("document.querySelector('#historyRows').textContent.includes('Dry-Run')")},'history dry-run result'); await evaluate("document.querySelector('[data-attempts]').click()");await until(()=>evaluate("document.querySelector('#attemptHistory').textContent.includes('dry_run')"),'attempt details'); const screenshot=await call('Page.captureScreenshot',{format:'png'});fs.writeFileSync(path.join(dir,'history.png'),Buffer.from(screenshot.data,'base64')); if(errors.length)throw Error(errors.join('\n')); console.log('Browser smoke passed: login, SMTP/mail forms, mapping, preview, CSRF save, queued ingress, history.');console.log('Screenshot: '+path.join(dir,'history.png')); await call('Browser.close'); })().catch(async e=>{console.error(e.stack);if(errors.length)console.error(errors.join('\n'));if(ws?.readyState===WebSocket.OPEN){try{console.error(await evaluate('JSON.stringify({url:location.href,text:document.body.innerText.slice(0,500)})'))}catch{}}if(browserLog)console.error(browserLog);process.exitCode=1}).finally(()=>{if(ws)ws.close();if(browser)browser.kill();if(gateway)gateway.kill()});