Files
workspace/code/fms/fms-vue/tests/components/fms-tree.spec.js
T
2026-09-15 22:10:13 +08:00

310 lines
12 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('全部分类')
// 有子节点:根不是叶子,渲染折叠箭头
expect(rows[0].classes()).not.toContain('is-leaf')
expect(rows[0].find('.tree-node-switcher').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').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').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').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')[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 虚拟根的选中约定(showRoot)', () => {
it('selectedKeys 为空时虚拟根呈现选中态,调用方无需传 rootKey', () => {
const wrapper = mount(FmsTree, {
props: { treeData: makeTreeData(), showRoot: true, selectedKeys: [] },
})
// 第一个节点是虚拟根,应带上选中类
expect(wrapper.findAll('.tree-node')[0].classes()).toContain('is-selected')
})
it('未传 selectedKeys 时同样把虚拟根当作选中(默认选中「全部」)', () => {
const wrapper = mount(FmsTree, { props: { treeData: makeTreeData(), showRoot: true } })
expect(wrapper.findAll('.tree-node')[0].classes()).toContain('is-selected')
})
it('选中业务节点时虚拟根不再高亮', () => {
const wrapper = mount(FmsTree, {
props: { treeData: makeTreeData(), showRoot: true, selectedKeys: ['1'] },
})
const rows = wrapper.findAll('.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(FmsTree, {
props: { treeData: makeTreeData(), showRoot: true },
attrs: { onSelect },
})
await wrapper.findAll('.tree-node-content')[0].trigger('click')
expect(onSelect).toHaveBeenCalledTimes(1)
// 归一化成 [],而不是 ['__fms_tree_root__']
expect(onSelect.mock.calls[0][0]).toEqual([])
})
it('点中业务节点时 select 原样回传该节点的 key', async () => {
const onSelect = vi.fn()
const wrapper = mount(FmsTree, {
props: { treeData: makeTreeData(), showRoot: true },
attrs: { onSelect },
})
await wrapper.findAll('.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(FmsTree, {
props: { treeData: makeTreeData(), selectedKeys: [] },
attrs: { onSelect },
})
expect(wrapper.find('.tree-node').classes()).not.toContain('is-selected')
await wrapper.find('.tree-node-content').trigger('click')
expect(onSelect.mock.calls[0][0]).toEqual(['1'])
})
})
describe('FmsTree 加载态(loading)', () => {
it('loading=false 时不渲染遮罩', () => {
const wrapper = mount(FmsTree, { props: { treeData: makeTreeData() } })
expect(wrapper.find('.fms-tree__loading').exists()).toBe(false)
})
it('loading=true 时渲染遮罩,且树仍在渲染(不是 v-else 替换)', () => {
const wrapper = mount(FmsTree, {
props: { treeData: makeTreeData(), loading: true },
})
expect(wrapper.find('.fms-tree__loading').exists()).toBe(true)
// 遮罩是覆盖层,业务节点照常存在,避免加载完成后尺寸跳动
expect(wrapper.find('.tree-node-content').text()).toBe('根节点')
})
it('#loading 插槽可覆盖默认的 Spin', () => {
const wrapper = mount(FmsTree, {
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(FmsTree, {
props: { treeData: makeTreeData(), loading: false },
slots: { loading: () => h('span', { class: 'my-loading' }, '载入中') },
})
expect(wrapper.find('.my-loading').exists()).toBe(false)
})
})
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')
})
})