fexd-mobile-tester
@fexd/mobile 组件库的测试管理工具。覆盖率驱动 + 场景驱动测试编写、运行定向测试、 生成修复建议与 TEST.md 评分报告。目标:每个导出 statements >= 90%。 触发词:测试、写测试、补测试、跑测试、检查测试、测试覆盖、test。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
@fexd/mobile 组件库的测试管理工具。覆盖率驱动 + 场景驱动测试编写、运行定向测试、 生成修复建议与 TEST.md 评分报告。目标:每个导出 statements >= 90%。 触发词:测试、写测试、补测试、跑测试、检查测试、测试覆盖、test。
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
@fexd/mobile Skill 质量评估工具。对 SKILL.md 及 references/ 进行 A/B 盲评, 运行确定性检查,生成评分报告并与 baseline 对比检测退步。 触发词:评估 skill、评估 fexd-mobile skill、skill eval、skill 评分。
React 移动端 H5 组件库 @fexd/mobile 的完整使用指南。涵盖 127 个公开导出/工具的用法、 代码片段、主题定制、样式修改、架构设计。当用户使用 @fexd/mobile 开发页面、查找组件 用法、定制主题、修改样式、查询 API 时使用。触发词:fexd、@fexd/mobile、fexd-mobile。
维护 @fexd/mobile 发布 skill 的文档质量工具。扫描组件源码变更、清洗 dumi 语法、 生成/更新 references/*.md 组件文档、执行质量审查。 触发词:检查组件 skill、更新组件文档、生成组件文档、审查组件文档。
| name | fexd-mobile-tester |
| description | @fexd/mobile 组件库的测试管理工具。覆盖率驱动 + 场景驱动测试编写、运行定向测试、 生成修复建议与 TEST.md 评分报告。目标:每个导出 statements >= 90%。 触发词:测试、写测试、补测试、跑测试、检查测试、测试覆盖、test。 |
为 @fexd/mobile 组件库编写、运行和维护测试用例。采用覆盖率驱动 + 场景驱动方法论。
| 用途 | 路径 |
|---|---|
| 组件源码 | packages/mobile/src/exports/{NAME}/ |
| 类型定义 | packages/mobile/src/exports/{NAME}/type.tsx |
| 测试输出 | packages/mobile/src/exports/{NAME}/tests/index.test.tsx |
| 测试报告 | packages/mobile/src/exports/{NAME}/tests/TEST.md |
| Jest 配置 | jest.config.js(含 coverage 配置) |
| SWC 配置 | .swcrc |
| 变更检测脚本 | .cursor/skills/fexd-mobile-tester/scripts/detect-test-scope.js |
| 测试脚手架脚本 | .cursor/skills/fexd-mobile-tester/scripts/scaffold-test.js |
| 测试模式参考 | .cursor/skills/fexd-mobile-tester/references/test-patterns.md |
| 组件策略参考 | .cursor/skills/fexd-mobile-tester/references/component-strategies.md |
| Prop 约束参考 | .cursor/skills/fexd-mobile-tester/references/prop-constraint-guide.md |
collectCoverage: true,覆盖 exports/*/index.{ts,tsx}from '..'),避免 barrel import @fexd/mobile 带来的全量模块解析| 指标 | 目标 |
|---|---|
| Statements | >= 90% |
| Branches | >= 85% |
| Functions | >= 90% |
| Lines | >= 90% |
IO 分层变体(cloneFC 产物)可放宽至 statements >= 80%(源码极短,覆盖率波动大)。
用户说「给 {NAME} 写测试」或「补测试」时:
type.tsx 提取全部 prop 定义(名称、类型、可选性)index.tsx 理解组件实现逻辑、DOM 结构、内部状态Panel/ 或 Group/ 子目录,一并读取(复合组件)useIOControl(= 支持受控/非受控模式)defaultProps 了解默认值demos/ 目录理解设计者意图的典型使用场景和常见组合index.zh.md 中记录的 edge case 和注意事项基于源码分析列出「场景清单」,每个场景包含:
场景来源优先级:
每个导出至少 3 个场景,交互组件至少 5 个。
场景类型映射(按组件类型自动识别):
规则:
userEvent.click() / userEvent.type() 替代 fireEvent.click() / fireEvent.change()fireEvent.touchStart → touchMove(多步) → touchEnd 序列fireEvent.mouseDown → mouseMove(多步) → mouseUp 序列npx jest --coverage --testPathPattern='exports/{NAME}/tests' --no-silent
解读覆盖率输出:
Stmts / Branch / Funcs / Lines 四项指标Uncovered Line #s 列,定位未覆盖的代码行如某些行因 jsdom 限制无法覆盖(如 Canvas API、真实 CSS 动画),在 TEST.md 中注明原因。
在 {NAME}/tests/TEST.md 中生成测试报告,格式见下方「TEST.md 规范」。
若测试失败,按「失败分类指南」处理:
test.skip + 注释用户说「批量生成测试」或「给多个组件补测试」时:
用户说「跑测试」或在代码变更后触发:
node .cursor/skills/fexd-mobile-tester/scripts/detect-test-scope.js
npx jest --coverage --testPathPattern='<jestPattern>' --no-silent
runFullSuite: true(公共模块变更),执行全量测试测试运行后出现失败时:
test.skip + 注释说明用户说「检查测试覆盖」或「审查测试」时:
npx jest --coverage --testPathPattern='exports/{NAME}/tests'| 层级 | 名称 | 描述 | 适用场景 |
|---|---|---|---|
| L1 | 冒烟测试 | 无 prop 渲染不崩溃 + 基础内容显示 | 所有组件 |
| L2 | Prop 逐项 | 枚举/布尔/数值 prop 的各取值渲染正确 | 有可视化 prop 的组件 |
| L3 | 事件回调 | onX 回调被正确触发、参数正确 | 有交互的组件 |
| L4 | Prop 约束 | 关联 prop 间的约束(如 min/max 夹逼) | 有范围/依赖关系 prop 的组件 |
| L5 | 受控/非受控 | value+onChange 受控 vs defaultValue 非受控 | 使用 useIOControl 的组件 |
| L6 | 边界异常 | undefined/null/空数组、类型错误、极端值 | 所有组件 |
| L7 | 异步与定时 | debounce、setTimeout、动画结束回调 | 有 useDebounceEffect、transition 的组件 |
| L8 | 复合与 Portal | 父子组件联动、命令式 API、Portal 渲染位置 | Collapse+Panel、showModal 等 |
关键变化:L3 不再是单次 click → 检查回调,而是操作序列(多步交互 → 验证中间状态和最终状态)。
八层的代码模板详见 test-patterns.md。
每个导出必须生成 {NAME}/tests/TEST.md,格式如下:
# {NAME} 测试概要
## 覆盖率
| 指标 | 值 |
| ---------- | --- |
| Statements | xx% |
| Branches | xx% |
| Functions | xx% |
| Lines | xx% |
## 测试场景
1. **场景名**:操作描述 → 预期结果
2. ...
## 评分
- 交互覆盖:x/5(是否包含真实用户操作序列)
- 分支覆盖:x/5(源码分支是否被测试覆盖)
- 边界处理:x/5(空值/极端值/异常输入)
- 场景真实度:x/5(是否反映真实业务使用模式)
- 综合评分:x/20
## 未覆盖说明(如有)
- 行 xx-xx:原因(如 jsdom 不支持 Canvas/Touch API 等)
评分标准:
// ✅ 推荐:直接导入,避免加载全部 120 个模块
import Switch from '..'
import Collapse from '..'
import Panel from '../Panel'
// ✅ 推荐:使用共享工具模块中的 helper
import { waitFakeTimers, mockDrag, mockTouch, cleanupModals } from '../../../tests/testing'
// ❌ 避免:barrel import 会拖慢测试
import { Switch } from '@fexd/mobile'
packages/mobile/src/tests/testing.tsx 提供四个通用 helper:
| 工具 | 用途 | 适用组件 |
|---|---|---|
waitFakeTimers(loops?, ms?) | 批量推进 fake timer,解决多层 setTimeout 嵌套 | toast / notify / loading / Modal / PickerView / Swiper / Collapse |
mockDrag(el, points) | mouseDown → mouseMove × N → mouseUp | Slider / Drag |
mockTouch(el, points) | touchStart → touchMove × N → touchEnd | Rate / Swiper / SwipeAction |
cleanupModals() | modalStore.destroyAll + map.clear + cleanup(不清空 body,避免 Portal debounce 竞态) | 所有弹窗类组件 |
import userEvent from '@testing-library/user-event'
// ✅ 推荐:userEvent 模拟真实用户行为(含 focus/blur/pointer 序列)
const user = userEvent.setup()
await user.click(button)
await user.type(input, 'hello')
// ⚠️ 允许:touch/drag 事件仍用 fireEvent(userEvent 不支持 touch API)
fireEvent.touchStart(el, { touches: [{ clientX: 0, clientY: 0 }] })
fireEvent.touchMove(el, { touches: [{ clientX: 100, clientY: 0 }] })
fireEvent.touchEnd(el)
// ⚠️ 允许:mouseDown/mouseMove/mouseUp 拖拽序列用 fireEvent
fireEvent.mouseDown(el, { clientX: 0 })
fireEvent.mouseMove(el, { clientX: 100 })
fireEvent.mouseUp(el, { clientX: 100 })
describe 和 test 使用中文描述test('拖拽滑块从 0 到 50% 位置值应为 50', ...)import { cleanupModals } from '../../../tests/testing'
afterEach(cleanupModals)
jest.useFakeTimers() 的测试必须在 afterEach 中恢复 jest.useRealTimers()cleanup() 即可transitionSpeed="none" 禁用 CSSTransition(timeout=0)waitFor 等待最终样式或 class 变化reduceMotion(那是 react-spring 的方案,fexd-mobile 使用 react-transition-group)DatePicker / DatePickerView / TimePicker 等依赖当前日期的组件,测试中必须固定"今天":
jest.useFakeTimers({ now: new Date('2025-06-15') })
// ... 测试代码 ...
jest.useRealTimers()
@testing-library/jest-dom 的语义断言getByRole > getByText > getByTestId > querySelectorgetByRole:Button → getByRole('button'),Switch 内部 <input type="checkbox">querySelector + class 是务实选择container.querySelector使用 useIOControl 的组件在不传 defaultXxx 时内部 value 为 undefined。 原则:测试时始终显式传入初始值。
编写 children 测试前,必须在源码中确认 {children} 出现在 JSX 返回值中。
每个 test() 只验证一个行为。避免在单个 test 中混合多个独立断言点。
| 原则 | 说明 |
|---|---|
| 黑盒优先 | 基于 prop/事件/渲染结果测试,不直接访问内部 state |
| 无纯快照 | 不使用 toMatchSnapshot(),所有断言都是明确的 |
| 不编造 Prop | 只测试 type.tsx 中定义的 prop |
| 真实交互 | 使用 userEvent / fireEvent 模拟用户操作 |
| 操作链优先 | 优先写多步操作序列,而非单次 click → 检查回调 |
| 覆盖率驱动 | 未覆盖的行必须有对应测试或注明无法覆盖的原因 |
| 独立运行 | 每个 test case 不依赖其他 test 的执行顺序 |
每个组件的 tests/TEST.md 是该组件测试状态的权威记录。任何测试文件变动都必须同步更新对应 TEST.md 中的测试场景、覆盖率、评分。
| 触发条件 | 需更新的 TEST.md 区域 |
|---|---|
| 新增 / 删除 / 修改测试用例 | 测试场景列表 + 覆盖率数据 + 评分 |
| 覆盖率发生变化 | 覆盖率数据 |
| 发现组件 Bug 导致 skip | 未覆盖说明 |
# {组件名} 测试概要
## 覆盖率
| 指标 | 值 |
| ---------- | --- |
| Statements | xx% |
| Branches | xx% |
| Functions | xx% |
| Lines | xx% |
验证命令: `npx jest --coverage --testPathPattern='exports/{组件名}/tests' --no-silent --collectCoverageFrom='packages/mobile/src/exports/{组件名}/index.tsx'`
## 测试场景
1. **场景名**:简述。 ...
## 评分
- 交互覆盖:x/5
- 分支覆盖:x/5
- 边界处理:x/5
- 场景真实度:x/5
- 综合评分:xx/20
## 未覆盖说明(如有)
- 说明未覆盖行的原因。
每次更新 TEST.md 前,必须运行覆盖率验证命令获取最新数据,不得估算或沿用旧值。
| 维度 | 5/5 标准 |
|---|---|
| 交互覆盖 | 所有可交互行为均有 userEvent / fireEvent 覆盖 |
| 分支覆盖 | 所有 prop 枚举值和条件分支均有对应 test |
| 边界处理 | 空值、极值、异常输入、ref 转发等边界场景 |
| 场景真实度 | 测试场景与实际业务使用方式一致 |
测试失败
├── 编译错误(import 不存在、类型不匹配)
│ └── → 测试问题:检查 import 路径和 prop 名称拼写
├── 渲染崩溃(TypeError / Cannot read property)
│ ├── 崩溃发生在测试代码中 → 测试问题:检查 mock 和 setup
│ └── 崩溃发生在组件源码中 → 组件 Bug:记录 FIXME.md 并 skip
├── 断言不匹配(Expected X, Received Y)
│ ├── DOM 选择器找不到元素 → 测试问题:检查 className / 角色名
│ ├── 组件行为与文档不符 → 组件 Bug:记录 FIXME.md 并 skip
│ └── 边界值行为不明确 → 对照源码确认预期行为后修正断言
├── 异步超时(exceeded timeout)
│ └── → 测试问题:检查是否需要 act()、waitFor()、fakeTimers
└── act() 警告
└── → 测试问题:用 act() 包裹状态更新,或用 waitFor() 等待异步
发现组件 Bug 时,在对应目录生成 FIXME.md:
# {NAME} 待修复问题
## 问题 N: 简述
- **现象**: 具体表现
- **原因**: 源码中的问题代码位置
- **建议方案**: 修复思路
- **修复置信度**: 高/中/低
同时在测试文件中标记 test.skip + BUG 注释。