写操作后原地重新拉取列表时,search 参数被写死成缺省的空串:搜索框里还显示着 用户输入的关键字,列表却退回全量。用户会把全量列表当成筛选结果,接下来的作废/ 删除操作就打在被误认的那一条上。store 层一直有这个缺陷却没被拦住,因为页面 那层没有测试。 修法照 stock.js / approval.js 已有的约定(store 存一份 filters,写操作原地刷新 时取出来用): - materials.js:编辑/作废/恢复沿用 search 和 status - users.js:编辑/删除沿用 search - 新增走反方向,清空筛选并回第 1 页:新建的项不一定匹配当前关键字,沿用搜索 就看不见它,用户会以为没建成功。物料按 id 倒序,回第 1 页正好能看见 - Materials.vue / Users.vue:新增成功后把搜索框一起清空,否则 store 清了筛选而 搜索框没清,同样是表里不一 测试补 14 条(两个 store + 两个页面)。全部做过变异验证:把实现改回原样后, 6 条 store 断言和 2 条页面断言确实变红,且只有它们变红。
158 lines
5.1 KiB
JavaScript
158 lines
5.1 KiB
JavaScript
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: '<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()
|
|
})
|
|
})
|
|
|
|
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()
|
|
})
|
|
})
|