20260915221013

This commit is contained in:
oneao committed 2026-09-15 22:10:13 +08:00
1 parent 1f56cc3efb
commit dce29f250e
54 files changed
+3732 -574

No files matched your search

@@ -118,9 +118,9 @@ describe('FmsTree 虚拟根(showRoot)', () => {
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)
expect(rows[0].find('.tree-node-switcher').exists()).toBe(true)
// 调用方的 expandedKeys 为空,根的子节点依然可见
expect(rows[1].text()).toContain('根节点')
})
@@ -130,7 +130,7 @@ describe('FmsTree 虚拟根(showRoot)', () => {
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)
expect(rows[0].find('.tree-node-switcher').exists()).toBe(true)
})
it('根节点以 rootKey 作为 key,业务节点不带根 key', () => {
@@ -161,19 +161,19 @@ describe('FmsTree 虚拟根(showRoot)', () => {
expect(wrapper.findAll('.tree-node')).toHaveLength(2)
// 第一个真实展开按钮属于虚拟根:点击收起整棵树
await wrapper.find('.tree-node-switcher:not(.is-placeholder)').trigger('click')
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:not(.is-placeholder)').trigger('click')
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:not(.is-placeholder)')[1].trigger('click')
await wrapper.findAll('.tree-node-switcher')[1].trigger('click')
await nextTick()
expect(wrapper.findAll('.tree-node')).toHaveLength(3)
})
@@ -184,6 +184,96 @@ describe('FmsTree 虚拟根(showRoot)', () => {
})
})
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()
+89 -12
View File
@@ -1,4 +1,5 @@
import { describe, it, expect, vi } from 'vitest'
import { describe, it, expect, vi, afterEach } from 'vitest'
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import Tree from '@/components/ui/tree/tree.vue'
@@ -91,27 +92,103 @@ describe('Tree 右键菜单', () => {
describe('Tree 行内元素顺序', () => {
/**
* 回归点:箭头槽(非叶为真箭头、叶子为等宽占位)必须排在拖拽手柄之前。
* 放到手柄之后时,叶子行的手柄与标题之间会空出一整个箭头宽,读起来像多余空隙;
* 排在前面则这段空白归入左侧缩进带,手柄紧跟标题(见 tree-node.vue 模板注释)。
* 回归点:行内只有「箭头槽 → 标题」,不再有拖拽手柄列 ——
* 拖拽改由长按整行发起,行与行之间不存在「有手柄 / 无手柄」的宽度差异。
* 箭头槽是全行唯一的固定槽:非叶为真箭头、叶子为等宽占位,
* 保证同层标题左边界一致(去掉叶子占位会让叶子标题左移一格)。
*/
const rowClasses = (row) => [...row.element.children].map((el) => el.className)
it('拖拽开启时:箭头槽在拖拽手柄之前', () => {
it('非叶行:箭头槽在标题之前,且不渲染拖拽手柄', () => {
const wrapper = mount(Tree, { props: { treeData, fieldNames, draggable: true } })
const parentRow = wrapper.findAll('.tree-node')[0]
expect(rowClasses(parentRow)).toEqual(['tree-node-switcher', 'tree-node-grip', 'tree-node-content'])
expect(rowClasses(parentRow)).toEqual(['tree-node-switcher', 'tree-node-content'])
})
it('叶子行的占位与手柄顺序一致,保证标题横坐标与非叶兄弟相同', () => {
it('叶子行渲染等宽箭头占位,标题与非叶兄弟对齐', () => {
const wrapper = mount(Tree, {
props: { treeData, fieldNames, draggable: true, defaultExpandedKeys: ['1'] },
})
const childRow = wrapper.findAll('.tree-node')[1]
expect(rowClasses(childRow)).toEqual([
'tree-node-switcher is-placeholder',
'tree-node-grip',
'tree-node-content',
])
expect(rowClasses(childRow)).toEqual(['tree-node-switcher is-placeholder', 'tree-node-content'])
})
it('不可拖拽的节点与可拖拽节点行内结构一致', () => {
const wrapper = mount(Tree, {
props: {
fieldNames,
draggable: true,
defaultExpandedKeys: ['1'],
treeData: [
{
b_id: '1',
b_name: '全部分类',
draggable: false,
children: [{ b_id: '2', b_name: '子节点' }],
},
],
},
})
const rows = wrapper.findAll('.tree-node')
expect(rowClasses(rows[0])).toEqual(['tree-node-switcher', 'tree-node-content'])
expect(rowClasses(rows[1])).toEqual(['tree-node-switcher is-placeholder', 'tree-node-content'])
})
})
describe('Tree 长按拖拽', () => {
const rowOf = (wrapper, index = 0) => wrapper.findAll('.tree-node')[index]
const press = (row) => row.trigger('mousedown', { button: 0, clientX: 0, clientY: 0 })
afterEach(() => vi.useRealTimers())
it('长按到位后才置为可拖拽,短按保持普通行', async () => {
vi.useFakeTimers()
const wrapper = mount(Tree, { props: { treeData, fieldNames, draggable: true } })
const row = rowOf(wrapper)
await press(row)
// 未到长按时长:仍是普通行,短按/双击/多选点击的语义不受影响
expect(row.attributes('draggable')).toBe('false')
vi.advanceTimersByTime(400)
await nextTick()
expect(row.attributes('draggable')).toBe('true')
wrapper.unmount()
})
it('长按期间位移超过余量则放弃本次拖拽', async () => {
vi.useFakeTimers()
const wrapper = mount(Tree, { props: { treeData, fieldNames, draggable: true } })
const row = rowOf(wrapper)
await press(row)
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 40, clientY: 0 }))
vi.advanceTimersByTime(400)
await nextTick()
expect(row.attributes('draggable')).toBe('false')
wrapper.unmount()
})
it('未开启 draggable 或节点标记 draggable:false 时不进入拖拽', async () => {
vi.useFakeTimers()
const plain = mount(Tree, { props: { treeData, fieldNames } })
await press(rowOf(plain))
vi.advanceTimersByTime(500)
await nextTick()
expect(rowOf(plain).attributes('draggable')).toBe('false')
plain.unmount()
const locked = mount(Tree, {
props: {
fieldNames,
draggable: true,
treeData: [{ b_id: '1', b_name: '根节点', draggable: false, children: [] }],
},
})
await press(rowOf(locked))
vi.advanceTimersByTime(500)
await nextTick()
expect(rowOf(locked).attributes('draggable')).toBe('false')
locked.unmount()
})
})