20260915221013
This commit is contained in:
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()
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user