/** * 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()) { 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() }) })