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([])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user