20260922224136
This commit is contained in:
1 parent
c8dad8d4a6
commit
0abe3706c8
113 files changed
+5057
-2771
No files matched your search
@@ -57,8 +57,8 @@ describe('fms-module-list components', () => {
|
||||
expect(item.classes()).toContain('fms-form-item')
|
||||
expect(item.classes()).not.toContain('fms-form-item-vertical')
|
||||
expect(item.find('.fms-form-item-label').text()).toBe('b_name')
|
||||
// 定宽标签会在短标签后拖出一段空白,查询区按内容收缩
|
||||
expect(item.find('.fms-form-item-label').attributes('style')).toContain('width: auto')
|
||||
// 查询区标签定宽(短标签也占满),同一行各条件的控件左缘因此对齐
|
||||
expect(item.find('.fms-form-item-label').attributes('style')).toContain('width: 72px')
|
||||
zh.unmount()
|
||||
|
||||
// 切到非中文:竖排,label 在上
|
||||
@@ -76,7 +76,7 @@ describe('fms-module-list components', () => {
|
||||
},
|
||||
})
|
||||
expect(one.find('.query-grid').element.style.gridTemplateColumns).toBe(
|
||||
'repeat(1, minmax(0, 200px))',
|
||||
'repeat(1, minmax(0, 288px))',
|
||||
)
|
||||
one.unmount()
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('fms-module-list components', () => {
|
||||
},
|
||||
})
|
||||
expect(four.find('.query-grid').element.style.gridTemplateColumns).toBe(
|
||||
'repeat(4, minmax(0, 200px))',
|
||||
'repeat(4, minmax(0, 288px))',
|
||||
)
|
||||
four.unmount()
|
||||
})
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* 模块绑定弹层(ModulePickerModal):模块树默认全部收起。
|
||||
*
|
||||
* 回归点:弹层曾用 default-expanded-keys 展开第一层根节点,一打开就铺开整棵树;
|
||||
* 现在不设该 prop,初始只渲染根节点,子节点由用户按需逐层点开。
|
||||
* 折叠态下子树不渲染(tree-node.vue 用 v-if 而非 v-show),因此可直接数 treeitem。
|
||||
*/
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import ModulePickerModal from '@/views/module/menu-management/ModulePickerModal.vue'
|
||||
|
||||
const TREE = [
|
||||
{
|
||||
b_id: 'base',
|
||||
b_module_type: 'module',
|
||||
b_name: '基础资料',
|
||||
children: [
|
||||
{
|
||||
b_id: 'base_otherdata',
|
||||
b_module_type: 'module',
|
||||
b_name: '其他数据',
|
||||
children: [{ b_id: 'b_port', b_module_type: 'data', b_name: '港口' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b_id: 'system',
|
||||
b_module_type: 'module',
|
||||
b_name: '系统',
|
||||
children: [{ b_id: 'b_user', b_module_type: 'data', b_name: '用户' }],
|
||||
},
|
||||
]
|
||||
|
||||
const MODULE_TYPES = {
|
||||
base: 'module',
|
||||
base_otherdata: 'module',
|
||||
b_port: 'data',
|
||||
system: 'module',
|
||||
b_user: 'data',
|
||||
}
|
||||
|
||||
async function mountModal(props = {}) {
|
||||
const wrapper = mount(ModulePickerModal, {
|
||||
props: {
|
||||
open: true,
|
||||
treeData: TREE,
|
||||
checkedKeys: [],
|
||||
moduleTypes: MODULE_TYPES,
|
||||
...props,
|
||||
},
|
||||
// Modal 用 Teleport 挂到 body;打桩后内容留在组件树内,便于断言
|
||||
global: { stubs: { teleport: true } },
|
||||
})
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
|
||||
describe('模块绑定弹层:树默认收起', () => {
|
||||
it('初始只渲染根节点,没有任何节点处于展开态', async () => {
|
||||
const wrapper = await mountModal()
|
||||
|
||||
const items = wrapper.findAll('[role="treeitem"]')
|
||||
expect(items).toHaveLength(TREE.length)
|
||||
expect(items.map((item) => item.attributes('aria-expanded'))).toEqual([
|
||||
'false',
|
||||
'false',
|
||||
])
|
||||
})
|
||||
|
||||
it('折叠态下子节点不渲染', async () => {
|
||||
const wrapper = await mountModal()
|
||||
|
||||
expect(wrapper.text()).toContain('基础资料')
|
||||
expect(wrapper.text()).not.toContain('其他数据')
|
||||
expect(wrapper.text()).not.toContain('港口')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* 模块权限面板(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([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,571 @@
|
||||
/**
|
||||
* 绑定模块面板(ModuleBindPanel):菜单配置右侧的常驻面板。
|
||||
*
|
||||
* 回归点:
|
||||
* 1) 树默认全部收起(不设 default-expanded-keys),子节点由用户按需逐层点开;
|
||||
* 折叠态下子树不渲染(tree-node 用 v-if 而非 v-show),因此可直接数 treeitem。
|
||||
* 2) 勾分类节点级联勾选其下全部模块,但回写给父级的只有 data / virtual 编码 ——
|
||||
* module 类型只是模块树上的分类,不参与权限,写进 s_menu_module 会留下脏关系行。
|
||||
* 3) 目录 / 外链菜单不绑定模块(不渲染可勾的树,只留一句说明)。
|
||||
* 4) 外部值变化(切换菜单 / 重载)时,树上的勾选跟随重置。
|
||||
* 5) 权限点:点模块名按 b_object_id 查 s_power 并用表格列出;分类节点与 virtual
|
||||
* 模块不挂权限点,不发查询、只给说明;切换菜单时连草稿一起清空。
|
||||
* 6) 就地增删(同「模块管理 → 权限」):新增先在草稿里加一行空动作行再进单元格编辑,
|
||||
* 填完动作按共用口径(@/utils/powerCodes)派生编码 / 性质 / 资源键 / 默认名,
|
||||
* 编码撞车时清掉编码并提示;启用与删除只改草稿(回写给父级),落库由菜单的保存统一提交。
|
||||
*/
|
||||
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 ModuleBindPanel from '@/views/module/menu-management/ModuleBindPanel.vue'
|
||||
import { clearMessages } from '@/stores/i18n'
|
||||
import { createTestPinia } from '../helpers/app.js'
|
||||
|
||||
// 与 main.js 一致:表格可编辑(单元格点击进编辑)依赖 stk-table 的 area-selection feature
|
||||
registerFeature(useAreaSelection)
|
||||
|
||||
// 权限点 / 权限库都直接取数(面板自带的查询),mock 这一个接缝即可
|
||||
vi.mock('@/services/api', () => ({ loadDataApi: vi.fn(), 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 { buildPowerLibrarySql } from '@/utils/powerCodes'
|
||||
import { loadDataApi, loadDataBySqlApi } from '@/services/api'
|
||||
import { Message } from '@/components/ui/message/message-manager.js'
|
||||
|
||||
// 权限点表是 FmsTable(stk-table):jsdom 无布局,容器 clientWidth 为 0 时库会
|
||||
// 启用虚拟列裁剪只渲染首列——patch 成宽容器,让全部列真实渲染(同 module-fields-panel 用例)
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return 5000
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const TREE = [
|
||||
{
|
||||
b_id: 'base',
|
||||
b_module_type: 'module',
|
||||
b_name: '基础资料',
|
||||
children: [
|
||||
{
|
||||
b_id: 'base_otherdata',
|
||||
b_module_type: 'module',
|
||||
b_name: '其他数据',
|
||||
children: [{ b_id: 'b_port', b_module_type: 'data', b_name: '港口' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
b_id: 'system',
|
||||
b_module_type: 'module',
|
||||
b_name: '系统',
|
||||
children: [{ b_id: 'b_user', b_module_type: 'data', b_name: '用户' }],
|
||||
},
|
||||
]
|
||||
|
||||
const MODULE_TYPES = {
|
||||
base: 'module',
|
||||
base_otherdata: 'module',
|
||||
b_port: 'data',
|
||||
system: 'module',
|
||||
b_user: 'data',
|
||||
}
|
||||
|
||||
/** 库中 b_port 的权限点:模块使用(启用)+ 审核(停用) */
|
||||
const POWERS = [
|
||||
{
|
||||
b_id: 'module.b_port',
|
||||
b_name: '模块使用',
|
||||
b_i18n: 'module.b_port',
|
||||
b_action: 'access',
|
||||
b_type: 'module',
|
||||
b_object_type: 'module',
|
||||
b_object_id: 'b_port',
|
||||
b_canuse: 1,
|
||||
b_xh: 10,
|
||||
},
|
||||
{
|
||||
b_id: 'action.b_port.audit',
|
||||
b_name: '审核',
|
||||
b_i18n: 'action.audit',
|
||||
b_action: 'audit',
|
||||
b_type: 'action',
|
||||
b_object_type: 'module',
|
||||
b_object_id: 'b_port',
|
||||
b_canuse: 0,
|
||||
b_xh: 20,
|
||||
},
|
||||
]
|
||||
|
||||
/** 权限库里的动作(跨模块去重后只留动作与一份名称) */
|
||||
const LIBRARY = [
|
||||
{ b_action: 'audit', b_name: '审核' },
|
||||
{ b_action: 'print', b_name: '打印' },
|
||||
]
|
||||
|
||||
function mountPanel(props = {}) {
|
||||
// attachTo:FmsTable(stk-table)要真实 DOM 尺寸,脱离文档流时量不到宽高
|
||||
const wrapper = mount(ModuleBindPanel, {
|
||||
props: {
|
||||
treeData: TREE,
|
||||
moduleTypes: MODULE_TYPES,
|
||||
menuType: 'page',
|
||||
modelValue: [],
|
||||
// 权限点草稿走 v-model:power-drafts:回写值要真的回到 prop,面板才继续拿得到草稿
|
||||
powerDrafts: {},
|
||||
'onUpdate:powerDrafts': (value) => wrapper.setProps({ powerDrafts: value }),
|
||||
...props,
|
||||
},
|
||||
attachTo: document.body,
|
||||
})
|
||||
return wrapper
|
||||
}
|
||||
|
||||
/** 按文案取按钮(工具栏按钮没有更稳定的属性可用) */
|
||||
function buttonByText(wrapper, text) {
|
||||
return wrapper.findAll('button').find((btn) => btn.text().includes(text))
|
||||
}
|
||||
|
||||
/**
|
||||
* 左权限点表的启用勾选框(树上的勾选框是 ui/Checkbox,类名不同)。
|
||||
* 按区块取:右栏权限库的选择列(含表头那枚)也是 .fms-checkbox。
|
||||
*/
|
||||
function powerCheckboxes(wrapper) {
|
||||
return wrapper.findAll('.mm-bind-panel__powers .fms-checkbox input')
|
||||
}
|
||||
|
||||
/** 取最后一次回写给父级的权限点草稿 */
|
||||
function lastDrafts(wrapper) {
|
||||
const events = wrapper.emitted('update:powerDrafts')
|
||||
return events[events.length - 1][0]
|
||||
}
|
||||
|
||||
/** 驱动 ui/Checkbox 内部的原生 input:勾上 / 取消 */
|
||||
function setCheck(wrapper, index, checked = true) {
|
||||
return wrapper.findAll('input.checkbox-input')[index].setValue(checked)
|
||||
}
|
||||
|
||||
/** 可勾选行的勾选态(顺序与 [role="treeitem"] 一致) */
|
||||
function checkedStates(wrapper) {
|
||||
return wrapper.findAll('input.checkbox-input').map((box) => box.element.checked)
|
||||
}
|
||||
|
||||
/** 回写给父级的模块编码(取最后一次 emit) */
|
||||
function lastEmitted(wrapper) {
|
||||
const events = wrapper.emitted('update:modelValue')
|
||||
return events[events.length - 1][0]
|
||||
}
|
||||
|
||||
/** 权限库区块的行文本(左权限点表也有表格,按区块取) */
|
||||
function libraryRowTexts(wrapper) {
|
||||
return wrapper
|
||||
.findAll('.mm-bind-panel__powers-lib .stk-tbody-main tr')
|
||||
.map((row) => row.text())
|
||||
}
|
||||
|
||||
/** 权限库区块的行复选框(选择列的表头也有一枚,取 tbody 里的才是行) */
|
||||
function libraryCheckboxes(wrapper) {
|
||||
return wrapper.findAll('.mm-bind-panel__powers-lib .stk-tbody-main .fms-checkbox input')
|
||||
}
|
||||
|
||||
/** 按节点名取树行([role="treeitem"]) */
|
||||
function nodeRow(wrapper, name) {
|
||||
return wrapper.findAll('[role="treeitem"]').find((row) => row.text().includes(name))
|
||||
}
|
||||
|
||||
/** 展开非叶子节点(箭头是行内唯一的 role="button") */
|
||||
async function expandNode(wrapper, name) {
|
||||
await nodeRow(wrapper, name).find('[role="button"]').trigger('click')
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
/** 点节点标题:选中即列出该模块的权限点 */
|
||||
async function clickNode(wrapper, name) {
|
||||
await nodeRow(wrapper, name).find('.tree-node-content').trigger('click')
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
clearMessages()
|
||||
// FmsTable 内取 appStore(深浅色主题);用 createTestPinia 保证 persist 插件真实注册
|
||||
createTestPinia()
|
||||
loadDataApi.mockReset()
|
||||
loadDataApi.mockResolvedValue({ data: [] })
|
||||
loadDataBySqlApi.mockReset()
|
||||
loadDataBySqlApi.mockResolvedValue({ data: [] })
|
||||
Message.success.mockClear()
|
||||
Message.info.mockClear()
|
||||
Message.warning.mockClear()
|
||||
})
|
||||
|
||||
describe('绑定模块面板:树默认收起', () => {
|
||||
it('初始只渲染根节点,没有任何节点处于展开态', () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
const items = wrapper.findAll('[role="treeitem"]')
|
||||
expect(items).toHaveLength(TREE.length)
|
||||
expect(items.map((item) => item.attributes('aria-expanded'))).toEqual(['false', 'false'])
|
||||
})
|
||||
|
||||
it('折叠态下子节点不渲染', () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
expect(wrapper.text()).toContain('基础资料')
|
||||
expect(wrapper.text()).not.toContain('其他数据')
|
||||
expect(wrapper.text()).not.toContain('港口')
|
||||
})
|
||||
})
|
||||
|
||||
describe('绑定模块面板:勾选与回写', () => {
|
||||
it('勾分类节点级联整棵子树,回写只保留 data 模块编码', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
await setCheck(wrapper, 0)
|
||||
await nextTick()
|
||||
|
||||
// 分类节点(base / base_otherdata)不落库,只回写 data 模块
|
||||
expect(lastEmitted(wrapper)).toEqual(['b_port'])
|
||||
})
|
||||
|
||||
it('子模块全勾时分类节点呈全选态,点它取消整棵子树并回写空数组', async () => {
|
||||
const wrapper = mountPanel({ modelValue: ['b_port'] })
|
||||
|
||||
await setCheck(wrapper, 0, false)
|
||||
await nextTick()
|
||||
expect(lastEmitted(wrapper)).toEqual([])
|
||||
|
||||
// 再点回来:级联勾选整棵子树,回写仍只有 data 模块
|
||||
await setCheck(wrapper, 0)
|
||||
await nextTick()
|
||||
expect(lastEmitted(wrapper)).toEqual(['b_port'])
|
||||
})
|
||||
|
||||
it('外部值变化(切换菜单)时树上的勾选跟随重置', async () => {
|
||||
const wrapper = mountPanel()
|
||||
// 展开「系统」露出子节点「用户」:此时只有未勾选的根节点
|
||||
await expandNode(wrapper, '系统')
|
||||
expect(checkedStates(wrapper)).toEqual([false, false, false])
|
||||
|
||||
await wrapper.setProps({ modelValue: ['b_user'] })
|
||||
await nextTick()
|
||||
expect(checkedStates(wrapper)).toEqual([false, true, true])
|
||||
})
|
||||
})
|
||||
|
||||
/** 展开到「港口」这一层(b_port 是唯一的 data 叶子) */
|
||||
async function openPort(wrapper) {
|
||||
await expandNode(wrapper, '基础资料')
|
||||
await expandNode(wrapper, '其他数据')
|
||||
await clickNode(wrapper, '港口')
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
/** 点「新增权限」:草稿里多一行空动作行 */
|
||||
async function addPowerRow(wrapper) {
|
||||
await buttonByText(wrapper, '新增权限').trigger('click')
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
/**
|
||||
* 驱动单元格变更:jsdom 不实现布局,单元格编辑器挂载与命中不可靠,
|
||||
* 直接触发组件真实监听的 change(row, col, value)——同 module-fields-panel 用例的做法。
|
||||
*/
|
||||
function changeCell(wrapper, row, dataIndex, value) {
|
||||
wrapper.findComponent({ name: 'FmsTable' }).vm.$emit('change', row, { dataIndex }, value)
|
||||
}
|
||||
|
||||
describe('绑定模块面板:权限点(点模块名查看)', () => {
|
||||
it('按模块编码查权限点,用表格列出(启停态反映到勾选框)', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
|
||||
expect(loadDataApi).toHaveBeenCalledWith(
|
||||
's_power',
|
||||
"b_object_type = 'module' AND b_object_id = N'b_port'",
|
||||
'b_xh ASC, b_id ASC',
|
||||
)
|
||||
expect(wrapper.findAll('.stk-tbody-main tr')).toHaveLength(2)
|
||||
expect(wrapper.text()).toContain('权限名称')
|
||||
expect(wrapper.text()).toContain('模块使用')
|
||||
expect(wrapper.text()).toContain('action.b_port.audit')
|
||||
expect(powerCheckboxes(wrapper).map((el) => el.element.checked)).toEqual([true, false])
|
||||
})
|
||||
|
||||
it('点分类节点:不发查询,只提示分类节点没有权限点', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await clickNode(wrapper, '基础资料')
|
||||
|
||||
expect(loadDataApi).not.toHaveBeenCalled()
|
||||
expect(wrapper.text()).toContain('分类节点没有权限点')
|
||||
})
|
||||
|
||||
it('同一模块来回点只查一次(草稿即缓存)', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
await clickNode(wrapper, '基础资料')
|
||||
await clickNode(wrapper, '港口')
|
||||
await flushPromises()
|
||||
|
||||
expect(loadDataApi).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('module.b_port')
|
||||
})
|
||||
|
||||
it('切换菜单:清掉选中的模块与权限点草稿', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
const wrapper = mountPanel({ menuId: 'otherdata' })
|
||||
await openPort(wrapper)
|
||||
expect(wrapper.text()).toContain('module.b_port')
|
||||
|
||||
await wrapper.setProps({ menuId: 'base' })
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.text()).not.toContain('module.b_port')
|
||||
expect(wrapper.text()).toContain('点击左侧模块')
|
||||
expect(lastDrafts(wrapper)).toEqual({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('绑定模块面板:权限点就地增删', () => {
|
||||
it('模块没有权限点时也能新增:先加一行空动作行,等填动作', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
expect(wrapper.findAll('.fms-action-btn')).toHaveLength(0)
|
||||
|
||||
await addPowerRow(wrapper)
|
||||
|
||||
const rows = lastDrafts(wrapper).b_port.rows
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0]).toMatchObject({
|
||||
b_id: '',
|
||||
b_action: '',
|
||||
b_type: 'action',
|
||||
b_object_type: 'module',
|
||||
b_object_id: 'b_port',
|
||||
b_canuse: 1,
|
||||
})
|
||||
expect(rows[0]._rowKey).toBeTruthy()
|
||||
expect(wrapper.findAll('.stk-tbody-main tr')).toHaveLength(1)
|
||||
// 未保存标记:改动只在草稿里,由菜单的「保存」统一落库
|
||||
expect(wrapper.text()).toContain('未保存')
|
||||
})
|
||||
|
||||
it('填动作:按共用口径派生编码 / 性质 / 资源键 / 默认名', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
await addPowerRow(wrapper)
|
||||
|
||||
changeCell(wrapper, lastDrafts(wrapper).b_port.rows[0], 'b_action', 'audit')
|
||||
await nextTick()
|
||||
|
||||
expect(lastDrafts(wrapper).b_port.rows[0]).toMatchObject({
|
||||
b_id: 'action.b_port.audit',
|
||||
b_name: '审核',
|
||||
b_i18n: 'action.audit',
|
||||
b_action: 'audit',
|
||||
b_type: 'action',
|
||||
b_object_id: 'b_port',
|
||||
})
|
||||
expect(wrapper.text()).toContain('action.b_port.audit')
|
||||
})
|
||||
|
||||
it('动作编码撞车:清掉该行编码(不落库)并提示', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
await addPowerRow(wrapper)
|
||||
|
||||
// 新增行(最后一行)填一个库里已存在的动作 → 编码撞车
|
||||
changeCell(wrapper, lastDrafts(wrapper).b_port.rows.at(-1), 'b_action', 'audit')
|
||||
await nextTick()
|
||||
|
||||
expect(Message.warning).toHaveBeenCalledWith('该动作已存在,重复行不会保存')
|
||||
expect(lastDrafts(wrapper).b_port.rows.at(-1)).toMatchObject({ b_action: 'audit', b_id: '' })
|
||||
})
|
||||
|
||||
it('启用勾选框:按 _rowKey 把勾选结果写回草稿(不就地改显示副本)', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
|
||||
await powerCheckboxes(wrapper)[1].setValue(true)
|
||||
await nextTick()
|
||||
|
||||
const rows = lastDrafts(wrapper).b_port.rows
|
||||
expect(rows.find((row) => row.b_id === 'action.b_port.audit').b_canuse).toBe(1)
|
||||
expect(rows.find((row) => row.b_id === 'module.b_port').b_canuse).toBe(1)
|
||||
expect(wrapper.text()).toContain('未保存')
|
||||
})
|
||||
|
||||
it('删除:文字按钮只从草稿里去掉,不回库', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
|
||||
// 操作列是文字按钮;删掉第二行(审核),第一行「模块使用」保留
|
||||
await wrapper.findAll('.fms-action-btn')[1].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastDrafts(wrapper).b_port.rows.map((row) => row.b_id)).toEqual(['module.b_port'])
|
||||
expect(wrapper.findAll('.stk-tbody-main tr')).toHaveLength(1)
|
||||
expect(wrapper.text()).toContain('未保存')
|
||||
})
|
||||
})
|
||||
|
||||
describe('绑定模块面板:权限区表头与表宽', () => {
|
||||
it('不再渲染「权限点 / 模块名 / 条数」表头行', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
|
||||
expect(wrapper.find('.mm-bind-panel__powers-head').exists()).toBe(false)
|
||||
// 权限区只剩工具栏(新增权限)+ 表格:模块名由左侧树写明了,这里不重复
|
||||
expect(wrapper.find('.mm-bind-panel__powers').text()).not.toContain('港口')
|
||||
expect(wrapper.find('.mm-bind-panel__powers').text()).toContain('新增权限')
|
||||
})
|
||||
|
||||
it('权限点表按列宽定宽:列宽收紧到 <500,且允许拖拽回写(自适应)', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
|
||||
const totalWidth = wrapper.vm.powerColumns.reduce((sum, col) => sum + (col.width || 0), 0)
|
||||
expect(totalWidth).toBeLessThan(500)
|
||||
|
||||
// colResizable + v-model:columns:表格按列宽定宽(不自铺满整栏),拖过的列宽可回写
|
||||
const powerTable = wrapper.findAllComponents({ name: 'FmsTable' })[0]
|
||||
expect(powerTable.props('colResizable')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('绑定模块面板:权限库(历史动作导入)', () => {
|
||||
it('选中 data 模块后按去重口径取库,列出名称与动作', async () => {
|
||||
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
|
||||
expect(loadDataBySqlApi).toHaveBeenCalledWith(buildPowerLibrarySql())
|
||||
// 名称固定一档窄宽,动作(最后一列)不指定宽度:由它自适应吃剩余
|
||||
const [nameCol, actionCol] = wrapper.vm.libraryColumns
|
||||
expect(nameCol.width).toBe(88)
|
||||
expect(actionCol.width).toBeUndefined()
|
||||
|
||||
const rows = libraryRowTexts(wrapper)
|
||||
expect(rows).toHaveLength(2)
|
||||
expect(rows[0]).toContain('审核')
|
||||
expect(rows[0]).toContain('audit')
|
||||
expect(rows[1]).toContain('打印')
|
||||
expect(rows[1]).toContain('print')
|
||||
})
|
||||
|
||||
it('没选模块 / 选中分类节点时不出权限库,也不发查询', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
// 未选模块:权限点区只有一句说明,右栏整块不渲染
|
||||
expect(wrapper.find('.mm-bind-panel__powers-lib').exists()).toBe(false)
|
||||
|
||||
await clickNode(wrapper, '基础资料')
|
||||
expect(wrapper.find('.mm-bind-panel__powers-lib').exists()).toBe(false)
|
||||
expect(loadDataBySqlApi).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('已在本模块的动作置灰,未用过的可勾选', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
|
||||
// audit 已在 b_port 的权限点里(POWERS),print 没有
|
||||
expect(libraryCheckboxes(wrapper).map((box) => box.element.disabled)).toEqual([true, false])
|
||||
})
|
||||
|
||||
it('导入选中动作:按共用口径派生行追进草稿,清空选中并提示', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
|
||||
await libraryCheckboxes(wrapper)[1].setValue(true)
|
||||
await nextTick()
|
||||
expect(wrapper.vm.librarySelectedKeys).toEqual(['action:print'])
|
||||
|
||||
wrapper.vm.importSelectedActions()
|
||||
await nextTick()
|
||||
|
||||
const rows = lastDrafts(wrapper).b_port.rows
|
||||
expect(rows).toHaveLength(3)
|
||||
expect(rows[2]).toMatchObject({
|
||||
b_id: 'action.b_port.print',
|
||||
b_name: '打印',
|
||||
b_i18n: 'action.print',
|
||||
b_action: 'print',
|
||||
b_type: 'action',
|
||||
b_object_type: 'module',
|
||||
b_object_id: 'b_port',
|
||||
b_canuse: 1,
|
||||
// 顺序号接着草稿末尾排(原有 10 / 20)
|
||||
b_xh: 30,
|
||||
})
|
||||
expect(wrapper.vm.librarySelectedKeys).toEqual([])
|
||||
expect(Message.success).toHaveBeenCalledWith('已导入 1 个权限点,随菜单保存一起落库')
|
||||
})
|
||||
|
||||
it('选中的动作本模块都已存在:不写回草稿,只提示一句', async () => {
|
||||
loadDataApi.mockResolvedValue({ data: POWERS })
|
||||
loadDataBySqlApi.mockResolvedValue({ data: [LIBRARY[0]] })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
const drafts = wrapper.emitted('update:powerDrafts') || []
|
||||
|
||||
// 库里该行是禁用态(勾不上),直接驱动内部状态覆盖兜底分支
|
||||
wrapper.vm.librarySelectedKeys = ['action:audit']
|
||||
wrapper.vm.importSelectedActions()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.emitted('update:powerDrafts') || []).toHaveLength(drafts.length)
|
||||
expect(Message.info).toHaveBeenCalledWith('选中的动作本模块都已存在')
|
||||
expect(Message.success).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('换模块:库重取一次,选中清空', async () => {
|
||||
loadDataBySqlApi.mockResolvedValue({ data: LIBRARY })
|
||||
const wrapper = mountPanel()
|
||||
await openPort(wrapper)
|
||||
await libraryCheckboxes(wrapper)[1].setValue(true)
|
||||
await nextTick()
|
||||
expect(wrapper.vm.librarySelectedKeys).toEqual(['action:print'])
|
||||
|
||||
// 换到另一个 data 模块(系统 / 用户)
|
||||
await expandNode(wrapper, '系统')
|
||||
await clickNode(wrapper, '用户')
|
||||
|
||||
expect(loadDataBySqlApi).toHaveBeenCalledTimes(2)
|
||||
expect(wrapper.vm.librarySelectedKeys).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('绑定模块面板:非页面菜单', () => {
|
||||
it('目录菜单不渲染可勾的树,只给一句说明', () => {
|
||||
const wrapper = mountPanel({ menuType: 'directory' })
|
||||
|
||||
expect(wrapper.findAll('[role="treeitem"]')).toHaveLength(0)
|
||||
expect(wrapper.text()).toContain('目录不绑定模块')
|
||||
})
|
||||
|
||||
it('外链菜单不渲染可勾的树,只给一句说明', () => {
|
||||
const wrapper = mountPanel({ menuType: 'external' })
|
||||
|
||||
expect(wrapper.findAll('[role="treeitem"]')).toHaveLength(0)
|
||||
expect(wrapper.text()).toContain('外链不绑定模块')
|
||||
})
|
||||
})
|
||||
@@ -1,15 +1,26 @@
|
||||
/**
|
||||
* 分配权限抽屉(UserPowerDrawer):头部结构。
|
||||
* 分配权限抽屉(UserPowerDrawer):头部结构 + 账号级批量操作。
|
||||
*
|
||||
* 数据加载与保存由本组件负责(面板只读展示),测试只锁结构两件事:
|
||||
* 数据加载与保存由本组件负责(面板只读展示),测试只锁这几件事:
|
||||
* 1. 头部常规显示:左侧账号标识标题 + 右侧关闭按钮,页签在内容区顶部;
|
||||
* 2. 三个页签对应三张权限表的内容面板(切换页签不销毁,用 v-show 承载)。
|
||||
* 2. 页签行右端的三个账号级批量按钮(全授权 / 全禁用 / 清空权限):挂在 Tabs 的
|
||||
* rightExtra 插槽上,横跨三个页签、与选中哪个菜单 / 模块无关,改完即落库;
|
||||
* 三者轻重不同,样式分三档(主色 / 描边 / 危险描边),不是三个同款;
|
||||
* 「全授权 / 全禁用」管功能权限,「清空权限」连字段权限与数据范围一起清(先过确认框);
|
||||
* 后两者的整表清除按 b_user_id 成删除条件提交,不逐行枚举主键(见最后一节);
|
||||
* 3. 三个页签对应三张权限表的内容面板(切换页签不销毁,用 v-show 承载);
|
||||
* 左右两栏都走 Splitter 分栏(栏宽可拖拽),模块树那一栏两个页签共用;
|
||||
* 4. 即时落库:底部没有「保存 / 取消」按钮,任何改动都立刻按 diff 提交(见最后一节)。
|
||||
*
|
||||
* 面板自身的规则由 user-power-panel.spec.js 覆盖(含「全授权 / 全禁用」),这里不重复。
|
||||
* 面板自身的规则由 user-power-panel.spec.js 覆盖,这里不重复。
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import UserPowerDrawer from '@/views/system/user/UserPowerDrawer.vue'
|
||||
import UserPowerPanel from '@/views/system/user/UserPowerPanel.vue'
|
||||
import UserFieldPowerPanel from '@/views/system/user/UserFieldPowerPanel.vue'
|
||||
import UserDataPowerPanel from '@/views/system/user/UserDataPowerPanel.vue'
|
||||
import { clearMessages } from '@/stores/i18n'
|
||||
|
||||
vi.mock('@/services/api', () => ({
|
||||
@@ -17,14 +28,87 @@ vi.mock('@/services/api', () => ({
|
||||
saveObjectApi: vi.fn(() => Promise.resolve({ code: 0, success: true })),
|
||||
}))
|
||||
|
||||
import { loadDataApi } from '@/services/api'
|
||||
// 提示只按"有没有给"断言(提交失败等分支的表现就是提示一句),不测具体渲染
|
||||
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 { loadDataApi, saveObjectApi } from '@/services/api'
|
||||
import { Message } from '@/components/ui/message/message-manager.js'
|
||||
|
||||
/** 按表返回的行:权限点与三张授权表各留一条,三个批量按钮的效果才有可验证的落差 */
|
||||
const TABLE_ROWS = {
|
||||
s_power: [
|
||||
{ b_id: 'menu.partner', b_object_type: 'menu', b_object_id: 'partner', b_xh: 10, b_canuse: 1 },
|
||||
{
|
||||
b_id: 'module.v_b_othercompany',
|
||||
b_object_type: 'module',
|
||||
b_object_id: 'v_b_othercompany',
|
||||
b_xh: 10,
|
||||
b_canuse: 1,
|
||||
},
|
||||
{
|
||||
b_id: 'action.v_b_othercompany.create',
|
||||
b_object_type: 'module',
|
||||
b_object_id: 'v_b_othercompany',
|
||||
b_xh: 20,
|
||||
b_canuse: 1,
|
||||
},
|
||||
],
|
||||
s_user_power: [{ b_user_id: '001', b_power_id: 'menu.partner', b_canuse: 1 }],
|
||||
s_user_field_power: [
|
||||
{
|
||||
b_user_id: '001',
|
||||
b_module_id: 'v_b_othercompany',
|
||||
b_field_id: 'b_name',
|
||||
b_view: 0,
|
||||
b_edit: 0,
|
||||
b_query: 1,
|
||||
b_export: 1,
|
||||
b_canuse: 1,
|
||||
},
|
||||
],
|
||||
s_user_data_power: [
|
||||
{
|
||||
b_user_id: '001',
|
||||
b_module_id: 'v_b_othercompany',
|
||||
b_operation: '*',
|
||||
b_xh: 1,
|
||||
b_scope_type: 'dept',
|
||||
b_canuse: 1,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** s_power 全量启用编码(含 menu.*):「全授权」要一次勾满这些 */
|
||||
const ALL_POWER_IDS = TABLE_ROWS.s_power.map((row) => row.b_id)
|
||||
|
||||
/** 页签行右端的批量按钮:0 = 全授权,1 = 全禁用,2 = 清空权限 */
|
||||
function actionButton(wrapper, index) {
|
||||
return wrapper.findAll('.power-drawer__actions button')[index]
|
||||
}
|
||||
|
||||
/** 命令式确认框(Modal.confirm 自建容器挂到 body,只能从 document 上找) */
|
||||
function confirmPanel() {
|
||||
return [...document.querySelectorAll('.fms-modal')].find((el) =>
|
||||
el.textContent.includes('全部权限'),
|
||||
)
|
||||
}
|
||||
|
||||
/** 确认框底部按钮顺序:[取消, 确定] */
|
||||
function confirmFooterButtons(panel) {
|
||||
return [...panel.querySelectorAll('.fms-modal-footer button')]
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
sessionStorage.clear()
|
||||
clearMessages()
|
||||
loadDataApi.mockReset()
|
||||
loadDataApi.mockImplementation(() => Promise.resolve({ data: [] }))
|
||||
saveObjectApi.mockClear()
|
||||
// 只有本用例关心的表返回数据,其余表(菜单、模块、字段……)空数组即可
|
||||
loadDataApi.mockImplementation((table) => Promise.resolve({ data: TABLE_ROWS[table] || [] }))
|
||||
})
|
||||
|
||||
async function mountDrawer() {
|
||||
@@ -46,7 +130,7 @@ describe('分配权限抽屉:头部', () => {
|
||||
// header 让给账号标识:页签不在 header 里,在内容区顶部
|
||||
expect(wrapper.find('.fms-drawer-header .tabs-nav').exists()).toBe(false)
|
||||
expect(wrapper.find('.power-drawer__nav .tabs-nav').exists()).toBe(true)
|
||||
// 账号级批量按钮不在头部(在功能权限面板的工具栏里)
|
||||
// 账号级批量按钮也不占头部:它们与页签同行,在页签行右端
|
||||
expect(wrapper.find('.fms-drawer-header button.button').exists()).toBe(false)
|
||||
})
|
||||
|
||||
@@ -76,4 +160,239 @@ describe('分配权限抽屉:头部', () => {
|
||||
'data',
|
||||
])
|
||||
})
|
||||
|
||||
it('字段权限 / 数据范围:左模块树与右内容卡同样可拖拽调宽', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
// 模块树那一栏由抽屉渲染(两个页签共用),是内容区的直接子节点(功能权限面板自带一棵菜单树)
|
||||
const moduleSplitter = wrapper.find('.power-drawer__body > .fms-splitter')
|
||||
expect(moduleSplitter.exists()).toBe(true)
|
||||
expect(moduleSplitter.find('.fms-splitter-bar').exists()).toBe(true)
|
||||
|
||||
const panels = moduleSplitter.findAll('.fms-splitter-panel')
|
||||
expect(panels).toHaveLength(2)
|
||||
expect(panels[0].find('.fms-tree-panel').exists()).toBe(true)
|
||||
// 两个面板叠在右栏里(v-show 只显一个):切页签只换内容,不重挂树
|
||||
expect(panels[1].findAll('[data-pane]').map((el) => el.attributes('data-pane'))).toEqual([
|
||||
'field',
|
||||
'data',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('分配权限抽屉:账号级批量操作', () => {
|
||||
it('三个按钮在页签行右端:全授权 / 全禁用 / 清空权限', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
const actions = wrapper.find('.power-drawer__nav .power-drawer__actions')
|
||||
expect(actions.exists()).toBe(true)
|
||||
// 走 Tabs 的 rightExtra 插槽:按钮落在标签栏右端(.tabs-nav-extra),与页签同一行
|
||||
expect(
|
||||
wrapper
|
||||
.find('.power-drawer__nav .tabs-nav .tabs-nav-extra .power-drawer__actions')
|
||||
.exists(),
|
||||
).toBe(true)
|
||||
expect(actions.findAll('button').map((el) => el.text())).toEqual([
|
||||
'全授权',
|
||||
'全禁用',
|
||||
'清空权限',
|
||||
])
|
||||
// 三者轻重不同,样式分三档:给(主色实心)/ 反向(描边)/ 破坏性(危险色描边)
|
||||
const buttonTypes = actions.findAll('button').map((el) => el.classes())
|
||||
expect(buttonTypes[0]).toContain('button-primary')
|
||||
expect(buttonTypes[1]).toContain('button-outline')
|
||||
expect(buttonTypes[2]).toContain('button-danger-outline')
|
||||
// 作用范围写在 title 里(按钮文字只够放下动作名)
|
||||
expect(actions.findAll('button').map((el) => el.attributes('title'))).toEqual([
|
||||
'勾满该账号的全部功能权限(菜单入口、模块与业务动作)',
|
||||
'取消该账号的全部功能权限(字段权限与数据范围不动)',
|
||||
'清空该账号的功能权限、字段权限与数据范围',
|
||||
])
|
||||
})
|
||||
|
||||
it('全授权:一次勾满 s_power 的全部权限点,与选中哪个菜单无关', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
await actionButton(wrapper, 0).trigger('click')
|
||||
|
||||
expect(wrapper.findComponent(UserPowerPanel).props('modelValue')).toEqual(ALL_POWER_IDS)
|
||||
// 字段权限与数据范围不是「功能」权限,全授权不动它们
|
||||
expect(wrapper.findComponent(UserFieldPowerPanel).props('modelValue')).toHaveLength(1)
|
||||
expect(wrapper.findComponent(UserDataPowerPanel).props('modelValue')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('全禁用:只取消功能权限,字段权限与数据范围不动', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
await actionButton(wrapper, 1).trigger('click')
|
||||
|
||||
expect(wrapper.findComponent(UserPowerPanel).props('modelValue')).toEqual([])
|
||||
expect(wrapper.findComponent(UserFieldPowerPanel).props('modelValue')).toHaveLength(1)
|
||||
expect(wrapper.findComponent(UserDataPowerPanel).props('modelValue')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('清空权限:确认前不动数据,确认后三类权限一起清空', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
// 三类都有数据,清空才有可验证的落差
|
||||
expect(wrapper.findComponent(UserPowerPanel).props('modelValue')).toEqual(['menu.partner'])
|
||||
expect(wrapper.findComponent(UserFieldPowerPanel).props('modelValue')).toHaveLength(1)
|
||||
expect(wrapper.findComponent(UserDataPowerPanel).props('modelValue')).toHaveLength(1)
|
||||
|
||||
await actionButton(wrapper, 2).trigger('click')
|
||||
|
||||
// 三类一次清到底是重操作:先问一次,确认前什么都不动
|
||||
const panel = confirmPanel()
|
||||
expect(panel).toBeTruthy()
|
||||
expect(panel.textContent).toContain('确定清空「张三(001)」的全部权限?')
|
||||
expect(wrapper.findComponent(UserPowerPanel).props('modelValue')).toEqual(['menu.partner'])
|
||||
|
||||
confirmFooterButtons(panel).at(-1).click()
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.findComponent(UserPowerPanel).props('modelValue')).toEqual([])
|
||||
expect(wrapper.findComponent(UserFieldPowerPanel).props('modelValue')).toEqual([])
|
||||
expect(wrapper.findComponent(UserDataPowerPanel).props('modelValue')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('分配权限抽屉:改动即时落库', () => {
|
||||
it('底部没有「保存 / 取消」按钮:改动不经过二次确认', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
|
||||
// Drawer 只在给了 footer 插槽时才渲染底部条:没有保存流程,也没有取消按钮
|
||||
expect(wrapper.find('.fms-drawer-footer').exists()).toBe(false)
|
||||
expect(wrapper.find('.power-footer').exists()).toBe(false)
|
||||
// 关闭入口只剩头部的关闭按钮
|
||||
expect(wrapper.find('.fms-drawer-close').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('全授权:改完立刻提交,只提交基线上没有的权限点(插入,不删原有的)', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
saveObjectApi.mockClear()
|
||||
|
||||
await actionButton(wrapper, 0).trigger('click')
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
|
||||
expect(saveObjectApi).toHaveBeenCalledTimes(1)
|
||||
const [requests] = saveObjectApi.mock.calls[0]
|
||||
expect(requests.map((req) => req.table)).toEqual(['s_user_power'])
|
||||
expect(requests[0].key_field).toBe('b_user_id,b_power_id')
|
||||
// 顺序按 s_power 的加载顺序,与断言无关,排序后再比
|
||||
expect(requests[0].inserts.map((row) => row.b_power_id).sort()).toEqual([
|
||||
'action.v_b_othercompany.create',
|
||||
'module.v_b_othercompany',
|
||||
])
|
||||
// 原有的菜单入口仍在勾选集合里,不产生删除
|
||||
expect(requests[0].deletes).toBeUndefined()
|
||||
})
|
||||
|
||||
it('全禁用:改完立刻提交,按账号整表撤销功能权限(不枚举主键行)', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
saveObjectApi.mockClear()
|
||||
|
||||
await actionButton(wrapper, 1).trigger('click')
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
|
||||
expect(saveObjectApi).toHaveBeenCalledTimes(1)
|
||||
const [requests] = saveObjectApi.mock.calls[0]
|
||||
expect(requests.map((req) => req.table)).toEqual(['s_user_power'])
|
||||
// 删除条件就是行里的列(b_user_id + 生效口径),不是逐行的主键值
|
||||
expect(requests[0].deletes).toEqual([{ b_user_id: '001', b_canuse: 1 }])
|
||||
expect(requests[0].inserts).toBeUndefined()
|
||||
})
|
||||
|
||||
it('清空权限:确认后三张表在同一个请求列表里提交', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
saveObjectApi.mockClear()
|
||||
|
||||
await actionButton(wrapper, 2).trigger('click')
|
||||
confirmFooterButtons(confirmPanel()).at(-1).click()
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
|
||||
expect(saveObjectApi).toHaveBeenCalledTimes(1)
|
||||
const [requests] = saveObjectApi.mock.calls[0]
|
||||
expect(requests.map((req) => req.table)).toEqual([
|
||||
's_user_power',
|
||||
's_user_field_power',
|
||||
's_user_data_power',
|
||||
])
|
||||
// 三类都按该账号整表清:条件相同(b_user_id + 生效口径),各自带上自己的主键列
|
||||
expect(requests.map((req) => req.key_field)).toEqual([
|
||||
'b_user_id,b_power_id',
|
||||
'b_user_id,b_module_id,b_field_id',
|
||||
'b_user_id,b_module_id,b_operation,b_xh',
|
||||
])
|
||||
expect(requests.map((req) => req.deletes)).toEqual([
|
||||
[{ b_user_id: '001', b_canuse: 1 }],
|
||||
[{ b_user_id: '001', b_canuse: 1 }],
|
||||
[{ b_user_id: '001', b_canuse: 1 }],
|
||||
])
|
||||
expect(requests.map((req) => req.inserts)).toEqual([undefined, undefined, undefined])
|
||||
})
|
||||
|
||||
it('提交在飞时又勾回来的权限:随整表清除那一批写回,不会在库里被删掉', async () => {
|
||||
// 第一轮卡住不返回,制造"提交在飞"的窗口
|
||||
let releaseFirst
|
||||
saveObjectApi.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
releaseFirst = resolve
|
||||
}),
|
||||
)
|
||||
const wrapper = await mountDrawer()
|
||||
saveObjectApi.mockClear()
|
||||
|
||||
// 第一轮:全授权(这一轮快照里含原有的菜单入口勾选)
|
||||
await actionButton(wrapper, 0).trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
// 在飞期间:先全禁用(登记整表清除),紧接着又把菜单入口勾回来
|
||||
await actionButton(wrapper, 1).trigger('click')
|
||||
await wrapper.findComponent(UserPowerPanel).vm.$emit('update:modelValue', ['menu.partner'])
|
||||
await flushPromises()
|
||||
|
||||
releaseFirst({ code: 0, success: true })
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
|
||||
expect(saveObjectApi).toHaveBeenCalledTimes(2)
|
||||
const [requests] = saveObjectApi.mock.calls[1]
|
||||
expect(requests[0].deletes).toEqual([{ b_user_id: '001', b_canuse: 1 }])
|
||||
// 这个权限点在基线里本来就有,diff 看不出变化,必须随本批写回
|
||||
expect(requests[0].inserts).toEqual([
|
||||
{ b_user_id: '001', b_power_id: 'menu.partner', b_canuse: 1 },
|
||||
])
|
||||
})
|
||||
|
||||
it('重复点同一个按钮不会重复提交:差异已落库,下一轮直接早退', async () => {
|
||||
const wrapper = await mountDrawer()
|
||||
saveObjectApi.mockClear()
|
||||
|
||||
await actionButton(wrapper, 1).trigger('click')
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
await actionButton(wrapper, 1).trigger('click')
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
|
||||
expect(saveObjectApi).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('提交失败:保留界面上的改动并提示(下一次改动连着重发)', async () => {
|
||||
saveObjectApi.mockRejectedValueOnce(new Error('网络异常'))
|
||||
const wrapper = await mountDrawer()
|
||||
saveObjectApi.mockClear()
|
||||
|
||||
await actionButton(wrapper, 1).trigger('click')
|
||||
await flushPromises()
|
||||
await nextTick()
|
||||
|
||||
// 失败不回退:面板上的勾选仍是"已清空",提示里说明这一批没有落库
|
||||
expect(wrapper.findComponent(UserPowerPanel).props('modelValue')).toEqual([])
|
||||
expect(Message.error).toHaveBeenCalledWith('权限保存失败(改动未落库):网络异常')
|
||||
})
|
||||
})
|
||||
@@ -4,20 +4,21 @@
|
||||
* 数据加载与保存由父级 UserPowerDrawer 负责,面板自身只做这几件事,测试只锁这几件:
|
||||
* 1. 打开时的初始状态:只展开「全部分类」虚拟根,业务目录收起、不选中任何菜单;
|
||||
* 2. 菜单选中语义:重复点击已选中的菜单不取消选中,只有点「全部分类」才清空;
|
||||
* 3. 右侧结构:页头(菜单名 + 入口勾选框 + 右端 x/y 进度)+ 一个模块一块的授权块列表。
|
||||
* 3. 右侧结构:左右两栏用 Splitter 分栏(左树栏可拖拽调宽),
|
||||
* 右栏是页头(菜单名 + 入口勾选框 + 右端 x/y 进度)+ 一个模块一块的授权块列表。
|
||||
* 块头是模块(模块级勾选框 + 「已选 x/y」):勾选框表示"这个数据给没给"——有任意
|
||||
* 一项权限即勾选、一项都没有才未勾选(不做半选),点一下即给模块使用 / 整块收回;
|
||||
* 块内是它的动作,层级靠块头的上下关系;
|
||||
* 没配权限点的模块不渲染;目录不渲染入口勾选框,说明放内容区中间而不是页头;
|
||||
* 本页没有可配置模块时内容区中间给一句说明(目录与外链 / 自定义页文案不同);
|
||||
* 4. 面板顶部工具栏的「全授权 / 全禁用」作用于该账号的全部功能权限(s_power 全量),
|
||||
* 与左侧选中哪个菜单无关;
|
||||
* 5. 勾选结果经 v-model 回传,三级是「逐级给、连带收」:
|
||||
* 4. 勾选结果经 v-model 回传,三级是「逐级给、连带收」:
|
||||
* 勾下级自动补上级(操作 → 模块使用 → 入口);勾上级不替下级勾选;
|
||||
* 取消上级连带收回下级(关入口收回本页全部,取消模块收回该模块全部权限点)。
|
||||
*
|
||||
* 勾选框是 ui/Checkbox(label 包原生 input),测试直接驱动 input 的 change ——
|
||||
* jsdom 不保证 label 点击会转发给内部控件,走 change 才是组件真实监听的入口。
|
||||
*
|
||||
* 账号级的批量操作(全授权 / 全禁用 / 清空权限)不在本面板,由 user-power-drawer.spec.js 覆盖。
|
||||
*/
|
||||
import { describe, it, expect, beforeEach } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
@@ -72,21 +73,6 @@ const POWER_BY_MODULE = {
|
||||
v_b_user: [{ b_id: 'module.v_b_user', b_name: '模块使用', b_action: 'access' }],
|
||||
}
|
||||
|
||||
/** s_power 全量启用编码(含 menu.*),父级加载后传给面板,供「全授权」使用 */
|
||||
const ALL_POWER_IDS = [
|
||||
'menu.partner',
|
||||
'menu.portal',
|
||||
'menu.user',
|
||||
'module.v_b_othercompany',
|
||||
'action.v_b_othercompany.create',
|
||||
'action.v_b_othercompany.update',
|
||||
'action.v_b_othercompany.audit',
|
||||
'module.v_b_contact',
|
||||
'action.v_b_contact.create',
|
||||
'action.v_b_contact.delete',
|
||||
'module.v_b_user',
|
||||
]
|
||||
|
||||
/** 合作伙伴模块(v_b_othercompany)的权限点全量:模块级勾选框勾上时就该是这些 */
|
||||
const OTHERCOMPANY_ALL = [
|
||||
'module.v_b_othercompany',
|
||||
@@ -109,7 +95,6 @@ function mountPanel(props = {}) {
|
||||
menuModules: MENU_MODULES,
|
||||
moduleById: MODULE_BY_ID,
|
||||
powerByModule: POWER_BY_MODULE,
|
||||
allPowerIds: ALL_POWER_IDS,
|
||||
...props,
|
||||
},
|
||||
})
|
||||
@@ -183,11 +168,6 @@ function setCheck(checkbox, checked = true) {
|
||||
return checkbox.find('.checkbox-input').setValue(checked)
|
||||
}
|
||||
|
||||
/** 面板顶部工具栏按钮:0 = 全授权,1 = 全禁用 */
|
||||
function toolbarButtons(wrapper) {
|
||||
return wrapper.findAll('.power-toolbar__actions button')
|
||||
}
|
||||
|
||||
/** 回传的勾选结果(取最后一次 emit) */
|
||||
function lastEmitted(wrapper) {
|
||||
const events = wrapper.emitted('update:modelValue')
|
||||
@@ -265,6 +245,19 @@ describe('功能权限面板:菜单选中语义', () => {
|
||||
})
|
||||
|
||||
describe('功能权限面板:右侧结构', () => {
|
||||
it('左右两栏用 Splitter 分栏:左栏菜单树可拖拽调宽,右栏吃满剩余宽度', async () => {
|
||||
const wrapper = await mountPanel()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.power-panel__body.fms-splitter').exists()).toBe(true)
|
||||
expect(wrapper.find('.power-panel__body .fms-splitter-bar').exists()).toBe(true)
|
||||
|
||||
const panels = wrapper.find('.power-panel__body').findAll('.fms-splitter-panel')
|
||||
expect(panels).toHaveLength(2)
|
||||
expect(panels[0].find('.fms-tree-panel').exists()).toBe(true)
|
||||
expect(panels[1].find('.power-panel__main').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('页头:菜单名做标题 + 入口勾选框 + 右端的授权进度', async () => {
|
||||
const wrapper = await mountWithMenu('partner')
|
||||
|
||||
@@ -426,45 +419,6 @@ describe('功能权限面板:右侧结构', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('功能权限面板:全授权 / 全禁用', () => {
|
||||
it('工具栏在面板顶部:说明 + 两个按钮', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.power-toolbar__label').text()).toBe('该账号的全部功能权限')
|
||||
expect(toolbarButtons(wrapper).map((el) => el.text())).toEqual(['全授权', '全禁用'])
|
||||
})
|
||||
|
||||
it('全授权:一次勾满 s_power 的全部权限点', async () => {
|
||||
const wrapper = await mountWithMenu('partner')
|
||||
|
||||
await toolbarButtons(wrapper)[0].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(ALL_POWER_IDS)
|
||||
})
|
||||
|
||||
it('全禁用:清空该账号的全部功能权限', async () => {
|
||||
const wrapper = await mountWithMenu('partner', { modelValue: ALL_POWER_IDS })
|
||||
|
||||
await toolbarButtons(wrapper)[1].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual([])
|
||||
})
|
||||
|
||||
it('工具栏不依赖选中菜单:未选中菜单时同样可用', async () => {
|
||||
const wrapper = mountPanel()
|
||||
await nextTick()
|
||||
expect(wrapper.find('.power-panel__main').text()).toContain('请选择左侧菜单')
|
||||
|
||||
await toolbarButtons(wrapper)[0].trigger('click')
|
||||
await nextTick()
|
||||
|
||||
expect(lastEmitted(wrapper)).toEqual(ALL_POWER_IDS)
|
||||
})
|
||||
})
|
||||
|
||||
describe('功能权限面板:三层联动', () => {
|
||||
it('勾模块级勾选框:只给该模块的模块使用权限,并补上菜单入口', async () => {
|
||||
const wrapper = await mountWithMenu('partner')
|
||||
|
||||
Reference in new issue
Block a user