Skip to content

Commit 518ff7b

Browse files
ymh6315431minghao.yang
andauthored
feat(collections): Add remark input for repositories (#1320)
* Draft MR * feat(collections): Add remark input for repositories --------- Co-authored-by: minghao.yang <[email protected]>
1 parent cff083a commit 518ff7b

File tree

11 files changed

+639
-55
lines changed

11 files changed

+639
-55
lines changed
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 3 additions & 0 deletions
Loading
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { mount } from '@vue/test-utils'
2+
import { nextTick, h } from 'vue'
3+
import { ElMessage } from 'element-plus'
4+
import CollectionsAddRepo from '@/components/collections/CollectionsAddRepo.vue'
5+
6+
// ---------------- Mock ----------------
7+
8+
vi.mock('element-plus', async () => {
9+
const actual = await vi.importActual('element-plus')
10+
return {
11+
...actual,
12+
ElMessage: vi.fn(),
13+
ElSelect: (props, { slots }) =>
14+
h(
15+
'select',
16+
{
17+
onChange: (e) => props['onUpdate:modelValue']?.(e.target.value)
18+
},
19+
slots.default ? slots.default() : []
20+
),
21+
ElOption: (props) => h('option', { value: props.value }, props.label),
22+
ElDialog: (props, { slots }) =>
23+
props.modelValue ? h('div', {}, slots.default ? slots.default() : []) : null,
24+
ElInput: (props) =>
25+
h('textarea', {
26+
value: props.modelValue,
27+
onInput: (e) => props['onUpdate:modelValue']?.(e.target.value)
28+
})
29+
}
30+
})
31+
32+
// 模拟 useFetchApi
33+
const mockJson = vi.fn()
34+
const mockPost = vi.fn(() => ({ json: mockJson }))
35+
vi.mock('@/packs/useFetchApi', () => ({
36+
default: vi.fn(() => ({
37+
post: mockPost,
38+
json: mockJson
39+
}))
40+
}))
41+
42+
describe('CollectionsAddRepo.vue', () => {
43+
let wrapper
44+
const fetchCollectionDetail = vi.fn()
45+
46+
const createWrapper = (props = {}) =>
47+
mount(CollectionsAddRepo, {
48+
props: {
49+
collectionsId: '123',
50+
canManage: true,
51+
...props
52+
},
53+
global: {
54+
mocks: {
55+
$t: (key) => key // 简化i18n
56+
},
57+
provide: { fetchCollectionDetail },
58+
stubs: ['SvgIcon']
59+
}
60+
})
61+
62+
beforeEach(() => {
63+
vi.clearAllMocks()
64+
})
65+
66+
it('当 canManage=true 时显示添加按钮', async () => {
67+
wrapper = createWrapper()
68+
expect(wrapper.text()).toContain('collections.edit.add')
69+
})
70+
71+
it('当 canManage=false 时不显示按钮', async () => {
72+
wrapper = createWrapper({ canManage: false })
73+
expect(wrapper.text()).not.toContain('collections.edit.add')
74+
})
75+
76+
it('点击添加按钮后显示对话框', async () => {
77+
wrapper = createWrapper()
78+
expect(wrapper.find('.invite_dialog').exists()).toBe(false)
79+
80+
await wrapper.find('.CollectionAddRepo > div').trigger('click')
81+
await nextTick()
82+
83+
expect(wrapper.findComponent({ name: 'ElDialog' }).exists()).toBe(true)
84+
})
85+
86+
it('切换 type 时清空 repoIdsInput 和 tempRemark', async () => {
87+
wrapper = createWrapper()
88+
wrapper.vm.repoIdsInput = 'xxx'
89+
wrapper.vm.tempRemark = '备注'
90+
91+
wrapper.vm.typeChange()
92+
await nextTick()
93+
94+
expect(wrapper.vm.repoIdsInput).toBe('')
95+
expect(wrapper.vm.tempRemark).toBe('')
96+
})
97+
98+
it('确认添加时如果未选择 repo,则弹出 warning', async () => {
99+
wrapper = createWrapper()
100+
wrapper.vm.repoIdsInput = ''
101+
await wrapper.vm.confirmAddRepo()
102+
103+
expect(ElMessage).toHaveBeenCalledWith({
104+
message: 'Please select Project',
105+
type: 'warning'
106+
})
107+
})
108+
109+
it('确认添加成功后关闭弹窗并提示 success', async () => {
110+
wrapper = createWrapper()
111+
wrapper.vm.repoIdsInput = '456'
112+
wrapper.vm.tempRemark = '测试备注'
113+
114+
mockJson.mockResolvedValue({
115+
data: { value: { ok: true } },
116+
error: { value: null }
117+
})
118+
119+
await wrapper.vm.confirmAddRepo()
120+
await nextTick()
121+
122+
expect(ElMessage).toHaveBeenCalledWith({
123+
message: 'Added successfully',
124+
type: 'success'
125+
})
126+
expect(fetchCollectionDetail).toHaveBeenCalled()
127+
expect(wrapper.vm.dialogVisible).toBe(false)
128+
})
129+
130+
it('确认添加失败时提示 warning', async () => {
131+
wrapper = createWrapper()
132+
wrapper.vm.repoIdsInput = '456'
133+
wrapper.vm.tempRemark = '测试备注'
134+
135+
mockJson.mockResolvedValue({
136+
data: { value: null },
137+
error: { value: { msg: '出错了' } }
138+
})
139+
140+
await wrapper.vm.confirmAddRepo()
141+
await nextTick()
142+
143+
expect(ElMessage).toHaveBeenCalledWith({
144+
message: '出错了',
145+
type: 'warning'
146+
})
147+
})
148+
})
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { mount } from '@vue/test-utils'
2+
import CollectionsRepoCard from '@/components/collections/CollectionsRepoCard.vue'
3+
import { nextTick } from 'vue'
4+
5+
// ---------------- Mock ----------------
6+
// 模拟 Element Plus 组件
7+
vi.mock('element-plus', async () => {
8+
const actual = await vi.importActual('element-plus')
9+
return {
10+
...actual,
11+
ElInput: () => null // 模拟 ElInput 组件
12+
}
13+
})
14+
15+
describe('CollectionsRepoCard.vue', () => {
16+
let wrapper
17+
18+
// 创建组件包装器的工厂函数
19+
const createWrapper = (props = {}) =>
20+
mount(CollectionsRepoCard, {
21+
props: {
22+
repo: {
23+
id: 1,
24+
name: 'test-repo',
25+
repository_type: 'model',
26+
remark: '', // 默认无备注
27+
...props.repo
28+
},
29+
canManage: true, // 默认可管理
30+
...props
31+
},
32+
global: {
33+
mocks: {
34+
$t: (key) => key // 简化i18n
35+
},
36+
stubs: ['SvgIcon', 'CsgButton', 'ElInput'] // 存根子组件和 Element Plus 组件
37+
}
38+
})
39+
40+
beforeEach(() => {
41+
vi.clearAllMocks()
42+
})
43+
44+
// 1. 渲染测试:无备注且可管理时
45+
it('当无备注且可管理时,显示添加备注图标', async () => {
46+
wrapper = createWrapper({ repo: { remark: '' }, canManage: true })
47+
await nextTick()
48+
49+
// 验证 SvgIcon 的 name 属性为 'plus-square'
50+
const svgIcon = wrapper.findComponent({ name: 'SvgIcon' })
51+
expect(svgIcon.exists()).toBe(true)
52+
expect(svgIcon.props('name')).toBe('plus-square')
53+
expect(wrapper.find('.text-sm.text-gray-600').exists()).toBe(false) // 不显示备注内容
54+
})
55+
56+
// 2. 渲染测试:有备注且可管理时
57+
it('当有备注且可管理时,显示编辑备注图标和备注内容', async () => {
58+
wrapper = createWrapper({ repo: { remark: '这是一个测试备注' }, canManage: true })
59+
await nextTick()
60+
61+
// 验证 SvgIcon 的 name 属性为 'edit-2'
62+
const svgIcon = wrapper.findComponent({ name: 'SvgIcon' })
63+
expect(svgIcon.exists()).toBe(true)
64+
expect(svgIcon.props('name')).toBe('edit-2')
65+
66+
// 验证备注内容是否显示
67+
const remarkDisplay = wrapper.find('.text-sm.text-gray-600')
68+
expect(remarkDisplay.exists()).toBe(true)
69+
expect(remarkDisplay.text()).toContain('collections.remarkTitle: 这是一个测试备注')
70+
})
71+
72+
// 3. 渲染测试:不可管理时
73+
it('当不可管理时,不显示编辑/添加备注图标和删除图标', async () => {
74+
wrapper = createWrapper({ canManage: false })
75+
await nextTick()
76+
77+
// 验证 SvgIcon 不存在
78+
expect(wrapper.findComponent({ name: 'SvgIcon' }).exists()).toBe(false)
79+
})
80+
81+
// 4. 交互测试:点击添加备注图标
82+
it('点击添加备注图标,显示 ElInput 和保存/取消按钮', async () => {
83+
wrapper = createWrapper({ repo: { remark: '' }, canManage: true })
84+
await nextTick()
85+
86+
// 初始状态下 ElInput 不可见
87+
expect(wrapper.findComponent({ name: 'ElInput' }).exists()).toBe(false)
88+
expect(wrapper.findComponent({ name: 'CsgButton', props: { name: 'all.save' } }).exists()).toBe(false)
89+
90+
// 点击 SvgIcon
91+
await wrapper.findComponent({ name: 'SvgIcon' }).trigger('click')
92+
await nextTick()
93+
94+
// 验证 ElInput 和按钮是否可见
95+
expect(wrapper.findComponent({ name: 'ElInput' }).exists()).toBe(true)
96+
expect(wrapper.findComponent({ name: 'CsgButton', props: { name: 'all.save' } }).exists()).toBe(true)
97+
expect(wrapper.findComponent({ name: 'CsgButton', props: { name: 'all.cancel' } }).exists()).toBe(true)
98+
expect(wrapper.vm.showRemarkInput).toBe(true)
99+
expect(wrapper.vm.tempRemark).toBe('') // 初始为空
100+
})
101+
102+
// 5. 交互测试:点击编辑备注图标
103+
it('点击编辑备注图标,显示 ElInput 并填充现有备注', async () => {
104+
wrapper = createWrapper({ repo: { remark: '现有备注' }, canManage: true })
105+
await nextTick()
106+
107+
// 点击 SvgIcon
108+
await wrapper.findComponent({ name: 'SvgIcon' }).trigger('click')
109+
await nextTick()
110+
111+
// 验证 ElInput 是否可见,并且 tempRemark 填充了现有备注
112+
expect(wrapper.findComponent({ name: 'ElInput' }).exists()).toBe(true)
113+
expect(wrapper.vm.showRemarkInput).toBe(true)
114+
expect(wrapper.vm.tempRemark).toBe('现有备注')
115+
})
116+
117+
// 6. 交互测试:保存备注
118+
it('输入备注并点击保存,触发 update:remark 事件并隐藏输入框', async () => {
119+
wrapper = createWrapper({ repo: { id: 100, remark: '' }, canManage: true })
120+
await nextTick()
121+
122+
await wrapper.findComponent({ name: 'SvgIcon' }).trigger('click') // 显示输入框
123+
await nextTick()
124+
125+
// 模拟输入备注
126+
wrapper.vm.tempRemark = '新的备注内容'
127+
await nextTick()
128+
129+
await wrapper.vm.saveRemark()
130+
await nextTick()
131+
132+
// 验证 update:remark 事件是否触发,并传递正确参数
133+
expect(wrapper.emitted('update:remark')).toBeTruthy()
134+
expect(wrapper.emitted('update:remark')[0][0]).toEqual({
135+
id: 100,
136+
remark: '新的备注内容'
137+
})
138+
expect(wrapper.vm.showRemarkInput).toBe(false) // 隐藏输入框
139+
})
140+
141+
// 7. 交互测试:取消备注
142+
it('点击取消按钮,隐藏输入框且不触发事件', async () => {
143+
wrapper = createWrapper({ repo: { id: 100, remark: '旧备注' }, canManage: true })
144+
await nextTick()
145+
146+
await wrapper.findComponent({ name: 'SvgIcon' }).trigger('click') // 显示输入框
147+
await nextTick()
148+
149+
wrapper.vm.tempRemark = '尝试修改的备注' // 模拟输入
150+
await nextTick()
151+
152+
// 点击取消按钮
153+
await wrapper.findComponent({ name: 'CsgButton', props: { name: 'all.cancel' } }).trigger('click')
154+
await nextTick()
155+
156+
expect(wrapper.emitted('update:remark')).toBeFalsy() // 不应触发 update:remark
157+
expect(wrapper.vm.showRemarkInput).toBe(false) // 隐藏输入框
158+
expect(wrapper.vm.tempRemark).toBe('尝试修改的备注') // tempRemark 保持不变,但因为输入框隐藏,不影响显示
159+
})
160+
161+
// 8. 交互测试:点击删除图标
162+
it('点击删除图标,触发 remove 事件', async () => {
163+
wrapper = createWrapper({ repo: { id: 200 }, canManage: true })
164+
await nextTick()
165+
166+
// 找到删除图标(第二个 SvgIcon)并点击
167+
const deleteIcon = wrapper.findAllComponents({ name: 'SvgIcon' })[1]
168+
expect(deleteIcon.exists()).toBe(true)
169+
expect(deleteIcon.props('name')).toBe('trash')
170+
171+
await deleteIcon.trigger('click')
172+
await nextTick()
173+
174+
// 验证 remove 事件是否触发,并传递正确参数
175+
expect(wrapper.emitted('remove')).toBeTruthy()
176+
expect(wrapper.emitted('remove')[0][0]).toBe(200)
177+
})
178+
})

0 commit comments

Comments
 (0)