Skip to content

Commit 0249547

Browse files
committed
test: add unit tests for various components and services
- Implement tests for DashboardLanes, MobileToolMenu, PrecipitationProbLane, RouteEditor, ToggleButtons, and hooks (useCompactMode, useThemeMode, useTimeCompactMode). - Create tests for services including apiTimeline, cache, sounding, themeColors, timelineLayout, urlParser, and weatherMetrics. - Ensure coverage for loading states, data rendering, and user interactions. - Validate functionality for caching, geolocation, and weather metrics calculations.
1 parent c0d57c4 commit 0249547

22 files changed

Lines changed: 1918 additions & 1 deletion

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ jobs:
2222

2323
- run: pnpm install --frozen-lockfile
2424

25+
- run: pnpm exec playwright install --with-deps chromium
26+
2527
- run: pnpm lint
2628

2729
- run: pnpm format:check
@@ -31,3 +33,7 @@ jobs:
3133
- run: pnpm test
3234

3335
- run: pnpm build
36+
37+
- run: pnpm test:e2e
38+
39+
- run: pnpm test:visual

docs/testing-strategy.md

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# Testing Strategy
2+
3+
本文档说明 Weather Dashboard 的测试分层、覆盖重点和落地顺序。目标不是追求形式上的高覆盖率,而是把最容易回归、最影响用户判断天气的路径稳定下来。
4+
5+
## Current State
6+
7+
项目已经具备测试基础:
8+
9+
- 单元/组件测试工具:Vitest、jsdom、Testing Library。
10+
- 浏览器自动化依赖:Playwright,目前主要用于截图生成脚本。
11+
- CI 已执行:`pnpm lint``pnpm typecheck``pnpm test``pnpm build`
12+
- 已有测试集中在 `src/services/api.test.ts``src/services/timeAggregation.test.ts``src/services/timelineCapture.test.ts``src/hooks/*.test.tsx``src/components/SoundingDrawer.test.tsx`
13+
14+
主要缺口:
15+
16+
- 大部分纯函数服务还没有直接测试,例如 `urlParser``cache``weatherMetrics``sounding``timelineLayout`
17+
- 大多数 UI 控件和 lane 组件缺少行为测试,例如 `RouteEditor`、模式切换按钮、`DashboardLanes` 的空态/加载态/截图态、AQI/风/降水等 lane 的缺失值处理。
18+
- 没有 Playwright E2E 覆盖完整用户路径。
19+
- 没有基于固定 fixture 的视觉回归,Canvas 渲染和布局回归主要靠人工看图。
20+
- 没有明确的测试数据策略,容易在测试里重复构造大型、不稳定、难读的数据。
21+
22+
## Test Pyramid
23+
24+
### 1. Static Checks
25+
26+
继续把静态检查作为第一道门:
27+
28+
- `pnpm lint`:代码风格、React Hooks 规则、明显错误。
29+
- `pnpm typecheck`:类型契约,尤其是 `WeatherPoint``WeatherTimeline`、API 响应处理。
30+
- `pnpm build`:Vite/React 打包、资源引用、生产模式编译。
31+
32+
这些检查应该在 PR 上必跑。它们快、稳定,能挡住很多低级回归。
33+
34+
### 2. Pure Unit Tests
35+
36+
优先覆盖纯数据逻辑,因为它们决定图表是否表达了正确天气含义。
37+
38+
推荐范围:
39+
40+
- `src/services/timeAggregation.ts`:跨城市、跨日期、缺失值、极值、事件坐标重映射。
41+
- `src/services/timelineCapture.ts`:选择区间、拖拽吸附、截图标签、文件名、事件裁剪。
42+
- `src/services/weatherMetrics.ts`:温度/气压范围、ensemble 成员、Beaufort 边界值。
43+
- `src/services/urlParser.ts`:城市、坐标、自定义显示名、同日期多城市切换、无参数时地理定位降级。
44+
- `src/services/cache.ts`:TTL 过期、坏缓存清理、localStorage 写入失败、429 retry/backoff、并发队列。
45+
- `src/services/sounding.ts`:露点、探空层级、缺失/异常压力层。
46+
- `src/services/themeColors.ts``src/services/timelineLayout.ts`:边界值和默认值。
47+
48+
原则:
49+
50+
- 用小型 builder 构造数据,避免在单元测试中直接依赖大型 fixture。
51+
- 对边界值写表格测试,例如 AQI 分级、Beaufort 分级、`timeCompact=3/6`
52+
- 用 fake timers 测缓存过期和 retry backoff。
53+
- 不在 PR 单元测试里访问真实 Open-Meteo/Nominatim 网络。
54+
55+
### 3. Service Integration Tests
56+
57+
这层验证多个服务组合后的业务行为,网络仍然用 mock。
58+
59+
推荐场景:
60+
61+
- `fetchCityDataForDate` 在 forecast 失败时使用 ensemble,并保持缺失字段为 `null`
62+
- forecast、ensemble、AQI 三个响应长度不一致时不崩溃。
63+
- 远期日期选择 ensemble 模型,近期日期选择更精细模型。
64+
- 经纬度路线触发 reverse geocode,城市路线触发 geocoding search。
65+
- `assembleTimeline` 按路线顺序拼接多城市数据,并保留 sun/moon/night 元数据。
66+
67+
这类测试应关注输入输出契约,不断言内部调用细节,除非调用细节本身就是业务规则,例如 URL 参数必须包含某些 Open-Meteo 字段。
68+
69+
### 4. Hook Tests
70+
71+
Hooks 连接 URL 状态、异步数据和用户操作,需要用 Testing Library 的 probe component 覆盖。
72+
73+
推荐范围:
74+
75+
- `useSearchParam`:pushState、popstate、helper 更新。
76+
- `useCompactMode` / `useTimeCompactMode`:URL 参数和 toggle 行为。
77+
- `useThemeMode`:系统主题、用户选择、localStorage。
78+
- `useDashboardData`:普通路线、switchable route、流式加载、切换城市、错误/空数据。
79+
- `useSoundingSelection`:URL hydrate、选择、步进、清空。
80+
- `useCanvas` / `canvasCapture`:canvas ref 生命周期、capture context 分支。
81+
82+
Hooks 测试应该以“组件能观察到的状态”为断言对象,少测 React 内部实现。
83+
84+
### 5. Component Tests
85+
86+
组件测试用于验证 DOM 行为、可访问名称、空态和关键条件渲染。Canvas 细节不要在 jsdom 里做像素断言。
87+
88+
推荐范围:
89+
90+
- 控件:`ThemeToggle``CompactToggle``TimeCompactToggle``MobileToolMenu`
91+
- 路线编辑:`RouteEditor` 打开/关闭、输入城市/坐标、保存到 URL、无效输入提示。
92+
- 主面板:`Dashboard` 进入/退出截图模式、导出失败状态、按钮禁用状态。
93+
- lane stack:空数据、加载中、ensemble fallback notice、compact/timeCompact 条件渲染。
94+
- 信息 lane:`AirQualityLane``PrecipitationProbLane``WindLane` 等对 `null`、边界值、标签显示的处理。
95+
- 探空:`SoundingDrawer` 已有较完整测试,后续保持新增交互必须有回归测试。
96+
97+
建议只断言用户可观察结果,例如按钮名称、状态文字、角色、class 是否代表状态。不要断言无意义的 DOM 层级。
98+
99+
### 6. Canvas Rendering Tests
100+
101+
Canvas lane 不适合在 jsdom 中做截图,但仍可测试关键绘图决策:
102+
103+
- 把颜色分级、坐标换算、标签选择、路径点计算提取为纯函数后单测。
104+
- 对少量复杂 Canvas 组件 mock `CanvasRenderingContext2D`,断言关键 draw command 被调用,例如温度曲线点数、降水柱数量、风向旋转角。
105+
- 像素级正确性放到 Playwright 视觉回归。
106+
107+
原则是少 mock Canvas,多测试绘图前的可复用计算。
108+
109+
### 7. Browser E2E Tests
110+
111+
用 Playwright 覆盖完整用户路径。E2E 应基于 `?fixture=default` 或专用 fixture,避免真实网络。
112+
113+
建议目录:
114+
115+
```text
116+
tests/e2e/
117+
dashboard.spec.ts
118+
route-editor.spec.ts
119+
capture.spec.ts
120+
```
121+
122+
建议首批场景:
123+
124+
- 打开 `/?fixture=default`,等待 dashboard 出现,关键 lane 和工具按钮可见。
125+
- 切换浅色/深色主题、compact、`timeCompact=3/6`,URL 和 UI 状态同步。
126+
- 打开路线编辑器,输入多城市路线,保存后 dashboard 开始加载或渲染 fixture/mock 结果。
127+
- 同日期多城市路线出现城市切换按钮,点击后 active city 变化。
128+
- 点击云层探空区域打开 drawer,Esc 和关闭按钮都能关闭。
129+
- 进入截图模式,拖动选择区间,取消和导出按钮状态正确。
130+
- 移动端 viewport 下工具菜单可用,按钮文字/图标不重叠。
131+
132+
E2E 只覆盖关键路径,不追求覆盖每个分支。
133+
134+
### 8. Visual Regression
135+
136+
这个项目是高密度 Canvas + CSS 信息图,视觉回归很重要。建议使用 Playwright screenshot snapshots,全部基于固定 fixture。
137+
138+
建议快照矩阵:
139+
140+
| Case | URL | Viewport |
141+
| -------------- | -------------------------------------------- | --------- |
142+
| Desktop dark | `/?fixture=default&theme=dark` | 1440x900 |
143+
| Desktop light | `/?fixture=default&theme=light` | 1440x900 |
144+
| Compact | `/?fixture=default&compact=1&theme=dark` | 1440x900 |
145+
| Time compact | `/?fixture=default&timeCompact=3&theme=dark` | 1440x900 |
146+
| Mobile | `/?fixture=default&theme=dark` | 390x844 |
147+
| Capture render | `/?fixture=default&capture=72&theme=dark` | 1920x1080 |
148+
| OG image | `/?fixture=default&og=1&theme=dark` | 1200x630 |
149+
150+
PR 上可以先跑少量 smoke 截图,完整视觉矩阵放到手动 workflow 或 nightly,避免快照维护成本过高。
151+
152+
### 9. Accessibility Tests
153+
154+
天气图表有大量视觉信息,至少保证控制面板和交互路径可访问:
155+
156+
- 所有 icon button 必须有 `aria-label` 或可访问名称。
157+
- 模态/抽屉支持 Esc、点击外部关闭、焦点不丢失。
158+
- 主题、compact、timeCompact、截图模式等状态变化可被按钮状态或文本表达。
159+
- Playwright 或 Testing Library 可加入 `axe-core` 检查主要页面和抽屉。
160+
161+
短期先把 a11y 作为组件测试断言的一部分;引入 `axe-core` 后再作为独立检查。
162+
163+
### 10. Performance And Smoke Tests
164+
165+
性能测试不需要一开始做成复杂基准,但要覆盖几个真实风险:
166+
167+
- 长路线 fixture,例如 7 天、14 天、多城市拼接,页面可在合理时间内完成首屏渲染。
168+
- `timeCompact=3/6` 后 DOM 宽度、列宽和滚动行为稳定。
169+
- Canvas lane 在频繁切换主题/compact 时不产生明显异常。
170+
- 截图导出在固定 fixture 下能生成文件,且捕获区域尺寸稳定。
171+
172+
建议用 Playwright 记录简单 budget:页面加载到关键 selector 的时间、截图元素尺寸、主要交互耗时。不要在普通 PR 上设置过严阈值。
173+
174+
## Test Data Strategy
175+
176+
测试数据分三层:
177+
178+
1. Builder:`src/test-utils/weather.ts` 继续作为单元测试默认入口,扩展 `makeWeatherTimeline``makeRouteEntry``makeDateSlot``makeOpenMeteoResponse` 等 builder。
179+
2. Scenario fixtures:新增小型 JSON 或 TS fixture,覆盖明确天气场景,例如晴天、强降水、强风、高 AQI、缺失字段、ensemble fallback、多城市切换。
180+
3. Full fixture:`fixtures/default.json` 用于截图、视觉回归和手工演示,不作为普通单元测试依赖。
181+
182+
Fixture 命名建议:
183+
184+
```text
185+
fixtures/
186+
default.json
187+
scenarios/
188+
clear-day.json
189+
storm.json
190+
polluted-low-visibility.json
191+
missing-fields.json
192+
ensemble-fallback.json
193+
multi-city-switch.json
194+
```
195+
196+
单元测试应优先使用 builder;浏览器和视觉测试使用 scenario/full fixture。
197+
198+
## Coverage Matrix
199+
200+
| Area | Tool | Must Cover | Current Status | Priority |
201+
| ---------------------- | ------------------------------ | ----------------------------------------- | -------------- | -------- |
202+
| Type/lint/build | ESLint, TypeScript, Vite | 基础质量门禁 | 已在 CI | Keep |
203+
| Data aggregation | Vitest | 跨城市/日期、缺失值、事件坐标 | 部分已有 | P0 |
204+
| API transform/fallback | Vitest mocks | forecast/ensemble/AQI、缺失字段、模型选择 | 部分已有 | P0 |
205+
| URL and route parsing | Vitest + jsdom | route 格式、坐标、switchable、fallback | 缺少 | P0 |
206+
| Cache/retry | Vitest fake timers | TTL、坏缓存、429、并发队列 | 缺少 | P1 |
207+
| Hooks | Testing Library | URL 状态、数据流、模式切换 | 部分已有 | P1 |
208+
| Core controls | Testing Library | 按钮状态、URL 同步、可访问名称 | 缺少 | P1 |
209+
| Lane components | Testing Library + pure helpers | null、阈值、compact 条件 | 缺少 | P1 |
210+
| Dashboard workflows | Playwright | fixture 加载、切换、探空、截图 | 缺少 | P2 |
211+
| Visual layout | Playwright screenshots | dark/light/mobile/capture/OG | 缺少 | P2 |
212+
| Accessibility | Testing Library, axe | 控件名称、键盘、抽屉 | 部分已有 | P2 |
213+
| Performance smoke | Playwright | 长时间线、截图尺寸、交互耗时 | 缺少 | P3 |
214+
| Live API smoke | Scheduled workflow | Open-Meteo/Nominatim 基本可用 | 缺少 | P3 |
215+
216+
## CI Plan
217+
218+
建议分阶段调整脚本和 CI。
219+
220+
第一阶段保留现有 CI,只新增更多 Vitest:
221+
222+
```json
223+
{
224+
"test": "vitest run",
225+
"test:watch": "vitest"
226+
}
227+
```
228+
229+
第二阶段引入 Playwright 配置和脚本:
230+
231+
```json
232+
{
233+
"test:unit": "vitest run",
234+
"test:e2e": "playwright test tests/e2e",
235+
"test:visual": "playwright test tests/visual",
236+
"test:all": "pnpm lint && pnpm typecheck && pnpm test:unit && pnpm build && pnpm test:e2e"
237+
}
238+
```
239+
240+
PR 必跑:
241+
242+
- `pnpm lint`
243+
- `pnpm typecheck`
244+
- `pnpm test:unit`
245+
- `pnpm build`
246+
- 少量 Playwright smoke
247+
248+
手动或 nightly:
249+
250+
- 完整 Playwright E2E
251+
- 完整视觉回归矩阵
252+
- 可选 live API smoke
253+
254+
## Definition Of Done
255+
256+
新增或修改代码时按以下规则补测试:
257+
258+
- 修改纯函数服务:必须有单元测试覆盖正常路径、缺失值、边界值。
259+
- 修改 API 字段映射:必须有 mocked API response 测试,不能只靠真实接口手测。
260+
- 修改 URL 参数或路由行为:必须覆盖 URL 读写和浏览器导航。
261+
- 修改 hook:必须有 probe component 测用户可观察状态。
262+
- 修改控件/抽屉/弹层:必须覆盖可访问名称、点击、键盘关闭或状态切换。
263+
- 修改 Canvas lane:至少测试绘图前计算或 DOM 条件;高风险视觉变化加截图回归。
264+
- 修 bug:先加能复现 bug 的回归测试,再修。
265+
266+
## Rollout Plan
267+
268+
建议按风险和收益分四步落地:
269+
270+
1. P0 单元补强:补 `urlParser``weatherMetrics``cache``sounding`,扩展 API fallback/aggregation 边界测试。
271+
2. P1 UI 行为:补 toggles、`RouteEditor``Dashboard` 截图模式、关键 lane 缺失值/阈值测试。
272+
3. P2 浏览器 smoke:新增 Playwright config 和 `tests/e2e/dashboard.spec.ts`,先覆盖 fixture 加载、模式切换、探空、截图模式。
273+
4. P2/P3 视觉和性能:建立截图基线,增加 dark/light/mobile/capture/OG 矩阵,再根据维护成本决定是否进 PR 必跑。
274+
275+
## Anti-patterns
276+
277+
- 不在 CI PR 测试中调用真实天气 API;真实接口只放 scheduled smoke。
278+
- 不用大型 screenshot snapshot 替代具体业务断言。
279+
- 不在 jsdom 中做 Canvas 像素断言。
280+
- 不因为追求覆盖率去测试 React/浏览器已经保证的行为。
281+
- 不让测试依赖当前日期,除非显式 mock `Date`
282+
- 不让测试之间共享可变全局状态;每个测试重置 URL、localStorage、timers、mocks。

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"preview": "vite preview",
1515
"test": "vitest run",
1616
"test:watch": "vitest",
17+
"test:e2e": "tsx scripts/e2e-smoke.ts",
18+
"test:visual": "tsx scripts/visual-smoke.ts",
1719
"deploy": "vite build && wrangler deploy",
1820
"fixture:fetch": "tsx scripts/fetch-fixture.ts",
1921
"demo:update": "tsx scripts/capture-screenshot.ts",

0 commit comments

Comments
 (0)