Files
workspace/code/fms/fms-vue/tests/components/fms-tree.spec.js
T
2026-09-14 17:28:20 +08:00

220 lines
8.2 KiB
JavaScript
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.
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import { h, nextTick } from 'vue'
import FmsTree from '@/components/fms-tree/FmsTree.vue'
import { setMessages, clearMessages } from '@/stores/i18n'
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))
beforeEach(() => {
clearMessages()
})
afterEach(() => {
clearMessages()
})
describe('FmsTree 业务约定', () => {
it('默认字段名按 b_id / b_name / children 渲染,无需传 fieldNames', () => {
const wrapper = mount(FmsTree, { props: { treeData: makeTreeData() } })
expect(wrapper.find('.tree-node-content').text()).toBe('根节点')
})
it('未传 fieldNames 时 b_name 缺失回退到 b_id', () => {
const wrapper = mount(FmsTree, { props: { treeData: [{ b_id: 'm1' }] } })
expect(wrapper.find('.tree-node-content').text()).toBe('m1')
})
it('fieldNames 可覆盖默认字段', () => {
const wrapper = mount(FmsTree, {
props: {
treeData: [{ code: 'a', label: '甲' }],
fieldNames: { key: 'code', title: 'label' },
},
})
expect(wrapper.find('.tree-node-content').text()).toBe('甲')
})
it('b_i18n 有译文时标题取译文', () => {
setMessages({ 'module.customer': '客户管理' })
const wrapper = mount(FmsTree, {
props: { treeData: [{ b_id: 'm1', b_name: 'Customer', b_i18n: 'module.customer' }] },
})
expect(wrapper.find('.tree-node-content').text()).toBe('客户管理')
})
it('translateTitle=false 时标题不做翻译', () => {
setMessages({ 'module.customer': '客户管理' })
const wrapper = mount(FmsTree, {
props: {
treeData: [{ b_id: 'm1', b_name: 'Customer', b_i18n: 'module.customer' }],
translateTitle: false,
},
})
expect(wrapper.find('.tree-node-content').text()).toBe('Customer')
})
})
describe('FmsTree #title 插槽', () => {
it('插槽透出翻译后的 title', () => {
const calls = []
mount(FmsTree, {
props: { treeData: makeTreeData() },
slots: {
title: (scope) => {
calls.push(scope)
return h('span', { class: 'node-name' }, scope.title)
},
},
})
expect(calls[0].title).toBe('根节点')
})
it('插槽拿到的 node 是业务树的活对象,改动能回写原数据并触发重渲染', async () => {
const data = makeTreeData()
const calls = []
const wrapper = mount(FmsTree, {
props: { treeData: data },
slots: {
title: (scope) => {
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('FmsTree 虚拟根(showRoot)', () => {
it('默认不注入根节点', () => {
const wrapper = mount(FmsTree, { props: { treeData: makeTreeData() } })
expect(wrapper.findAll('.tree-node')).toHaveLength(1)
expect(wrapper.find('.tree-node-content').text()).toBe('根节点')
})
it('showRoot 注入「全部分类」根,默认展开且带折叠箭头', () => {
const wrapper = mount(FmsTree, { props: { treeData: makeTreeData(), showRoot: true } })
const rows = wrapper.findAll('.tree-node')
expect(rows).toHaveLength(2)
expect(rows[0].text()).toContain('全部分类')
// 有子节点:根不是叶子,渲染真实折叠箭头(不是 is-placeholder 占位)
expect(rows[0].classes()).not.toContain('is-leaf')
expect(rows[0].find('.tree-node-switcher:not(.is-placeholder)').exists()).toBe(true)
// 调用方的 expandedKeys 为空,根的子节点依然可见
expect(rows[1].text()).toContain('根节点')
})
it('业务树为空时虚拟根仍显示折叠箭头(分组节点不是叶子)', () => {
const wrapper = mount(FmsTree, { props: { treeData: [], showRoot: true } })
const rows = wrapper.findAll('.tree-node')
expect(rows).toHaveLength(1)
expect(rows[0].classes()).not.toContain('is-leaf')
expect(rows[0].find('.tree-node-switcher:not(.is-placeholder)').exists()).toBe(true)
})
it('根节点以 rootKey 作为 key,业务节点不带根 key', () => {
const wrapper = mount(FmsTree, {
props: { treeData: makeTreeData(), showRoot: true, rootKey: 'ROOT' },
slots: { title: (scope) => 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(FmsTree, {
props: { treeData: makeTreeData(), showRoot: true, rootTitle: '所有分类' },
})
expect(wrapper.find('.tree-node-content').text()).toBe('所有分类')
})
it('受控 expandedKeys 已包含根 key 时不再重复追加', () => {
const wrapper = mount(FmsTree, {
props: { treeData: makeTreeData(), showRoot: true, expandedKeys: ['__fms_tree_root__'] },
})
expect(wrapper.findAll('.tree-node')).toHaveLength(2)
})
it('非受控 + showRoot 时根可收起再展开,业务节点仍可自行展开', async () => {
const wrapper = mount(FmsTree, { props: { treeData: makeTreeData(), showRoot: true } })
expect(wrapper.findAll('.tree-node')).toHaveLength(2)
// 第一个真实展开按钮属于虚拟根:点击收起整棵树
await wrapper.find('.tree-node-switcher:not(.is-placeholder)').trigger('click')
await nextTick()
expect(wrapper.findAll('.tree-node')).toHaveLength(1)
expect(wrapper.emitted('update:expandedKeys')[0][0]).not.toContain('__fms_tree_root__')
// 再点一次展开,业务节点回来
await wrapper.find('.tree-node-switcher:not(.is-placeholder)').trigger('click')
await nextTick()
expect(wrapper.findAll('.tree-node')).toHaveLength(2)
expect(wrapper.find('.tree-node-content').text()).toBe('全部分类')
// 业务节点自己的展开按钮照常工作
await wrapper.findAll('.tree-node-switcher:not(.is-placeholder)')[1].trigger('click')
await nextTick()
expect(wrapper.findAll('.tree-node')).toHaveLength(3)
})
it('非受控且无虚拟根时展开键交给 ui/Tree,default-expand-all 照常生效', () => {
const wrapper = mount(FmsTree, { props: { treeData: makeTreeData(), defaultExpandAll: true } })
expect(wrapper.findAll('.tree-node')).toHaveLength(2)
})
})
describe('FmsTree 事件透传', () => {
it('select 事件原样透传给调用方', async () => {
const onSelect = vi.fn()
const wrapper = mount(FmsTree, { props: { treeData: makeTreeData() }, attrs: { onSelect } })
await wrapper.find('.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(FmsTree, { props: { treeData: makeTreeData() } })
await wrapper.find('.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(FmsTree, {
props: { treeData: makeTreeData(), contextMenu: nodeContextMenu },
attrs: { onContextMenuClick },
})
await wrapper.find('.tree-node-content').trigger('contextmenu')
const menu = document.body.querySelector('.tree-context-menu')
expect(menu.textContent).toContain('新增子项')
menu.querySelector('.tree-context-menu__item').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')
})
})