Files
CA/frontend/e2e/overview.spec.ts
Akiba So 9a94156acc feat: Phase 2 — leadership 大屏 (/overview) + district normalization + drawer a11y
Phase 2 of the UX modernization. Three conflict-free workstreams.

Leadership 驾驶舱 (/overview):
- Wuhan 13-district Leaflet choropleth (public/wuhan_districts.geojson, keyed
  on name, darker=higher per 高风险高亮), legend, hover/click-zoom
- 全部/门诊/住院 Segmented toggle drives choropleth + Top-5 district bar
- literal "数据截至2023-12" as-of badge (D3 honesty); raw spinner → LoadingState
- decompose OverviewDashboard 501→273; 6 components + 2 helpers under components/overview/

District normalization (backend data boundary):
- case_loader.normalize_district + load_cases_by_district_daily collapse the
  26 dirty labels (武昌/武昌区…) → 13 canonical; analysis/grid/insights repointed
  (fixes a grid-merge row-drop bug as a bonus); in-memory, schema unchanged

Shell a11y (code-review carryover):
- drawer is now a proper modal: ESC, body scroll-lock, focus-in + focus-trap
  cycle + focus-restore, role=dialog/aria-modal/aria-label, hamburger aria-expanded
- SideNav expanded state lifted to AppShell so rail+drawer stay in sync
- RouteErrorBoundary around <Outlet/> keeps shell chrome on page/chunk failure

Gates: tsc 0 · vitest 64 · e2e 19/19 (17 user-flows + 2 overview) · build ok
· backend pytest 6 new + 48 regression green

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 20:24:26 +08:00

120 lines
4.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 综合概览大屏 (/overview) 验收测试。
*
* 与 user-flows.spec.ts 一致:用 addInitScript 注入 cbpoa_token 绕过登录门,
* page.route 拦截 /api/** 使套件 hermetic无需 :8000。/wuhan_districts.geojson
* 走真实静态资源dev server 提供),由 Leaflet 取用。
*/
import { test, expect, Page } from '@playwright/test';
import { TESTIDS } from '../src/utils/testids';
/** 13 区里造两条数据,断言 choropleth 能着色、toggle 能切换。 */
function districtPayload() {
return {
districts: [
{ district: '武昌区', outpatient: 120, inpatient: 30, total: 150, outpatient_ratio: 0.8, inpatient_ratio: 0.2 },
{ district: '江岸', outpatient: 60, inpatient: 10, total: 70, outpatient_ratio: 0.86, inpatient_ratio: 0.14 },
],
total: 220,
};
}
async function seedAuthAndMockApi(page: Page) {
await page.addInitScript(() => {
localStorage.setItem('cbpoa_token', 'e2e-test-token');
});
await page.route('/api/**', (route) => {
const url = route.request().url();
const json = (body: unknown) =>
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
if (url.includes('/alerts')) {
return json({ alerts: [], total: 0 });
}
if (url.includes('/cases/stats')) {
return json({
total_outpatient: 1000,
total_inpatient: 200,
date_range: { start: '2023-01-01', end: '2023-12-01' },
top_districts: [],
top_diagnoses: [
{ diagnosis: '上呼吸道感染', outpatient: 300, inpatient: 40 },
{ diagnosis: '肺炎', outpatient: 120, inpatient: 80 },
],
});
}
if (url.includes('/cases/trend')) {
return json({
trend: [
{ date: '2023-11-01', outpatient: 10, inpatient: 2, total: 12 },
{ date: '2023-11-02', outpatient: 14, inpatient: 3, total: 17 },
],
summary: {
total_outpatient: 24,
total_inpatient: 5,
period_count: 2,
avg_daily_outpatient: 12,
avg_daily_inpatient: 2.5,
},
});
}
if (url.includes('/cases/districts')) {
return json(districtPayload());
}
if (url.includes('/risk/stats')) {
return json({ high_risk_count: 7, total_grids: 100, avg_risk: 0.4 });
}
if (url.includes('/environment/pollutants')) {
return json({ data: [{ date: '2023-11-01', AQI: 80 }, { date: '2023-11-02', AQI: 95 }] });
}
return json({ data: [], items: [], total: 0 });
});
}
test.describe('Overview 大屏', () => {
test.use({ viewport: { width: 1280, height: 900 } });
test.beforeEach(async ({ page }) => {
await seedAuthAndMockApi(page);
});
test('renders kpi-row, choropleth, as-of badge and metric toggle', async ({ page }) => {
await page.goto('/overview');
await expect(page.locator(`[data-testid="${TESTIDS.pageOverview}"]`)).toBeVisible();
await expect(page.locator(`[data-testid="${TESTIDS.kpiRow}"]`)).toBeVisible();
await expect(page.locator(`[data-testid="${TESTIDS.choroplethWrapper}"]`)).toBeVisible();
// Literal honesty badge — exact text.
const badge = page.locator(`[data-testid="${TESTIDS.asofBadge}"]`);
await expect(badge).toBeVisible();
await expect(badge).toHaveText('数据截至2023-12');
});
test('门诊/住院 toggle switches active segment without error', async ({ page }) => {
await page.goto('/overview');
await expect(page.locator(`[data-testid="${TESTIDS.choroplethWrapper}"]`)).toBeVisible();
const outBtn = page.locator(`[data-testid="${TESTIDS.outinpatientToggle}-outpatient"]`);
const inBtn = page.locator(`[data-testid="${TESTIDS.outinpatientToggle}-inpatient"]`);
const allBtn = page.locator(`[data-testid="${TESTIDS.outinpatientToggle}-all"]`);
// Default: 全部 active (primary background).
await expect(allBtn).toHaveClass(/bg-primary/);
await outBtn.click();
await expect(outBtn).toHaveClass(/bg-primary/);
await expect(allBtn).not.toHaveClass(/bg-primary/);
await inBtn.click();
await expect(inBtn).toHaveClass(/bg-primary/);
await expect(outBtn).not.toHaveClass(/bg-primary/);
// Wrapper still mounted after toggling — no render crash.
await expect(page.locator(`[data-testid="${TESTIDS.choroplethWrapper}"]`)).toBeVisible();
});
});