chore: complete 2.0.2 maintenance baseline

This commit is contained in:
cppla
2026-08-20 12:35:47 +08:00
parent 1147f9c5dd
commit 5e7a58c263
16 changed files with 749 additions and 81 deletions
+193
View File
@@ -0,0 +1,193 @@
const { test, expect } = require('@playwright/test');
const serverSpecs = [
{ name: 'alpha-online', location: 'SG-Singapore', os: 'ubuntu', online4: true },
{ name: 'bravo-online', location: 'JP-Tokyo', os: 'debian', online4: true },
{ name: 'charlie-online', location: 'US-LosAngeles', os: 'windows', online4: true },
{ name: 'delta-online', location: 'HK-HongKong', os: 'alpine', online4: true },
{ name: 'echo-offline', location: 'FR-Paris', os: 'ubuntu' },
{ name: 'foxtrot-alert', location: 'DE-Frankfurt', os: 'debian', online4: true, cpu: 95 },
{ name: 'golf-blocked', location: 'UK-London', os: 'ubuntu', online4: true, loss: 100 },
{ name: 'hotel-online', location: 'AU-Sydney', os: 'windows', online4: true },
{ name: 'india-online', location: 'CA-Toronto', os: 'debian', online4: true },
{ name: 'juliet-online', location: 'KR-Seoul', os: 'alpine', online4: true },
{ name: 'kilo-online', location: 'SG-Singapore', os: 'ubuntu', online4: true, online6: true },
{ name: 'zulu-online', location: 'JP-Osaka', os: 'debian', online4: true }
];
function serverFixture(spec, index) {
const received = 2_000_000_000 + index * 100_000_000;
const sent = 1_000_000_000 + index * 50_000_000;
return {
name: spec.name,
type: index % 2 ? 'kvm' : 'docker',
host: `host-${index + 1}`,
location: spec.location,
os: spec.os,
online4: !!spec.online4,
online6: !!spec.online6,
uptime: spec.online4 || spec.online6 ? `${index + 1}` : '-',
load_1: 0.05 + index / 100,
load_5: 0.04 + index / 100,
load_15: 0.03 + index / 100,
cpu: spec.cpu || 12 + index,
cpu_cores: 2 + index % 4,
cpu_model: 'Test CPU Model',
memory_total: 8 * 1024 * 1024,
memory_used: (2 + index / 10) * 1024 * 1024,
swap_total: 1024 * 1024,
swap_used: 128 * 1024,
hdd_total: 100 * 1024,
hdd_used: (20 + index) * 1024,
network_rx: 20_000 + index * 100,
network_tx: 10_000 + index * 100,
network_in: received,
network_out: sent,
last_network_in: received - 500_000_000,
last_network_out: sent - 250_000_000,
ping_10010: spec.loss || 1,
ping_189: spec.loss || 2,
ping_10086: spec.loss || 3,
time_10010: 35 + index,
time_189: 45 + index,
time_10086: 55 + index,
tcp_count: 20 + index,
udp_count: 5 + index,
process_count: 80 + index,
thread_count: 160 + index,
io_read: 100_000,
io_write: 50_000,
custom: 'example=35'
};
}
const statsFixture = {
updated: String(Math.floor(Date.now() / 1000)),
servers: serverSpecs.map(serverFixture),
sslcerts: []
};
test.beforeEach(async ({ page }) => {
await page.route(/\/json\/stats\.json(?:\?|$)/, route => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(statsFixture)
}));
await page.goto('/');
if (page.viewportSize().width <= 700) {
await expect(page.locator('#serversCards .card')).toHaveCount(12);
} else {
await expect(page.locator('#serversBody .row-server')).toHaveCount(12);
}
});
test('filters, focused OS selector and sorting stay stable across refreshes', async ({ page }) => {
await expect(page.locator('#serversToolbar')).toBeVisible();
await page.getByRole('button', { name: '离线', exact: true }).click();
await expect(page.locator('#serversBody .row-server')).toHaveCount(1);
await expect(page.locator('#serversBody')).toContainText('echo-offline');
await page.getByRole('button', { name: '全部', exact: true }).click();
await page.locator('#osFilter').selectOption({ label: 'Windows' });
await expect(page.locator('#serversBody .row-server')).toHaveCount(2);
await page.locator('#osFilter').focus();
const optionCount = await page.locator('#osFilter option').count();
await page.waitForTimeout(1200);
await expect(page.locator('#osFilter option')).toHaveCount(optionCount);
await expect(page.locator('#osFilter')).toBeFocused();
await page.locator('#osFilter').selectOption('all');
await page.locator('#serverSearch').fill('KR-Seoul');
await expect(page.locator('#serversBody .row-server')).toHaveCount(1);
await expect(page.locator('#serversBody')).toContainText('juliet-online');
await page.locator('#serverSearch').fill('');
await page.locator('#sortSelect').selectOption('name');
await page.locator('#sortDirection').click();
await expect(page.locator('#serversBody .row-server').first().locator('.node-name')).toHaveText('zulu-online');
});
test('status filters distinguish offline and online alerts', async ({ page }) => {
await page.getByRole('button', { name: '异常', exact: true }).click();
await expect(page.locator('#serversBody .row-server')).toHaveCount(2);
await expect(page.locator('#serversBody')).toContainText('foxtrot-alert');
await expect(page.locator('#serversBody')).toContainText('golf-blocked');
await expect(page.locator('#serversBody')).not.toContainText('echo-offline');
});
test('online node detail shows resources and labeled charts', async ({ page }) => {
await page.locator('#serversBody .row-server', { hasText: 'alpha-online' }).click();
await expect(page.locator('#detailModal')).toBeVisible();
await expect(page.locator('#detailTitle')).toContainText('alpha-online');
await expect(page.locator('#detailContent h4')).toHaveText([
'身份', '资源', '网络', '连接', '负载趋势', '三网延迟'
]);
await expect(page.locator('.chart-legend')).toContainText(['load1load5load15', '联通电信移动']);
await expect(page.locator('.resource-meter')).toHaveCount(4);
await page.locator('#detailClose').click();
await expect(page.locator('#detailModal')).toBeHidden();
});
test('theme choice persists after reload', async ({ page }) => {
await expect(page.locator('body')).not.toHaveClass(/light/);
await page.locator('#themeToggle').click();
await expect(page.locator('body')).toHaveClass(/light/);
await expect.poll(() => page.evaluate(() => localStorage.getItem('theme'))).toBe('light');
await page.reload();
await expect(page.locator('body')).toHaveClass(/light/);
});
test('configuration page creates, updates and deletes a node through the real API', async ({ page }) => {
await page.locator('#navTabs button[data-tab="config"]').click();
await expect(page.locator('#adminStatus')).toContainText('管理 API 已启用');
await page.locator('#adminToken').fill('test-token');
await page.locator('#adminTokenForm button[type="submit"]').click();
await expect(page.locator('#adminStatus')).toContainText('已连接管理 API');
await page.locator('#configForm [name="username"]').fill('web-e2e');
await page.locator('#configForm [name="name"]').fill('web-e2e-node');
await page.locator('#configForm [name="type"]').fill('kvm');
await page.locator('#configForm [name="host"]').fill('web-e2e-host');
await page.locator('#configForm [name="location"]').fill('TEST');
await page.locator('#configForm [name="password"]').fill('web-e2e-password');
const created = page.waitForResponse(response => response.url().endsWith('/api/servers') && response.request().method() === 'POST');
await page.locator('#configForm button[type="submit"]').click();
await expect((await created).status()).toBe(201);
await expect(page.locator('#configItemList')).toContainText('web-e2e-node');
await page.locator('#configForm [name="name"]').fill('web-e2e-node-updated');
const updated = page.waitForResponse(response => response.url().endsWith('/api/servers/web-e2e') && response.request().method() === 'PUT');
await page.locator('#configForm button[type="submit"]').click();
await expect((await updated).status()).toBe(200);
await expect(page.locator('#configItemList')).toContainText('web-e2e-node-updated');
page.once('dialog', dialog => dialog.accept());
const deleted = page.waitForResponse(response => response.url().endsWith('/api/servers/web-e2e') && response.request().method() === 'DELETE');
await page.locator('#deleteConfigItemBtn').click();
await expect((await deleted).status()).toBe(200);
await expect(page.locator('#configItemList')).not.toContainText('web-e2e-node-updated');
});
test.describe('mobile layout', () => {
test.use({ viewport: { width: 390, height: 844 } });
test('uses cards, hides overview and keeps the detail drawer inside the viewport', async ({ page }) => {
await expect(page.locator('#overviewCards')).toBeHidden();
await expect(page.locator('#serversCards')).toBeVisible();
await expect(page.locator('#serversCards .card')).toHaveCount(12);
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(391);
await page.locator('#serversCards .card', { hasText: 'alpha-online' }).click();
await expect(page.locator('#detailModal')).toBeVisible();
await expect.poll(async () => {
const drawer = await page.locator('#detailModal .detail-drawer').boundingBox();
return Math.ceil(drawer.x + drawer.width);
}).toBeLessThanOrEqual(391);
const drawer = await page.locator('#detailModal .detail-drawer').boundingBox();
expect(drawer.x).toBeGreaterThanOrEqual(0);
});
});