import { test, expect } from '@playwright/test';
const PORTAL_IP = process.env.TOLLGATE_IP || '10.192.45.1';
const PORTAL_URL = `http://${PORTAL_IP}`;
const SETUP_HTML = `
TollGate Setup
`;
const MOCK_AP_LIST = [
{ ssid: 'HomeNetwork', rssi: -42, secured: true },
{ ssid: 'CafeWiFi', rssi: -67, secured: true },
{ ssid: 'OpenPublic', rssi: -75, secured: false },
{ ssid: 'Neighbor5G', rssi: -81, secured: true },
];
async function setupMockRoutes(page, overrides = {}) {
const scanResponse = overrides.scanResponse || MOCK_AP_LIST;
const connectHandler = overrides.connectHandler || (() => ({ ok: true }));
await page.route('**/setup', async route => {
await route.fulfill({
status: 200,
contentType: 'text/html',
body: SETUP_HTML,
});
});
await page.route('**/wifi/scan', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(scanResponse),
});
});
await page.route('**/wifi/connect', async route => {
const request = route.request();
const body = request.postDataJSON();
const response = connectHandler(body);
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(response),
});
});
}
async function loadSetupPage(page) {
await page.goto('http://tollgate.test/setup', { waitUntil: 'networkidle' });
}
test.describe('WiFi Setup \u2014 Layer 1: API Endpoints (needs live board)', () => {
test('GET /setup redirects to portal on configured board', async ({ request }) => {
const resp = await request.fetch(`${PORTAL_URL}/setup`, {
maxRedirects: 0,
});
expect(resp.status()).toBe(302);
const location = resp.headers()['location'];
expect(location).toContain(PORTAL_IP);
expect(location).toMatch(/\/$/);
});
test('GET /wifi/scan returns JSON array with valid AP objects', async ({ request }) => {
const resp = await request.get(`${PORTAL_URL}/wifi/scan`);
expect(resp.status()).toBe(200);
const data = await resp.json();
expect(Array.isArray(data)).toBe(true);
if (data.length > 0) {
const ap = data[0];
expect(ap).toHaveProperty('ssid');
expect(typeof ap.ssid).toBe('string');
expect(ap).toHaveProperty('rssi');
expect(typeof ap.rssi).toBe('number');
expect(ap).toHaveProperty('secured');
expect(typeof ap.secured).toBe('boolean');
}
});
test('GET /wifi/status returns connection state', async ({ request }) => {
const resp = await request.get(`${PORTAL_URL}/wifi/status`);
expect(resp.status()).toBe(200);
const data = await resp.json();
expect(data).toHaveProperty('connected');
expect(typeof data.connected).toBe('boolean');
if (data.connected) {
expect(data).toHaveProperty('ip');
expect(data.ip).toMatch(/\d+\.\d+\.\d+\.\d+/);
expect(data).toHaveProperty('ssid');
}
});
test('POST /wifi/connect rejects empty body', async ({ request }) => {
const resp = await request.post(`${PORTAL_URL}/wifi/connect`, {
data: '',
headers: { 'Content-Type': 'application/json' },
});
const data = await resp.json();
expect(data.ok).toBe(false);
});
test('POST /wifi/connect rejects invalid JSON', async ({ request }) => {
const resp = await request.post(`${PORTAL_URL}/wifi/connect`, {
data: 'not json at all',
headers: { 'Content-Type': 'application/json' },
});
const data = await resp.json();
expect(data.ok).toBe(false);
expect(data.error).toBeDefined();
});
test('POST /wifi/connect rejects missing ssid', async ({ request }) => {
const resp = await request.post(`${PORTAL_URL}/wifi/connect`, {
data: JSON.stringify({ password: 'testpass' }),
headers: { 'Content-Type': 'application/json' },
});
const data = await resp.json();
expect(data.ok).toBe(false);
expect(data.error).toContain('ssid');
});
test('POST /wifi/connect with valid SSID returns ok or ECONNRESET', async ({ request }) => {
const resp = await request.post(`${PORTAL_URL}/wifi/connect`, {
data: JSON.stringify({ ssid: 'TestSetupAP', password: 'testpass123' }),
headers: { 'Content-Type': 'application/json' },
maxRedirects: 0,
timeout: 10000,
}).catch(() => null);
if (resp) {
const text = await resp.text();
try {
const data = JSON.parse(text);
expect(data.ok).toBe(true);
} catch {
expect(resp.status()).toBeLessThan(500);
}
}
});
});
test.describe('WiFi Setup \u2014 Layer 1.5: Redirect (needs live board)', () => {
test('redirect Location header contains correct AP IP', async ({ request }) => {
const resp = await request.fetch(`${PORTAL_URL}/setup`, {
maxRedirects: 0,
});
const location = resp.headers()['location'];
expect(location).toBe(`http://${PORTAL_IP}/`);
});
});
test.describe('WiFi Setup \u2014 Layer 2: HTML UI Interaction', () => {
test('page renders with title and subtitle', async ({ page }) => {
await setupMockRoutes(page);
await loadSetupPage(page);
await expect(page.locator('h1')).toHaveText('TollGate Setup');
await expect(page.locator('.subtitle')).toHaveText('Configure upstream WiFi');
});
test('scan auto-triggers on load and shows network count', async ({ page }) => {
await setupMockRoutes(page);
await loadSetupPage(page);
await expect(page.locator('#scanStatus')).toHaveText(/4 networks found/, { timeout: 5000 });
});
test('network list shows SSID and RSSI for each AP', async ({ page }) => {
await setupMockRoutes(page);
await loadSetupPage(page);
await expect(page.locator('.net-item')).toHaveCount(4);
await expect(page.locator('.net-ssid').first()).toContainText('HomeNetwork');
await expect(page.locator('.net-rssi').first()).toContainText('-42 dBm');
});
test('secured networks show lock icon', async ({ page }) => {
await setupMockRoutes(page);
await loadSetupPage(page);
const securedItems = page.locator('.net-item');
const firstSecured = securedItems.first();
await expect(firstSecured.locator('.net-lock')).toBeVisible();
});
test('open networks have no lock icon', async ({ page }) => {
await setupMockRoutes(page);
await loadSetupPage(page);
const openItem = page.locator('.net-item').nth(2);
await expect(openItem.locator('.net-lock')).toHaveCount(0);
await expect(openItem.locator('.net-ssid')).toContainText('OpenPublic');
});
test('clicking secured network shows password form and hides list', async ({ page }) => {
await setupMockRoutes(page);
await loadSetupPage(page);
await expect(page.locator('.net-item').first()).toBeVisible();
await page.locator('.net-item').first().click();
await expect(page.locator('#passwordForm')).toBeVisible();
await expect(page.locator('#selectedNetwork')).toHaveText('Connect to: HomeNetwork');
await expect(page.locator('#networkList')).toBeHidden();
await expect(page.locator('#scanStatus')).toBeHidden();
});
test('clicking open network auto-connects without password form', async ({ page }) => {
let connectBody = null;
await setupMockRoutes(page, {
connectHandler: (body) => {
connectBody = body;
return { ok: true };
},
});
await loadSetupPage(page);
const openItem = page.locator('.net-item').nth(2);
await openItem.click();
await expect(page.locator('#status')).toHaveClass(/processing|success/, { timeout: 5000 });
expect(connectBody).toBeTruthy();
expect(connectBody.ssid).toBe('OpenPublic');
});
test('manual entry button toggles form visibility', async ({ page }) => {
await setupMockRoutes(page);
await loadSetupPage(page);
await expect(page.locator('#manualForm')).toBeHidden();
await page.locator('button:has-text("Manual entry")').click();
await expect(page.locator('#manualForm')).toBeVisible();
await expect(page.locator('#manualSsid')).toBeVisible();
await expect(page.locator('#manualPass')).toBeVisible();
});
test('manual connect with empty SSID shows error', async ({ page }) => {
await setupMockRoutes(page);
await loadSetupPage(page);
await page.locator('button:has-text("Manual entry")').click();
await page.locator('#manualSsid').fill('');
await page.locator('#manualForm .btn').click();
await expect(page.locator('#status')).toHaveClass(/error/);
await expect(page.locator('#status')).toContainText('Enter SSID');
});
test('connect sends correct JSON body to /wifi/connect', async ({ page }) => {
let capturedBody = null;
await setupMockRoutes(page, {
connectHandler: (body) => {
capturedBody = body;
return { ok: true };
},
});
await loadSetupPage(page);
await page.locator('.net-item').first().click();
await page.locator('#wifiPass').fill('mysecretpass');
await page.locator('#passwordForm .btn').click();
await expect(page.locator('#status')).toHaveClass(/success|processing/, { timeout: 5000 });
expect(capturedBody).toEqual({ ssid: 'HomeNetwork', password: 'mysecretpass' });
});
test('success response shows green status with Connected message', async ({ page }) => {
await setupMockRoutes(page, {
connectHandler: () => ({ ok: true }),
});
await loadSetupPage(page);
await page.locator('.net-item').first().click();
await page.locator('#wifiPass').fill('testpass');
await page.locator('#passwordForm .btn').click();
await expect(page.locator('#status')).toHaveClass(/success/, { timeout: 5000 });
await expect(page.locator('#status')).toContainText('Connected!');
});
test('error response shows red status with failure reason', async ({ page }) => {
await setupMockRoutes(page, {
connectHandler: () => ({ ok: false, error: 'save failed' }),
});
await loadSetupPage(page);
await page.locator('.net-item').first().click();
await page.locator('#wifiPass').fill('wrongpass');
await page.locator('#passwordForm .btn').click();
await expect(page.locator('#status')).toHaveClass(/error/, { timeout: 5000 });
await expect(page.locator('#status')).toContainText('Failed: save failed');
});
test('rescan button clears list and fetches fresh data', async ({ page }) => {
let scanCount = 0;
await page.route('**/setup', async route => {
await route.fulfill({ status: 200, contentType: 'text/html', body: SETUP_HTML });
});
await page.route('**/wifi/scan', async route => {
scanCount++;
const data = scanCount === 1 ? MOCK_AP_LIST : [
{ ssid: 'NewNetwork1', rssi: -30, secured: true },
{ ssid: 'NewNetwork2', rssi: -55, secured: false },
];
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(data),
});
});
await page.route('**/wifi/connect', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ ok: true }),
});
});
await loadSetupPage(page);
await expect(page.locator('.net-item')).toHaveCount(4, { timeout: 5000 });
expect(scanCount).toBe(1);
await page.locator('button:has-text("Rescan")').click();
await expect(page.locator('.net-item')).toHaveCount(2, { timeout: 5000 });
await expect(page.locator('.net-ssid').first()).toContainText('NewNetwork1');
expect(scanCount).toBe(2);
});
});
test.describe('WiFi Setup \u2014 Layer 3: Full E2E (needs unconfigured board)', () => {
test.skip('full phone flow: scan \u2192 select \u2192 password \u2192 connect \u2192 status', async ({ page }) => {
await page.goto(`${PORTAL_URL}/setup`);
await expect(page.locator('h1')).toHaveText('TollGate Setup');
await expect(page.locator('#scanStatus')).not.toHaveText('Scanning...', { timeout: 10000 });
const networkCount = await page.locator('.net-item').count();
expect(networkCount).toBeGreaterThan(0);
const firstSsid = await page.locator('.net-ssid').first().textContent();
await page.locator('.net-item').first().click();
await expect(page.locator('#passwordForm')).toBeVisible();
await page.locator('#wifiPass').fill('test-password');
await page.locator('#passwordForm .btn').click();
await expect(page.locator('#status')).toHaveClass(/success|error|processing/, { timeout: 15000 });
});
});