import { flushPromises, mount } from '@vue/test-utils'
import ElementPlus, { ElMessage } from 'element-plus'
import { beforeEach, describe, expect, test, vi } from 'vitest'
import Users from './Users.vue'
import { organizationsApi } from '../api/organizations'
const RoleSelectStub = {
props: {
modelValue: Array,
multiple: Boolean
},
emits: ['update:modelValue'],
template: '
'
}
const RoleOptionStub = {
props: ['label', 'value', 'disabled'],
template: '{{ label }}'
}
const usersStore = vi.hoisted(() => ({
users: [],
loading: false,
page: { currentPage: 1, pageSize: 20, total: 0, totalPages: 1 },
fetchUsers: vi.fn(),
createUser: vi.fn(),
updateUser: vi.fn(),
deleteUser: vi.fn()
}))
vi.mock('../stores/users', () => ({ useUsersStore: () => usersStore }))
vi.mock('../stores/auth', () => ({ useAuthStore: () => ({ isAdmin: true }) }))
vi.mock('../api/organizations', () => ({
organizationsApi: {
listRoles: vi.fn()
}
}))
function mountUsers() {
return mount(Users, {
global: {
plugins: [ElementPlus],
stubs: { teleport: true, ElSelect: RoleSelectStub, ElOption: RoleOptionStub }
}
})
}
describe('用户管理角色', () => {
beforeEach(() => {
usersStore.users = [{
id: 7,
username: 'zhangsan',
description: '测试用户',
created_at: '2026-09-16T09:00:00+08:00',
roles: [
{ id: 2, code: 'warehouse_admin', name: '仓库管理员', enabled: true, permissions: [] },
{ id: 3, code: 'purchaser', name: '采购人员', enabled: true, permissions: [] }
]
}]
usersStore.fetchUsers.mockReset()
organizationsApi.listRoles.mockResolvedValue({
data: [
{ id: 2, code: 'warehouse_admin', name: '仓库管理员', enabled: true, permissions: [] },
{ id: 3, code: 'purchaser', name: '采购人员', enabled: true, permissions: [] }
]
})
})
test('列表显示用户的所有中文角色名称', async () => {
const wrapper = mount(Users, {
global: {
plugins: [ElementPlus],
stubs: { teleport: true, ElSelect: RoleSelectStub, ElOption: RoleOptionStub }
}
})
await flushPromises()
expect(wrapper.text()).toContain('仓库管理员')
expect(wrapper.text()).toContain('采购人员')
expect(wrapper.text()).not.toContain('普通用户')
wrapper.unmount()
})
test('新增和编辑使用角色与权限中的多选角色', async () => {
const wrapper = mount(Users, {
global: {
plugins: [ElementPlus],
stubs: { teleport: true, ElSelect: RoleSelectStub, ElOption: RoleOptionStub }
}
})
await flushPromises()
const addButton = wrapper.findAll('button').find(button => button.text().includes('新增用户'))
await addButton.trigger('click')
await flushPromises()
const roleSelect = wrapper.findComponent(RoleSelectStub)
expect(roleSelect.props('multiple')).toBe(true)
expect(wrapper.text()).toContain('仓库管理员')
expect(wrapper.text()).toContain('采购人员')
wrapper.unmount()
})
})
describe('新增用户后列表与搜索框保持一致', () => {
beforeEach(() => {
usersStore.fetchUsers.mockReset()
usersStore.createUser.mockReset()
usersStore.updateUser.mockReset()
usersStore.createUser.mockResolvedValue({ data: {} })
usersStore.updateUser.mockResolvedValue({ data: {} })
organizationsApi.listRoles.mockResolvedValue({ data: [] })
vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
})
// store 里新建后的那次刷新是刻意清空筛选的(好让刚建的用户出现在列表里),页面这份
// 状态(搜索框)必须跟着一起清空:否则会出现搜索框写着用户名、列表却是全量的
// 表里不一,用户会把全量列表当成筛选结果。
test('新增用户成功后搜索框要清空', async () => {
const wrapper = mountUsers()
await flushPromises()
const state = wrapper.vm.$.setupState
await wrapper.find('.search-bar input').setValue('zhangsan')
await flushPromises()
expect(state.searchKeyword).toBe('zhangsan')
state.form.username = 'lisi'
state.form.password = '123456'
state.form.role_ids = [2]
await state.handleSave()
await flushPromises()
expect(usersStore.createUser).toHaveBeenCalled()
expect(state.searchKeyword).toBe('')
wrapper.unmount()
})
// 编辑走的是「沿用搜索」那条路:刷新后列表仍是筛选结果,搜索框不该被动。
test('编辑用户成功后搜索框要保留', async () => {
const wrapper = mountUsers()
await flushPromises()
const state = wrapper.vm.$.setupState
await wrapper.find('.search-bar input').setValue('zhangsan')
await flushPromises()
state.editingId = 7
state.form.username = 'zhangsan'
state.form.role_ids = [2]
await state.handleSave()
await flushPromises()
expect(usersStore.updateUser).toHaveBeenCalled()
expect(state.searchKeyword).toBe('zhangsan')
wrapper.unmount()
})
})