Files
workspace/code/fms/fms-vue/tests/unit/module-list-config-panel-dnd.spec.js
2026-09-11 17:31:34 +08:00

262 lines
8.3 KiB
JavaScript
Raw Permalink 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 { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import { describe, expect, it, vi, beforeEach } from 'vitest'
import { createPinia, setActivePinia } from 'pinia'
import ModuleListConfigPanel from '@/views/module/module-management/ModuleListConfigPanel.vue'
/**
* 列表配置面板(V2:view JSON 节点模型)结构编辑测试。
* 面板对外数据模型为 s_module_schema.b_schema_json({ schemaVersion, columns: [...] }),
* 列节点为 { field, ... },分组节点为 { type: 'group', title, children: [...] }。
* 编辑器内部经 schemaModel 适配复用结构编辑内核;外部只观察 update:modelValue 的 JSON。
*/
const fields = [
{ b_field: 'name', b_name: '姓名', b_type: 'input' },
{ b_field: 'age', b_name: '年龄', b_type: 'number' },
{ b_field: 'status', b_name: '状态', b_type: 'select' },
{ b_field: 'remark', b_name: '备注', b_type: 'textarea' },
]
function baseSchema() {
return {
schemaVersion: 1,
columns: [
{ field: 'name', width: 120 },
{
type: 'group',
title: '基本信息',
children: [
{ field: 'age', width: 100 },
{ field: 'status', width: 160 },
],
},
],
}
}
const StubDraggable = {
name: 'draggable',
props: {
list: { type: Array, default: () => [] },
itemKey: { type: String, default: undefined },
},
emits: ['start', 'end'],
template: `
<div class="stub-draggable">
<template v-for="el in list" :key="el[itemKey || 'key']">
<slot name="item" :element="el" />
</template>
<slot name="footer" />
</div>
`,
}
class FakeDataTransfer {
constructor() {
this.store = new Map()
this.types = []
this.effectAllowed = 'none'
this.dropEffect = 'none'
}
setData(type, value) {
if (!this.types.includes(type)) this.types.push(type)
this.store.set(type, String(value))
}
getData(type) {
return this.store.has(type) ? this.store.get(type) : ''
}
}
function mountPanel(extraProps = {}) {
return mount(ModuleListConfigPanel, {
props: {
moduleId: 'm1',
fields,
modelValue: baseSchema(),
...extraProps,
},
global: { stubs: { draggable: StubDraggable } },
})
}
function treeRow(wrapper, title) {
const row = wrapper.findAll('.mst-node').find((w) => w.text().includes(title))
expect(row, `未找到树节点:${title}`).toBeTruthy()
return row.element
}
function fireMouse(el, type, { clientX = 0, clientY = 20, button = 0 } = {}) {
const ev = new Event(type, { bubbles: true, cancelable: true })
Object.defineProperty(ev, 'clientX', { value: clientX })
Object.defineProperty(ev, 'clientY', { value: clientY })
Object.defineProperty(ev, 'button', { value: button })
el.dispatchEvent(ev)
}
function fireDrag(el, type, { clientX = 10, clientY = 20, dataTransfer } = {}) {
const ev = new Event(type, { bubbles: true, cancelable: true })
Object.defineProperty(ev, 'clientX', { value: clientX })
Object.defineProperty(ev, 'clientY', { value: clientY })
Object.defineProperty(ev, 'dataTransfer', { value: dataTransfer })
el.dispatchEvent(ev)
}
let hitElement = null
function startMouseDrag(wrapper, srcTitle) {
const row = treeRow(wrapper, srcTitle)
const nodeKey = row.getAttribute('data-key')
expect(nodeKey, `节点缺少 data-key:${srcTitle}`).toBeTruthy()
wrapper.findComponent({ name: 'draggable' }).vm.$emit('start', {
item: { dataset: { key: nodeKey } },
})
}
function moveMouseTo(wrapper, targetTitle, { clientX = 0, clientY = 20 } = {}) {
const row = treeRow(wrapper, targetTitle)
hitElement = row.querySelector('.mst-node__label') || row
fireMouse(window, 'mousemove', { clientX, clientY })
}
function endMouseDrag(wrapper) {
wrapper.findComponent({ name: 'draggable' }).vm.$emit('end')
}
function lastSchema(wrapper) {
const updates = wrapper.emitted('update:modelValue')
expect(updates?.length).toBeGreaterThan(0)
return updates.at(-1)[0]
}
function groupNode(schema, title) {
return (schema.columns || []).find((n) => n.type === 'group' && n.title === title)
}
function topLevelFields(schema) {
return (schema.columns || []).filter((n) => n.type !== 'group').map((n) => n.field)
}
describe('ModuleListConfigPanel 结构编辑(view JSON 节点模型)', () => {
beforeEach(() => {
setActivePinia(createPinia())
document.elementFromPoint = () => hitElement
Element.prototype.getBoundingClientRect = () => ({
top: 0,
bottom: 40,
left: 0,
right: 200,
width: 200,
height: 40,
x: 0,
y: 0,
toJSON: () => ({}),
})
Element.prototype.scrollIntoView = vi.fn()
})
it('列拖到分组节点(inside):移入该分组子节点末尾', async () => {
const wrapper = mountPanel()
startMouseDrag(wrapper, '姓名')
moveMouseTo(wrapper, '基本信息', { clientY: 20 })
endMouseDrag(wrapper)
await nextTick()
const schema = lastSchema(wrapper)
const group = groupNode(schema, '基本信息')
expect(group.children.map((n) => n.field)).toEqual(['age', 'status', 'name'])
expect(topLevelFields(schema)).toEqual([])
})
it('列拖到「未分组」收纳节点:移出分组回到顶层', async () => {
const wrapper = mountPanel()
startMouseDrag(wrapper, '年龄')
moveMouseTo(wrapper, '未分组', { clientY: 20 })
endMouseDrag(wrapper)
await nextTick()
const schema = lastSchema(wrapper)
expect(groupNode(schema, '基本信息').children.map((n) => n.field)).toEqual(['status'])
expect(topLevelFields(schema).sort()).toEqual(['age', 'name'])
})
it('分组拖到分组带上(inside):嵌套为子分组', async () => {
const wrapper = mountPanel({
modelValue: {
schemaVersion: 1,
columns: [
{
type: 'group',
title: '基本信息',
children: [{ field: 'name' }],
},
{
type: 'group',
title: '扩展信息',
children: [{ field: 'remark' }],
},
],
},
})
startMouseDrag(wrapper, '扩展信息')
moveMouseTo(wrapper, '基本信息', { clientY: 20 })
endMouseDrag(wrapper)
await nextTick()
const schema = lastSchema(wrapper)
const outer = groupNode(schema, '基本信息')
const nested = outer.children.find((n) => n.type === 'group')
expect(nested.title).toBe('扩展信息')
})
it('删除分组:分组节点消失,组内字段回到顶层', async () => {
const wrapper = mountPanel()
const row = wrapper.findAll('.mst-node').find((w) => w.text().includes('基本信息'))
expect(row).toBeTruthy()
const delBtn = row.findAll('button').find((b) => b.attributes('title') === '删除')
expect(delBtn).toBeTruthy()
await delBtn.trigger('click')
const schema = lastSchema(wrapper)
expect(groupNode(schema, '基本信息')).toBeUndefined()
expect(topLevelFields(schema).sort()).toEqual(['age', 'name', 'status'])
})
it('字段库点击未加入字段:追加为顶层列节点', async () => {
const wrapper = mountPanel()
const libItem = wrapper
.findAll('.mm-fdesign__lib-item')
.find((w) => w.attributes('data-field-id') === 'remark')
expect(libItem).toBeTruthy()
await libItem.trigger('click')
const schema = lastSchema(wrapper)
expect(topLevelFields(schema)).toContain('remark')
})
it('字段库字段拖入分组带(inside):加入该分组子节点末尾', async () => {
const wrapper = mountPanel()
const libItem = wrapper
.findAll('.mm-fdesign__lib-item')
.find((w) => w.attributes('data-field-id') === 'remark')
const dt = new FakeDataTransfer()
fireDrag(libItem.element, 'dragstart', { dataTransfer: dt })
fireDrag(treeRow(wrapper, '基本信息'), 'dragover', { dataTransfer: dt, clientY: 20 })
fireDrag(treeRow(wrapper, '基本信息'), 'drop', { dataTransfer: dt })
await nextTick()
const schema = lastSchema(wrapper)
expect(groupNode(schema, '基本信息').children.map((n) => n.field)).toEqual([
'age',
'status',
'remark',
])
})
it('预览表渲染多级表头:band 行含分组标题', () => {
const wrapper = mountPanel()
const bands = wrapper.findAll('th.mm-tdesign__band')
expect(bands.map((th) => th.text())).toContain('基本信息')
})
})