20260914172820

This commit is contained in:
oneao committed 2026-09-14 17:28:20 +08:00
1 parent c31329d387
commit 549c8bd2eb
38 files changed
+3430 -502

No files matched your search

@@ -0,0 +1,219 @@
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')
})
})