20260922172938

This commit is contained in:
oneao committed 2026-09-22 17:29:39 +08:00
1 parent 583984c24f
commit c8dad8d4a6
46 files changed
+3366 -1246

No files matched your search

+1 -1
View File
@@ -27,7 +27,7 @@ const mockMenus = [
},
{
b_id: 'contact',
b_name: '往来单位',
b_name: '合作伙伴',
b_menu_type: 'page',
b_parent_id: 'base',
b_route: '/base/contact',
@@ -1,5 +1,5 @@
/**
* 往来单位分类树的页面级纯函数(仍内联在 views/base/othercompany/index.vue)。
* 合作伙伴分类树的页面级纯函数(仍内联在 views/base/othercompany/index.vue)。
*
* 建树、位置派生、排序号重排、整树重建、拖拽移动等通用逻辑已收敛到 @/utils/tree
* (其测试在 tests/unit/utils.spec.js),选中「全部分类」的虚拟根约定收敛到 FmsTree。
@@ -8,7 +8,8 @@
*
* 2) 菜单入口权限点(s_power, b_object_type = 'menu')
* 页面 / 外链菜单各派生一行 menu.{菜单编码}(b_action = 'access',名称/顺序/启用跟随菜单),
* 目录没有;保存时按菜单行与该菜单的加载基线 diff;删除菜单时连同授权行一并清理。
* 目录没有;保存时按菜单行与该菜单的加载基线 diff;删除菜单时只删权限点行,
* 引用它的授权行用 dependent_deletes 声明,由保存层按同一条件查出编码再删(设计 §14.6)。
*/
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { flushPromises } from '@vue/test-utils'
@@ -295,7 +296,7 @@ describe('菜单管理:入口权限点(s_power, menu.*)', () => {
expect(saveObjectApi).not.toHaveBeenCalled()
})
it('删除页面菜单:入口权限点与授权行一并清理', async () => {
it('删除页面菜单:删掉入口权限点(授权行由保存层连带清理)', async () => {
const { wrapper } = await mountPage(MenuManagement)
await flushPromises()
@@ -305,12 +306,13 @@ describe('菜单管理:入口权限点(s_power, menu.*)', () => {
await flushPromises()
const requests = saveObjectApi.mock.calls.at(-1)[0]
expect(requests.find((request) => request.table === 's_power').deletes).toEqual([
{ b_id: 'menu.otherdata' },
const powerReq = requests.find((request) => request.table === 's_power')
expect(powerReq.deletes).toEqual([{ b_id: 'menu.otherdata' }])
// 授权行不在这里拼:用 dependent_deletes 声明依赖表 / 键列 / 取值列,
// 由保存层按同一条件查出权限编码再删(前端不查询、不逐个拼授权行)
expect(powerReq.dependent_deletes).toEqual([
{ table: 's_user_power', key_field: 'b_power_id', source_field: 'b_id' },
])
const granted = requests.find((request) => request.table === 's_user_power')
expect(granted.key_field).toBe('b_power_id')
expect(granted.deletes).toEqual([{ b_power_id: 'menu.otherdata' }])
// 菜单主表最后删
expect(requests.at(-1)).toMatchObject({ table: 's_menu' })
})
@@ -64,6 +64,8 @@ function moduleRow(id, name) {
b_canuse: 1,
b_xh: 10,
b_bz: null,
// 迁移后 s_module 才有这一列(见 sql/fms_module_updated_at.sql)
b_updated_at: '2026-09-01 08:00:00',
}
}
@@ -572,3 +574,136 @@ describe('模块管理:保存后的未保存判定', () => {
)
})
})
describe('模块管理:卡头信息与刷新', () => {
const LOCAL_DATETIME = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/
it('保存主表变更时写入 b_updated_at,且时间戳不进草稿、不会让表单重新变脏', async () => {
const wrapper = await mountManagement()
await flushPromises()
wrapper.vm.moduleSelectedId = 'demo'
await flushPromises()
await flushPromises()
wrapper.vm.maindata.b_name = '改名后'
await wrapper.vm.handleSave()
await settleConfigReload(wrapper)
const moduleReq = savedRequests.find((request) => request.table === 's_module')
expect(moduleReq.updates?.[0]).toMatchObject({
b_id: 'demo',
b_name: '改名后',
b_updated_at: expect.stringMatching(LOCAL_DATETIME),
})
// 时间戳若被写进草稿,重载后的基线与草稿就会不一致,这里会判定为仍有未保存改动
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
})
it('只改子表(多语言)时也刷新模块的最后保存时间', async () => {
const wrapper = await mountManagement()
await flushPromises()
wrapper.vm.moduleSelectedId = 'demo'
await flushPromises()
await flushPromises()
const row = wrapper.vm.i18n.find((item) => item.b_key === 'module.demo')
row['en-US'] = 'Demo module v2'
await wrapper.vm.handleSave()
await settleConfigReload(wrapper)
// 主表行本身没有变化,仍要补一条只写时间戳的 update,否则卡头时间会停在改主表的时刻
const moduleReq = savedRequests.find((request) => request.table === 's_module')
expect(moduleReq.updates).toEqual([
{ b_id: 'demo', b_updated_at: expect.stringMatching(LOCAL_DATETIME) },
])
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
})
it('库里还没有 b_updated_at 列时(迁移未执行)不补时间戳更新', async () => {
delete db.s_module.find((item) => item.b_id === 'demo').b_updated_at
const wrapper = await mountManagement()
await flushPromises()
wrapper.vm.moduleSelectedId = 'demo'
await flushPromises()
await flushPromises()
const row = wrapper.vm.i18n.find((item) => item.b_key === 'module.demo')
row['en-US'] = 'Demo module v2'
await wrapper.vm.handleSave()
await settleConfigReload(wrapper)
// 只写不存在列的 update 会被后端以「没有可修改的字段」拒绝,整次保存回滚
expect(savedRequests.some((request) => request.table === 's_module')).toBe(false)
})
it('刷新:有未保存草稿时先弹确认,放弃后原地重载并丢弃草稿', async () => {
const wrapper = await mountManagement()
await flushPromises()
wrapper.vm.moduleSelectedId = 'demo'
await flushPromises()
await flushPromises()
wrapper.vm.maindata.b_name = '改名后'
wrapper.vm.requestReloadConfig()
await flushPromises()
expect(wrapper.vm.switchConfirm).toMatchObject({ pendingId: 'demo', reload: true })
await wrapper.vm.onSwitchDiscard()
await settleConfigReload(wrapper)
expect(String(wrapper.vm.moduleSelectedId)).toBe('demo')
expect(wrapper.vm.maindata.b_name).toBe('示例模块')
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
})
it('刷新:确认框选「保存并刷新」时保存后原地重载,不切换目标', async () => {
const wrapper = await mountManagement()
await flushPromises()
wrapper.vm.moduleSelectedId = 'demo'
await flushPromises()
await flushPromises()
wrapper.vm.maindata.b_name = '改名后'
wrapper.vm.requestReloadConfig()
await flushPromises()
await wrapper.vm.onSwitchSave()
await settleConfigReload(wrapper)
expect(wrapper.vm.switchConfirm).toBe(null)
expect(String(wrapper.vm.moduleSelectedId)).toBe('demo')
expect(wrapper.vm.maindata.b_name).toBe('改名后')
expect(wrapper.vm.hasUnsavedChanges()).toBe(false)
})
it('刷新:无未保存改动时直接重载,不弹确认框', async () => {
const wrapper = await mountManagement()
await flushPromises()
wrapper.vm.moduleSelectedId = 'demo'
await flushPromises()
await flushPromises()
loadDataApi.mockClear()
wrapper.vm.requestReloadConfig()
await settleConfigReload(wrapper)
expect(wrapper.vm.switchConfirm).toBe(null)
expect(loadDataApi).toHaveBeenCalledWith('s_module', "b_id = N'demo'")
})
it('卡头「更多」删除模块走与树右键相同的入口', async () => {
const wrapper = await mountManagement()
await flushPromises()
wrapper.vm.moduleSelectedId = 'demo'
await flushPromises()
await flushPromises()
wrapper.vm.onMoreAction({ key: 'delete' })
await flushPromises()
expect(wrapper.vm.confirmVisible).toBe(true)
expect(wrapper.vm.confirmState?.title).toBe('删除模块')
})
})
@@ -1,11 +1,11 @@
/**
* 回归用例:往来单位分类树(b_othercompany_category)的新增 / 修改 / 删除 / 拖拽排序。
* 回归用例:合作伙伴分类树(b_othercompany_category)的新增 / 修改 / 删除 / 拖拽排序。
*
* 1) b_path 为 not null 且无默认值(b_depth / b_xh / b_canuse 才有默认值),新增分类时
* 必须按父级派生 b_depth / b_path 一起写入,否则插入直接失败;
* 且两个派生字段与 b_parent_id 必须同批落库,否则会留下「路径按新父级、父级列还是旧值」。
*
* 2) 删除分类有两条守卫:还有子分类、分类下还挂着往来单位。两条都必须拦在发请求之前。
* 2) 删除分类有两条守卫:还有子分类、分类下还挂着合作伙伴。两条都必须拦在发请求之前。
*
* 3) 分类编码 b_id 是主键,修改时只提交名称与排序号,不能改编码。
*/
@@ -102,7 +102,7 @@ async function mountOthercompany() {
return wrapper
}
describe('往来单位:新增分类写派生树字段', () => {
describe('合作伙伴:新增分类写派生树字段', () => {
it('新增顶级分类写入 b_parent_id=null 与根路径', async () => {
const wrapper = await mountOthercompany()
@@ -140,7 +140,7 @@ describe('往来单位:新增分类写派生树字段', () => {
})
})
describe('往来单位:修改分类', () => {
describe('合作伙伴:修改分类', () => {
it('只提交名称与排序号,分类编码保持为主键值', async () => {
const wrapper = await mountOthercompany()
@@ -155,7 +155,7 @@ describe('往来单位:修改分类', () => {
})
})
describe('往来单位:删除分类的两条守卫', () => {
describe('合作伙伴:删除分类的两条守卫', () => {
it('还有子分类时直接拦下,不发任何请求', async () => {
const wrapper = await mountOthercompany()
@@ -185,13 +185,13 @@ describe('往来单位:删除分类的两条守卫', () => {
expect(Message.error).not.toHaveBeenCalled()
})
it('分类下还有往来单位时拦下,不发删除请求', async () => {
it('分类下还有合作伙伴时拦下,不发删除请求', async () => {
const wrapper = await mountOthercompany()
pageDataApi.mockResolvedValueOnce({ data: { rows: [{ b_id: 'OC001' }], total: 1 } })
await wrapper.vm.onCategoryDelete({ b_id: 'child1' })
expect(Message.warning).toHaveBeenCalledWith('该分类下还有往来单位数据,不能删除')
expect(Message.warning).toHaveBeenCalledWith('该分类下还有合作伙伴数据,不能删除')
expect(modalConfirm).not.toHaveBeenCalled()
expect(saveObjectApi).not.toHaveBeenCalled()
})
@@ -213,7 +213,7 @@ describe('往来单位:删除分类的两条守卫', () => {
})
})
describe('往来单位:拖拽分类', () => {
describe('合作伙伴:拖拽分类', () => {
it('跨级拖入时同批落库 b_parent_id 与重建后的 b_depth / b_path', async () => {
const wrapper = await mountOthercompany()
@@ -247,7 +247,7 @@ describe('往来单位:拖拽分类', () => {
})
})
describe('往来单位分类树:搜索', () => {
describe('合作伙伴分类树:搜索', () => {
it('按名称与编码过滤,并把命中的层级展开', async () => {
const wrapper = await mountOthercompany()
@@ -68,7 +68,9 @@ describe('分配权限抽屉:头部', () => {
'字段权限',
'数据范围',
])
expect(wrapper.findAll('.power-pane').map((el) => el.attributes('data-pane'))).toEqual([
// 功能权限自带一棵菜单树,是独立面板;字段权限 / 数据范围共用一个面板容器
// (同一棵模块树 + 各自的内容区),所以 data-pane 落在内容区上
expect(wrapper.findAll('[data-pane]').map((el) => el.attributes('data-pane'))).toEqual([
'power',
'field',
'data',
@@ -4,13 +4,17 @@
* 数据加载与保存由父级 UserPowerDrawer 负责,面板自身只做这几件事,测试只锁这几件:
* 1. 打开时的初始状态:只展开「全部分类」虚拟根,业务目录收起、不选中任何菜单;
* 2. 菜单选中语义:重复点击已选中的菜单不取消选中,只有点「全部分类」才清空;
* 3. 右侧结构:页头(菜单名 + 入口勾选框 + 本页进度)+ 一张「模块 / 动作 / 已选」三列表。
* 模块级勾选在「模块」列、动作勾选在「动作」列,层级靠列而不是靠嵌套容器;
* 目录不渲染入口勾选框、没配权限点的模块不渲染,都不出空态文案;
* 3. 右侧结构:页头(菜单名 + 入口勾选框 + 右端 x/y 进度)+ 一个模块一块的授权块列表。
* 块头是模块(模块级勾选框 + 「已选 x/y」):勾选框表示"这个数据给没给"——有任意
* 一项权限即勾选、一项都没有才未勾选(不做半选),点一下即给模块使用 / 整块收回;
* 块内是它的动作,层级靠块头的上下关系;
* 没配权限点的模块不渲染;目录不渲染入口勾选框,说明放内容区中间而不是页头;
* 本页没有可配置模块时内容区中间给一句说明(目录与外链 / 自定义页文案不同);
* 4. 面板顶部工具栏的「全授权 / 全禁用」作用于该账号的全部功能权限(s_power 全量),
* 与左侧选中哪个菜单无关;
* 5. 勾选结果经 v-model 回传,三层是「保护性单向联动」:
* 勾动作补模块使用与入口,模块级勾选框全给全撤,关入口收回该菜单下的全部授权。
* 5. 勾选结果经 v-model 回传,三级是「逐级给、连带收」:
* 勾下级自动补上级(操作 → 模块使用 → 入口);勾上级不替下级勾选;
* 取消上级连带收回下级(关入口收回本页全部,取消模块收回该模块全部权限点)。
*
* 勾选框是 ui/Checkbox(label 包原生 input),测试直接驱动 input 的 change ——
* jsdom 不保证 label 点击会转发给内部控件,走 change 才是组件真实监听的入口。
@@ -124,9 +128,9 @@ function findRow(wrapper, text) {
return wrapper.findAll('.tree-node').find((row) => row.text().includes(text))
}
/** 模块行的模块名清单(按行顺序) */
/** 授权块的模块名清单(按块顺序) */
function moduleNames(wrapper) {
return wrapper.findAll('.power-row__module .checkbox-label').map((el) => el.text())
return wrapper.findAll('.power-row__head .checkbox-label').map((el) => el.text())
}
/** 页头(菜单名 + 入口)的入口勾选框(目录没有入口权限点时返回 undefined) */
@@ -135,31 +139,31 @@ function entryCheck(wrapper) {
return found.exists() ? found : undefined
}
/** 第 index 个模块行的模块级勾选框 */
/** 第 index 个授权块的模块级勾选框 */
function moduleCheck(wrapper, index = 0) {
return wrapper.findAll('.power-row__module .checkbox')[index]
return wrapper.findAll('.power-row__head .checkbox')[index]
}
/** 勾选框形态的开关总数:页头入口 1 个 + 每行模块 1 个(动作勾选框在「动作」列,不计入) */
/** 勾选框形态的开关总数:页头入口 1 个 + 每块模块 1 个(动作勾选框在块内,不计入) */
function useChecks(wrapper) {
return wrapper.findAll('.power-head__entry, .power-row__module .checkbox')
return wrapper.findAll('.power-head__entry, .power-row__head .checkbox')
}
/** 第 index 行行尾的「已选」计数 */
/** 第 index 个授权块块头的「已选」计数(没有动作的模块不渲染该节点) */
function rowCount(wrapper, index = 0) {
return wrapper.findAll('.power-row .power-col--count')[index]
return wrapper.findAll('.power-row .power-row__count')[index]
}
/** 某个模块行(按模块名定位,比按下标稳) */
/** 某个授权块(按模块名定位,比按下标稳) */
function moduleRow(wrapper, moduleName) {
return wrapper
.findAll('.power-row')
.find((row) => row.find('.power-row__module .checkbox-label').text() === moduleName)
.find((row) => row.find('.power-row__head .checkbox-label').text() === moduleName)
}
/**
* 动作勾选框:按「模块名 + 动作名」定位。
* 动作只有「动作」列有,所以按名字找比按下标找稳,动作数量变化也不影响。
* 动作只出现在所属模块的块内,所以按名字找比按下标找稳,动作数量变化也不影响。
*/
function actionCheck(wrapper, moduleName, actionName) {
return moduleRow(wrapper, moduleName)
@@ -261,14 +265,18 @@ describe('功能权限面板:菜单选中语义', () => {
})
describe('功能权限面板:右侧结构', () => {
it('页头:菜单名做标题 + 入口勾选框 + 本页进度', async () => {
it('页头:菜单名做标题 + 入口勾选框 + 右端的授权进度', async () => {
const wrapper = await mountWithMenu('partner')
expect(wrapper.find('.power-head__name').text()).toBe('合作伙伴')
expect(entryCheck(wrapper).find('.checkbox-label').text()).toBe('可访问')
expect(wrapper.find('.power-head__meta').text()).toBe('本页 2 个模块,已授权 0 个')
// 进度常显的只是两个数字,完整说法在 title 里
expect(wrapper.find('.power-head__meta').text()).toBe('0/2')
expect(wrapper.find('.power-head__meta').attributes('title')).toBe(
'本页 2 个模块,已授权 0 个',
)
// 页头里只有菜单名、勾选框与进度,没有多余的说明句
expect(wrapper.find('.power-head').text()).toBe('合作伙伴 可访问 本页 2 个模块,已授权 0 个')
expect(wrapper.find('.power-head').text()).toBe('合作伙伴 可访问 0/2')
})
it('本页进度:至少给了一项权限的模块才计入已授权', async () => {
@@ -276,24 +284,25 @@ describe('功能权限面板:右侧结构', () => {
modelValue: ['menu.partner', 'module.v_b_othercompany', 'action.v_b_othercompany.create'],
})
expect(wrapper.find('.power-head__meta').text()).toBe('本页 2 个模块,已授权 1 个')
expect(wrapper.find('.power-head__meta').text()).toBe('1/2')
})
it('入口的连带影响只进 title,不常显在页头', async () => {
const wrapper = await mountWithMenu('partner')
expect(entryCheck(wrapper).attributes('title')).toBe(
'可访问「合作伙伴」(关闭会收回该页面下的全部模块授权)',
'可访问「合作伙伴」(打开只给入口,本页模块与操作要自己勾;关闭会全部收回)',
)
})
it('表格:模块 / 动作 / 已选三列,一个模块一行', async () => {
it('授权块:块头是模块(勾选框 + 已选),块内是该模块的动作', async () => {
const wrapper = await mountWithMenu('partner')
expect(wrapper.find('.power-grid__head').text()).toBe('模块动作已选')
expect(moduleNames(wrapper)).toEqual(['合作伙伴', '联系人'])
expect(wrapper.findAll('.power-row')).toHaveLength(2)
// 勾选框形态的开关:页头入口 1 个 + 模块级 2 个(动作勾选框在「动作」列,不计入)
// 模块在上、动作在下:块头(.power-row__head)里的勾选框只有模块级那一个
expect(wrapper.findAll('.power-row__head .checkbox')).toHaveLength(2)
// 勾选框形态的开关:页头入口 1 个 + 模块级 2 个(动作勾选框在块内,不计入)
expect(useChecks(wrapper)).toHaveLength(3)
})
@@ -309,51 +318,55 @@ describe('功能权限面板:右侧结构', () => {
expect(actionCheck(wrapper, '合作伙伴', '新增').attributes('title')).toBe('合作伙伴 新增')
})
it('行尾「已选 x/y」= 该模块已授权的动作数 / 动作总数', async () => {
it('块头「已选 x/y」= 该模块已授权的动作数 / 动作总数', async () => {
const wrapper = await mountWithMenu('partner', {
modelValue: ['module.v_b_othercompany', 'action.v_b_othercompany.create'],
})
// 「模块使用」不计入已选:它并入模块级勾选框,与「动作」列不是一回事
expect(rowCount(wrapper, 0).text()).toBe('1/3')
expect(rowCount(wrapper, 1).text()).toBe('0/2')
// 「模块使用」不计入已选:它并入模块级勾选框,与块内的动作不是一回事
expect(rowCount(wrapper, 0).text()).toBe('已选 1/3')
expect(rowCount(wrapper, 1).text()).toBe('已选 0/2')
})
it('只有模块使用、没有动作的模块不显示计数', async () => {
const wrapper = await mountWithMenu('user')
expect(moduleNames(wrapper)).toEqual(['用户管理'])
expect(rowCount(wrapper, 0).text()).toBe('')
expect(rowCount(wrapper, 0)).toBeUndefined()
})
it('模块级勾选框三态:全选 / 半选 / 未选', async () => {
it('模块级勾选框两态:有任意一项权限就勾选,一项都没有才未勾选', async () => {
const wrapper = await mountWithMenu('partner')
// 全选:模块使用 + 全部动作都在
// 全给:模块使用 + 全部动作都在 → 勾选
await wrapper.setProps({ modelValue: OTHERCOMPANY_ALL })
expect(moduleCheck(wrapper, 0).classes()).toContain('checkbox-checked')
expect(moduleCheck(wrapper, 0).classes()).not.toContain('checkbox-indeterminate')
// 半选:进了这个模块,但只给了部分动作
await wrapper.setProps({
modelValue: ['module.v_b_othercompany', 'action.v_b_othercompany.create'],
})
expect(moduleCheck(wrapper, 0).classes()).not.toContain('checkbox-checked')
expect(moduleCheck(wrapper, 0).classes()).toContain('checkbox-indeterminate')
// 只勾了一个动作 → 数据权限也已给(勾动作会补模块使用),勾选框照样是勾选
await wrapper.setProps({ modelValue: ['action.v_b_othercompany.create'] })
expect(moduleCheck(wrapper, 0).classes()).toContain('checkbox-checked')
// 未选
// 只留了模块使用、一个动作都没给:这个数据仍算有权限 → 勾选
await wrapper.setProps({ modelValue: ['module.v_b_othercompany'] })
expect(moduleCheck(wrapper, 0).classes()).toContain('checkbox-checked')
expect(moduleCheck(wrapper, 0).classes()).not.toContain('checkbox-indeterminate')
// 一项都没有 → 未勾选(不做半选,没有减号这个形态)
await wrapper.setProps({ modelValue: [] })
expect(moduleCheck(wrapper, 0).classes()).not.toContain('checkbox-checked')
expect(moduleCheck(wrapper, 0).classes()).not.toContain('checkbox-indeterminate')
})
it('模块级勾选框的 title 说清点它会全给还是全撤', async () => {
it('模块级勾选框的 title 说清点它是给数据权限还是整块收回', async () => {
const wrapper = await mountWithMenu('partner')
expect(moduleCheck(wrapper, 0).attributes('title')).toBe('全选「合作伙伴」的 4 项权限')
expect(moduleCheck(wrapper, 0).attributes('title')).toBe('给「合作伙伴」模块权限(不含操作)')
await wrapper.setProps({ modelValue: OTHERCOMPANY_ALL })
expect(moduleCheck(wrapper, 0).attributes('title')).toBe('取消「合作伙伴」的全部授权')
expect(moduleCheck(wrapper, 0).attributes('title')).toBe(
'取消「合作伙伴」的全部授权(含全部操作)',
)
})
it('已授权的入口渲染为打开态', async () => {
@@ -363,23 +376,27 @@ describe('功能权限面板:右侧结构', () => {
expect(entryCheck(wrapper).classes()).toContain('checkbox-checked')
})
it('目录:不渲染入口勾选框(没有 menu.* 权限点),只在页头说明原因', async () => {
it('目录:不渲染入口勾选框(没有 menu.* 权限点),说明放在内容区中间', async () => {
const wrapper = await mountWithMenu('base')
expect(useChecks(wrapper)).toHaveLength(0)
// 目录没有可勾的入口:页头只显示名称,不出现勾选框
// 目录没有可勾的入口:页头只显示名称,不出现勾选框,也不在页头写说明
// (说明会撑高页头,与页面菜单的页头高度对不齐)
expect(entryCheck(wrapper)).toBeUndefined()
expect(wrapper.find('.power-head__name').text()).toBe('基础资料')
expect(wrapper.find('.power-head__note').text()).toBe('目录不需要单独授权')
expect(wrapper.find('.power-head').text()).toBe('基础资料')
expect(wrapper.find('.power-panel__state').text()).toBe('目录不需要单独授权')
})
it('外链菜单:没有可授权模块,连表头都不渲染,入口勾选框独立可用', async () => {
it('外链菜单:没有可授权模块,授权列表换成中间一句说明,入口勾选框独立可用', async () => {
const wrapper = await mountWithMenu('portal')
expect(moduleNames(wrapper)).toEqual([])
expect(wrapper.findAll('.power-row')).toHaveLength(0)
expect(wrapper.find('.power-grid').exists()).toBe(false)
// 没有模块可配:进度不渲染(没有分母),内容区给一句说明而不是一片空白
expect(wrapper.find('.power-head__meta').exists()).toBe(false)
expect(wrapper.find('.power-panel__state').text()).toBe('本页没有可配置的模块')
await setCheck(entryCheck(wrapper))
expect(lastEmitted(wrapper)).toEqual(['menu.portal'])
@@ -389,13 +406,14 @@ describe('功能权限面板:右侧结构', () => {
const wrapper = await mountWithMenu('partner', { menuModules: {} })
expect(wrapper.find('.power-grid').exists()).toBe(false)
expect(wrapper.find('.power-panel__state').text()).toBe('本页没有可配置的模块')
expect(useChecks(wrapper)).toHaveLength(1)
await setCheck(entryCheck(wrapper))
expect(lastEmitted(wrapper)).toEqual(['menu.partner'])
})
it('未配置权限点的模块不渲染,也不显示空态文案', async () => {
it('未配置权限点的模块不渲染,也不为它单独出提示', async () => {
const wrapper = await mountWithMenu('user')
// 「币种」绑定了但没配权限点:不渲染成行,也不出任何提示
@@ -403,6 +421,8 @@ describe('功能权限面板:右侧结构', () => {
// 只有模块使用权限点的模块没有动作勾选框
expect(wrapper.findAll('.power-row__actions .checkbox')).toHaveLength(0)
expect(wrapper.find('.power-panel__main').text()).not.toContain('未配置权限点')
// 本页仍有可配置模块 → 内容区不出现整体的空态说明
expect(wrapper.find('.power-panel__state').exists()).toBe(false)
})
})
@@ -446,24 +466,26 @@ describe('功能权限面板:全授权 / 全禁用', () => {
})
describe('功能权限面板:三层联动', () => {
it('勾模块级勾选框:一次给全该模块的权限点,并补上菜单入口', async () => {
it('勾模块级勾选框:只给该模块的模块使用权限,并补上菜单入口', async () => {
const wrapper = await mountWithMenu('partner')
await setCheck(moduleCheck(wrapper))
await nextTick()
expect(lastEmitted(wrapper)).toEqual([...OTHERCOMPANY_ALL, 'menu.partner'])
// 勾上级不替下级勾选:只给「模块使用」,下面的操作要自己勾
expect(lastEmitted(wrapper)).toEqual(['module.v_b_othercompany', 'menu.partner'])
})
it('半选时点模块级勾选框:补齐到全选,不产生重复项', async () => {
it('已授权的模块点勾选框:收回该模块全部权限', async () => {
const wrapper = await mountWithMenu('partner', {
modelValue: ['module.v_b_othercompany', 'action.v_b_othercompany.create'],
})
await setCheck(moduleCheck(wrapper))
// 勾选框显示的是"这个数据给没给"(已给 → 勾选),点一下就是它的反面:整块收回
await setCheck(moduleCheck(wrapper), false)
await nextTick()
expect(lastEmitted(wrapper)).toEqual([...OTHERCOMPANY_ALL, 'menu.partner'])
expect(lastEmitted(wrapper)).toEqual([])
})
it('取消模块级勾选框:收回该模块的全部权限点,不动菜单入口', async () => {
@@ -490,7 +512,7 @@ describe('功能权限面板:三层联动', () => {
])
})
it('取消单个动作:不动模块使用与入口,模块级勾选框落到半选', async () => {
it('取消单个动作:不动模块使用与入口,模块仍显示为已授权', async () => {
const wrapper = await mountWithMenu('partner', {
modelValue: ['menu.partner', 'module.v_b_othercompany', 'action.v_b_othercompany.create'],
})
@@ -499,16 +521,18 @@ describe('功能权限面板:三层联动', () => {
await nextTick()
expect(lastEmitted(wrapper)).toEqual(['menu.partner', 'module.v_b_othercompany'])
expect(moduleCheck(wrapper, 0).classes()).toContain('checkbox-indeterminate')
expect(rowCount(wrapper, 0).text()).toBe('0/3')
// 这个数据本身还要用(模块使用还在)→ 勾选框保持勾选;撤了几项动作看块头计数
expect(moduleCheck(wrapper, 0).classes()).toContain('checkbox-checked')
expect(rowCount(wrapper, 0).text()).toBe('已选 0/3')
})
it('开菜单入口:只补入口本身,不给任何模块权限', async () => {
it('开菜单入口:只给入口本身,本页模块要自己勾', async () => {
const wrapper = await mountWithMenu('partner')
await setCheck(entryCheck(wrapper))
await nextTick()
// 勾上级不替下级勾选:打开入口只给入口,模块与操作留给下面自己勾
expect(lastEmitted(wrapper)).toEqual(['menu.partner'])
})
@@ -523,7 +547,7 @@ describe('功能权限面板:三层联动', () => {
expect(lastEmitted(wrapper)).toEqual([])
})
it('勾选态回显:各行动作各管各的,只勾动作时模块级是半选', async () => {
it('勾选态回显:各行动作各管各的,只勾动作时模块级也是已勾选', async () => {
const wrapper = await mountWithMenu('partner', {
modelValue: ['action.v_b_othercompany.create'],
})
@@ -532,7 +556,8 @@ describe('功能权限面板:三层联动', () => {
expect(actionCheck(wrapper, '合作伙伴', '修改').classes()).not.toContain('checkbox-checked')
// 同名动作在别的模块行里不受影响
expect(actionCheck(wrapper, '联系人', '新增').classes()).not.toContain('checkbox-checked')
// 只勾了动作、没勾模块使用 → 模块级勾选框半选
expect(moduleCheck(wrapper, 0).classes()).toContain('checkbox-indeterminate')
// 操作有权限 → 数据也有权限:模块级勾选框是勾选的(没有半选态)
expect(moduleCheck(wrapper, 0).classes()).toContain('checkbox-checked')
expect(moduleCheck(wrapper, 0).classes()).not.toContain('checkbox-indeterminate')
})
})