material_manage_system/frontend/src/views/Users.test.js

288 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { flushPromises, mount } from '@vue/test-utils'
import ElementPlus, { ElMessage } from 'element-plus'
import { defineComponent, ref } from 'vue'
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: '<div data-test="role-select" :data-multiple="multiple"><slot /></div>'
}
const RoleOptionStub = {
props: ['label', 'value', 'disabled'],
template: '<span class="role-option">{{ label }}</span>'
}
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()
})
// 列表里同样要标出来:停用角色的 tag 和启用的一模一样,管理员会以为这个用户还在
// 通过它拿权限,实际上 GenerateToken 已经把它过滤掉了。
test('列表里已停用的角色也带后缀,启用的不带', async () => {
usersStore.users = [{
...usersStore.users[0],
roles: [
{ id: 2, code: 'warehouse_admin', name: '仓库管理员', enabled: true, permissions: [] },
{ id: 3, code: 'purchaser', name: '采购人员', enabled: false, permissions: [] }
]
}]
const wrapper = mountUsers()
await flushPromises()
expect(wrapper.text()).toContain('采购人员(已停用)')
expect(wrapper.text()).toContain('仓库管理员')
expect(wrapper.text()).not.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()
})
})
// 已停用角色在下拉里怎么呈现,直接决定"编辑用户"这条路走不走得通:保存是全量替换
// 角色,前端若摘不掉那个停用角色、后端又要求角色全部启用,这个用户就永远存不回去。
// 现在后端已放行"本来就持有的停用角色",前端这两条是配套:既让人摘得掉,也不让人
// 新分配一个停用角色。
describe('已停用角色的选择状态', () => {
const disabledRole = { id: 3, code: 'purchaser', name: '采购人员', enabled: false, permissions: [] }
const unassignedDisabledRole = { id: 4, code: 'obsolete_role', name: '作废角色', enabled: false, permissions: [] }
beforeEach(() => {
// 用户 7 正持有已停用的角色 3;角色 4 同样停用但该用户没有
usersStore.users = [{
id: 7,
username: 'zhangsan',
description: '测试用户',
created_at: '2026-09-16T09:00:00+08:00',
roles: [disabledRole]
}]
usersStore.updateUser.mockReset()
usersStore.updateUser.mockResolvedValue({ data: {} })
organizationsApi.listRoles.mockResolvedValue({
data: [
{ id: 2, code: 'warehouse_admin', name: '仓库管理员', enabled: true, permissions: [] },
disabledRole,
unassignedDisabledRole
]
})
})
async function openEdit(wrapper) {
const state = wrapper.vm.$.setupState
state.showEditModal(usersStore.users[0])
await flushPromises()
return state
}
function findOption(wrapper, value) {
return wrapper.findAllComponents(RoleOptionStub).find(option => option.props('value') === value)
}
test('用户已持有的停用角色不禁用,否则它摘不掉', async () => {
const wrapper = mountUsers()
await flushPromises()
const state = await openEdit(wrapper)
expect(state.form.role_ids).toEqual([3])
// disabled 跟着 form.role_ids 走:已选中就不禁用,tag 上才有关闭按钮
expect(findOption(wrapper, 3).props('disabled')).toBe(false)
wrapper.unmount()
})
test('未持有的停用角色仍然禁用,不能通过编辑新分配', async () => {
const wrapper = mountUsers()
await flushPromises()
await openEdit(wrapper)
expect(findOption(wrapper, 4).props('disabled')).toBe(true)
wrapper.unmount()
})
test('停用的角色在下拉里标注出来', async () => {
const wrapper = mountUsers()
await flushPromises()
await openEdit(wrapper)
expect(findOption(wrapper, 3).props('label')).toBe('采购人员(已停用)')
expect(findOption(wrapper, 2).props('label')).toBe('仓库管理员')
wrapper.unmount()
})
})
// 上面几条用的是 stub,只能证明 :disabled 这个 prop 算对了;而"管理员究竟能不能把那个
// 停用角色摘掉"取决于 element-plus 内部——multiple 模式下 tag 的关闭按钮绑在 option 的
// isDisabled 上(select2.mjs 的 closable: !selectDisabled && !item.isDisabled)。这里是
// 用真实组件把这条依赖验一遍,否则 Users.vue 的修复只是纸面上成立。
describe('element-plus 多选下停用角色 tag 的可关闭性', () => {
const roles = [
{ id: 2, name: 'warehouse_admin', enabled: true },
{ id: 3, name: 'purchaser', enabled: false }
]
// 只铺一个最小场景:已选中那个停用的角色 3
function buildSelect(disabledExpression) {
return defineComponent({
template: `
<el-select v-model="ids" multiple>
<el-option v-for="role in roles" :key="role.id" :label="role.name" :value="role.id"
:disabled="${disabledExpression}" />
</el-select>
`,
setup() {
return { ids: ref([3]), roles }
}
})
}
async function closeIconCount(disabledExpression) {
const wrapper = mount(buildSelect(disabledExpression), { global: { plugins: [ElementPlus] } })
await flushPromises()
return wrapper.findAll('.el-tag__close').length
}
test('Users.vue 的写法:已选中的停用角色 tag 保留关闭按钮', async () => {
expect(await closeIconCount('!role.enabled && !ids.includes(role.id)')).toBe(1)
})
test('静态 disabled 的写法:同一个 tag 没有关闭按钮(原死锁现场)', async () => {
expect(await closeIconCount('!role.enabled')).toBe(0)
})
})