@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.
- package/dist/config-schema.json +1 -1
- package/dist/registry.json +65 -5
- package/dist/skills/reef-gen-frontend/variants/react/steps.md +31 -0
- package/dist/skills/reef-gen-frontend/variants/vue/steps.md +34 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-react/examples/testing.md +283 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-react/quick-reference.md +152 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/examples/testing.md +457 -0
- package/dist/skills/reef-style-frontend/fragments/test/vitest-vue/quick-reference.md +186 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/examples/ui-components.md +257 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd/quick-reference.md +166 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/examples/ui-components.md +353 -0
- package/dist/skills/reef-style-frontend/fragments/ui-lib/antd-vue/quick-reference.md +267 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/code-wrapping.md +124 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/component-types-pipes.md +195 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/entity-types.md +127 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/forms-layer.md +251 -0
- package/dist/skills/reef-style-frontend/variants/react/examples/service-routing.md +226 -0
- package/dist/skills/reef-style-frontend/variants/react/quick-reference.md +118 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/code-wrapping.md +162 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/component-types-pipes.md +270 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/entity-types.md +145 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/forms-layer.md +231 -0
- package/dist/skills/reef-style-frontend/variants/vue/examples/service-routing.md +224 -0
- package/dist/skills/reef-style-frontend/variants/vue/quick-reference.md +145 -0
- package/package.json +3 -1
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# Vue 组件和组合式函数测试代码示例
|
|
2
|
+
|
|
3
|
+
## 组件渲染测试
|
|
4
|
+
|
|
5
|
+
```vue
|
|
6
|
+
<!-- UserAvatar.vue -->
|
|
7
|
+
<script setup lang="ts">
|
|
8
|
+
interface Props {
|
|
9
|
+
username: string
|
|
10
|
+
avatar?: string
|
|
11
|
+
size?: 'sm' | 'md' | 'lg'
|
|
12
|
+
onClick?: () => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
16
|
+
size: 'md',
|
|
17
|
+
})
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<a-avatar
|
|
22
|
+
:src="avatar"
|
|
23
|
+
:alt="username"
|
|
24
|
+
:size="size === 'sm' ? 24 : size === 'md' ? 32 : 48"
|
|
25
|
+
:style="{ cursor: onClick ? 'pointer' : 'default' }"
|
|
26
|
+
@click="onClick"
|
|
27
|
+
/>
|
|
28
|
+
</template>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// UserAvatar.test.ts
|
|
33
|
+
import { mount } from '@vue/test-utils'
|
|
34
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
35
|
+
import UserAvatar from './UserAvatar.vue'
|
|
36
|
+
|
|
37
|
+
describe('UserAvatar', () => {
|
|
38
|
+
const mockUser = { username: 'alice', avatar: 'https://example.com/avatar.png' }
|
|
39
|
+
|
|
40
|
+
it('renders with avatar image', () => {
|
|
41
|
+
const wrapper = mount(UserAvatar, {
|
|
42
|
+
props: {
|
|
43
|
+
username: mockUser.username,
|
|
44
|
+
avatar: mockUser.avatar,
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const img = wrapper.find('img')
|
|
49
|
+
expect(img.attributes('alt')).toBe('alice')
|
|
50
|
+
expect(img.attributes('src')).toBe(mockUser.avatar)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('renders with size classes', () => {
|
|
54
|
+
const wrapper = mount(UserAvatar, {
|
|
55
|
+
props: {
|
|
56
|
+
username: mockUser.username,
|
|
57
|
+
size: 'lg',
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// Ant Design Vue Avatar 通过 size 属性控制尺寸
|
|
62
|
+
expect(wrapper.findComponent({ name: 'AAvatar' }).exists()).toBe(true)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('is clickable when onClick provided', async () => {
|
|
66
|
+
const handleClick = vi.fn()
|
|
67
|
+
const wrapper = mount(UserAvatar, {
|
|
68
|
+
props: {
|
|
69
|
+
username: mockUser.username,
|
|
70
|
+
onClick: handleClick,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await wrapper.findComponent({ name: 'AAvatar' }).trigger('click')
|
|
75
|
+
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 表单测试
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<!-- CreateUserForm.vue -->
|
|
84
|
+
<script setup lang="ts">
|
|
85
|
+
import { reactive, ref } from 'vue'
|
|
86
|
+
import { message } from 'ant-design-vue'
|
|
87
|
+
|
|
88
|
+
interface FormState {
|
|
89
|
+
username: string
|
|
90
|
+
email: string
|
|
91
|
+
role: string | undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const emit = defineEmits<{
|
|
95
|
+
success: []
|
|
96
|
+
}>()
|
|
97
|
+
|
|
98
|
+
const formState = reactive<FormState>({
|
|
99
|
+
username: '',
|
|
100
|
+
email: '',
|
|
101
|
+
role: undefined,
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const loading = ref(false)
|
|
105
|
+
const formRef = ref()
|
|
106
|
+
|
|
107
|
+
const rules = {
|
|
108
|
+
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
|
109
|
+
email: [
|
|
110
|
+
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
|
111
|
+
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' },
|
|
112
|
+
],
|
|
113
|
+
role: [{ required: true, message: '请选择角色', trigger: 'change' }],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handleSubmit() {
|
|
117
|
+
try {
|
|
118
|
+
await formRef.value?.validate()
|
|
119
|
+
loading.value = true
|
|
120
|
+
|
|
121
|
+
const res = await fetch('/api/v1/users', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify(formState),
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (!res.ok) throw new Error('创建失败')
|
|
128
|
+
message.success('用户创建成功')
|
|
129
|
+
emit('success')
|
|
130
|
+
} catch (e) {
|
|
131
|
+
if (e instanceof Error) message.error(e.message)
|
|
132
|
+
} finally {
|
|
133
|
+
loading.value = false
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
</script>
|
|
137
|
+
|
|
138
|
+
<template>
|
|
139
|
+
<a-form
|
|
140
|
+
ref="formRef"
|
|
141
|
+
:model="formState"
|
|
142
|
+
:rules="rules"
|
|
143
|
+
:label-col="{ span: 6 }"
|
|
144
|
+
:wrapper-col="{ span: 16 }"
|
|
145
|
+
@finish="handleSubmit"
|
|
146
|
+
>
|
|
147
|
+
<a-form-item label="用户名" name="username">
|
|
148
|
+
<a-input v-model:value="formState.username" />
|
|
149
|
+
</a-form-item>
|
|
150
|
+
|
|
151
|
+
<a-form-item label="邮箱" name="email">
|
|
152
|
+
<a-input v-model:value="formState.email" />
|
|
153
|
+
</a-form-item>
|
|
154
|
+
|
|
155
|
+
<a-form-item label="角色" name="role">
|
|
156
|
+
<a-select v-model:value="formState.role">
|
|
157
|
+
<a-select-option value="EDITOR">编辑</a-select-option>
|
|
158
|
+
</a-select>
|
|
159
|
+
</a-form-item>
|
|
160
|
+
|
|
161
|
+
<a-form-item :wrapper-col="{ offset: 6, span: 16 }">
|
|
162
|
+
<a-button type="primary" html-type="submit" :loading="loading">
|
|
163
|
+
创建
|
|
164
|
+
</a-button>
|
|
165
|
+
</a-form-item>
|
|
166
|
+
</a-form>
|
|
167
|
+
</template>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// CreateUserForm.test.ts
|
|
172
|
+
import { mount } from '@vue/test-utils'
|
|
173
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
174
|
+
import { nextTick } from 'vue'
|
|
175
|
+
import CreateUserForm from './CreateUserForm.vue'
|
|
176
|
+
|
|
177
|
+
// Mock Ant Design Vue message
|
|
178
|
+
vi.mock('ant-design-vue', async () => {
|
|
179
|
+
const actual = await vi.importActual('ant-design-vue')
|
|
180
|
+
return { ...actual, message: { success: vi.fn(), error: vi.fn() } }
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
describe('CreateUserForm', () => {
|
|
184
|
+
beforeEach(() => {
|
|
185
|
+
vi.clearAllMocks()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('renders all form fields', () => {
|
|
189
|
+
const wrapper = mount(CreateUserForm)
|
|
190
|
+
|
|
191
|
+
expect(wrapper.text()).toContain('用户名')
|
|
192
|
+
expect(wrapper.text()).toContain('邮箱')
|
|
193
|
+
expect(wrapper.text()).toContain('角色')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('shows validation errors on empty submit', async () => {
|
|
197
|
+
const wrapper = mount(CreateUserForm)
|
|
198
|
+
|
|
199
|
+
await wrapper.find('form').trigger('submit')
|
|
200
|
+
await nextTick()
|
|
201
|
+
|
|
202
|
+
expect(wrapper.text()).toContain('请输入用户名')
|
|
203
|
+
expect(wrapper.text()).toContain('请输入邮箱')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('submits form with valid data', async () => {
|
|
207
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
208
|
+
ok: true,
|
|
209
|
+
json: () => Promise.resolve({ id: '1' }),
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const wrapper = mount(CreateUserForm)
|
|
213
|
+
|
|
214
|
+
// 填充表单
|
|
215
|
+
await wrapper.find('input').setValue('newuser')
|
|
216
|
+
const inputs = wrapper.findAll('input')
|
|
217
|
+
await inputs[1].setValue('new@example.com')
|
|
218
|
+
|
|
219
|
+
// 提交
|
|
220
|
+
await wrapper.find('form').trigger('submit')
|
|
221
|
+
await nextTick()
|
|
222
|
+
|
|
223
|
+
expect(global.fetch).toHaveBeenCalled()
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## 组合式函数测试
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
// useUsers.test.ts
|
|
232
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
233
|
+
import { withSetup } from '@/test/utils'
|
|
234
|
+
|
|
235
|
+
// withSetup 辅助函数:在测试环境中执行组合式函数
|
|
236
|
+
function withSetup<T>(hook: () => T): T {
|
|
237
|
+
let result: T
|
|
238
|
+
const wrapper = defineComponent({
|
|
239
|
+
setup() {
|
|
240
|
+
result = hook()
|
|
241
|
+
return () => null
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
mount(wrapper)
|
|
245
|
+
return result!
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
describe('useUsers', () => {
|
|
249
|
+
const mockUsers = [
|
|
250
|
+
{ id: '1', username: 'alice', email: 'alice@example.com', role: 'ADMIN' },
|
|
251
|
+
]
|
|
252
|
+
|
|
253
|
+
beforeEach(() => {
|
|
254
|
+
vi.clearAllMocks()
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('returns loading state initially', () => {
|
|
258
|
+
global.fetch = vi.fn().mockImplementation(() => new Promise(() => {}))
|
|
259
|
+
|
|
260
|
+
const { loading, users, error } = useUsers()
|
|
261
|
+
|
|
262
|
+
expect(loading.value).toBe(true)
|
|
263
|
+
expect(users.value).toEqual([])
|
|
264
|
+
expect(error.value).toBeNull()
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('returns users on successful fetch', async () => {
|
|
268
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
269
|
+
ok: true,
|
|
270
|
+
json: () => Promise.resolve({ content: mockUsers, totalElements: 1 }),
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
// 在 setup 中调用组合式函数
|
|
274
|
+
let result: ReturnType<typeof useUsers>
|
|
275
|
+
mount(
|
|
276
|
+
defineComponent({
|
|
277
|
+
setup() {
|
|
278
|
+
result = useUsers()
|
|
279
|
+
return () => null
|
|
280
|
+
},
|
|
281
|
+
})
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
await vi.waitFor(() => {
|
|
285
|
+
expect(result!.loading.value).toBe(false)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
expect(result!.users.value).toEqual(mockUsers)
|
|
289
|
+
expect(result!.error.value).toBeNull()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('returns error on fetch failure', async () => {
|
|
293
|
+
global.fetch = vi.fn().mockRejectedValue(new Error('Network error'))
|
|
294
|
+
|
|
295
|
+
let result: ReturnType<typeof useUsers>
|
|
296
|
+
mount(
|
|
297
|
+
defineComponent({
|
|
298
|
+
setup() {
|
|
299
|
+
result = useUsers()
|
|
300
|
+
return () => null
|
|
301
|
+
},
|
|
302
|
+
})
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
await vi.waitFor(() => {
|
|
306
|
+
expect(result!.loading.value).toBe(false)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
expect(result!.error.value).toBeInstanceOf(Error)
|
|
310
|
+
expect(result!.error.value?.message).toBe('Network error')
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('refresh resets loading state', async () => {
|
|
314
|
+
let resolvePromise!: (value: unknown) => void
|
|
315
|
+
global.fetch = vi.fn().mockImplementation(
|
|
316
|
+
() => new Promise((resolve) => { resolvePromise = resolve })
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
let result: ReturnType<typeof useUsers>
|
|
320
|
+
mount(
|
|
321
|
+
defineComponent({
|
|
322
|
+
setup() {
|
|
323
|
+
result = useUsers()
|
|
324
|
+
return () => null
|
|
325
|
+
},
|
|
326
|
+
})
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
result!.refresh()
|
|
330
|
+
expect(result!.loading.value).toBe(true)
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Ant Design Vue 组件集成测试
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// UserTable.test.ts
|
|
339
|
+
import { mount } from '@vue/test-utils'
|
|
340
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
341
|
+
import { defineComponent } from 'vue'
|
|
342
|
+
import { Table } from 'ant-design-vue'
|
|
343
|
+
|
|
344
|
+
interface User {
|
|
345
|
+
id: string
|
|
346
|
+
username: string
|
|
347
|
+
email: string
|
|
348
|
+
role: string
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
describe('UserTable with Ant Design Vue', () => {
|
|
352
|
+
const columns = [
|
|
353
|
+
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
|
354
|
+
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
|
355
|
+
{ title: '角色', dataIndex: 'role', key: 'role' },
|
|
356
|
+
]
|
|
357
|
+
|
|
358
|
+
const mockData: User[] = [
|
|
359
|
+
{ id: '1', username: 'alice', email: 'alice@example.com', role: 'ADMIN' },
|
|
360
|
+
{ id: '2', username: 'bob', email: 'bob@example.com', role: 'EDITOR' },
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
it('renders table with data', () => {
|
|
364
|
+
const wrapper = mount(
|
|
365
|
+
defineComponent({
|
|
366
|
+
template: `
|
|
367
|
+
<a-table
|
|
368
|
+
:data-source="data"
|
|
369
|
+
:columns="columns"
|
|
370
|
+
row-key="id"
|
|
371
|
+
/>
|
|
372
|
+
`,
|
|
373
|
+
components: { ATable: Table },
|
|
374
|
+
setup: () => ({ data: mockData, columns }),
|
|
375
|
+
})
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
expect(wrapper.text()).toContain('alice')
|
|
379
|
+
expect(wrapper.text()).toContain('bob')
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('shows empty state when no data', () => {
|
|
383
|
+
const wrapper = mount(
|
|
384
|
+
defineComponent({
|
|
385
|
+
template: `
|
|
386
|
+
<a-table
|
|
387
|
+
:data-source="[]"
|
|
388
|
+
:columns="columns"
|
|
389
|
+
row-key="id"
|
|
390
|
+
:locale="{ emptyText: '暂无数据' }"
|
|
391
|
+
/>
|
|
392
|
+
`,
|
|
393
|
+
components: { ATable: Table },
|
|
394
|
+
setup: () => ({ columns }),
|
|
395
|
+
})
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
expect(wrapper.text()).toContain('暂无数据')
|
|
399
|
+
})
|
|
400
|
+
})
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## 路由组件测试
|
|
404
|
+
|
|
405
|
+
```vue
|
|
406
|
+
<!-- ProtectedRoute.vue -->
|
|
407
|
+
<script setup lang="ts">
|
|
408
|
+
import { useAuthStore } from '@/stores/auth'
|
|
409
|
+
import { useRouter } from 'vue-router'
|
|
410
|
+
|
|
411
|
+
const auth = useAuthStore()
|
|
412
|
+
const router = useRouter()
|
|
413
|
+
|
|
414
|
+
if (!auth.isAuthenticated) {
|
|
415
|
+
router.replace({ name: 'login' })
|
|
416
|
+
}
|
|
417
|
+
</script>
|
|
418
|
+
|
|
419
|
+
<template>
|
|
420
|
+
<slot v-if="auth.isAuthenticated" />
|
|
421
|
+
</template>
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// ProtectedRoute.test.ts
|
|
426
|
+
import { mount } from '@vue/test-utils'
|
|
427
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
428
|
+
import { createRouter, createWebHistory } from 'vue-router'
|
|
429
|
+
import { createTestingPinia } from '@pinia/testing'
|
|
430
|
+
import ProtectedRoute from './ProtectedRoute.vue'
|
|
431
|
+
|
|
432
|
+
describe('ProtectedRoute', () => {
|
|
433
|
+
it('redirects to login when not authenticated', () => {
|
|
434
|
+
const wrapper = mount(ProtectedRoute, {
|
|
435
|
+
global: {
|
|
436
|
+
plugins: [
|
|
437
|
+
createTestingPinia({
|
|
438
|
+
createSpy: vi.fn,
|
|
439
|
+
initialState: {
|
|
440
|
+
auth: { user: null, token: null },
|
|
441
|
+
},
|
|
442
|
+
}),
|
|
443
|
+
createRouter({
|
|
444
|
+
history: createWebHistory(),
|
|
445
|
+
routes: [
|
|
446
|
+
{ path: '/', name: 'home', component: { template: '<div>Home</div>' } },
|
|
447
|
+
{ path: '/login', name: 'login', component: { template: '<div>Login</div>' } },
|
|
448
|
+
],
|
|
449
|
+
}),
|
|
450
|
+
],
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
expect(wrapper.text()).not.toContain('Protected Content')
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Vitest + Vue Test Utils 测试规范
|
|
2
|
+
|
|
3
|
+
## 测试配置
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// vitest.config.ts
|
|
7
|
+
import { defineConfig } from 'vitest/config'
|
|
8
|
+
import vue from '@vitejs/plugin-vue'
|
|
9
|
+
|
|
10
|
+
export default defineConfig({
|
|
11
|
+
plugins: [vue()],
|
|
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/**/*.{vue,ts}'],
|
|
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
|
+
```typescript
|
|
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
|
+
## 组件测试(Vue Test Utils)
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { mount } from '@vue/test-utils'
|
|
48
|
+
import { describe, it, expect } from 'vitest'
|
|
49
|
+
|
|
50
|
+
// ✅ 正确:通过文本寻找元素
|
|
51
|
+
const wrapper = mount(MyComponent)
|
|
52
|
+
expect(wrapper.text()).toContain('提交')
|
|
53
|
+
|
|
54
|
+
// ✅ 正确:模拟用户交互
|
|
55
|
+
await wrapper.find('button').trigger('click')
|
|
56
|
+
|
|
57
|
+
// ✅ 正确:通过 findComponent 查找子组件
|
|
58
|
+
expect(wrapper.findComponent({ name: 'AButton' }).exists()).toBe(true)
|
|
59
|
+
|
|
60
|
+
// ❌ 避免:通过 CSS 类名选择
|
|
61
|
+
// wrapper.find('.btn-primary') // 禁止
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Hook / 组合式函数测试
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
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.vue` → `UserList.test.ts`
|
|
85
|
+
- 通用测试放在 `src/test/` 目录
|
|
86
|
+
- 组合式函数测试:`useUsers.ts` → `useUsers.test.ts`
|
|
87
|
+
|
|
88
|
+
## Mock 策略
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { vi } from 'vitest'
|
|
92
|
+
|
|
93
|
+
// Mock fetch
|
|
94
|
+
global.fetch = vi.fn()
|
|
95
|
+
|
|
96
|
+
// Mock 模块
|
|
97
|
+
vi.mock('ant-design-vue', async () => {
|
|
98
|
+
const actual = await vi.importActual('ant-design-vue')
|
|
99
|
+
return {
|
|
100
|
+
...actual,
|
|
101
|
+
message: {
|
|
102
|
+
success: vi.fn(),
|
|
103
|
+
error: vi.fn(),
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Mock 组合式函数
|
|
109
|
+
vi.mock('@/composables/useUsers', () => ({
|
|
110
|
+
useUsers: () => ({
|
|
111
|
+
users: mockUsers,
|
|
112
|
+
loading: false,
|
|
113
|
+
error: null,
|
|
114
|
+
refresh: vi.fn(),
|
|
115
|
+
}),
|
|
116
|
+
}))
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## 异步测试
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
it('should load and display users', async () => {
|
|
123
|
+
// Arrange
|
|
124
|
+
const mockUsers = [
|
|
125
|
+
{ id: '1', username: 'alice', email: 'alice@example.com', role: 'ADMIN' },
|
|
126
|
+
{ id: '2', username: 'bob', email: 'bob@example.com', role: 'EDITOR' },
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
// 模拟 API 响应
|
|
130
|
+
const mockFetch = vi.fn().mockResolvedValue({
|
|
131
|
+
ok: true,
|
|
132
|
+
json: () => Promise.resolve({ content: mockUsers, totalElements: 2 }),
|
|
133
|
+
})
|
|
134
|
+
global.fetch = mockFetch
|
|
135
|
+
|
|
136
|
+
// Act
|
|
137
|
+
const wrapper = mount(UserList)
|
|
138
|
+
|
|
139
|
+
// Assert — 等待异步渲染完成
|
|
140
|
+
await nextTick()
|
|
141
|
+
expect(wrapper.text()).toContain('alice')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
// @testing-library/vue 方案(自动等待)
|
|
145
|
+
import { render, screen } from '@testing-library/vue'
|
|
146
|
+
|
|
147
|
+
it('should display users with testing-library', async () => {
|
|
148
|
+
render(UserList)
|
|
149
|
+
expect(await screen.findByText('alice')).toBeInTheDocument()
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Pinia 测试
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { mount } from '@vue/test-utils'
|
|
157
|
+
import { createTestingPinia } from '@pinia/testing'
|
|
158
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
159
|
+
import UserList from '@/views/UserList.vue'
|
|
160
|
+
|
|
161
|
+
describe('UserList with Pinia', () => {
|
|
162
|
+
it('renders loading state', () => {
|
|
163
|
+
const wrapper = mount(UserList, {
|
|
164
|
+
global: {
|
|
165
|
+
plugins: [
|
|
166
|
+
createTestingPinia({
|
|
167
|
+
createSpy: vi.fn,
|
|
168
|
+
initialState: {
|
|
169
|
+
auth: { user: null, loading: true },
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
expect(wrapper.findComponent({ name: 'ASpin' }).exists()).toBe(true)
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 覆盖率要求
|
|
182
|
+
|
|
183
|
+
- 功能性组件覆盖率达到 80%+(branches, functions, lines)
|
|
184
|
+
- 核心业务逻辑的组合式函数需要 100% 覆盖
|
|
185
|
+
- 工具函数/纯函数需要 100% 覆盖
|
|
186
|
+
- UI 展示组件(页面布局)可低于 80%,但至少覆盖渲染正常路径和空状态
|