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: `
`, } 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('基本信息') }) })