Files
LinkPort/web/tests/optimization.spec.ts
2026-09-27 16:39:38 +08:00

120 lines
6.6 KiB
TypeScript

import { test, expect } from '@playwright/test';
test('切换历史资源后忽略延迟列表和差异响应', async ({ page }) => {
await page.goto('/');
const session = await (await page.request.get('/api/v1/session')).json();
const headers = { 'X-CSRF-Token': session.csrfToken };
const ids: string[] = [];
let releaseHistory!: () => void, releaseDiff!: () => void;
const historyGate = new Promise<void>(resolve => { releaseHistory = resolve; });
const diffGate = new Promise<void>(resolve => { releaseDiff = resolve; });
let firstHistory = true, historyHeld = false, diffHeld = false;
try {
for (const [name, port] of [['历史时序 A', 24681], ['历史时序 B', 24682]] as const) {
const result = await page.request.post('/api/v1/proxies', { headers, data: {
name, type: 'mysql', listenHost: '127.0.0.1', listenPort: port,
gatewayAccounts: [{ id: 'history', username: 'history', password: 'isolated-history-password' }],
} });
expect(result.ok()).toBeTruthy();
ids.push((await result.json()).config.id);
}
// Hold selected responses explicitly to reproduce out-of-order network delivery.
await page.route('**/api/v1/configuration/history?*', async route => {
const id = new URL(route.request().url()).searchParams.get('id');
if (id === ids[0] && firstHistory) {
firstHistory = false; historyHeld = true; await historyGate;
}
await route.fulfill({ json: [{ revision: id === ids[0] ? 11 : 22, at: 'fixture-history' }] });
});
await page.route('**/api/v1/configuration/diff?*', async route => {
const id = new URL(route.request().url()).searchParams.get('id');
if (id === ids[0]) { diffHeld = true; await diffGate; }
await route.fulfill({ json: { changes: [{ path: '/name', before: 'current', after: id === ids[0] ? 'old-A' : 'old-B' }] } });
});
await page.reload();
await page.getByRole('navigation').getByRole('button', { name: '配置管理', exact: true }).click();
const choose = async (name: string) => {
await page.locator('.el-select').filter({ has: page.getByRole('combobox', { name: '历史资源', exact: true }) }).click();
await page.getByRole('option', { name, exact: true }).click();
};
const history = page.locator('[aria-label="配置历史列表"]');
await choose('历史时序 A');
await expect.poll(() => historyHeld).toBe(true);
await choose('历史时序 B');
await expect(history.getByRole('cell', { name: '22', exact: true })).toBeVisible();
const delayedHistory = page.waitForResponse(r => r.url().includes('/configuration/history?') && r.url().includes(ids[0]));
releaseHistory(); await delayedHistory;
await page.evaluate(() => new Promise(requestAnimationFrame));
await expect(history.getByRole('cell', { name: '11', exact: true })).toHaveCount(0);
await choose('历史时序 A');
await history.getByRole('button', { name: '查看差异 / 恢复', exact: true }).click();
await expect.poll(() => diffHeld).toBe(true);
await choose('历史时序 B');
await history.getByRole('button', { name: '查看差异 / 恢复', exact: true }).click();
const dialog = page.getByRole('dialog', { name: '配置变更对比', exact: true });
await expect(dialog).toContainText('历史时序 B');
await expect(dialog).toContainText('old-B');
const delayedDiff = page.waitForResponse(r => r.url().includes('/configuration/diff?') && r.url().includes(ids[0]));
releaseDiff(); await delayedDiff;
await page.evaluate(() => new Promise(requestAnimationFrame));
await expect(dialog).toContainText('历史时序 B');
await expect(dialog).not.toContainText('old-A');
await dialog.getByRole('button', { name: '恢复此版本', exact: true }).click();
const confirm = page.getByRole('dialog', { name: '恢复版本', exact: true });
await expect(confirm).toContainText('历史时序 B');
await expect(confirm).toContainText('22');
await confirm.getByRole('button', { name: /取消|Cancel/ }).click();
} finally {
releaseHistory(); releaseDiff();
await page.unrouteAll({ behavior: 'wait' }).catch(() => {});
for (const id of ids) await page.request.delete(`/api/v1/proxies/${id}`, { headers }).catch(() => {});
}
});
test('监控自动刷新保留间隔草稿并避免重叠请求', async ({ page }) => {
await page.clock.install();
let requests = 0, release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
await page.route('**/api/v1/monitoring', async route => {
requests++;
if (requests === 2) await gate;
await route.fulfill({ json: { gateways: [], health: [], intervalSeconds: 30 } });
});
try {
await page.goto('/');
await page.getByRole('navigation').getByRole('button', { name: '监控运维', exact: true }).click();
const interval = page.getByRole('spinbutton', { name: '健康检查间隔秒', exact: true });
await expect(interval).toHaveValue('30');
await interval.fill('60'); await interval.press('Tab');
await page.clock.runFor(5000);
await expect.poll(() => requests).toBe(2);
await page.clock.runFor(10000);
expect(requests).toBe(2);
const refreshed = page.waitForResponse(r => r.url().endsWith('/api/v1/monitoring'));
release(); await refreshed;
await expect(interval).toHaveValue('60');
await expect(page.getByText('(未保存)', { exact: true })).toBeVisible();
await interval.fill('5'); await interval.press('Tab');
await expect(page.getByRole('button', { name: '保存检查间隔', exact: true })).toBeDisabled();
// Pause only browser refresh; it must not send a setting that stops health checks.
const refreshSwitch = page.locator('.el-switch').filter({ has: page.getByRole('switch', { name: '自动刷新监控', exact: true }) });
await refreshSwitch.click();
await page.clock.runFor(10000);
expect(requests).toBe(2);
await refreshSwitch.click();
await page.evaluate(() => Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'hidden' }));
await page.clock.runFor(10000);
expect(requests).toBe(2);
await page.evaluate(() => {
Object.defineProperty(document, 'visibilityState', { configurable: true, value: 'visible' });
document.dispatchEvent(new Event('visibilitychange'));
});
await expect.poll(() => requests).toBe(3);
await page.getByRole('navigation').getByRole('button', { name: '代理管理', exact: true }).click();
await page.clock.runFor(10000);
expect(requests).toBe(3);
} finally {
release(); await page.unrouteAll({ behavior: 'wait' }).catch(() => {});
}
});