Files
workspace/code/g3soft-erp/web/tests/views/module/module-management/ModuleTreePanel.spec.ts
T
2026-10-15 17:31:35 +08:00

160 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ModuleTreePanel 行尾启用勾选。
*
* 勾选框不走 G3Tree 内置的 checkable(那是行首、多选语义),而是画在 #title 插槽的
* 行尾。自己画就要自己兜住三件事,也正是这里要锁住的行为:
* 1. 勾选上报 toggleEnabled(id + 目标启用态),由父级落库;
* 2. 点击不冒泡 —— 不能顺带选中这一行(行上挂了 click 选中 / mousedown 武装拖拽);
* 3. 未保存草稿不可勾(库里没有这行,改不了它的 b_canuse)。
*/
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import { G3Checkbox, G3Tree } from '@g3soft/ui'
import ModuleTreePanel from '@/views/module/module-management/ModuleTreePanel.vue'
import type { ModuleNode } from '@/views/module/module-management/utils'
import BizTreePanel from '@/components/biz-tree/BizTreePanel.vue'
const TREE = [
{
b_id: 'm1',
b_name: '启用的模块',
b_module_type: 'module',
b_canuse: 1,
},
{
b_id: 'm2',
b_name: '停用的模块',
b_module_type: 'module',
b_canuse: 0,
},
{
b_id: '-1',
b_name: '未保存草稿',
b_module_type: 'module',
b_canuse: 1,
},
] as unknown as ModuleNode[]
function mountPanel(unsavedIds = new Set<string>()) {
return mount(ModuleTreePanel, {
props: { treeData: TREE, unsavedIds },
})
}
/** 按标题取节点行(BizTreePanel 默认插槽由本组件接管,树节点带 role=treeitem) */
const rowOf = (wrapper: VueWrapper, label: string) =>
wrapper.findAll('[role="treeitem"]').find((row) => row.text().includes(label))
/** 取某节点行里的勾选框 */
const checkboxOf = (wrapper: VueWrapper, label: string) =>
rowOf(wrapper, label)?.findComponent(G3Checkbox)
describe('ModuleTreePanel 行尾启用勾选', () => {
it('每个节点行都带一个勾选框,勾选态反映 b_canuse', () => {
const wrapper = mountPanel()
expect(checkboxOf(wrapper, '启用的模块')?.props('modelValue')).toBe(true)
expect(checkboxOf(wrapper, '停用的模块')?.props('modelValue')).toBe(false)
})
it('勾选上报 toggleEnabled(id + 目标启用态)', async () => {
const wrapper = mountPanel()
checkboxOf(wrapper, '停用的模块')?.vm.$emit('update:modelValue', true)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('toggleEnabled')?.[0]?.[0]).toEqual({ id: 'm2', enabled: true })
})
it('取消勾选同样上报(enabled: false)', async () => {
const wrapper = mountPanel()
checkboxOf(wrapper, '启用的模块')?.vm.$emit('update:modelValue', false)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('toggleEnabled')?.[0]?.[0]).toEqual({ id: 'm1', enabled: false })
})
it('点击勾选框不触发行的选中(事件不冒泡)', () => {
const wrapper = mountPanel()
const row = rowOf(wrapper, '启用的模块')!
// 勾选框外层用 @click.stop 拦住冒泡:直接点勾选框本体,行上不应收到 click
const box = checkboxOf(wrapper, '启用的模块')!.find('input')
void box.trigger('click')
expect(wrapper.emitted('select')).toBeUndefined()
// 行自身仍可正常选中,说明拦的是勾选框那一下、不是把整行点击弄坏了
void row.trigger('click')
expect(wrapper.emitted('select')?.[0]?.[0]).toBe('m1')
})
it('未保存草稿的勾选框禁用且不上报(库里还没有这行)', async () => {
const wrapper = mountPanel(new Set(['-1']))
const box = checkboxOf(wrapper, '未保存草稿')!
expect(box.props('disabled')).toBe(true)
box.vm.$emit('update:modelValue', false)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('toggleEnabled')).toBeUndefined()
})
it('已保存节点不受草稿集合影响,仍可勾选', async () => {
const wrapper = mountPanel(new Set(['-1']))
expect(checkboxOf(wrapper, '启用的模块')?.props('disabled')).toBe(false)
checkboxOf(wrapper, '启用的模块')?.vm.$emit('update:modelValue', false)
await wrapper.vm.$nextTick()
expect(wrapper.emitted('toggleEnabled')?.[0]?.[0]).toEqual({ id: 'm1', enabled: false })
})
it('勾选框渲染在名称之后的行尾(不是行首的多选位)', () => {
const wrapper = mountPanel()
const row = rowOf(wrapper, '启用的模块')!
// 行内文本顺序:名称在前;勾选框的 DOM 位置在名称节点之后
const nameEl = row.find('.mm-tree-node__name')
const enableEl = row.find('.mm-tree-node__enable')
expect(nameEl.exists()).toBe(true)
expect(enableEl.exists()).toBe(true)
// compareDocumentPosition: 4 = 名称之后(DOCUMENT_POSITION_FOLLOWING)
const relation = nameEl.element.compareDocumentPosition(enableEl.element)
expect(relation & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
})
it('面板必须开 block-node:否则内容层不撑满,勾选框贴不到行尾', () => {
const wrapper = mountPanel()
// G3Tree 的节点内容层默认 inline-flex 且无 flex:1(宽度=文字宽度),
// 只有 blockNode 会给它 flex:1。少了它 .mm-tree-node 的 flex:1 只撑到
// 「文字那一小段」,勾选框会紧跟名称而不是贴右边 —— 这条断言把这个前提钉住。
//
// 断言必须落在 G3Tree 上:BizTreePanel / BizTree 都是 inheritAttrs:false,
// 模板写的 'block-node'(kebab)会原样留在 BizTree 的 $attrs 里,
// 所以 BizTree.props('blockNode') 是 undefined —— 但往下传给 G3Tree 时
// Vue 会把 kebab attr 归一到 camelCase prop,G3Tree 那边是 true。
// 在中间层断言会得到假阴性(这条一开始就是这么写错的)。
expect(wrapper.findComponent(G3Tree).props('blockNode')).toBe(true)
})
it('block-node 生效:内容层带 is-block(flex:1),勾选才有行尾可贴', () => {
const wrapper = mountPanel()
const row = rowOf(wrapper, '启用的模块')!
// 库内样式:.g3-tree-node__content 只有 .is-block 才 flex:1
expect(row.find('.g3-tree-node__content').classes()).toContain('is-block')
})
it('面板不启用 G3Tree 内置 checkable(避免行首出现多选勾选框)', () => {
const wrapper = mountPanel()
const panel = wrapper.findComponent(BizTreePanel)
// checkable 未透传:内置勾选框列不渲染
expect(panel.attributes('checkable')).toBeUndefined()
})
})