@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
@@ -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%,但至少覆盖渲染正常路径和空状态