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>
120 lines
4.3 KiB
TypeScript
120 lines
4.3 KiB
TypeScript
/**
|
||
* 综合概览大屏 (/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();
|
||
});
|
||
});
|