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:
43
e2e/drain.spec.ts
Normal file
43
e2e/drain.spec.ts
Normal 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
39
e2e/health.spec.ts
Normal 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
14
e2e/login.spec.ts
Normal 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
34
e2e/overview.spec.ts
Normal 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
48
e2e/switch.spec.ts
Normal 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
58
e2e/utils/auth.ts
Normal 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' }],
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user