ui-smoke.cjs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
  2. const assert = require('node:assert/strict');
  3. const fs = require('node:fs');
  4. const root = process.env.UI_TEST_ARTIFACTS || '/tmp/debtik-ui-tests';
  5. fs.mkdirSync(root,{recursive:true});
  6. (async () => {
  7. const browser = await chromium.launch({headless:true,args:['--no-sandbox'],env:process.env});
  8. const context = await browser.newContext({viewport:{width:1440,height:1000}});
  9. const page = await context.newPage();
  10. const errors = [], consoleErrors = [], badResponses = [];
  11. page.on('pageerror', e => errors.push(e.message));
  12. page.on('console', m => { if(m.type()==='error') consoleErrors.push(m.text()); });
  13. page.on('response', r => { if(r.status()>=400) badResponses.push({path:new URL(r.url()).pathname,status:r.status()}); });
  14. const results = [];
  15. try {
  16. await page.goto(process.env.DEBTIK_TEST_URL || 'http://127.0.0.1:8080', {waitUntil:'domcontentloaded'});
  17. await page.locator('#login-username').fill(process.env.DEBTIK_TEST_USER || 'admin');
  18. await page.locator('#login-password').fill(process.env.DEBTIK_TEST_PASSWORD || 'admin');
  19. await page.locator('#login-submit-btn').click();
  20. await page.waitForFunction(() => state.currentUser !== null);
  21. await page.evaluate(() => fetchData(false));
  22. 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','logs'];
  23. for(const tab of tabs) {
  24. await page.evaluate(async tab => { navigate(tab); await fetchData(false); }, tab);
  25. await page.waitForTimeout(200);
  26. assert.equal(await page.locator('#section-'+tab).isVisible(),true,tab+' not visible');
  27. results.push('view '+tab);
  28. }
  29. await page.evaluate(() => {
  30. state.bgpInstance={enabled:true,running:true,as:64512};renderBgpStatusPill();
  31. });
  32. const pills=await page.locator('.bgp-live-status-pill').allTextContents();
  33. assert.equal(pills.length,2);assert.ok(pills.every(t=>t==='BGP ACTIVE (AS 64512)'));
  34. results.push('both BGP badges update');
  35. await page.evaluate(() => {
  36. 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()});
  37. processBwTestStatus({session_id:'audit',running:true,recent_samples:Array.from({length:200},(_,i)=>sample(i+1))});
  38. processBwTestStatus({session_id:'audit',running:true,recent_samples:Array.from({length:200},(_,i)=>sample(i+2))});
  39. });
  40. assert.equal(await page.evaluate(()=>bwtestState.lastSampleElapsed),100.5);
  41. results.push('rolling sample window continues after 200 samples');
  42. let count=0;
  43. await page.route('**/api/review/dedup', async route => {count++;await new Promise(r=>setTimeout(r,100));await route.fulfill({json:{ok:true}})});
  44. const values=await page.evaluate(async()=>Promise.all([fetch('/api/review/dedup').then(r=>r.json()),fetch('/api/review/dedup').then(r=>r.json())]));
  45. assert.equal(count,1);assert.deepEqual(values,[{ok:true},{ok:true}]);
  46. results.push('concurrent GET deduplication and response copies');
  47. await page.route('**/api/tools/bwtest/stop',route=>route.fulfill({status:403,json:{message:'Denied for review'}}));
  48. await page.evaluate(async()=>{document.getElementById('toast-container').innerHTML='';await stopBwTest()});
  49. assert.match(await page.locator('#toast-container').innerText(),/Stop Failed/);
  50. assert.doesNotMatch(await page.locator('#toast-container').innerText(),/Stop Requested/);
  51. results.push('rejected Stop is reported as failure');
  52. await page.route('**/api/review/failure',route=>route.fulfill({status:503,body:'unavailable'}));
  53. await page.evaluate(()=>fetch('/api/review/failure'));
  54. assert.match(await page.locator('#telemetry-health').innerText(),/unavailable/);
  55. await page.unroute('**/api/review/failure');
  56. await page.route('**/api/review/failure',route=>route.fulfill({json:{ok:true}}));
  57. await page.evaluate(()=>fetch('/api/review/failure'));
  58. assert.doesNotMatch(await page.locator('#telemetry-health').getAttribute('title'),/review\/failure/);
  59. results.push('failed telemetry visible and cleared after recovery');
  60. await page.evaluate(()=>{navigate('aerotun');openAddAeroTUNModal()});
  61. await page.waitForFunction(()=>document.getElementById('aerotun-modal-psk').value.length>0);
  62. assert.match(await page.locator('#aerotun-setup-hint').innerText(),/Save disabled/);
  63. assert.equal(await page.locator('#aerotun-modal-disabled').isChecked(),true);
  64. const existing = await page.evaluate(() => state.aerotunTunnels || []);
  65. let draft;
  66. await page.route('**/api/vpn/aerotun/tunnels', async route => {
  67. if (route.request().method() === 'POST') {
  68. const request = route.request().postDataJSON();
  69. assert.equal(request.disabled,true);
  70. assert.equal(request.protocol_version,3);
  71. assert.equal('private_key' in request,false);
  72. draft = {...request,id:'audit-draft',public_key:'audit-public-key',has_psk:true};
  73. delete draft.psk;
  74. await route.fulfill({json:draft});
  75. } else await route.fulfill({json:[...existing,...(draft?[draft]:[])]});
  76. });
  77. await page.evaluate(()=>submitSaveAeroTUN({preventDefault(){}}));
  78. assert.equal(await page.locator('#aerotun-modal-id').inputValue(),'audit-draft');
  79. assert.equal(await page.locator('#aerotun-modal-public').inputValue(),'audit-public-key');
  80. assert.equal(await page.locator('#aerotun-modal-psk').inputValue(),'');
  81. assert.equal(await page.locator('#aerotun-modal').isVisible(),true);
  82. results.push('saved AeroTUN draft stays open and presents generated identity');
  83. await page.screenshot({path:root+'/aerotun-setup.png'});
  84. await page.evaluate(()=>closeAeroTUNModal());
  85. results.push('AeroTUN guided setup and disabled default');
  86. await page.evaluate(()=>{navigate('bwtest');openBwTestServerModal()});
  87. await page.locator('#bwtest-server-port-input').fill('34567');
  88. await page.evaluate(()=>fetchBwTestServerStatus());
  89. assert.equal(await page.locator('#bwtest-server-port-input').inputValue(),'34567');
  90. await page.evaluate(()=>closeBwTestServerModal());
  91. results.push('telemetry preserves unsaved server modal values');
  92. await page.evaluate(()=>handleLogoutClick());
  93. assert.equal(await page.evaluate(()=>!!window._telemetryInterval || !!trafficEventSource || !!trafficPollFallback || !!bwtestState.pollTimer || !!logsTimer || !!state.fwStatInterval || !!state.natStatInterval || !!state.mangleStatInterval),false);
  94. results.push('logout stops all session pollers');
  95. assert.deepEqual(errors,[],'browser runtime errors');
  96. assert.ok(consoleErrors.every(e => e.startsWith('Failed to load resource:')), 'Unexpected application console error');
  97. assert.ok(badResponses.every(r => ['/api/auth/me','/api/tools/bwtest/stop','/api/review/failure'].includes(r.path)), 'Unexpected failed API request');
  98. console.log(JSON.stringify({passed:results,errors,consoleErrors,badResponses},null,2));
  99. } catch(error) {
  100. await page.screenshot({path:root+'/browser-failure.png'}).catch(()=>{});
  101. console.log(JSON.stringify({passed:results,errors,consoleErrors,badResponses},null,2));
  102. throw error;
  103. } finally { await browser.close(); }
  104. })().catch(error=>{console.error(error);process.exitCode=1});