292 lines
9.2 KiB
JavaScript
292 lines
9.2 KiB
JavaScript
/**
|
||
* 用户管理页(/system/user):左部门树 + 右用户列表的页面级业务规则。
|
||
*
|
||
* 本页自身只做三件事,测试也只锁这三件:
|
||
* 1. 选中部门 → 固定查询条件(含全部下级部门,按 b_parent_id 展开);
|
||
* 2. 新增用户时把选中部门补成默认值(只补新增、且表单未带该字段时);
|
||
* 3. 未选中部门时不加任何固定条件(显示全部用户)。
|
||
*
|
||
* 树的增删改与拖拽已在 DeptTreePanel 内,由 fms-tree / utils 的既有用例覆盖,这里不重复。
|
||
*/
|
||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||
import { flushPromises } from '@vue/test-utils'
|
||
import SystemUser from '@/views/system/user/index.vue'
|
||
import { mountPage } from '../helpers/app.js'
|
||
|
||
vi.mock('@/services/api', () => ({
|
||
loadDataApi: vi.fn(),
|
||
pageDataApi: vi.fn(() => Promise.resolve({ data: { rows: [], total: 0 } })),
|
||
saveObjectApi: vi.fn(() => Promise.resolve({ code: 0, success: true })),
|
||
nextIdApi: vi.fn(() => Promise.resolve({ data: 9001, code: 0, success: true })),
|
||
describeApi: vi.fn(() => Promise.resolve({ data: [], code: 0, success: true })),
|
||
}))
|
||
|
||
vi.mock('@/stores/permissions', () => ({
|
||
usePermissionStore: () => ({
|
||
load: () => Promise.resolve(),
|
||
canAccess: () => true,
|
||
canPower: () => true,
|
||
}),
|
||
}))
|
||
|
||
import { loadDataApi } from '@/services/api'
|
||
|
||
/** 造一条 b_dept 行;默认启用 */
|
||
function makeDept(over = {}) {
|
||
return {
|
||
b_id: 'd1',
|
||
b_parent_id: null,
|
||
b_depth: 0,
|
||
b_path: '/d1/',
|
||
b_name: 'D1',
|
||
b_xh: 10,
|
||
b_canuse: 1,
|
||
...over,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 固定夹具:
|
||
* d_root
|
||
* ├─ d_a
|
||
* │ └─ d_a1
|
||
* └─ d_b
|
||
* d_other(无父,与所选部门无关)
|
||
*/
|
||
const FIXTURE = [
|
||
makeDept({ b_id: 'd_root', b_parent_id: null, b_depth: 0, b_path: '/d_root/', b_name: '总部' }),
|
||
makeDept({
|
||
b_id: 'd_a',
|
||
b_parent_id: 'd_root',
|
||
b_depth: 1,
|
||
b_path: '/d_root/d_a/',
|
||
b_name: '华东',
|
||
}),
|
||
makeDept({
|
||
b_id: 'd_a1',
|
||
b_parent_id: 'd_a',
|
||
b_depth: 2,
|
||
b_path: '/d_root/d_a/d_a1/',
|
||
b_name: '上海',
|
||
}),
|
||
makeDept({
|
||
b_id: 'd_b',
|
||
b_parent_id: 'd_root',
|
||
b_depth: 1,
|
||
b_path: '/d_root/d_b/',
|
||
b_name: '华南',
|
||
}),
|
||
makeDept({ b_id: 'd_other', b_parent_id: null, b_depth: 0, b_path: '/d_other/', b_name: '独立' }),
|
||
]
|
||
|
||
beforeEach(() => {
|
||
localStorage.clear()
|
||
sessionStorage.clear()
|
||
loadDataApi.mockReset()
|
||
loadDataApi.mockImplementation(() =>
|
||
Promise.resolve({ data: FIXTURE.map((row) => ({ ...row })) }),
|
||
)
|
||
})
|
||
|
||
/** 只渲染默认插槽的直通壳,用于布局容器(否则插槽内容不渲染) */
|
||
const passthrough = {
|
||
template: '<div><slot /></div>',
|
||
}
|
||
|
||
async function mountUser() {
|
||
// DeptTreePanel 必须真实挂载:本页的「选中部门 → 查询条件」依赖它加载完部门后 emit('loaded'),
|
||
// 若被默认打桩,emit 不会触发,页面拿到的 depts 恒为空。
|
||
//
|
||
// 布局容器必须按「Vue 实际推断出的组件名」打桩。
|
||
// Splitter/SplitterPanel 两个 SFC 的文件名分别是 splitter.vue / panel.vue,
|
||
// 推断名因此是 'splitter' / 'panel'(不是 'SplitterPanel');写错键就匹配不上,
|
||
// 容器被 shallowMount 默认打成空壳,插槽内容不渲染,页面拿到的 depts 恒为空。
|
||
const { wrapper } = await mountPage(SystemUser, {
|
||
stubs: {
|
||
splitter: passthrough,
|
||
panel: passthrough,
|
||
DeptTreePanel: false,
|
||
},
|
||
})
|
||
await flushPromises()
|
||
return wrapper
|
||
}
|
||
|
||
describe('用户管理:选中部门 → 固定查询条件', () => {
|
||
it('未选中部门时不加任何固定条件', async () => {
|
||
const wrapper = await mountUser()
|
||
|
||
expect(wrapper.vm.deptFilterCondition).toBe('')
|
||
})
|
||
|
||
it('选中部门时条件含自身与全部下级', async () => {
|
||
const wrapper = await mountUser()
|
||
wrapper.vm.selectedDeptKey = 'd_root'
|
||
await flushPromises()
|
||
|
||
// IN 子句里 id 的顺序无意义,collectDescendantIds 按遍历序返回,这里只比对集合
|
||
const ids = [...wrapper.vm.deptFilterCondition.matchAll(/'([^']+)'/g)].map((m) => m[1])
|
||
expect(new Set(ids)).toEqual(new Set(['d_root', 'd_a', 'd_a1', 'd_b']))
|
||
expect(ids).toHaveLength(4)
|
||
})
|
||
|
||
it('选中中间层只含该分支,不含兄弟分支', async () => {
|
||
const wrapper = await mountUser()
|
||
wrapper.vm.selectedDeptKey = 'd_a'
|
||
await flushPromises()
|
||
|
||
const condition = wrapper.vm.deptFilterCondition
|
||
expect(condition).toContain("'d_a'")
|
||
expect(condition).toContain("'d_a1'")
|
||
expect(condition).not.toContain("'d_b'")
|
||
expect(condition).not.toContain("'d_root'")
|
||
})
|
||
|
||
it('选中无下级的部门时只匹配自身', async () => {
|
||
const wrapper = await mountUser()
|
||
wrapper.vm.selectedDeptKey = 'd_a1'
|
||
await flushPromises()
|
||
|
||
expect(wrapper.vm.deptFilterCondition).toBe("[b_dept_id] IN ('d_a1')")
|
||
})
|
||
|
||
it('b_parent_id 与 b_path 不一致时以 b_parent_id 为准(b_path 只是派生字段)', async () => {
|
||
loadDataApi.mockImplementation(() =>
|
||
Promise.resolve({
|
||
data: [
|
||
...FIXTURE.map((row) => ({ ...row })),
|
||
makeDept({
|
||
b_id: 'd_moved',
|
||
b_parent_id: 'd_a',
|
||
b_depth: 2,
|
||
b_path: '/stale/d_moved/',
|
||
b_name: '搬迁部门',
|
||
}),
|
||
],
|
||
}),
|
||
)
|
||
|
||
const wrapper = await mountUser()
|
||
wrapper.vm.selectedDeptKey = 'd_a'
|
||
await flushPromises()
|
||
|
||
expect(wrapper.vm.deptFilterCondition).toContain("'d_moved'")
|
||
})
|
||
})
|
||
|
||
describe('用户管理:新增时默认部门', () => {
|
||
it('选中部门时新增补上 b_dept_id', async () => {
|
||
const wrapper = await mountUser()
|
||
wrapper.vm.selectedDeptKey = 'd_a'
|
||
await flushPromises()
|
||
|
||
const row = {}
|
||
wrapper.vm.onBeforeSave(row, { isNew: true })
|
||
|
||
expect(row.b_dept_id).toBe('d_a')
|
||
})
|
||
|
||
it('编辑时不覆盖已有部门', async () => {
|
||
const wrapper = await mountUser()
|
||
wrapper.vm.selectedDeptKey = 'd_a'
|
||
await flushPromises()
|
||
|
||
const row = { b_id: 'u1', b_dept_id: 'd_b' }
|
||
wrapper.vm.onBeforeSave(row, { isNew: false })
|
||
|
||
expect(row.b_dept_id).toBe('d_b')
|
||
})
|
||
|
||
it('表单已填部门时不覆盖用户的输入', async () => {
|
||
const wrapper = await mountUser()
|
||
wrapper.vm.selectedDeptKey = 'd_a'
|
||
await flushPromises()
|
||
|
||
const row = { b_dept_id: 'd_b' }
|
||
wrapper.vm.onBeforeSave(row, { isNew: true })
|
||
|
||
expect(row.b_dept_id).toBe('d_b')
|
||
})
|
||
|
||
it('未选中部门时新增不做任何补充', async () => {
|
||
const wrapper = await mountUser()
|
||
|
||
const row = {}
|
||
wrapper.vm.onBeforeSave(row, { isNew: true })
|
||
|
||
expect(row.b_dept_id).toBeUndefined()
|
||
})
|
||
})
|
||
|
||
describe('用户管理:密码字段', () => {
|
||
/**
|
||
* 视图 v_b_user 刻意不含 b_password(避免全员口令随列表接口下发),
|
||
* 因此编辑态表单恒为空 —— 留空必须理解为「保持原密码」,
|
||
* 把字段摘掉再提交,否则会被 diff 当成「改成空」覆盖库中原值。
|
||
*/
|
||
it('编辑时密码留空 → 摘掉字段,不提交', async () => {
|
||
const wrapper = await mountUser()
|
||
|
||
const row = { b_id: 'u1', b_name: '张三', b_password: '' }
|
||
expect(wrapper.vm.onBeforeSave(row, { isNew: false })).toBe(true)
|
||
expect('b_password' in row).toBe(false)
|
||
})
|
||
|
||
it('编辑时填了新密码 → 原样保留待提交', async () => {
|
||
const wrapper = await mountUser()
|
||
|
||
const row = { b_id: 'u1', b_name: '张三', b_password: 'new-secret' }
|
||
expect(wrapper.vm.onBeforeSave(row, { isNew: false })).toBe(true)
|
||
expect(row.b_password).toBe('new-secret')
|
||
})
|
||
|
||
it('新增时密码为空 → 拦截保存', async () => {
|
||
const wrapper = await mountUser()
|
||
|
||
const row = { b_id: 'u1', b_name: '张三' }
|
||
expect(wrapper.vm.onBeforeSave(row, { isNew: true })).toBe(false)
|
||
})
|
||
|
||
it('新增时填了密码 → 通过,且不误删字段', async () => {
|
||
const wrapper = await mountUser()
|
||
|
||
const row = { b_id: 'u1', b_name: '张三', b_password: 'secret' }
|
||
expect(wrapper.vm.onBeforeSave(row, { isNew: true })).toBe(true)
|
||
expect(row.b_password).toBe('secret')
|
||
})
|
||
})
|
||
|
||
describe('用户管理:行操作', () => {
|
||
it('提供编辑操作,点开时把该行交给弹窗', async () => {
|
||
const wrapper = await mountUser()
|
||
|
||
const editAction = wrapper.vm.rowActions.find((action) => action.key === 'edit')
|
||
expect(editAction).toBeTruthy()
|
||
|
||
const row = { b_id: 'u1', b_name: '张三' }
|
||
editAction.onClick(row)
|
||
|
||
// 用 toStrictEqual 而非 toBe:editingUser 是 ref,读出来的是响应式代理,与传入对象不同一
|
||
expect(wrapper.vm.editingUser).toStrictEqual(row)
|
||
expect(wrapper.vm.editOpen).toBe(true)
|
||
})
|
||
|
||
it('有删除权限时提供删除操作', async () => {
|
||
const wrapper = await mountUser()
|
||
|
||
const keys = wrapper.vm.rowActions.map((action) => action.key)
|
||
expect(keys).toEqual(['edit', 'delete'])
|
||
})
|
||
|
||
it('新增时把编辑对象清空,避免带入上一次编辑的行', async () => {
|
||
const wrapper = await mountUser()
|
||
|
||
wrapper.vm.onEdit({ b_id: 'u1' })
|
||
expect(wrapper.vm.editingUser).toEqual({ b_id: 'u1' })
|
||
|
||
wrapper.vm.onCreate()
|
||
expect(wrapper.vm.editingUser).toBeNull()
|
||
expect(wrapper.vm.editOpen).toBe(true)
|
||
})
|
||
})
|