/** * 模块权限面板(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([]) }) })