Files
workspace/code/fms/fms-vue/tests/unit/module-power-panel.spec.js
T
2026-09-22 22:41:36 +08:00

332 lines
12 KiB
JavaScript

/**
* 模块权限面板(ModulePowerPanel):
* - 权限库取数:只在「权限」页签可见(active)时查一次,按共用口径去重(@/utils/powerCodes
* .buildPowerLibrarySql)——标准动作不进库,否则导入会与模块使用权限撞编码;
* - 库行:名称 / 动作 / 使用模块数;已在本模块的动作置灰(勾不上);
* - 导入:按动作追加派生好的权限行(只改草稿:_rowKey / 编码 / 性质 / 资源键 / 顺序由面板物化),
* 清空选中并提示;选中的动作本模块都已存在时不追加、只提示;
* - 切换模块:库重取一次并清空选中;库查询失败留空,不打断手打动作。
*/
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import { registerFeature, useAreaSelection } from 'stk-table-vue'
import ModulePowerPanel from '@/views/module/module-management/ModulePowerPanel.vue'
import { buildPowerLibrarySql } from '@/utils/powerCodes'
import { createTestPinia } from '../helpers/app.js'
// 与 main.js 一致:表格可编辑(单元格点击进编辑)依赖 stk-table 的 area-selection feature
registerFeature(useAreaSelection)
// 权限库走只读 SQL 接口,mock 这一个接缝即可
vi.mock('@/services/api', () => ({ loadDataBySqlApi: vi.fn() }))
// 命令式 Message 在 jsdom 下会建 DOM 容器,按测试约定打桩
vi.mock('@/components/ui/message/message-manager.js', () => {
const m = { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }
return { Message: m, default: m }
})
import { loadDataBySqlApi } from '@/services/api'
import { Message } from '@/components/ui/message/message-manager.js'
// jsdom 无布局:StkTable 在 clientWidth 为 0 时启用虚拟列裁剪,补成宽容器
beforeAll(() => {
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
configurable: true,
get() {
return 5000
},
})
})
/** 权限库里的动作(跨模块去重后只留动作与一份名称) */
const LIBRARY = [
{ b_action: 'audit', b_name: '审核' },
{ b_action: 'print', b_name: '打印' },
{ b_action: 'settle', b_name: '结算' },
]
/** 当前模块已有的权限点:模块使用 + 审核(每次取新副本:面板会往行上补 _rowKey) */
function powersFixture() {
return [
{
b_id: 'module.order',
b_name: '订单',
b_i18n: 'module.order',
b_action: 'access',
b_type: 'module',
b_object_type: 'module',
b_object_id: 'order',
b_canuse: 1,
b_xh: 10,
},
{
b_id: 'action.order.audit',
b_name: '审核',
b_i18n: 'action.audit',
b_action: 'audit',
b_type: 'action',
b_object_type: 'module',
b_object_id: 'order',
b_canuse: 1,
b_xh: 20,
},
]
}
async function mountPanel(props = {}) {
const pinia = createTestPinia()
const wrapper = mount(ModulePowerPanel, {
props: {
moduleId: 'order',
moduleName: '订单',
active: true,
loading: false,
modelValue: powersFixture(),
...props,
},
global: { plugins: [pinia] },
attachTo: document.body,
})
await flushPromises()
await nextTick()
return wrapper
}
/** 库表的行文本(按区块取:左侧主表也有表格) */
function libraryRowTexts(wrapper) {
return wrapper
.findAll('.mm-power__section--library .stk-tbody-main tr')
.map((row) => row.text())
}
/** 库表的行复选框(选择列的表头也有一枚,取 tbody 里的才是行) */
function libraryCheckboxes(wrapper) {
return wrapper.findAll('.mm-power__section--library .stk-tbody-main .fms-checkbox input')
}
/** 最后一次回写给父级的权限点草稿(没有写回时为 null) */
function lastPowers(wrapper) {
const events = wrapper.emitted('update:modelValue') || []
return events.length ? events[events.length - 1][0] : null
}
beforeEach(() => {
document.body.innerHTML = ''
localStorage.clear()
sessionStorage.clear()
loadDataBySqlApi.mockReset()
loadDataBySqlApi.mockResolvedValue({ data: [] })
Message.success.mockClear()
Message.info.mockClear()
})
describe('模块权限面板:权限库取数', () => {
it('页签可见时按去重口径取一次库:同动作合并、排除标准动作', async () => {
await mountPanel()
expect(loadDataBySqlApi).toHaveBeenCalledWith(buildPowerLibrarySql())
const sql = loadDataBySqlApi.mock.calls[0][0]
expect(sql).toContain('group by [b_action]')
// 标准动作的编码是 module.{模块编码},导入会与模块使用权限撞编码:不进库
expect(sql).toContain(
"and [b_action] not in ('access', 'read', 'create', 'update', 'delete', 'export')",
)
expect(sql).toContain("where [b_object_type] = 'module' and [b_canuse] = 1")
})
it('页签不可见时不取库(面板是 v-show 常驻挂载)', async () => {
await mountPanel({ active: false })
expect(loadDataBySqlApi).not.toHaveBeenCalled()
})
it('切到该页签才取库', async () => {
const wrapper = await mountPanel({ active: false })
await wrapper.setProps({ active: true })
await flushPromises()
expect(loadDataBySqlApi).toHaveBeenCalledTimes(1)
})
it('库查询失败:留空不打断编辑(仍可手打动作)', async () => {
loadDataBySqlApi.mockRejectedValue(new Error('boom'))
const wrapper = await mountPanel()
expect(libraryRowTexts(wrapper)).toEqual([])
expect(wrapper.vm.libraryLoading).toBe(false)
})
})
describe('模块权限面板:库行与置灰', () => {
it('列出库里的动作:名称 + 动作两列(不带使用模块数)', async () => {
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
const wrapper = await mountPanel()
await flushPromises()
const rows = libraryRowTexts(wrapper)
expect(rows).toHaveLength(3)
expect(rows[0]).toContain('审核')
expect(rows[0]).toContain('audit')
expect(rows[1]).toContain('打印')
expect(rows[1]).toContain('print')
expect(wrapper.find('.mm-power__section--library').text()).not.toContain('模块数')
})
it('库表铺满右栏(auto-fit-width):右栏拖窄也不出现横向滚动', async () => {
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
const wrapper = await mountPanel()
await flushPromises()
// 左主表按列宽展示,右库表 auto-fit-width:表宽跟随容器(库内会强制关掉列宽拖拽)
const tables = wrapper.findAllComponents({ name: 'FmsTable' })
expect(tables).toHaveLength(2)
expect(tables[1].props('autoFitWidth')).toBe(true)
expect(wrapper.find('.mm-power__section--library .fms-table').classes()).toContain(
'fms-table--auto-fit-width',
)
// 名称固定一档窄宽,动作(最后一列)不指定宽度:由它自适应吃剩余
const [nameCol, actionCol] = wrapper.vm.libraryColumns
expect(nameCol.width).toBe(88)
expect(actionCol.width).toBeUndefined()
})
it('已在本模块的动作置灰,未用过的可勾选', async () => {
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
const wrapper = await mountPanel()
await flushPromises()
// audit 已在本模块(powers 里有),print / settle 没有
expect(libraryCheckboxes(wrapper).map((box) => box.element.disabled)).toEqual([
true,
false,
false,
])
})
})
describe('模块权限面板:权限点表(定宽 / 密集)', () => {
it('列宽收紧到 <700,且允许拖拽回写(表按列宽定宽,不自铺满整栏)', async () => {
const wrapper = await mountPanel()
const totalWidth = wrapper.vm.columns.reduce((sum, col) => sum + (col.width || 0), 0)
expect(totalWidth).toBeLessThan(700)
// colResizable + v-model:columns:表格按列宽定宽,拖过的列宽可回写
const powerTable = wrapper.findAllComponents({ name: 'FmsTable' })[0]
expect(powerTable.props('colResizable')).toBe(true)
})
})
describe('模块权限面板:导入', () => {
it('导入选中动作:追加派生好的行、清空选中并提示', async () => {
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
const wrapper = await mountPanel()
await flushPromises()
await libraryCheckboxes(wrapper)[1].setValue(true)
await nextTick()
expect(wrapper.vm.librarySelectedKeys).toEqual(['action:print'])
wrapper.vm.importSelectedActions()
await nextTick()
const next = lastPowers(wrapper)
expect(next).toHaveLength(3)
expect(next[2]).toMatchObject({
b_action: 'print',
b_name: '打印',
b_id: 'action.order.print',
b_i18n: 'action.print',
b_type: 'action',
b_object_type: 'module',
b_object_id: 'order',
b_canuse: 1,
})
// 行键(表格行身份)与显示顺序都在导入时就地物化
expect(next[2]._rowKey).toBeTruthy()
expect(next[2].b_xh).toBe(30)
expect(wrapper.vm.librarySelectedKeys).toEqual([])
expect(Message.success).toHaveBeenCalledWith('已导入 1 个权限点,保存模块后生效')
})
it('一次导入多个:按库行顺序追加,编号连续', async () => {
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
const wrapper = await mountPanel()
await flushPromises()
await libraryCheckboxes(wrapper)[1].setValue(true)
await libraryCheckboxes(wrapper)[2].setValue(true)
await nextTick()
wrapper.vm.importSelectedActions()
await nextTick()
expect(lastPowers(wrapper).map((row) => row.b_action)).toEqual([
'access',
'audit',
'print',
'settle',
])
expect(lastPowers(wrapper).map((row) => row.b_id)).toEqual([
'module.order',
'action.order.audit',
'action.order.print',
'action.order.settle',
])
})
it('选中的动作本模块都已存在:不写回草稿,只提示一句', async () => {
loadDataBySqlApi.mockResolvedValue({ data: [LIBRARY[0]] })
const wrapper = await mountPanel()
await flushPromises()
const emits = wrapper.emitted('update:modelValue') || []
// 库里该行是禁用态(勾不上),直接驱动内部状态覆盖兜底分支
wrapper.vm.librarySelectedKeys = ['action:audit']
wrapper.vm.importSelectedActions()
await nextTick()
expect(wrapper.emitted('update:modelValue') || []).toHaveLength(emits.length)
expect(Message.info).toHaveBeenCalledWith('选中的动作本模块都已存在')
expect(Message.success).not.toHaveBeenCalled()
})
it('导入的行可再次被批量删除(行键与选中集合联动)', async () => {
loadDataBySqlApi.mockResolvedValue({ data: [LIBRARY[1]] })
const wrapper = await mountPanel()
await flushPromises()
wrapper.vm.librarySelectedKeys = ['action:print']
wrapper.vm.importSelectedActions()
await nextTick()
const added = lastPowers(wrapper)[2]
wrapper.vm.selectedRowKeys = [String(added._rowKey)]
wrapper.vm.removeSelectedRows()
await nextTick()
expect(lastPowers(wrapper).map((row) => row.b_action)).toEqual(['access', 'audit'])
expect(wrapper.vm.selectedRowKeys).toEqual([])
})
})
describe('模块权限面板:切换模块', () => {
it('切模块:库重取一次,选中清空', async () => {
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
const wrapper = await mountPanel()
await flushPromises()
await libraryCheckboxes(wrapper)[1].setValue(true)
await nextTick()
expect(wrapper.vm.librarySelectedKeys).toEqual(['action:print'])
await wrapper.setProps({ moduleId: 'other' })
await flushPromises()
expect(loadDataBySqlApi).toHaveBeenCalledTimes(2)
expect(wrapper.vm.librarySelectedKeys).toEqual([])
})
})