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

195 lines
7.8 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, vi, afterEach } from 'vitest'
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import Tree from '@/components/ui/tree/tree.vue'
const treeData = [{ b_id: '1', b_name: '根节点', children: [{ b_id: '2', b_name: '子节点' }] }]
const fieldNames = { key: 'b_id', title: 'b_name', children: 'children' }
describe('Tree 右键菜单', () => {
it('节点右键发出 contextmenu 事件(携带 node 与 key)', async () => {
const wrapper = mount(Tree, { props: { treeData, fieldNames } })
await wrapper.find('.tree-node-content').trigger('contextmenu')
const emitted = wrapper.emitted('contextmenu')
expect(emitted).toHaveLength(1)
expect(emitted[0][0].node.b_id).toBe('1')
expect(emitted[0][0].key).toBe('1')
expect(emitted[0][0].event).toBeTruthy()
})
it('节点右键不会重复触发树空白处右键逻辑', async () => {
const wrapper = mount(Tree, { props: { treeData, fieldNames } })
await wrapper.find('.tree-node-content').trigger('contextmenu')
expect(wrapper.emitted('contextmenu')).toHaveLength(1)
})
it('空白区域右键发出 contextmenu 事件(node 为 null)', async () => {
const wrapper = mount(Tree, { props: { treeData, fieldNames } })
await wrapper.find('.tree').trigger('contextmenu')
const emitted = wrapper.emitted('contextmenu')
expect(emitted).toHaveLength(1)
expect(emitted[0][0].node).toBeNull()
expect(emitted[0][0].key).toBeNull()
})
it('传入 contextMenu 时按节点渲染菜单(Teleport 到 body)并发出 contextMenuClick', async () => {
const contextMenu = vi.fn((node) =>
node
? [
{ key: 'add', label: '新增子项' },
{ divider: true },
{ key: 'delete', label: '删除', danger: true },
]
: [{ key: 'add-root', label: '新增顶级' }],
)
const wrapper = mount(Tree, { props: { treeData, fieldNames, contextMenu } })
// 节点右键:渲染节点菜单
await wrapper.find('.tree-node-content').trigger('contextmenu')
expect(contextMenu).toHaveBeenCalled()
expect(contextMenu.mock.calls[0][0].b_id).toBe('1')
let menu = document.body.querySelector('.tree-context-menu')
expect(menu).toBeTruthy()
expect(menu.textContent).toContain('新增子项')
const divider = menu.querySelector('.tree-context-menu__divider')
expect(divider).toBeTruthy()
menu.querySelector('.tree-context-menu__item').click()
expect(wrapper.emitted('contextMenuClick')).toHaveLength(1)
expect(wrapper.emitted('contextMenuClick')[0][0].item.key).toBe('add')
expect(wrapper.emitted('contextMenuClick')[0][0].node.b_id).toBe('1')
// 点击后菜单关闭(等待离开动画结束)
await new Promise((r) => setTimeout(r, 150))
expect(document.body.querySelector('.tree-context-menu')).toBeNull()
// 空白右键:渲染顶级菜单
await wrapper.find('.tree').trigger('contextmenu')
menu = document.body.querySelector('.tree-context-menu')
expect(menu.textContent).toContain('新增顶级')
})
it('disabled 菜单项点击不发出 contextMenuClick', async () => {
const contextMenu = () => [{ key: 'x', label: '禁用项', disabled: true }]
const wrapper = mount(Tree, { props: { treeData, fieldNames, contextMenu } })
await wrapper.find('.tree-node-content').trigger('contextmenu')
const menu = document.body.querySelector('.tree-context-menu')
menu.querySelector('.tree-context-menu__item').click()
expect(wrapper.emitted('contextMenuClick')).toBeUndefined()
})
it('点击菜单外部关闭菜单', async () => {
const contextMenu = () => [{ key: 'x', label: '项目' }]
const wrapper = mount(Tree, { props: { treeData, fieldNames, contextMenu } })
await wrapper.find('.tree-node-content').trigger('contextmenu')
expect(document.body.querySelector('.tree-context-menu')).toBeTruthy()
document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
await new Promise((r) => setTimeout(r, 0))
expect(document.body.querySelector('.tree-context-menu')).toBeNull()
wrapper.unmount()
})
})
describe('Tree 行内元素顺序', () => {
/**
* 回归点:行内只有「箭头槽 → 标题」,不再有拖拽手柄列 ——
* 拖拽改由长按整行发起,行与行之间不存在「有手柄 / 无手柄」的宽度差异。
* 箭头槽是全行唯一的固定槽:非叶为真箭头、叶子为等宽占位,
* 保证同层标题左边界一致(去掉叶子占位会让叶子标题左移一格)。
*/
const rowClasses = (row) => [...row.element.children].map((el) => el.className)
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-content'])
})
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-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()
})
})