material_manage_system/frontend/src/views/Users.test.js
文睿 张 bcee877278 fix: 物料/用户增删改后列表的搜索关键字被丢弃
写操作后原地重新拉取列表时,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 条页面断言确实变红,且只有它们变红。
2026-09-22 14:40:21 +08:00

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()
})
})