332 lines
12 KiB
JavaScript
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([])
|
|
})
|
|
})
|