@deepstorm/cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/dist/config-schema.json +1 -1
  2. package/dist/registry.json +65 -5
  3. package/dist/skills/reef-gen-frontend/variants/react/steps.md +31 -0
  4. package/dist/skills/reef-gen-frontend/variants/vue/steps.md +34 -0
  5. package/dist/skills/reef-style-frontend/fragments/test/vitest-react/examples/testing.md +283 -0
  6. package/dist/skills/reef-style-frontend/fragments/test/vitest-react/quick-reference.md +152 -0
  7. package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/examples/testing.md +457 -0
  8. package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/quick-reference.md +186 -0
  9. package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/examples/ui-components.md +257 -0
  10. package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/quick-reference.md +166 -0
  11. package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/examples/ui-components.md +353 -0
  12. package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/quick-reference.md +267 -0
  13. package/dist/skills/reef-style-frontend/variants/react/examples/code-wrapping.md +124 -0
  14. package/dist/skills/reef-style-frontend/variants/react/examples/component-types-pipes.md +195 -0
  15. package/dist/skills/reef-style-frontend/variants/react/examples/entity-types.md +127 -0
  16. package/dist/skills/reef-style-frontend/variants/react/examples/forms-layer.md +251 -0
  17. package/dist/skills/reef-style-frontend/variants/react/examples/service-routing.md +226 -0
  18. package/dist/skills/reef-style-frontend/variants/react/quick-reference.md +118 -0
  19. package/dist/skills/reef-style-frontend/variants/vue/examples/code-wrapping.md +162 -0
  20. package/dist/skills/reef-style-frontend/variants/vue/examples/component-types-pipes.md +270 -0
  21. package/dist/skills/reef-style-frontend/variants/vue/examples/entity-types.md +145 -0
  22. package/dist/skills/reef-style-frontend/variants/vue/examples/forms-layer.md +231 -0
  23. package/dist/skills/reef-style-frontend/variants/vue/examples/service-routing.md +224 -0
  24. package/dist/skills/reef-style-frontend/variants/vue/quick-reference.md +145 -0
  25. package/package.json +3 -1
@@ -25,7 +25,7 @@
25
25
  "properties": {
26
26
  "framework": {
27
27
  "type": "string",
28
- "enum": ["angular", "none"],
28
+ "enum": ["angular", "react", "none"],
29
29
  "description": "前端框架"
30
30
  },
31
31
  "tsConfig": {
@@ -114,6 +114,42 @@
114
114
  "skills/reef-gen-frontend/SKILL.md.tmpl"
115
115
  ]
116
116
  },
117
+ {
118
+ "value": "react",
119
+ "label": "React",
120
+ "template": {
121
+ "label": "React 19 + TypeScript + Ant Design + Tailwind CSS 4",
122
+ "buildTool": "npx:*",
123
+ "formatCmd": "npx",
124
+ "fileExt": "*.tsx / *.ts",
125
+ "sourcePath": "src/",
126
+ "styleRef": "→ 参考 [React 前端速查](quick-reference.md)"
127
+ },
128
+ "affectedTemplates": [
129
+ "skills/reef-style-frontend/SKILL.md.tmpl",
130
+ "agents/reef-review-frontend.md.tmpl",
131
+ "skills/reef-review/SKILL.md.tmpl",
132
+ "skills/reef-gen-frontend/SKILL.md.tmpl"
133
+ ]
134
+ },
135
+ {
136
+ "value": "vue",
137
+ "label": "Vue",
138
+ "template": {
139
+ "label": "Vue 3 + TypeScript + Ant Design Vue + Tailwind CSS 4",
140
+ "buildTool": "npx:*",
141
+ "formatCmd": "npx",
142
+ "fileExt": "*.vue / *.ts",
143
+ "sourcePath": "src/",
144
+ "styleRef": "→ 参考 [Vue 前端速查](quick-reference.md)"
145
+ },
146
+ "affectedTemplates": [
147
+ "skills/reef-style-frontend/SKILL.md.tmpl",
148
+ "agents/reef-review-frontend.md.tmpl",
149
+ "skills/reef-review/SKILL.md.tmpl",
150
+ "skills/reef-gen-frontend/SKILL.md.tmpl"
151
+ ]
152
+ },
117
153
  {
118
154
  "value": "none",
119
155
  "label": "不选择",
@@ -138,10 +174,6 @@
138
174
  "key": "reef.frontend.uiLibrary",
139
175
  "label": "UI 组件库",
140
176
  "type": "select",
141
- "dependsOn": {
142
- "key": "reef.frontend.framework",
143
- "value": "angular"
144
- },
145
177
  "options": [
146
178
  {
147
179
  "value": "primeng",
@@ -156,6 +188,32 @@
156
188
  "ui-lib/primeng"
157
189
  ]
158
190
  },
191
+ {
192
+ "value": "antd",
193
+ "label": "Ant Design (React)",
194
+ "template": {
195
+ "styleRef": "→ 参考 [Ant Design 规范](antd.md)"
196
+ },
197
+ "affectedTemplates": [
198
+ "skills/reef-style-frontend/SKILL.md.tmpl"
199
+ ],
200
+ "fragments": [
201
+ "ui-lib/antd"
202
+ ]
203
+ },
204
+ {
205
+ "value": "antd-vue",
206
+ "label": "Ant Design Vue",
207
+ "template": {
208
+ "styleRef": "→ 参考 [Ant Design Vue 规范](antd-vue.md)"
209
+ },
210
+ "affectedTemplates": [
211
+ "skills/reef-style-frontend/SKILL.md.tmpl"
212
+ ],
213
+ "fragments": [
214
+ "ui-lib/antd-vue"
215
+ ]
216
+ },
159
217
  {
160
218
  "value": "none",
161
219
  "label": "不使用 UI 组件库",
@@ -253,7 +311,9 @@
253
311
  "skills/reef-style-frontend/SKILL.md.tmpl"
254
312
  ],
255
313
  "fragments": [
256
- "test/vitest"
314
+ "test/vitest",
315
+ "test/vitest-react",
316
+ "test/vitest-vue"
257
317
  ]
258
318
  },
259
319
  {
@@ -0,0 +1,31 @@
1
+ # React 前端编码步骤
2
+
3
+ 按以下顺序逐块编写,依赖关系由前向后:
4
+
5
+ 1. **类型定义** — 实体接口/类型、Props 类型、API 响应类型
6
+ 2. **API Hooks** — 自定义 Hook 封装数据获取(loading / error / data 三态)
7
+ 3. **组件** — Ant Design 函数组件 + Hooks 状态管理
8
+ 4. **页面组装** — React Router 路由配置、Layout 页面布局
9
+
10
+ 每完成一块对照 `reef:reef-style-frontend` 中对应章节检查。
11
+
12
+ ## 核心约束
13
+
14
+ - 函数组件 + TypeScript(禁止 Class 组件)
15
+ - Props 使用 `interface` 定义(公共 API),联合类型使用 `type`
16
+ - 自定义 Hook 使用 `use` 前缀命名
17
+ - `useEffect` 依赖数组必须穷举闭包内所有响应值
18
+ - 数据获取层与 UI 渲染层分离(自定义 Hook 封装业务逻辑)
19
+ - Ant Design 组件按需导入(`import { Button } from 'antd'`)
20
+ - 路由组件使用 `React.lazy()` + `Suspense` 懒加载
21
+
22
+ ## 构建命令
23
+
24
+ ```bash
25
+ # 快速验证
26
+ npx tsc --noEmit
27
+ npx lint
28
+
29
+ # 完整检查
30
+ npx tsc --noEmit && npx lint && npx vitest run
31
+ ```
@@ -0,0 +1,34 @@
1
+ # Vue 3 前端编码步骤
2
+
3
+ 按以下顺序逐块编写,依赖关系由前向后:
4
+
5
+ 1. **类型定义** — 实体接口/类型、Props 类型(`defineProps<Props>()`)、Emits 类型(`defineEmits<{...}>()`)、API 响应类型
6
+ 2. **组合式函数** — 自定义 Composables 封装数据获取和业务逻辑(loading / error / data 三态),使用 `use` 前缀命名
7
+ 3. **组件** — `<script setup lang="ts">` + Ant Design Vue 组件 + 组合式 API 状态管理
8
+ 4. **页面组装** — Vue Router 路由配置(`createRouter` + `createWebHistory`)、懒加载、Layout 页面布局
9
+
10
+ 每完成一块对照 `reef:reef-style-frontend` 中对应章节检查。
11
+
12
+ ## 核心约束
13
+
14
+ - `<script setup lang="ts">` 语法(禁止 Options API)
15
+ - Props 使用 `defineProps<Props>()` 类型推断语法(公共 API),联合类型使用 `type`
16
+ - 事件使用 `defineEmits<{ event: [arg: type] }>()` 命名元组语法
17
+ - 组合式函数使用 `use` 前缀命名(`useAuth`、`usePagination`)
18
+ - 响应式状态优先使用 `ref()`,`reactive()` 仅用于不需要整体替换的固定结构
19
+ - 数据获取层与 UI 渲染层分离(组合式函数封装业务逻辑)
20
+ - Ant Design Vue 组件通过 `unplugin-vue-components` 自动按需导入(`<a-button>`、`<a-table>`)
21
+ - 页面组件使用动态 `import()` 实现懒加载
22
+ - 状态管理使用 Pinia(setup store 语法)
23
+ - 生命周期钩子中注册的监听器/定时器必须在 `onUnmounted` 中清理
24
+
25
+ ## 构建命令
26
+
27
+ ```bash
28
+ # 快速验证
29
+ npx vue-tsc --noEmit
30
+ npx lint
31
+
32
+ # 完整检查
33
+ npx vue-tsc --noEmit && npx lint && npx vitest run
34
+ ```
@@ -0,0 +1,283 @@
1
+ # React 组件和 Hook 测试代码示例
2
+
3
+ ## 组件渲染测试
4
+
5
+ ```tsx
6
+ // UserAvatar.test.tsx
7
+ import { render, screen } from '@testing-library/react'
8
+ import { describe, it, expect } from 'vitest'
9
+ import { UserAvatar } from './UserAvatar'
10
+
11
+ describe('UserAvatar', () => {
12
+ const mockUser = { username: 'alice', avatar: 'https://example.com/avatar.png' }
13
+
14
+ it('renders with avatar image', () => {
15
+ render(<UserAvatar user={mockUser} />)
16
+ const img = screen.getByAltText('alice')
17
+ expect(img).toBeInTheDocument()
18
+ expect(img).toHaveAttribute('src', mockUser.avatar)
19
+ })
20
+
21
+ it('renders with size classes', () => {
22
+ const { container } = render(<UserAvatar user={mockUser} size="lg" />)
23
+ // Ant Design Avatar 通过 size 属性控制尺寸
24
+ expect(container.querySelector('.ant-avatar')).toBeInTheDocument()
25
+ })
26
+
27
+ it('is clickable when onClick provided', async () => {
28
+ const handleClick = vi.fn()
29
+ render(<UserAvatar user={mockUser} onClick={handleClick} />)
30
+
31
+ await userEvent.click(screen.getByAltText('alice'))
32
+ expect(handleClick).toHaveBeenCalledTimes(1)
33
+ })
34
+ })
35
+ ```
36
+
37
+ ## 表单测试
38
+
39
+ ```tsx
40
+ // CreateUserForm.test.tsx
41
+ import { render, screen, waitFor } from '@testing-library/react'
42
+ import userEvent from '@testing-library/user-event'
43
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
44
+ import { CreateUserForm } from './CreateUserForm'
45
+
46
+ // Mock message
47
+ vi.mock('antd', async () => {
48
+ const actual = await vi.importActual('antd')
49
+ return { ...actual, message: { success: vi.fn(), error: vi.fn() } }
50
+ })
51
+
52
+ describe('CreateUserForm', () => {
53
+ beforeEach(() => {
54
+ vi.clearAllMocks()
55
+ })
56
+
57
+ it('renders all form fields', () => {
58
+ render(<CreateUserForm />)
59
+
60
+ expect(screen.getByLabelText('用户名')).toBeInTheDocument()
61
+ expect(screen.getByLabelText('邮箱')).toBeInTheDocument()
62
+ expect(screen.getByLabelText('角色')).toBeInTheDocument()
63
+ expect(screen.getByRole('button', { name: '创建' })).toBeInTheDocument()
64
+ })
65
+
66
+ it('shows validation errors on empty submit', async () => {
67
+ render(<CreateUserForm />)
68
+
69
+ await userEvent.click(screen.getByRole('button', { name: '创建' }))
70
+
71
+ await waitFor(() => {
72
+ expect(screen.getByText('请输入用户名')).toBeInTheDocument()
73
+ expect(screen.getByText('请输入邮箱')).toBeInTheDocument()
74
+ })
75
+ })
76
+
77
+ it('submits form with valid data', async () => {
78
+ const createUser = vi.fn().mockResolvedValue({ id: '1' })
79
+ render(<CreateUserForm onSubmit={createUser} />)
80
+
81
+ await userEvent.type(screen.getByLabelText('用户名'), 'newuser')
82
+ await userEvent.type(screen.getByLabelText('邮箱'), 'new@example.com')
83
+
84
+ // Ant Design Select 需要特殊处理
85
+ const roleSelect = screen.getByLabelText('角色')
86
+ await userEvent.click(roleSelect)
87
+ await userEvent.click(screen.getByText('编辑'))
88
+
89
+ await userEvent.click(screen.getByRole('button', { name: '创建' }))
90
+
91
+ await waitFor(() => {
92
+ expect(createUser).toHaveBeenCalledWith({
93
+ username: 'newuser',
94
+ email: 'new@example.com',
95
+ role: 'EDITOR',
96
+ })
97
+ })
98
+ })
99
+ })
100
+ ```
101
+
102
+ ## Hook 测试
103
+
104
+ ```tsx
105
+ // useUsers.test.ts
106
+ import { renderHook, act, waitFor } from '@testing-library/react'
107
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
108
+ import { useUsers } from './useUsers'
109
+
110
+ describe('useUsers', () => {
111
+ const mockUsers = [
112
+ { id: '1', username: 'alice', email: 'alice@example.com', role: 'ADMIN' },
113
+ ]
114
+
115
+ beforeEach(() => {
116
+ vi.clearAllMocks()
117
+ })
118
+
119
+ it('returns loading state initially', () => {
120
+ global.fetch = vi.fn().mockImplementation(() => new Promise(() => {})) // 永不 resolve
121
+
122
+ const { result } = renderHook(() => useUsers())
123
+
124
+ expect(result.current.loading).toBe(true)
125
+ expect(result.current.users).toEqual([])
126
+ expect(result.current.error).toBeNull()
127
+ })
128
+
129
+ it('returns users on successful fetch', async () => {
130
+ global.fetch = vi.fn().mockResolvedValue({
131
+ ok: true,
132
+ json: () => Promise.resolve({ content: mockUsers, totalElements: 1 }),
133
+ })
134
+
135
+ const { result } = renderHook(() => useUsers())
136
+
137
+ await waitFor(() => {
138
+ expect(result.current.loading).toBe(false)
139
+ })
140
+
141
+ expect(result.current.users).toEqual(mockUsers)
142
+ expect(result.current.error).toBeNull()
143
+ })
144
+
145
+ it('returns error on fetch failure', async () => {
146
+ global.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
147
+
148
+ const { result } = renderHook(() => useUsers())
149
+
150
+ await waitFor(() => {
151
+ expect(result.current.loading).toBe(false)
152
+ })
153
+
154
+ expect(result.current.error).toBeInstanceOf(Error)
155
+ expect(result.current.error?.message).toBe('Network error')
156
+ expect(result.current.users).toEqual([])
157
+ })
158
+
159
+ it('refresh resets loading state', async () => {
160
+ let resolvePromise!: (value: unknown) => void
161
+ global.fetch = vi.fn().mockImplementation(
162
+ () => new Promise((resolve) => { resolvePromise = resolve })
163
+ )
164
+
165
+ const { result } = renderHook(() => useUsers())
166
+
167
+ act(() => {
168
+ result.current.refresh()
169
+ })
170
+
171
+ // refresh should trigger a new fetch, loading goes back to true
172
+ expect(result.current.loading).toBe(true)
173
+ })
174
+ })
175
+ ```
176
+
177
+ ## Ant Design 组件集成测试
178
+
179
+ ```tsx
180
+ // UserTable.test.tsx
181
+ import { render, screen, within } from '@testing-library/react'
182
+ import userEvent from '@testing-library/user-event'
183
+ import { describe, it, expect, vi } from 'vitest'
184
+ import { Table } from 'antd'
185
+ import type { ColumnsType } from 'antd/es/table'
186
+
187
+ interface User {
188
+ id: string
189
+ username: string
190
+ email: string
191
+ role: string
192
+ }
193
+
194
+ describe('UserTable with Ant Design', () => {
195
+ const columns: ColumnsType<User> = [
196
+ { title: '用户名', dataIndex: 'username', key: 'username' },
197
+ { title: '邮箱', dataIndex: 'email', key: 'email' },
198
+ { title: '角色', dataIndex: 'role', key: 'role' },
199
+ ]
200
+
201
+ const mockData: User[] = [
202
+ { id: '1', username: 'alice', email: 'alice@example.com', role: 'ADMIN' },
203
+ { id: '2', username: 'bob', email: 'bob@example.com', role: 'EDITOR' },
204
+ ]
205
+
206
+ it('renders table with data', () => {
207
+ render(
208
+ <Table
209
+ columns={columns}
210
+ dataSource={mockData}
211
+ rowKey="id"
212
+ />
213
+ )
214
+
215
+ expect(screen.getByText('alice')).toBeInTheDocument()
216
+ expect(screen.getByText('bob')).toBeInTheDocument()
217
+ })
218
+
219
+ it('shows empty state when no data', () => {
220
+ render(
221
+ <Table
222
+ columns={columns}
223
+ dataSource={[]}
224
+ rowKey="id"
225
+ locale={{ emptyText: '暂无数据' }}
226
+ />
227
+ )
228
+
229
+ expect(screen.getByText('暂无数据')).toBeInTheDocument()
230
+ })
231
+
232
+ it('handles row click', async () => {
233
+ const handleRow = vi.fn()
234
+ render(
235
+ <Table
236
+ columns={columns}
237
+ dataSource={mockData}
238
+ rowKey="id"
239
+ onRow={() => ({
240
+ onClick: () => handleRow(),
241
+ })}
242
+ />
243
+ )
244
+
245
+ const rows = screen.getAllByRole('row')
246
+ // 第一行是 header, 第二行是第一个数据行
247
+ await userEvent.click(rows[1])
248
+ expect(handleRow).toHaveBeenCalled()
249
+ })
250
+ })
251
+ ```
252
+
253
+ ## 路由组件测试
254
+
255
+ ```tsx
256
+ // ProtectedRoute.test.tsx
257
+ import { render, screen } from '@testing-library/react'
258
+ import { MemoryRouter, Routes, Route } from 'react-router-dom'
259
+ import { describe, it, expect, vi } from 'vitest'
260
+
261
+ // Mock auth hook
262
+ vi.mock('../hooks/useAuth', () => ({
263
+ useAuth: () => ({ user: null, loading: false }),
264
+ }))
265
+
266
+ import { ProtectedRoute } from './ProtectedRoute'
267
+
268
+ describe('ProtectedRoute', () => {
269
+ it('redirects to login when not authenticated', () => {
270
+ render(
271
+ <MemoryRouter initialEntries={['/dashboard']}>
272
+ <Routes>
273
+ <Route path="/login" element={<div>登录页</div>} />
274
+ <Route path="/dashboard" element={<ProtectedRoute><div>仪表盘</div></ProtectedRoute>} />
275
+ </Routes>
276
+ </MemoryRouter>
277
+ )
278
+
279
+ expect(screen.getByText('登录页')).toBeInTheDocument()
280
+ expect(screen.queryByText('仪表盘')).not.toBeInTheDocument()
281
+ })
282
+ })
283
+ ```
@@ -0,0 +1,152 @@
1
+ # Vitest + React Testing Library 测试规范
2
+
3
+ ## 测试配置
4
+
5
+ ```ts
6
+ // vitest.config.ts
7
+ import { defineConfig } from 'vitest/config'
8
+ import react from '@vitejs/plugin-react'
9
+
10
+ export default defineConfig({
11
+ plugins: [react()],
12
+ test: {
13
+ globals: true,
14
+ environment: 'jsdom',
15
+ setupFiles: './src/test/setup.ts',
16
+ css: true,
17
+ coverage: {
18
+ provider: 'v8',
19
+ include: ['src/**/*.{ts,tsx}'],
20
+ exclude: ['src/**/*.d.ts', 'src/test/**'],
21
+ thresholds: {
22
+ branches: 80,
23
+ functions: 80,
24
+ lines: 80,
25
+ statements: 80,
26
+ },
27
+ },
28
+ },
29
+ })
30
+ ```
31
+
32
+ ```ts
33
+ // src/test/setup.ts
34
+ import '@testing-library/jest-dom'
35
+ ```
36
+
37
+ ## 通用原则
38
+
39
+ - 遵循 AAA 模式:Arrange(准备)→ Act(执行)→ Assert(断言)
40
+ - 测试应与实现解耦 — 通过用户可见的行为(文本、角色、aria-label)选择元素
41
+ - 测试用户交互而非组件内部状态
42
+ - 每个测试聚焦一个场景
43
+
44
+ ## 组件测试
45
+
46
+ ```tsx
47
+ import { render, screen } from '@testing-library/react'
48
+ import userEvent from '@testing-library/user-event'
49
+ import { describe, it, expect, vi } from 'vitest'
50
+
51
+ // ✅ 正确:通过文本寻找元素
52
+ render(<Button>提交</Button>)
53
+ expect(screen.getByRole('button', { name: '提交' })).toBeInTheDocument()
54
+
55
+ // ✅ 正确:模拟用户交互
56
+ await userEvent.click(screen.getByRole('button'))
57
+
58
+ // ❌ 避免:通过测试 ID 或实现细节选择
59
+ // screen.getByTestId('submit-btn') // 仅在无其他方式时使用
60
+ // wrapper.find('.btn-primary') // 禁止通过 CSS 类选择
61
+ ```
62
+
63
+ ## Hook 测试
64
+
65
+ ```tsx
66
+ import { renderHook, act } from '@testing-library/react'
67
+ import { describe, it, expect } from 'vitest'
68
+
69
+ describe('useCounter', () => {
70
+ it('should increment count', () => {
71
+ const { result } = renderHook(() => useCounter())
72
+
73
+ act(() => {
74
+ result.current.increment()
75
+ })
76
+
77
+ expect(result.current.count).toBe(1)
78
+ })
79
+ })
80
+ ```
81
+
82
+ ## 测试文件命名与位置
83
+
84
+ - 测试文件放在被测模块旁:`UserList.tsx` → `UserList.test.tsx`
85
+ - 通用测试放在 `src/test/` 目录
86
+ - Hook 测试:`useUsers.ts` → `useUsers.test.ts`
87
+
88
+ ## Mock 策略
89
+
90
+ ```tsx
91
+ // 全局 mock API 调用
92
+ import { vi } from 'vitest'
93
+
94
+ // Mock fetch
95
+ global.fetch = vi.fn()
96
+
97
+ // Mock 模块
98
+ vi.mock('antd', async () => {
99
+ const actual = await vi.importActual('antd')
100
+ return {
101
+ ...actual,
102
+ message: {
103
+ success: vi.fn(),
104
+ error: vi.fn(),
105
+ },
106
+ }
107
+ })
108
+
109
+ // Mock 自定义 Hook
110
+ vi.mock('../hooks/useUsers', () => ({
111
+ useUsers: () => ({
112
+ users: mockUsers,
113
+ loading: false,
114
+ error: null,
115
+ refresh: vi.fn(),
116
+ }),
117
+ }))
118
+ ```
119
+
120
+ ## 异步测试
121
+
122
+ ```tsx
123
+ it('should load and display users', async () => {
124
+ // Arrange
125
+ const mockUsers = [
126
+ { id: '1', username: 'alice', email: 'alice@example.com', role: 'ADMIN' },
127
+ { id: '2', username: 'bob', email: 'bob@example.com', role: 'EDITOR' },
128
+ ]
129
+
130
+ // 模拟 API 响应
131
+ const mockFetch = vi.fn().mockResolvedValue({
132
+ ok: true,
133
+ json: () => Promise.resolve({ content: mockUsers, totalElements: 2 }),
134
+ })
135
+ global.fetch = mockFetch
136
+
137
+ // Act
138
+ render(<UserList />)
139
+
140
+ // Assert — 等待异步渲染完成
141
+ expect(await screen.findByText('alice')).toBeInTheDocument()
142
+ expect(screen.getByText('bob')).toBeInTheDocument()
143
+ expect(screen.getByText('ADMIN')).toBeInTheDocument()
144
+ })
145
+ ```
146
+
147
+ ## 覆盖率要求
148
+
149
+ - 功能性组件覆盖率达到 80%+(branches, functions, lines)
150
+ - 核心业务逻辑的 Hook 需要 100% 覆盖
151
+ - 工具函数/纯函数需要 100% 覆盖
152
+ - UI 展示组件(页面布局)可低于 80%,但至少覆盖渲染正常路径和空状态