Skip to content

Commit 88668cf

Browse files
hiveerHiveerLi
andauthored
feat(tests): more unit test (#1309)
Co-authored-by: HiveerLi <[email protected]>
1 parent 33ef268 commit 88668cf

File tree

6 files changed

+471
-21
lines changed

6 files changed

+471
-21
lines changed

.codesouler/rules/vue-unit-test-guide.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,95 @@ vi.mock('element-plus', () => ({
8383
info: vi.fn(),
8484
},
8585
ElMessageBox: {
86-
confirm: vi.fn((message, title, options) => Promise.resolve())
86+
confirm: vi.fn(() => Promise.resolve()) // 简化确认对话框mock
8787
}
8888
}))
8989
```
9090

91+
### 使用 vi.hoisted mock useFetchApi
92+
```javascript
93+
let { useFetchApiMock } = vi.hoisted(() => {
94+
return {
95+
useFetchApiMock: vi.fn(() => ({
96+
json: () => Promise.resolve({ data: { value: {} }, error: { value: null } }),
97+
get: () => ({
98+
json: () => Promise.resolve({ data: { value: {} }, error: { value: null } })
99+
}),
100+
post: () => ({
101+
json: () => Promise.resolve({ data: { value: {} }, error: { value: null } })
102+
}),
103+
put: () => ({
104+
json: () => Promise.resolve({
105+
data: { value: { token: 'new-token', msg: 'Success' } },
106+
error: { value: null }
107+
})
108+
})
109+
}))
110+
}
111+
})
112+
113+
vi.mock('@/packs/useFetchApi', () => ({
114+
default: useFetchApiMock
115+
}))
116+
```
117+
118+
#### 使用说明:
119+
1. **vi.hoisted** 用于将mock变量提升到文件顶部,确保在mock时可用
120+
2. 可以模拟各种HTTP方法(get/post/put等)的响应
121+
3. 每个方法返回的json() Promise可以自定义数据结构和错误状态
122+
4. 支持在测试用例中动态修改mock实现:
123+
```javascript
124+
// 在特定测试用例中覆盖默认mock
125+
useFetchApiMock.mockImplementationOnce(() => ({
126+
get: () => ({
127+
json: () => Promise.resolve({
128+
data: { value: customData },
129+
error: { value: customError }
130+
})
131+
})
132+
}))
133+
```
134+
135+
### 测试经验总结:SyncAccessTokenSettings组件
136+
137+
1. **API调用测试要点**
138+
- 使用`vi.fn()` mock API函数,模拟不同响应场景
139+
- 测试成功/失败/空数据等边界情况
140+
- 验证API调用参数和次数
141+
142+
2. **UI交互测试要点**
143+
- 测试不同状态下的UI渲染(有Token/无Token)
144+
- 验证按钮点击事件触发
145+
- 测试表单元素交互
146+
147+
3. **异步操作测试**
148+
- 使用`await nextTick()`等待DOM更新
149+
- 对异步API调用使用`await`等待完成
150+
- 验证异步操作后的UI状态
151+
152+
4. **第三方库mock技巧**
153+
```javascript
154+
// mock clipboard库
155+
vi.mock('@/packs/clipboard', () => ({
156+
copyToClipboard: vi.fn()
157+
}))
158+
159+
// mock uuid库
160+
vi.mock('uuid', () => ({
161+
v4: vi.fn(() => 'mocked-uuid') // 返回固定值便于断言
162+
}))
163+
```
164+
165+
5. **组件方法测试**
166+
- 测试`copyToken`方法是否调用剪贴板API
167+
- 测试`refreshAccessToken`的确认流程
168+
- 验证错误处理逻辑
169+
170+
6. **最佳实践**
171+
- 每个测试用例后使用`vi.clearAllMocks()`
172+
- 为每个测试场景编写描述性名称
173+
- 保持测试独立,不依赖其他测试的状态
174+
91175
### use vi.hoisted to mock useFetchApi Function
92176
```javascript
93177
let { useFetchApiMock } = vi.hoisted(() => {

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,5 @@ app.log
4848
frontend/coverage
4949

5050
.idea/
51+
52+
frontend/junit-report.xml
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { mount } from '@vue/test-utils'
2+
import CollectionsSettings from '@/components/collections/CollectionsSettings.vue'
3+
import { ElMessage, ElMessageBox } from 'element-plus'
4+
import { nextTick } from 'vue'
5+
6+
// Mock 依赖项
7+
vi.mock('element-plus', () => ({
8+
ElMessage: {
9+
warning: vi.fn(),
10+
success: vi.fn(),
11+
error: vi.fn()
12+
},
13+
ElMessageBox: {
14+
confirm: vi.fn(() => Promise.resolve())
15+
}
16+
}))
17+
18+
const { mockPut, mockDelete, useFetchApiMock } = vi.hoisted(() => {
19+
const mockPut = vi.fn(() => Promise.resolve({ data: { value: {} }, error: { value: null } }))
20+
const mockDelete = vi.fn(() => Promise.resolve({ data: { value: {} }, error: { value: null } }))
21+
22+
return {
23+
mockPut,
24+
mockDelete,
25+
useFetchApiMock: vi.fn(() => ({
26+
put: () => ({ json: mockPut }),
27+
delete: () => ({ json: mockDelete })
28+
}))
29+
}
30+
})
31+
32+
vi.mock('@/packs/useFetchApi', () => ({
33+
default: useFetchApiMock
34+
}))
35+
36+
describe('CollectionsSettings.vue', () => {
37+
const mockCollection = {
38+
name: 'test-collection',
39+
username: 'test-user',
40+
nickname: 'Test Collection',
41+
description: 'Test description',
42+
privateVisibility: false,
43+
theme: '#F5F3FF'
44+
}
45+
46+
const createWrapper = (props = {}) => {
47+
return mount(CollectionsSettings, {
48+
props: {
49+
collection: mockCollection,
50+
userName: 'test-user',
51+
collectionsId: '123',
52+
...props
53+
},
54+
global: {
55+
provide: {
56+
fetchCollectionDetail: vi.fn()
57+
},
58+
mocks: {
59+
$t: (key) => key
60+
}
61+
},
62+
attachTo: document.body // 确保组件挂载到DOM
63+
})
64+
}
65+
66+
beforeEach(() => {
67+
vi.clearAllMocks()
68+
mockPut.mockResolvedValue({ data: { value: {} }, error: { value: null } })
69+
mockDelete.mockResolvedValue({ data: { value: {} }, error: { value: null } })
70+
})
71+
72+
it('正确渲染初始状态', async () => {
73+
const wrapper = createWrapper()
74+
await nextTick()
75+
76+
// 使用更可靠的选择器,找到第一个el-input组件(集合名称输入框)
77+
const nicknameInputs = wrapper.findAll('.el-input__inner')
78+
const nicknameInput = nicknameInputs[0]
79+
80+
expect(nicknameInput.exists()).toBe(true)
81+
expect(nicknameInput.element.value).toBe(mockCollection.nickname)
82+
})
83+
84+
it('更新集合名称和描述', async () => {
85+
const wrapper = createWrapper()
86+
const updateButtons = wrapper.findAll('.btn-secondary-gray')
87+
const nicknameInputs = wrapper.findAll('.el-input__inner')
88+
89+
// 使用第一个输入框(集合名称)
90+
await nicknameInputs[0].setValue('New Name')
91+
await updateButtons[0].trigger('click')
92+
93+
expect(useFetchApiMock).toHaveBeenCalledWith(
94+
'/collections/123',
95+
expect.objectContaining({
96+
body: JSON.stringify({
97+
description: 'Test description',
98+
name: 'test-collection',
99+
nickname: 'New Name',
100+
private: false,
101+
theme: '#F5F3FF'
102+
})
103+
})
104+
)
105+
})
106+
107+
it('变更可见性时显示确认对话框', async () => {
108+
const wrapper = createWrapper()
109+
// 找到所有ElSelect组件,第二个是可见性选择器
110+
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
111+
const visibilitySelect = selects[1]
112+
await visibilitySelect.vm.$emit('select', 'true')
113+
114+
const elMessageBox = wrapper.findAllComponents({ name: 'ElMessageBox' })
115+
expect(elMessageBox).toBeTruthy()
116+
})
117+
118+
it('删除集合需要正确输入名称', async () => {
119+
const wrapper = createWrapper()
120+
121+
// 1. 通过组件选择器找到 CsgButton
122+
const deleteBtn = wrapper.findComponent('#confirmDelete')
123+
124+
// 2. 检查初始禁用状态(通过 props)
125+
expect(deleteBtn.props('disabled')).toBe(true)
126+
127+
// 3. 找到删除确认输入框(第三个el-input)
128+
const inputs = wrapper.findAll('.el-input__inner')
129+
const confirmInput = inputs[2]
130+
131+
// 4. 输入正确名称
132+
await confirmInput.setValue('test-user/test-collection')
133+
134+
// 5. 检查启用状态
135+
expect(deleteBtn.props('disabled')).toBe(false)
136+
})
137+
138+
it('处理API错误情况', async () => {
139+
mockPut.mockResolvedValueOnce({ data: { value: null }, error: { value: { msg: 'Error' } } })
140+
const wrapper = createWrapper()
141+
// 使用更精确的选择器找到第一个更新按钮
142+
const updateButtons = wrapper.findAll('.btn-secondary-gray')
143+
await updateButtons[0].trigger('click')
144+
145+
expect(ElMessage.warning).toHaveBeenCalled()
146+
})
147+
})
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { mount } from '@vue/test-utils'
2+
import ProfileSettings from '@/components/user_settings/ProfileSettings.vue'
3+
import Menu from '@/components/user_settings/Menu.vue'
4+
import ProfileEdit from '@/components/user_settings/ProfileEdit.vue'
5+
6+
describe('ProfileSettings.vue', () => {
7+
// 创建测试包装器
8+
const createWrapper = () => {
9+
return mount(ProfileSettings, {
10+
global: {
11+
components: {
12+
Menu,
13+
ProfileEdit
14+
}
15+
}
16+
})
17+
}
18+
19+
// 测试1:验证基础渲染
20+
it('正确渲染容器和子组件', () => {
21+
const wrapper = createWrapper()
22+
23+
// 验证容器元素
24+
const container = wrapper.find('.page-responsive-width')
25+
expect(container.exists()).toBe(true)
26+
expect(container.classes()).toContain('bg-white')
27+
28+
// 验证子组件存在
29+
expect(wrapper.findComponent(Menu).exists()).toBe(true)
30+
expect(wrapper.findComponent(ProfileEdit).exists()).toBe(true)
31+
})
32+
33+
// 测试2:验证布局类名
34+
it('正确应用响应式布局类', () => {
35+
const wrapper = createWrapper()
36+
const container = wrapper.find('div')
37+
38+
// 验证响应式类
39+
expect(container.classes()).toContain('md:flex-col')
40+
expect(container.classes()).toContain('justify-center')
41+
})
42+
43+
// 测试3:验证props传递
44+
it('正确传递hasSave到Menu组件', () => {
45+
const wrapper = createWrapper()
46+
const menu = wrapper.findComponent(Menu)
47+
48+
// 验证初始值传递
49+
expect(menu.props('hasSave')).toBe(true)
50+
51+
// 验证class传递
52+
expect(menu.vm.$attrs.class).toContain('max-w-[411px]')
53+
})
54+
55+
// 测试4:验证事件处理
56+
it('正确响应update-has-save事件', async () => {
57+
const wrapper = createWrapper()
58+
const profileEdit = wrapper.findComponent(ProfileEdit)
59+
60+
// 模拟事件触发
61+
await profileEdit.vm.$emit('update-has-save', false)
62+
63+
// 验证状态更新
64+
expect(wrapper.findComponent(Menu).props('hasSave')).toBe(false)
65+
})
66+
67+
// 测试5:验证子组件class传递
68+
it('正确传递class到ProfileEdit组件', () => {
69+
const wrapper = createWrapper()
70+
const profileEdit = wrapper.findComponent(ProfileEdit)
71+
72+
// 验证grow类传递
73+
expect(profileEdit.classes()).toContain('grow')
74+
})
75+
})

0 commit comments

Comments
 (0)