chore: clean up repo structure

- Add node_modules/, test-results/, e2e-report/, .omp/ to .gitignore
- Remove throwaway debug scripts (debug-pw*.js, debug-login-dom.js)
- Remove empty mock-server/ directory
- Untrack harness artifact (.omp/plans/)
- Add missing project files to git (e2e tests, Dockerfiles, tooling configs)
This commit is contained in:
2026-07-12 23:42:54 +05:30
parent eb04a2597d
commit c6c5a04aca
25 changed files with 1400 additions and 1458 deletions

43
e2e/drain.spec.ts Normal file
View File

@@ -0,0 +1,43 @@
import { test, expect } from '@playwright/test';
import { login, resetToAmsterdam } from './utils/auth';
const ZT_PAGE = '/cgi-bin/luci/admin/services/zt-gateway';
test.describe('drain UI', () => {
test.beforeEach(async ({ page }) => {
await login(page);
await resetToAmsterdam(page);
await page.goto(ZT_PAGE);
await page.waitForSelector('.zt-mode-select');
});
test.afterEach(async ({ page }) => {
await resetToAmsterdam(page);
});
test('selecting graceful mode shows drain timeout', async ({ page }) => {
await page.locator('.zt-mode-select').selectOption('graceful');
const timeoutRow = page.locator('.zt-drain-timeout-row');
await expect(timeoutRow).toBeVisible();
});
test('switching with graceful mode shows drain panel', async ({ page }) => {
// Select tirunelveli radio
await page.locator('input[name="zt_gateway_region"][value="tirunelveli"]').check();
// Change mode to graceful
await page.locator('.zt-mode-select').selectOption('graceful');
// Click "Switch to selected"
await page.locator('.cbi-button-positive').click();
// Wait for the drain panel to appear (may be brief if conntrack is unavailable)
const drainPanel = page.locator('.zt-drain-panel');
await expect(drainPanel).toBeVisible({ timeout: 45_000 });
await expect(drainPanel).toContainText('Graceful drain progress');
// Panel references old and new IP addresses
await expect(drainPanel).toContainText(/\d+\.\d+\.\d+\.\d+/);
});
});

39
e2e/health.spec.ts Normal file
View File

@@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test';
import { login, resetToAmsterdam } from './utils/auth';
const PAGE = '/cgi-bin/luci/admin/services/zt-gateway';
test.describe('Health dots', () => {
test.beforeEach(async ({ page }) => {
await login(page);
await resetToAmsterdam(page);
await page.goto(PAGE);
await page.waitForSelector('.zt-gateways', { timeout: 15_000 });
});
test.afterEach(async ({ page }) => {
await resetToAmsterdam(page);
});
test('health dots update after poll refresh', async ({ page }) => {
// On initial render every health dot is DOWN (renderHealthDot(false, null)).
const allDots = page.locator('.zt-health');
const count = await allDots.count();
expect(count).toBeGreaterThan(0);
for (let i = 0; i < count; i++) {
await expect(allDots.nth(i)).toHaveClass(/zt-health-down/);
}
// Wait for the first Poll.add(refreshState, 5) cycle plus
// async health-check latency.
await page.waitForTimeout(8000);
// At least one dot should now be UP (amsterdam is reachable on 10.99.12.3).
await expect(page.locator('.zt-health-up').first()).toBeVisible();
// Amsterdam's row specifically must be healthy.
const amsterdamRow = page.locator('.zt-gateway-row[data-region="amsterdam"]');
await expect(amsterdamRow.locator('.zt-health')).toHaveClass(/zt-health-up/);
});
});

14
e2e/login.spec.ts Normal file
View File

@@ -0,0 +1,14 @@
import { test, expect } from '@playwright/test';
import { login } from './utils/auth';
test('login navigates to admin area', async ({ page }) => {
// login() waits for the LuCI JS framework to render the login view,
// fills root / empty password, submits, and waits for the admin menu.
await login(page);
// After login, LuCI lands on the admin overview at /cgi-bin/luci/
await expect(page).toHaveURL(/\/cgi-bin\/luci\//);
// The bootstrap admin menu should be visible.
await expect(page.locator('.nav').first()).toBeVisible();
});

34
e2e/overview.spec.ts Normal file
View File

@@ -0,0 +1,34 @@
import { test, expect } from '@playwright/test';
import { login } from './utils/auth';
test.beforeEach(async ({ page }) => {
await login(page);
await page.goto('/cgi-bin/luci/admin/services/zt-gateway');
await page.waitForSelector('#zt-gateway-root', { timeout: 15_000 });
});
test('displays gateway table with correct rows', async ({ page }) => {
const root = page.locator('#zt-gateway-root');
await expect(root).toBeVisible();
const rows = page.locator('.zt-gateways tbody tr.zt-gateway-row');
await expect(rows).toHaveCount(3);
await expect(page.locator('.zt-gateway-row[data-region="amsterdam"]')).toContainText('Amsterdam');
await expect(page.locator('.zt-gateway-row[data-region="tirunelveli"]')).toContainText('Tirunelveli');
const activeRow = page.locator('.zt-gateway-row', { hasText: 'active' });
await expect(activeRow).toHaveCount(1);
});
test('active gateway radio is disabled', async ({ page }) => {
const activeRow = page.locator('.zt-gateway-row', { hasText: 'active' });
const radio = activeRow.locator('input[type=radio]');
await expect(radio).toBeDisabled();
});
test('non-active gateway radios are enabled', async ({ page }) => {
const tirunelveliRow = page.locator('.zt-gateway-row[data-region="tirunelveli"]');
const radio = tirunelveliRow.locator('input[type=radio]');
await expect(radio).toBeEnabled();
});

48
e2e/switch.spec.ts Normal file
View File

@@ -0,0 +1,48 @@
import { test, expect } from '@playwright/test';
import { login, resetToAmsterdam } from './utils/auth';
const ZT_PAGE = '/cgi-bin/luci/admin/services/zt-gateway';
test.describe('ZT Gateway switch', () => {
test.describe.configure({ mode: 'serial' });
test.beforeEach(async ({ page }) => {
await login(page);
await resetToAmsterdam(page);
await page.goto(ZT_PAGE);
await page.waitForSelector('.zt-gateway-row', { timeout: 15_000 });
});
test.afterEach(async ({ page }) => {
await resetToAmsterdam(page);
});
test('switches active gateway to tirunelveli', async ({ page }) => {
// Select tirunelveli radio button
const tirunelveliRow = page.locator('tr.zt-gateway-row[data-region="tirunelveli"]');
await tirunelveliRow.locator('input[type="radio"]').click();
// Click the "Switch to selected" button
const switchBtn = page.locator('button.cbi-button-positive', { hasText: 'Switch' });
await switchBtn.click();
// Wait for a notification to appear
await page.waitForSelector('.alert-message', { timeout: 10_000 });
// Poll for the active gateway to change — the RPC takes 12-30s
await page.waitForFunction(
() => {
const row = document.querySelector('tr.zt-gateway-row[data-region="tirunelveli"]');
return row?.textContent?.includes('active') ?? false;
},
{ timeout: 45_000, polling: 2_000 },
);
// Verify tirunelveli row now contains "active"
await expect(tirunelveliRow).toContainText('active');
// Verify amsterdam row no longer contains "active"
const amsterdamRow = page.locator('tr.zt-gateway-row[data-region="amsterdam"]');
await expect(amsterdamRow).not.toContainText('active');
});
});

58
e2e/utils/auth.ts Normal file
View File

@@ -0,0 +1,58 @@
import { Page } from '@playwright/test';
/**
* Log in to LuCI with empty root password.
* Handles the "No password set" notification if it appears.
*/
export async function login(page: Page): Promise<void> {
await page.goto('/cgi-bin/luci');
// Wait for the JS framework to finish instantiating the login view.
await page.waitForFunction(() => {
const view = document.querySelector('#view');
return view && !view.querySelector('.spinning');
}, { timeout: 30_000, polling: 500 });
await page.waitForSelector('#luci_username', { state: 'visible', timeout: 30_000 });
await page.fill('#luci_username', 'root');
await page.fill('#luci_password', '');
await page.click('.cbi-button-positive');
// Wait for navigation into the admin overview.
await page.waitForURL(/\/cgi-bin\/luci\//, { timeout: 30_000 });
// LuCI admin page can take ~15s on first cold load; give it time to render.
await page.waitForFunction(() => {
const view = document.querySelector('#view');
return view && !view.querySelector('.spinning');
}, { timeout: 30_000, polling: 500 });
// Dismiss "No password set" modal/notification if present.
const closeBtn = page.locator('.alert-message .close, .alert-message button, [data-dismiss="alert"]').first();
try {
await closeBtn.click({ timeout: 2_000 });
} catch {
// No notification to dismiss.
}
}
/**
* Reset the active gateway back to amsterdam via direct ubus RPC.
*/
export async function resetToAmsterdam(page: Page): Promise<void> {
const loginRes = await page.request.post('/ubus', {
data: {
jsonrpc: '2.0',
id: 1,
method: 'call',
params: ['00000000000000000000000000000000', 'session', 'login', { username: 'root', password: '' }],
},
});
const loginData = await loginRes.json();
const token = loginData.result?.[1]?.ubus_rpc_session;
if (!token) throw new Error('ubus login failed');
await page.request.post('/ubus', {
data: {
jsonrpc: '2.0',
id: 2,
method: 'call',
params: [token, 'zt-gateway', 'switch', { region: 'amsterdam', mode: 'force' }],
},
});
}