const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const root = process.env.UI_TEST_ARTIFACTS || '/tmp/debtik-ui-tests'; fs.mkdirSync(root,{recursive:true}); (async () => { const browser = await chromium.launch({headless:true,args:['--no-sandbox'],env:process.env}); const context = await browser.newContext({viewport:{width:1440,height:1000}}); const page = await context.newPage(); const errors = [], consoleErrors = [], badResponses = []; page.on('pageerror', e => errors.push(e.message)); page.on('console', m => { if(m.type()==='error') consoleErrors.push(m.text()); }); page.on('response', r => { if(r.status()>=400) badResponses.push({path:new URL(r.url()).pathname,status:r.status()}); }); const results = []; try { await page.goto(process.env.DEBTIK_TEST_URL || 'http://127.0.0.1:8080', {waitUntil:'domcontentloaded'}); await page.locator('#login-username').fill(process.env.DEBTIK_TEST_USER || 'admin'); await page.locator('#login-password').fill(process.env.DEBTIK_TEST_PASSWORD || 'admin'); await page.locator('#login-submit-btn').click(); await page.waitForFunction(() => state.currentUser !== null); await page.evaluate(() => fetchData(false)); const tabs = ['dashboard','interfaces','bridge','firewall','routes','bgp','ospf','routing-filters','ppp','eoip','aerotun','pppoe','wireguard','ipsec','l2tp','pptp','dhcp-client','dhcp-server','vrrp','tools','bwtest','settings','users','certificates','services','updates','logs']; for(const tab of tabs) { await page.evaluate(async tab => { navigate(tab); await fetchData(false); }, tab); await page.waitForTimeout(200); assert.equal(await page.locator('#section-'+tab).isVisible(),true,tab+' not visible'); results.push('view '+tab); } await page.evaluate(() => { state.bgpInstance={enabled:true,running:true,as:64512};renderBgpStatusPill(); }); const pills=await page.locator('.bgp-live-status-pill').allTextContents(); assert.equal(pills.length,2);assert.ok(pills.every(t=>t==='BGP ACTIVE (AS 64512)')); results.push('both BGP badges update'); await page.evaluate(() => { const sample = i => ({elapsed_sec:i/2,tx_mbps:1,rx_mbps:2,tx_bytes:i*100,rx_bytes:i*200,tx_packets:i,rx_packets:i,loss_pct:0,jitter_ms:0,timestamp:new Date().toISOString()}); processBwTestStatus({session_id:'audit',running:true,recent_samples:Array.from({length:200},(_,i)=>sample(i+1))}); processBwTestStatus({session_id:'audit',running:true,recent_samples:Array.from({length:200},(_,i)=>sample(i+2))}); }); assert.equal(await page.evaluate(()=>bwtestState.lastSampleElapsed),100.5); results.push('rolling sample window continues after 200 samples'); let count=0; await page.route('**/api/review/dedup', async route => {count++;await new Promise(r=>setTimeout(r,100));await route.fulfill({json:{ok:true}})}); const values=await page.evaluate(async()=>Promise.all([fetch('/api/review/dedup').then(r=>r.json()),fetch('/api/review/dedup').then(r=>r.json())])); assert.equal(count,1);assert.deepEqual(values,[{ok:true},{ok:true}]); results.push('concurrent GET deduplication and response copies'); await page.route('**/api/tools/bwtest/stop',route=>route.fulfill({status:403,json:{message:'Denied for review'}})); await page.evaluate(async()=>{document.getElementById('toast-container').innerHTML='';await stopBwTest()}); assert.match(await page.locator('#toast-container').innerText(),/Stop Failed/); assert.doesNotMatch(await page.locator('#toast-container').innerText(),/Stop Requested/); results.push('rejected Stop is reported as failure'); await page.route('**/api/review/failure',route=>route.fulfill({status:503,body:'unavailable'})); await page.evaluate(()=>fetch('/api/review/failure')); assert.match(await page.locator('#telemetry-health').innerText(),/unavailable/); await page.unroute('**/api/review/failure'); await page.route('**/api/review/failure',route=>route.fulfill({json:{ok:true}})); await page.evaluate(()=>fetch('/api/review/failure')); assert.doesNotMatch(await page.locator('#telemetry-health').getAttribute('title'),/review\/failure/); results.push('failed telemetry visible and cleared after recovery'); await page.evaluate(()=>{navigate('aerotun');openAddAeroTUNModal()}); await page.waitForFunction(()=>document.getElementById('aerotun-modal-psk').value.length>0); assert.match(await page.locator('#aerotun-setup-hint').innerText(),/Save disabled/); assert.equal(await page.locator('#aerotun-modal-disabled').isChecked(),true); const existing = await page.evaluate(() => state.aerotunTunnels || []); let draft; await page.route('**/api/vpn/aerotun/tunnels', async route => { if (route.request().method() === 'POST') { const request = route.request().postDataJSON(); assert.equal(request.disabled,true); assert.equal(request.protocol_version,3); assert.equal('private_key' in request,false); draft = {...request,id:'audit-draft',public_key:'audit-public-key',has_psk:true}; delete draft.psk; await route.fulfill({json:draft}); } else await route.fulfill({json:[...existing,...(draft?[draft]:[])]}); }); await page.evaluate(()=>submitSaveAeroTUN({preventDefault(){}})); assert.equal(await page.locator('#aerotun-modal-id').inputValue(),'audit-draft'); assert.equal(await page.locator('#aerotun-modal-public').inputValue(),'audit-public-key'); assert.equal(await page.locator('#aerotun-modal-psk').inputValue(),''); assert.equal(await page.locator('#aerotun-modal').isVisible(),true); results.push('saved AeroTUN draft stays open and presents generated identity'); await page.screenshot({path:root+'/aerotun-setup.png'}); await page.evaluate(()=>closeAeroTUNModal()); results.push('AeroTUN guided setup and disabled default'); await page.evaluate(()=>{navigate('bwtest');openBwTestServerModal()}); await page.locator('#bwtest-server-port-input').fill('34567'); await page.evaluate(()=>fetchBwTestServerStatus()); assert.equal(await page.locator('#bwtest-server-port-input').inputValue(),'34567'); await page.evaluate(()=>closeBwTestServerModal()); results.push('telemetry preserves unsaved server modal values'); await page.evaluate(()=>handleLogoutClick()); assert.equal(await page.evaluate(()=>!!window._telemetryInterval || !!trafficEventSource || !!trafficPollFallback || !!bwtestState.pollTimer || !!logsTimer || !!state.fwStatInterval || !!state.natStatInterval || !!state.mangleStatInterval),false); results.push('logout stops all session pollers'); assert.deepEqual(errors,[],'browser runtime errors'); assert.ok(consoleErrors.every(e => e.startsWith('Failed to load resource:')), 'Unexpected application console error'); assert.ok(badResponses.every(r => ['/api/auth/me','/api/tools/bwtest/stop','/api/review/failure'].includes(r.path)), 'Unexpected failed API request'); console.log(JSON.stringify({passed:results,errors,consoleErrors,badResponses},null,2)); } catch(error) { await page.screenshot({path:root+'/browser-failure.png'}).catch(()=>{}); console.log(JSON.stringify({passed:results,errors,consoleErrors,badResponses},null,2)); throw error; } finally { await browser.close(); } })().catch(error=>{console.error(error);process.exitCode=1});