Files
CA/frontend/e2e/user-flows.spec.ts
Akiba So f092c3c550 chore: add test infrastructure and update risk router
- Add vitest config and unit tests for components, api, stores
- Add Playwright e2e test for user flows
- Add backend test files
- Update risk.py with LOD grid KDTree optimization
2026-06-09 12:54:55 +08:00

243 lines
7.8 KiB
TypeScript

/**
* US-007 + US-008: E2E user flow and UI state tests.
* Simulates real user workflows through the CBPOA system.
*/
import { test, expect } from '@playwright/test';
const BASE_URL = 'http://localhost:3000';
test.describe('认证流程 (Authentication Flow)', () => {
test('显示登录页面', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(1000);
// Should see login form or app (if cached token)
const isLogin = await page.locator('input').count();
const isApp = await page.locator('nav').count();
expect(isLogin > 0 || isApp > 0).toBeTruthy();
});
test('登录表单可交互', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(1000);
const inputs = page.locator('input');
const count = await inputs.count();
if (count >= 2) {
// Login page is shown
await inputs.first().fill('admin');
await inputs.nth(1).fill('admin123');
const loginBtn = page.locator('button[type="submit"], button:has-text("登录"), button:has-text("Login")');
const btnCount = await loginBtn.count();
if (btnCount > 0) {
await loginBtn.first().click();
await page.waitForTimeout(2000);
}
}
// If no inputs, user is already logged in (token in localStorage)
});
});
test.describe('监测面板 (Monitoring Dashboard)', () => {
test('面板加载并显示统计卡片', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(3000);
// Should show monitoring page by default
const statCards = page.locator('[class*="stat"], [class*="card"], [class*="Stat"]');
const cardsCount = await statCards.count();
// Should see some content
const bodyText = await page.textContent('body');
expect(bodyText).toBeTruthy();
});
test('时间线控件可交互', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(3000);
// Look for timeline controls
const playButton = page.locator('button:has-text("播放"), button[title*="play" i], button[class*="play" i]');
const prevButton = page.locator('button:has-text("前一天"), button[title*="prev" i]');
const nextButton = page.locator('button:has-text("后一天"), button[title*="next" i]');
if (await playButton.count() > 0) {
await playButton.first().click();
await page.waitForTimeout(1000);
}
});
test('疾病筛选器可用', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(3000);
const selects = page.locator('select, [role="combobox"], [class*="select" i], [class*="filter" i]');
const count = await selects.count();
expect(count >= 0).toBeTruthy();
});
});
test.describe('预警面板 (Alerts Dashboard)', () => {
test('导航到预警面板', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
// Navigate to alerts - click sidebar link
const alertsLink = page.locator('a[href*="alert" i], button:has-text("预警"), button:has-text("告警"), span:has-text("预警"), span:has-text("告警")');
if (await alertsLink.count() > 0) {
await alertsLink.first().click();
await page.waitForTimeout(2000);
}
});
test('预警列表加载', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
const alertsLink = page.locator('a[href*="alert" i], button:has-text("预警"), span:has-text("预警")');
if (await alertsLink.count() > 0) {
await alertsLink.first().click();
await page.waitForTimeout(3000);
const bodyText = await page.textContent('body');
expect(bodyText).toBeTruthy();
}
});
});
test.describe('趋势分析 (Trend Analysis)', () => {
test('导航到趋势分析页面', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
const trendLink = page.locator('button:has-text("趋势"), span:has-text("趋势"), a[href*="trend" i]');
if (await trendLink.count() > 0) {
await trendLink.first().click();
await page.waitForTimeout(2000);
}
});
test('趋势图渲染', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
const trendLink = page.locator('button:has-text("趋势"), span:has-text("趋势")');
if (await trendLink.count() > 0) {
await trendLink.first().click();
await page.waitForTimeout(3000);
// Recharts renders SVG charts
const svgCharts = page.locator('svg.recharts-surface');
const chartCount = await svgCharts.count();
expect(chartCount >= 0).toBeTruthy();
}
});
});
test.describe('区县对比 (District Comparison)', () => {
test('导航到区县对比页面', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
const districtLink = page.locator('button:has-text("区县"), button:has-text("对比"), span:has-text("区县")');
if (await districtLink.count() > 0) {
await districtLink.first().click();
await page.waitForTimeout(2000);
}
});
});
test.describe('报告中心 (Reports Center)', () => {
test('导航到报告中心', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
const reportsLink = page.locator('button:has-text("报告"), span:has-text("报告"), a[href*="report" i]');
if (await reportsLink.count() > 0) {
await reportsLink.first().click();
await page.waitForTimeout(2000);
}
});
test('报告列表加载', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
const reportsLink = page.locator('button:has-text("报告"), span:has-text("报告")');
if (await reportsLink.count() > 0) {
await reportsLink.first().click();
await page.waitForTimeout(3000);
const bodyText = await page.textContent('body');
expect(bodyText).toBeTruthy();
}
});
});
test.describe('UI 状态与错误处理 (UI States & Error Handling)', () => {
test('页面加载显示加载指示器而非白屏', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(500);
const bodyHTML = await page.innerHTML('body');
// Should have some content, even during loading
expect(bodyHTML.length).toBeGreaterThan(0);
});
test('侧边栏导航切换页面正常', async ({ page }) => {
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
const navLinks = page.locator('nav a, nav button, [class*="side" i] a, [class*="side" i] button');
const count = await navLinks.count();
if (count >= 2) {
await navLinks.first().click();
await page.waitForTimeout(1000);
await navLinks.nth(1).click();
await page.waitForTimeout(1000);
}
});
test('未出现明显 console 报错', async ({ page }) => {
const errors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
errors.push(msg.text());
}
});
page.on('pageerror', (err) => {
errors.push(err.message);
});
await page.goto(BASE_URL);
await page.waitForTimeout(3000);
const filtered = errors.filter(
(e) => !e.includes('favicon') && !e.includes('404') && !e.includes('OLMap')
);
expect(filtered).toHaveLength(0);
});
});
test.describe('响应式布局 (Responsive Layout)', () => {
test('移动端视口下不崩溃', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
const bodyText = await page.textContent('body');
expect(bodyText).toBeTruthy();
});
test('平板视口下正常显示', async ({ page }) => {
await page.setViewportSize({ width: 768, height: 1024 });
await page.goto(BASE_URL);
await page.waitForTimeout(2000);
const bodyText = await page.textContent('body');
expect(bodyText).toBeTruthy();
});
});