262 lines
8.3 KiB
JavaScript
262 lines
8.3 KiB
JavaScript
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('基本信息')
|
||
})
|
||
})
|