diff options
Diffstat (limited to 'tests/e2e')
| -rw-r--r-- | tests/e2e/wifi-setup.spec.mjs | 440 |
1 files changed, 440 insertions, 0 deletions
diff --git a/tests/e2e/wifi-setup.spec.mjs b/tests/e2e/wifi-setup.spec.mjs new file mode 100644 index 0000000..31bc2cf --- /dev/null +++ b/tests/e2e/wifi-setup.spec.mjs | |||
| @@ -0,0 +1,440 @@ | |||
| 1 | import { test, expect } from '@playwright/test'; | ||
| 2 | |||
| 3 | const PORTAL_IP = process.env.TOLLGATE_IP || '10.192.45.1'; | ||
| 4 | const PORTAL_URL = `http://${PORTAL_IP}`; | ||
| 5 | |||
| 6 | const SETUP_HTML = `<!DOCTYPE html> | ||
| 7 | <html><head> | ||
| 8 | <meta charset='utf-8'> | ||
| 9 | <meta name='viewport' content='width=device-width, initial-scale=1'> | ||
| 10 | <title>TollGate Setup</title> | ||
| 11 | <style> | ||
| 12 | *{box-sizing:border-box;margin:0;padding:0} | ||
| 13 | body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; | ||
| 14 | background:#0a0a0a;color:#fff;display:flex;align-items:center;justify-content:center; | ||
| 15 | min-height:100vh;padding:20px} | ||
| 16 | .card{background:#1a1a1a;border:1px solid #333;border-radius:16px;padding:32px; | ||
| 17 | max-width:400px;width:100%;text-align:center} | ||
| 18 | h1{font-size:24px;margin-bottom:8px;color:#f7931a} | ||
| 19 | .subtitle{color:#888;margin-bottom:20px;font-size:13px} | ||
| 20 | .networks{margin-top:16px;text-align:left} | ||
| 21 | .net-item{background:#252525;border:1px solid #333;border-radius:8px; | ||
| 22 | padding:12px;margin-bottom:8px;cursor:pointer;display:flex;justify-content:space-between;align-items:center} | ||
| 23 | .net-item:hover{border-color:#f7931a} | ||
| 24 | .net-item:active{background:#333} | ||
| 25 | .net-ssid{font-size:14px} | ||
| 26 | .net-rssi{font-size:11px;color:#888} | ||
| 27 | .net-lock{color:#f7931a;margin-right:4px} | ||
| 28 | .manual{margin-top:12px} | ||
| 29 | input{width:100%;background:#252525;border:1px solid #333;border-radius:8px; | ||
| 30 | color:#fff;padding:12px;font-size:14px;margin-bottom:8px;outline:none} | ||
| 31 | input:focus{border-color:#f7931a} | ||
| 32 | .btn{background:#f7931a;color:#000;border:none;border-radius:8px;padding:14px 28px; | ||
| 33 | font-size:16px;font-weight:bold;cursor:pointer;width:100%;margin-top:8px} | ||
| 34 | .btn:hover{background:#e8850f} | ||
| 35 | .btn:disabled{background:#333;color:#666;cursor:not-allowed} | ||
| 36 | #status{margin-top:12px;padding:10px;border-radius:8px;display:none;font-size:13px} | ||
| 37 | #status.success{display:block;background:#1a472a;color:#4caf50} | ||
| 38 | #status.error{display:block;background:#471a1a;color:#f44336} | ||
| 39 | #status.processing{display:block;background:#1a3a47;color:#2196f3} | ||
| 40 | .refresh{background:none;border:1px solid #444;color:#aaa;border-radius:6px; | ||
| 41 | padding:6px 12px;font-size:12px;cursor:pointer;margin-top:4px} | ||
| 42 | .refresh:hover{border-color:#f7931a;color:#f7931a} | ||
| 43 | #manualForm{display:none;margin-top:12px} | ||
| 44 | </style> | ||
| 45 | </head><body> | ||
| 46 | <div class='card'> | ||
| 47 | <h1>TollGate Setup</h1> | ||
| 48 | <p class='subtitle'>Configure upstream WiFi</p> | ||
| 49 | <div id='scanStatus'>Scanning...</div> | ||
| 50 | <div class='networks' id='networkList'></div> | ||
| 51 | <button class='refresh' onclick='scanWifi()'>Rescan</button> | ||
| 52 | <button class='refresh' onclick='showManual()'>Manual entry</button> | ||
| 53 | <div id='manualForm'> | ||
| 54 | <input id='manualSsid' placeholder='SSID'> | ||
| 55 | <input id='manualPass' type='password' placeholder='Password'> | ||
| 56 | <button class='btn' onclick='connectManual()'>Connect</button> | ||
| 57 | </div> | ||
| 58 | <div id='passwordForm' style='display:none'> | ||
| 59 | <p style='margin:12px 0 8px;text-align:left' id='selectedNetwork'></p> | ||
| 60 | <input id='wifiPass' type='password' placeholder='WiFi password'> | ||
| 61 | <button class='btn' onclick='connectSelected()'>Connect</button> | ||
| 62 | </div> | ||
| 63 | <div id='status'></div> | ||
| 64 | </div> | ||
| 65 | <script> | ||
| 66 | const apIp='${PORTAL_IP}'; | ||
| 67 | let selectedSsid=''; | ||
| 68 | function showStatus(msg,type){const s=document.getElementById('status'); | ||
| 69 | s.textContent=msg;s.className=type;} | ||
| 70 | function scanWifi(){ | ||
| 71 | document.getElementById('scanStatus').textContent='Scanning...'; | ||
| 72 | document.getElementById('networkList').innerHTML=''; | ||
| 73 | fetch('/wifi/scan').then(r=>r.json()).then(aps=>{ | ||
| 74 | document.getElementById('scanStatus').textContent=aps.length+' networks found'; | ||
| 75 | const list=document.getElementById('networkList'); | ||
| 76 | aps.forEach(ap=>{ | ||
| 77 | const div=document.createElement('div'); | ||
| 78 | div.className='net-item'; | ||
| 79 | const lock=ap.secured?'<span class=net-lock>🔒</span>':''; | ||
| 80 | div.innerHTML='<span class=net-ssid>'+lock+ap.ssid+'</span><span class=net-rssi>'+ap.rssi+' dBm</span>'; | ||
| 81 | div.onclick=()=>selectNetwork(ap.ssid,ap.secured); | ||
| 82 | list.appendChild(div); | ||
| 83 | }); | ||
| 84 | }).catch(e=>{document.getElementById('scanStatus').textContent='Scan failed';}); | ||
| 85 | } | ||
| 86 | function selectNetwork(ssid,secured){ | ||
| 87 | selectedSsid=ssid; | ||
| 88 | document.getElementById('selectedNetwork').textContent='Connect to: '+ssid; | ||
| 89 | document.getElementById('passwordForm').style.display='block'; | ||
| 90 | document.getElementById('scanStatus').style.display='none'; | ||
| 91 | document.getElementById('networkList').style.display='none'; | ||
| 92 | document.querySelector('.refresh').style.display='none'; | ||
| 93 | if(!secured){connectSelected();} | ||
| 94 | } | ||
| 95 | function showManual(){ | ||
| 96 | document.getElementById('manualForm').style.display='block'; | ||
| 97 | } | ||
| 98 | function connectSelected(){ | ||
| 99 | const pass=document.getElementById('wifiPass').value; | ||
| 100 | doConnect(selectedSsid,pass); | ||
| 101 | } | ||
| 102 | function connectManual(){ | ||
| 103 | const ssid=document.getElementById('manualSsid').value.trim(); | ||
| 104 | const pass=document.getElementById('manualPass').value; | ||
| 105 | if(!ssid){showStatus('Enter SSID','error');return;} | ||
| 106 | doConnect(ssid,pass); | ||
| 107 | } | ||
| 108 | function doConnect(ssid,pass){ | ||
| 109 | showStatus('Connecting to '+ssid+'...','processing'); | ||
| 110 | fetch('/wifi/connect',{method:'POST',headers:{'Content-Type':'application/json'}, | ||
| 111 | body:JSON.stringify({ssid:ssid,password:pass})}) | ||
| 112 | .then(r=>r.json()).then(d=>{ | ||
| 113 | if(d.ok){showStatus('Connected! Device is restarting...','success');} | ||
| 114 | else{showStatus('Failed: '+(d.error||'unknown'),'error');} | ||
| 115 | }).catch(e=>{showStatus('Connection error','error');}); | ||
| 116 | } | ||
| 117 | scanWifi(); | ||
| 118 | </script> | ||
| 119 | </body></html>`; | ||
| 120 | |||
| 121 | const MOCK_AP_LIST = [ | ||
| 122 | { ssid: 'HomeNetwork', rssi: -42, secured: true }, | ||
| 123 | { ssid: 'CafeWiFi', rssi: -67, secured: true }, | ||
| 124 | { ssid: 'OpenPublic', rssi: -75, secured: false }, | ||
| 125 | { ssid: 'Neighbor5G', rssi: -81, secured: true }, | ||
| 126 | ]; | ||
| 127 | |||
| 128 | async function setupMockRoutes(page, overrides = {}) { | ||
| 129 | const scanResponse = overrides.scanResponse || MOCK_AP_LIST; | ||
| 130 | const connectHandler = overrides.connectHandler || (() => ({ ok: true })); | ||
| 131 | |||
| 132 | await page.route('**/setup', async route => { | ||
| 133 | await route.fulfill({ | ||
| 134 | status: 200, | ||
| 135 | contentType: 'text/html', | ||
| 136 | body: SETUP_HTML, | ||
| 137 | }); | ||
| 138 | }); | ||
| 139 | |||
| 140 | await page.route('**/wifi/scan', async route => { | ||
| 141 | await route.fulfill({ | ||
| 142 | status: 200, | ||
| 143 | contentType: 'application/json', | ||
| 144 | body: JSON.stringify(scanResponse), | ||
| 145 | }); | ||
| 146 | }); | ||
| 147 | |||
| 148 | await page.route('**/wifi/connect', async route => { | ||
| 149 | const request = route.request(); | ||
| 150 | const body = request.postDataJSON(); | ||
| 151 | const response = connectHandler(body); | ||
| 152 | await route.fulfill({ | ||
| 153 | status: 200, | ||
| 154 | contentType: 'application/json', | ||
| 155 | body: JSON.stringify(response), | ||
| 156 | }); | ||
| 157 | }); | ||
| 158 | } | ||
| 159 | |||
| 160 | async function loadSetupPage(page) { | ||
| 161 | await page.goto('http://tollgate.test/setup', { waitUntil: 'networkidle' }); | ||
| 162 | } | ||
| 163 | |||
| 164 | test.describe('WiFi Setup \u2014 Layer 1: API Endpoints (needs live board)', () => { | ||
| 165 | |||
| 166 | test('GET /setup redirects to portal on configured board', async ({ request }) => { | ||
| 167 | const resp = await request.fetch(`${PORTAL_URL}/setup`, { | ||
| 168 | maxRedirects: 0, | ||
| 169 | }); | ||
| 170 | expect(resp.status()).toBe(302); | ||
| 171 | const location = resp.headers()['location']; | ||
| 172 | expect(location).toContain(PORTAL_IP); | ||
| 173 | expect(location).toMatch(/\/$/); | ||
| 174 | }); | ||
| 175 | |||
| 176 | test('GET /wifi/scan returns JSON array with valid AP objects', async ({ request }) => { | ||
| 177 | const resp = await request.get(`${PORTAL_URL}/wifi/scan`); | ||
| 178 | expect(resp.status()).toBe(200); | ||
| 179 | const data = await resp.json(); | ||
| 180 | expect(Array.isArray(data)).toBe(true); | ||
| 181 | if (data.length > 0) { | ||
| 182 | const ap = data[0]; | ||
| 183 | expect(ap).toHaveProperty('ssid'); | ||
| 184 | expect(typeof ap.ssid).toBe('string'); | ||
| 185 | expect(ap).toHaveProperty('rssi'); | ||
| 186 | expect(typeof ap.rssi).toBe('number'); | ||
| 187 | expect(ap).toHaveProperty('secured'); | ||
| 188 | expect(typeof ap.secured).toBe('boolean'); | ||
| 189 | } | ||
| 190 | }); | ||
| 191 | |||
| 192 | test('GET /wifi/status returns connection state', async ({ request }) => { | ||
| 193 | const resp = await request.get(`${PORTAL_URL}/wifi/status`); | ||
| 194 | expect(resp.status()).toBe(200); | ||
| 195 | const data = await resp.json(); | ||
| 196 | expect(data).toHaveProperty('connected'); | ||
| 197 | expect(typeof data.connected).toBe('boolean'); | ||
| 198 | if (data.connected) { | ||
| 199 | expect(data).toHaveProperty('ip'); | ||
| 200 | expect(data.ip).toMatch(/\d+\.\d+\.\d+\.\d+/); | ||
| 201 | expect(data).toHaveProperty('ssid'); | ||
| 202 | } | ||
| 203 | }); | ||
| 204 | |||
| 205 | test('POST /wifi/connect rejects empty body', async ({ request }) => { | ||
| 206 | const resp = await request.post(`${PORTAL_URL}/wifi/connect`, { | ||
| 207 | data: '', | ||
| 208 | headers: { 'Content-Type': 'application/json' }, | ||
| 209 | }); | ||
| 210 | const data = await resp.json(); | ||
| 211 | expect(data.ok).toBe(false); | ||
| 212 | }); | ||
| 213 | |||
| 214 | test('POST /wifi/connect rejects invalid JSON', async ({ request }) => { | ||
| 215 | const resp = await request.post(`${PORTAL_URL}/wifi/connect`, { | ||
| 216 | data: 'not json at all', | ||
| 217 | headers: { 'Content-Type': 'application/json' }, | ||
| 218 | }); | ||
| 219 | const data = await resp.json(); | ||
| 220 | expect(data.ok).toBe(false); | ||
| 221 | expect(data.error).toBeDefined(); | ||
| 222 | }); | ||
| 223 | |||
| 224 | test('POST /wifi/connect rejects missing ssid', async ({ request }) => { | ||
| 225 | const resp = await request.post(`${PORTAL_URL}/wifi/connect`, { | ||
| 226 | data: JSON.stringify({ password: 'testpass' }), | ||
| 227 | headers: { 'Content-Type': 'application/json' }, | ||
| 228 | }); | ||
| 229 | const data = await resp.json(); | ||
| 230 | expect(data.ok).toBe(false); | ||
| 231 | expect(data.error).toContain('ssid'); | ||
| 232 | }); | ||
| 233 | |||
| 234 | test('POST /wifi/connect with valid SSID returns ok or ECONNRESET', async ({ request }) => { | ||
| 235 | const resp = await request.post(`${PORTAL_URL}/wifi/connect`, { | ||
| 236 | data: JSON.stringify({ ssid: 'TestSetupAP', password: 'testpass123' }), | ||
| 237 | headers: { 'Content-Type': 'application/json' }, | ||
| 238 | maxRedirects: 0, | ||
| 239 | timeout: 10000, | ||
| 240 | }).catch(() => null); | ||
| 241 | |||
| 242 | if (resp) { | ||
| 243 | const text = await resp.text(); | ||
| 244 | try { | ||
| 245 | const data = JSON.parse(text); | ||
| 246 | expect(data.ok).toBe(true); | ||
| 247 | } catch { | ||
| 248 | expect(resp.status()).toBeLessThan(500); | ||
| 249 | } | ||
| 250 | } | ||
| 251 | }); | ||
| 252 | }); | ||
| 253 | |||
| 254 | test.describe('WiFi Setup \u2014 Layer 1.5: Redirect (needs live board)', () => { | ||
| 255 | test('redirect Location header contains correct AP IP', async ({ request }) => { | ||
| 256 | const resp = await request.fetch(`${PORTAL_URL}/setup`, { | ||
| 257 | maxRedirects: 0, | ||
| 258 | }); | ||
| 259 | const location = resp.headers()['location']; | ||
| 260 | expect(location).toBe(`http://${PORTAL_IP}/`); | ||
| 261 | }); | ||
| 262 | }); | ||
| 263 | |||
| 264 | test.describe('WiFi Setup \u2014 Layer 2: HTML UI Interaction', () => { | ||
| 265 | |||
| 266 | test('page renders with title and subtitle', async ({ page }) => { | ||
| 267 | await setupMockRoutes(page); | ||
| 268 | await loadSetupPage(page); | ||
| 269 | await expect(page.locator('h1')).toHaveText('TollGate Setup'); | ||
| 270 | await expect(page.locator('.subtitle')).toHaveText('Configure upstream WiFi'); | ||
| 271 | }); | ||
| 272 | |||
| 273 | test('scan auto-triggers on load and shows network count', async ({ page }) => { | ||
| 274 | await setupMockRoutes(page); | ||
| 275 | await loadSetupPage(page); | ||
| 276 | await expect(page.locator('#scanStatus')).toHaveText(/4 networks found/, { timeout: 5000 }); | ||
| 277 | }); | ||
| 278 | |||
| 279 | test('network list shows SSID and RSSI for each AP', async ({ page }) => { | ||
| 280 | await setupMockRoutes(page); | ||
| 281 | await loadSetupPage(page); | ||
| 282 | await expect(page.locator('.net-item')).toHaveCount(4); | ||
| 283 | await expect(page.locator('.net-ssid').first()).toContainText('HomeNetwork'); | ||
| 284 | await expect(page.locator('.net-rssi').first()).toContainText('-42 dBm'); | ||
| 285 | }); | ||
| 286 | |||
| 287 | test('secured networks show lock icon', async ({ page }) => { | ||
| 288 | await setupMockRoutes(page); | ||
| 289 | await loadSetupPage(page); | ||
| 290 | const securedItems = page.locator('.net-item'); | ||
| 291 | const firstSecured = securedItems.first(); | ||
| 292 | await expect(firstSecured.locator('.net-lock')).toBeVisible(); | ||
| 293 | }); | ||
| 294 | |||
| 295 | test('open networks have no lock icon', async ({ page }) => { | ||
| 296 | await setupMockRoutes(page); | ||
| 297 | await loadSetupPage(page); | ||
| 298 | const openItem = page.locator('.net-item').nth(2); | ||
| 299 | await expect(openItem.locator('.net-lock')).toHaveCount(0); | ||
| 300 | await expect(openItem.locator('.net-ssid')).toContainText('OpenPublic'); | ||
| 301 | }); | ||
| 302 | |||
| 303 | test('clicking secured network shows password form and hides list', async ({ page }) => { | ||
| 304 | await setupMockRoutes(page); | ||
| 305 | await loadSetupPage(page); | ||
| 306 | await expect(page.locator('.net-item').first()).toBeVisible(); | ||
| 307 | await page.locator('.net-item').first().click(); | ||
| 308 | await expect(page.locator('#passwordForm')).toBeVisible(); | ||
| 309 | await expect(page.locator('#selectedNetwork')).toHaveText('Connect to: HomeNetwork'); | ||
| 310 | await expect(page.locator('#networkList')).toBeHidden(); | ||
| 311 | await expect(page.locator('#scanStatus')).toBeHidden(); | ||
| 312 | }); | ||
| 313 | |||
| 314 | test('clicking open network auto-connects without password form', async ({ page }) => { | ||
| 315 | let connectBody = null; | ||
| 316 | await setupMockRoutes(page, { | ||
| 317 | connectHandler: (body) => { | ||
| 318 | connectBody = body; | ||
| 319 | return { ok: true }; | ||
| 320 | }, | ||
| 321 | }); | ||
| 322 | await loadSetupPage(page); | ||
| 323 | const openItem = page.locator('.net-item').nth(2); | ||
| 324 | await openItem.click(); | ||
| 325 | await expect(page.locator('#status')).toHaveClass(/processing|success/, { timeout: 5000 }); | ||
| 326 | expect(connectBody).toBeTruthy(); | ||
| 327 | expect(connectBody.ssid).toBe('OpenPublic'); | ||
| 328 | }); | ||
| 329 | |||
| 330 | test('manual entry button toggles form visibility', async ({ page }) => { | ||
| 331 | await setupMockRoutes(page); | ||
| 332 | await loadSetupPage(page); | ||
| 333 | await expect(page.locator('#manualForm')).toBeHidden(); | ||
| 334 | await page.locator('button:has-text("Manual entry")').click(); | ||
| 335 | await expect(page.locator('#manualForm')).toBeVisible(); | ||
| 336 | await expect(page.locator('#manualSsid')).toBeVisible(); | ||
| 337 | await expect(page.locator('#manualPass')).toBeVisible(); | ||
| 338 | }); | ||
| 339 | |||
| 340 | test('manual connect with empty SSID shows error', async ({ page }) => { | ||
| 341 | await setupMockRoutes(page); | ||
| 342 | await loadSetupPage(page); | ||
| 343 | await page.locator('button:has-text("Manual entry")').click(); | ||
| 344 | await page.locator('#manualSsid').fill(''); | ||
| 345 | await page.locator('#manualForm .btn').click(); | ||
| 346 | await expect(page.locator('#status')).toHaveClass(/error/); | ||
| 347 | await expect(page.locator('#status')).toContainText('Enter SSID'); | ||
| 348 | }); | ||
| 349 | |||
| 350 | test('connect sends correct JSON body to /wifi/connect', async ({ page }) => { | ||
| 351 | let capturedBody = null; | ||
| 352 | await setupMockRoutes(page, { | ||
| 353 | connectHandler: (body) => { | ||
| 354 | capturedBody = body; | ||
| 355 | return { ok: true }; | ||
| 356 | }, | ||
| 357 | }); | ||
| 358 | await loadSetupPage(page); | ||
| 359 | await page.locator('.net-item').first().click(); | ||
| 360 | await page.locator('#wifiPass').fill('mysecretpass'); | ||
| 361 | await page.locator('#passwordForm .btn').click(); | ||
| 362 | await expect(page.locator('#status')).toHaveClass(/success|processing/, { timeout: 5000 }); | ||
| 363 | expect(capturedBody).toEqual({ ssid: 'HomeNetwork', password: 'mysecretpass' }); | ||
| 364 | }); | ||
| 365 | |||
| 366 | test('success response shows green status with Connected message', async ({ page }) => { | ||
| 367 | await setupMockRoutes(page, { | ||
| 368 | connectHandler: () => ({ ok: true }), | ||
| 369 | }); | ||
| 370 | await loadSetupPage(page); | ||
| 371 | await page.locator('.net-item').first().click(); | ||
| 372 | await page.locator('#wifiPass').fill('testpass'); | ||
| 373 | await page.locator('#passwordForm .btn').click(); | ||
| 374 | await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 5000 }); | ||
| 375 | await expect(page.locator('#status')).toContainText('Connected!'); | ||
| 376 | }); | ||
| 377 | |||
| 378 | test('error response shows red status with failure reason', async ({ page }) => { | ||
| 379 | await setupMockRoutes(page, { | ||
| 380 | connectHandler: () => ({ ok: false, error: 'save failed' }), | ||
| 381 | }); | ||
| 382 | await loadSetupPage(page); | ||
| 383 | await page.locator('.net-item').first().click(); | ||
| 384 | await page.locator('#wifiPass').fill('wrongpass'); | ||
| 385 | await page.locator('#passwordForm .btn').click(); | ||
| 386 | await expect(page.locator('#status')).toHaveClass(/error/, { timeout: 5000 }); | ||
| 387 | await expect(page.locator('#status')).toContainText('Failed: save failed'); | ||
| 388 | }); | ||
| 389 | |||
| 390 | test('rescan button clears list and fetches fresh data', async ({ page }) => { | ||
| 391 | let scanCount = 0; | ||
| 392 | await page.route('**/setup', async route => { | ||
| 393 | await route.fulfill({ status: 200, contentType: 'text/html', body: SETUP_HTML }); | ||
| 394 | }); | ||
| 395 | await page.route('**/wifi/scan', async route => { | ||
| 396 | scanCount++; | ||
| 397 | const data = scanCount === 1 ? MOCK_AP_LIST : [ | ||
| 398 | { ssid: 'NewNetwork1', rssi: -30, secured: true }, | ||
| 399 | { ssid: 'NewNetwork2', rssi: -55, secured: false }, | ||
| 400 | ]; | ||
| 401 | await route.fulfill({ | ||
| 402 | status: 200, | ||
| 403 | contentType: 'application/json', | ||
| 404 | body: JSON.stringify(data), | ||
| 405 | }); | ||
| 406 | }); | ||
| 407 | await page.route('**/wifi/connect', async route => { | ||
| 408 | await route.fulfill({ | ||
| 409 | status: 200, | ||
| 410 | contentType: 'application/json', | ||
| 411 | body: JSON.stringify({ ok: true }), | ||
| 412 | }); | ||
| 413 | }); | ||
| 414 | await loadSetupPage(page); | ||
| 415 | await expect(page.locator('.net-item')).toHaveCount(4, { timeout: 5000 }); | ||
| 416 | expect(scanCount).toBe(1); | ||
| 417 | await page.locator('button:has-text("Rescan")').click(); | ||
| 418 | await expect(page.locator('.net-item')).toHaveCount(2, { timeout: 5000 }); | ||
| 419 | await expect(page.locator('.net-ssid').first()).toContainText('NewNetwork1'); | ||
| 420 | expect(scanCount).toBe(2); | ||
| 421 | }); | ||
| 422 | |||
| 423 | }); | ||
| 424 | |||
| 425 | test.describe('WiFi Setup \u2014 Layer 3: Full E2E (needs unconfigured board)', () => { | ||
| 426 | |||
| 427 | test.skip('full phone flow: scan \u2192 select \u2192 password \u2192 connect \u2192 status', async ({ page }) => { | ||
| 428 | await page.goto(`${PORTAL_URL}/setup`); | ||
| 429 | await expect(page.locator('h1')).toHaveText('TollGate Setup'); | ||
| 430 | await expect(page.locator('#scanStatus')).not.toHaveText('Scanning...', { timeout: 10000 }); | ||
| 431 | const networkCount = await page.locator('.net-item').count(); | ||
| 432 | expect(networkCount).toBeGreaterThan(0); | ||
| 433 | const firstSsid = await page.locator('.net-ssid').first().textContent(); | ||
| 434 | await page.locator('.net-item').first().click(); | ||
| 435 | await expect(page.locator('#passwordForm')).toBeVisible(); | ||
| 436 | await page.locator('#wifiPass').fill('test-password'); | ||
| 437 | await page.locator('#passwordForm .btn').click(); | ||
| 438 | await expect(page.locator('#status')).toHaveClass(/success|error|processing/, { timeout: 15000 }); | ||
| 439 | }); | ||
| 440 | }); | ||