u
This commit is contained in:
1 parent
15e0ae5b67
commit
e434374db5
71 files changed
+5726
-329
No files matched your search
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* BizTree 业务约定测试。
|
||||
*
|
||||
* 移植自 fms-vue 的 tests/components/fms-tree.spec.js(删掉 i18n 相关用例),
|
||||
* DOM 断言换成本项目 G3Tree 的实际结构:class 前缀 g3,行上挂 role="treeitem" + aria-selected,
|
||||
* 右键菜单 Teleport 到 body(.g3-tree__context-menu)。
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { h, nextTick } from 'vue'
|
||||
import BizTree from '@/components/biz-tree/BizTree.vue'
|
||||
|
||||
const makeTreeData = () => [
|
||||
{
|
||||
b_id: '1',
|
||||
b_name: '根节点',
|
||||
b_canuse: 1,
|
||||
children: [{ b_id: '2', b_name: '子节点' }],
|
||||
},
|
||||
]
|
||||
|
||||
const nodeContextMenu = () => [{ key: 'add', label: '新增子项' }]
|
||||
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
describe('BizTree 业务约定', () => {
|
||||
it('默认字段名按 b_id / b_name / children 渲染,无需传 fieldNames', () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: makeTreeData() } })
|
||||
expect(wrapper.find('.g3-tree-node__content').text()).toBe('根节点')
|
||||
})
|
||||
|
||||
it('未传 fieldNames 时 b_name 缺失回退到 b_id', () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: [{ b_id: 'm1' }] } })
|
||||
expect(wrapper.find('.g3-tree-node__content').text()).toBe('m1')
|
||||
})
|
||||
|
||||
it('fieldNames 可覆盖默认字段', () => {
|
||||
const wrapper = mount(BizTree, {
|
||||
props: {
|
||||
treeData: [{ code: 'a', label: '甲' }],
|
||||
fieldNames: { key: 'code', title: 'label' },
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.g3-tree-node__content').text()).toBe('甲')
|
||||
})
|
||||
})
|
||||
|
||||
describe('BizTree #title 插槽', () => {
|
||||
it('插槽透出解析后的 title', () => {
|
||||
const calls: { title?: string }[] = []
|
||||
mount(BizTree, {
|
||||
props: { treeData: makeTreeData() },
|
||||
slots: {
|
||||
title: (scope: { title?: string }) => {
|
||||
calls.push(scope)
|
||||
return h('span', { class: 'node-name' }, scope.title)
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(calls[0].title).toBe('根节点')
|
||||
})
|
||||
|
||||
it('插槽拿到的 node 是业务树的活对象,改动能回写原数据并触发重渲染', async () => {
|
||||
const data = makeTreeData()
|
||||
const calls: { node: Record<string, unknown> }[] = []
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: data },
|
||||
slots: {
|
||||
title: (scope: { node: Record<string, unknown> }) => {
|
||||
calls.push(scope)
|
||||
return h('span', { class: 'node-used' }, String(scope.node.b_canuse))
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(wrapper.find('.node-used').text()).toBe('1')
|
||||
|
||||
const node = calls[0].node
|
||||
node.b_canuse = 0
|
||||
node.b_name = '改名'
|
||||
await nextTick()
|
||||
|
||||
expect(data[0].b_canuse).toBe(0)
|
||||
expect(data[0].b_name).toBe('改名')
|
||||
expect(wrapper.find('.node-used').text()).toBe('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('BizTree 虚拟根(showRoot)', () => {
|
||||
it('默认不注入根节点', () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: makeTreeData() } })
|
||||
expect(wrapper.findAll('.g3-tree-node')).toHaveLength(1)
|
||||
expect(wrapper.find('.g3-tree-node__content').text()).toBe('根节点')
|
||||
})
|
||||
|
||||
it('showRoot 注入「全部分类」根,默认展开且带折叠箭头', () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: makeTreeData(), showRoot: true } })
|
||||
const rows = wrapper.findAll('.g3-tree-node')
|
||||
expect(rows).toHaveLength(2)
|
||||
expect(rows[0].text()).toContain('全部分类')
|
||||
// 有子节点:根不是叶子,渲染折叠箭头
|
||||
expect(rows[0].classes()).not.toContain('is-leaf')
|
||||
expect(rows[0].find('.g3-tree-node__switcher').exists()).toBe(true)
|
||||
// 调用方的 expandedKeys 为空,根的子节点依然可见
|
||||
expect(rows[1].text()).toContain('根节点')
|
||||
})
|
||||
|
||||
it('业务树为空时虚拟根仍显示折叠箭头(分组节点不是叶子)', () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: [], showRoot: true } })
|
||||
const rows = wrapper.findAll('.g3-tree-node')
|
||||
expect(rows).toHaveLength(1)
|
||||
expect(rows[0].classes()).not.toContain('is-leaf')
|
||||
expect(rows[0].find('.g3-tree-node__switcher').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('根节点以 rootKey 作为 key,业务节点不带根 key', () => {
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), showRoot: true, rootKey: 'ROOT' },
|
||||
slots: {
|
||||
title: (scope: { node: { b_id: string } }) =>
|
||||
h('span', { class: 'node-id' }, String(scope.node.b_id)),
|
||||
},
|
||||
})
|
||||
const ids = wrapper.findAll('.node-id').map((el) => el.text())
|
||||
expect(ids).toEqual(['ROOT', '1'])
|
||||
})
|
||||
|
||||
it('rootTitle 可自定义根标题', () => {
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), showRoot: true, rootTitle: '所有分类' },
|
||||
})
|
||||
expect(wrapper.find('.g3-tree-node__content').text()).toBe('所有分类')
|
||||
})
|
||||
|
||||
it('受控 expandedKeys 已包含根 key 时不再重复追加', () => {
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), showRoot: true, expandedKeys: ['__biz_tree_root__'] },
|
||||
})
|
||||
expect(wrapper.findAll('.g3-tree-node')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('非受控 + showRoot 时根可收起再展开,业务节点仍可自行展开', async () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: makeTreeData(), showRoot: true } })
|
||||
expect(wrapper.findAll('.g3-tree-node')).toHaveLength(2)
|
||||
|
||||
// 第一个真实展开按钮属于虚拟根:点击收起整棵树
|
||||
await wrapper.find('.g3-tree-node__switcher').trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.findAll('.g3-tree-node')).toHaveLength(1)
|
||||
// 回传的展开键是归一化后的业务键,不含 rootKey
|
||||
expect(wrapper.emitted('update:expandedKeys')?.[0][0]).toEqual([])
|
||||
|
||||
// 再点一次展开,业务节点回来
|
||||
await wrapper.find('.g3-tree-node__switcher').trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.findAll('.g3-tree-node')).toHaveLength(2)
|
||||
expect(wrapper.find('.g3-tree-node__content').text()).toBe('全部分类')
|
||||
|
||||
// 业务节点自己的展开按钮照常工作
|
||||
await wrapper.findAll('.g3-tree-node__switcher')[1].trigger('click')
|
||||
await nextTick()
|
||||
expect(wrapper.findAll('.g3-tree-node')).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('非受控且无虚拟根时展开键交给 G3Tree,default-expand-all 照常生效', () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: makeTreeData(), defaultExpandAll: true } })
|
||||
expect(wrapper.findAll('.g3-tree-node')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('BizTree 虚拟根的选中约定(showRoot)', () => {
|
||||
it('selectedKeys 为空时虚拟根呈现选中态,调用方无需传 rootKey', () => {
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), showRoot: true, selectedKeys: [] },
|
||||
})
|
||||
// 第一个节点是虚拟根,应带上选中类
|
||||
expect(wrapper.findAll('.g3-tree-node')[0].classes()).toContain('is-selected')
|
||||
})
|
||||
|
||||
it('未传 selectedKeys 时同样把虚拟根当作选中(默认选中「全部」)', () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: makeTreeData(), showRoot: true } })
|
||||
expect(wrapper.findAll('.g3-tree-node')[0].classes()).toContain('is-selected')
|
||||
})
|
||||
|
||||
it('选中业务节点时虚拟根不再高亮', () => {
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), showRoot: true, selectedKeys: ['1'] },
|
||||
})
|
||||
const rows = wrapper.findAll('.g3-tree-node')
|
||||
expect(rows[0].classes()).not.toContain('is-selected')
|
||||
expect(rows[1].classes()).toContain('is-selected')
|
||||
})
|
||||
|
||||
it('点中虚拟根时 select 回传空数组,调用方看不到 rootKey', async () => {
|
||||
const onSelect = vi.fn()
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), showRoot: true },
|
||||
attrs: { onSelect },
|
||||
})
|
||||
await wrapper.findAll('.g3-tree-node__content')[0].trigger('click')
|
||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||
// 归一化成 [],而不是 ['__biz_tree_root__']
|
||||
expect(onSelect.mock.calls[0][0]).toEqual([])
|
||||
})
|
||||
|
||||
it('点中业务节点时 select 原样回传该节点的 key', async () => {
|
||||
const onSelect = vi.fn()
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), showRoot: true },
|
||||
attrs: { onSelect },
|
||||
})
|
||||
await wrapper.findAll('.g3-tree-node__content')[1].trigger('click')
|
||||
expect(onSelect.mock.calls[0][0]).toEqual(['1'])
|
||||
})
|
||||
|
||||
it('无虚拟根时不改写 selectedKeys,也不改写 select 回传值', async () => {
|
||||
const onSelect = vi.fn()
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), selectedKeys: [] },
|
||||
attrs: { onSelect },
|
||||
})
|
||||
expect(wrapper.find('.g3-tree-node').classes()).not.toContain('is-selected')
|
||||
|
||||
await wrapper.find('.g3-tree-node__content').trigger('click')
|
||||
expect(onSelect.mock.calls[0][0]).toEqual(['1'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('BizTree 加载态(loading)', () => {
|
||||
it('loading=false 时不渲染遮罩', () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: makeTreeData() } })
|
||||
expect(wrapper.find('.biz-tree__loading').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('loading=true 时渲染遮罩,且树仍在渲染(不是 v-else 替换)', () => {
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), loading: true },
|
||||
})
|
||||
expect(wrapper.find('.biz-tree__loading').exists()).toBe(true)
|
||||
// 遮罩是覆盖层,业务节点照常存在,避免加载完成后尺寸跳动
|
||||
expect(wrapper.find('.g3-tree-node__content').text()).toBe('根节点')
|
||||
})
|
||||
|
||||
it('#loading 插槽可覆盖默认的 Spin', () => {
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), loading: true },
|
||||
slots: { loading: () => h('span', { class: 'my-loading' }, '载入中') },
|
||||
})
|
||||
expect(wrapper.find('.my-loading').text()).toBe('载入中')
|
||||
})
|
||||
|
||||
it('loading=false 时 #loading 插槽不渲染', () => {
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), loading: false },
|
||||
slots: { loading: () => h('span', { class: 'my-loading' }, '载入中') },
|
||||
})
|
||||
expect(wrapper.find('.my-loading').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('BizTree 事件透传', () => {
|
||||
it('select 事件原样透传给调用方', async () => {
|
||||
const onSelect = vi.fn()
|
||||
const wrapper = mount(BizTree, { props: { treeData: makeTreeData() }, attrs: { onSelect } })
|
||||
await wrapper.find('.g3-tree-node__content').trigger('click')
|
||||
expect(onSelect).toHaveBeenCalledTimes(1)
|
||||
expect(onSelect.mock.calls[0][0]).toEqual(['1'])
|
||||
expect(onSelect.mock.calls[0][1].node.b_id).toBe('1')
|
||||
})
|
||||
|
||||
it('展开折叠抛出 update:expandedKeys', async () => {
|
||||
const wrapper = mount(BizTree, { props: { treeData: makeTreeData() } })
|
||||
await wrapper.find('.g3-tree-node__switcher').trigger('click')
|
||||
expect(wrapper.emitted('update:expandedKeys')).toHaveLength(1)
|
||||
expect(wrapper.emitted('update:expandedKeys')?.[0][0]).toEqual(['1'])
|
||||
})
|
||||
|
||||
it('右键菜单点击事件透传给调用方', async () => {
|
||||
const onContextMenuClick = vi.fn()
|
||||
const wrapper = mount(BizTree, {
|
||||
props: { treeData: makeTreeData(), contextMenu: nodeContextMenu },
|
||||
attrs: { onContextMenuClick },
|
||||
})
|
||||
await wrapper.find('.g3-tree-node__content').trigger('contextmenu')
|
||||
await nextTick()
|
||||
|
||||
// 菜单 Teleport 到 body,外加 Transition,断言前要先等一拍
|
||||
const menu = document.body.querySelector('.g3-tree__context-menu')
|
||||
if (!menu) throw new Error('右键菜单未渲染到 body')
|
||||
expect(menu.textContent).toContain('新增子项')
|
||||
;(menu.querySelector('.g3-tree__context-menu-item') as HTMLElement).click()
|
||||
await flush()
|
||||
expect(onContextMenuClick).toHaveBeenCalledTimes(1)
|
||||
expect(onContextMenuClick.mock.calls[0][0].item.key).toBe('add')
|
||||
expect(onContextMenuClick.mock.calls[0][0].node.b_id).toBe('1')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* BizTreePanel:面板骨架把未声明属性经 $attrs 透传给 BizTree。
|
||||
*
|
||||
* 这里用渲染函数而非 props 传参,是为了还原模板编译后的真实形态:
|
||||
* 页面写的是 `:selected-keys`(kebab),编译出来 vnode.props 的键名就是 'selected-keys',
|
||||
* 而不是 'selectedKeys'。用 props 传 camelCase 会绕过 Vue 的键名归一化,测不出问题。
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { h } from 'vue'
|
||||
import BizTreePanel from '@/components/biz-tree/BizTreePanel.vue'
|
||||
|
||||
const TREE = [
|
||||
{
|
||||
b_id: 'c1',
|
||||
b_name: '分类一',
|
||||
children: [{ b_id: 'd1', b_name: '数据一' }],
|
||||
},
|
||||
{ b_id: 'd2', b_name: '数据二' },
|
||||
]
|
||||
|
||||
const renderPanel =
|
||||
(extraProps: Record<string, unknown> = {}) =>
|
||||
() =>
|
||||
h(BizTreePanel, { treeData: TREE, showRoot: true, ...extraProps })
|
||||
|
||||
/** 按标题取节点行(children 渲染在行外的兄弟容器里,行自身的 text 只含本节点标题) */
|
||||
const rowOf = (wrapper: VueWrapper, label: string) =>
|
||||
wrapper.findAll('[role="treeitem"]').find((row) => row.text().includes(label))
|
||||
|
||||
describe('BizTreePanel 选中态(selected-keys 透传)', () => {
|
||||
it('kebab 写的 selected-keys 能选中业务节点,虚拟根让位', () => {
|
||||
const wrapper = mount({ render: renderPanel({ 'selected-keys': ['d2'] }) })
|
||||
|
||||
const root = wrapper.findAll('[role="treeitem"]')[0]
|
||||
expect(root.text()).toContain('全部分类')
|
||||
expect(root.attributes('aria-selected')).toBe('false')
|
||||
expect(rowOf(wrapper, '数据二')?.attributes('aria-selected')).toBe('true')
|
||||
})
|
||||
|
||||
it('分类节点同样按 selected-keys 高亮', () => {
|
||||
const wrapper = mount({ render: renderPanel({ 'selected-keys': ['c1'] }) })
|
||||
|
||||
expect(rowOf(wrapper, '分类一')?.attributes('aria-selected')).toBe('true')
|
||||
expect(wrapper.findAll('[role="treeitem"]')[0].attributes('aria-selected')).toBe('false')
|
||||
})
|
||||
|
||||
it('selected-keys 为空时由虚拟根呈现选中态', () => {
|
||||
const wrapper = mount({ render: renderPanel({ 'selected-keys': [] }) })
|
||||
|
||||
expect(wrapper.findAll('[role="treeitem"]')[0].attributes('aria-selected')).toBe('true')
|
||||
})
|
||||
|
||||
it('点击节点抛出 select,交由调用方回写 selected-keys', async () => {
|
||||
const selected: string[] = []
|
||||
const onSelect = (keys: string[]) => selected.push(...keys)
|
||||
const wrapper = mount({ render: renderPanel({ 'selected-keys': [], onSelect }) })
|
||||
|
||||
await rowOf(wrapper, '数据二')?.find('.g3-tree-node__content').trigger('click')
|
||||
|
||||
expect(selected).toEqual(['d2'])
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user