20260911173133
This commit is contained in:
1 parent
3838ed74b1
commit
f3ad727f1c
60 files changed
+4971
-6621
No files matched your search
@@ -1,443 +1,121 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { nextTick } from 'vue'
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import ModuleTableEditPanel from '@/views/module/module-management/components/table-edit/TableEditPanel.vue'
|
||||
|
||||
/**
|
||||
* 专用结构树组件 ModuleStructureTree 的状态驱动拖拽,在结构树内完成全部落点
|
||||
* 语义计算后通过事件交给面板 commit;本文件聚焦这些语义事件的正确性。
|
||||
* 界面配置 JSON 节点模型适配层测试(schemaModel)。
|
||||
* 覆盖 view / edit 配置对象与编辑器行/分组草稿的互转(含分组嵌套与未知属性保留),
|
||||
* 以及字段参与(增删/可见性)的节点操作。
|
||||
*/
|
||||
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) : ''
|
||||
}
|
||||
}
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
schemaToEditor,
|
||||
editorToSchema,
|
||||
addSchemaField,
|
||||
removeSchemaField,
|
||||
hasSchemaField,
|
||||
setSchemaFieldVisible,
|
||||
hasQueryCondition,
|
||||
addQueryCondition,
|
||||
removeQueryCondition,
|
||||
defaultQueryCondition,
|
||||
} from '@/views/module/module-management/components/design-editor/common/schemaModel'
|
||||
|
||||
const fields = [
|
||||
{ b_id: '1', b_name: '姓名', b_field: 'name', b_type: 'input' },
|
||||
{ b_id: '2', b_name: '年龄', b_field: 'age', b_type: 'number' },
|
||||
{ b_id: '3', b_name: '状态', b_field: 'status', b_type: 'select' },
|
||||
{ b_id: '4', b_name: '备注', b_field: 'remark', b_type: 'textarea' },
|
||||
{ b_id: '5', b_name: '金额', b_field: 'amount', b_type: 'money' },
|
||||
]
|
||||
const groups = [
|
||||
{ b_id: 'g1', b_module_id: 'm1', b_parent_id: null, b_title: '基本信息', b_xh: 10, b_canuse: 1 },
|
||||
{ b_id: 'g2', b_module_id: 'm1', b_parent_id: null, b_title: '扩展信息', b_xh: 20, b_canuse: 1 },
|
||||
]
|
||||
const editConfig = [
|
||||
{ b_id: 'e1', b_module_id: 'm1', b_field_id: '1', b_group_id: null, b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 120, b_xh: 10 },
|
||||
{ b_id: 'e2', b_module_id: 'm1', b_field_id: '2', b_group_id: 'g1', b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 100, b_xh: 20 },
|
||||
{ b_id: 'e3', b_module_id: 'm1', b_field_id: '3', b_group_id: 'g1', b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 160, b_xh: 30 },
|
||||
{ b_id: 'e4', b_module_id: 'm1', b_field_id: '4', b_group_id: 'g2', b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 220, b_xh: 40 },
|
||||
]
|
||||
describe('schemaModel:view/edit 配置 ↔ 编辑器草稿', () => {
|
||||
it('view:分组嵌套解析为 b_group_id / b_parent_id,回写保留分组结构', () => {
|
||||
const schema = {
|
||||
schemaVersion: 1,
|
||||
columns: [
|
||||
{ field: 'name', width: 120 },
|
||||
{
|
||||
type: 'group',
|
||||
title: '基本信息',
|
||||
children: [
|
||||
{ field: 'age', width: 100 },
|
||||
{
|
||||
type: 'group',
|
||||
title: '扩展',
|
||||
children: [{ field: 'remark', width: 200 }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
const { rows, groups } = schemaToEditor('view', schema, 'm1')
|
||||
expect(rows.map((r) => r.b_field)).toEqual(['name', 'age', 'remark'])
|
||||
// name 未分组;age 属顶级分组;remark 属子分组
|
||||
expect(rows[0].b_group_id).toBeNull()
|
||||
const outer = groups.find((g) => g.b_title === '基本信息')
|
||||
const inner = groups.find((g) => g.b_title === '扩展')
|
||||
expect(rows[1].b_group_id).toBe(outer.b_id)
|
||||
expect(rows[2].b_group_id).toBe(inner.b_id)
|
||||
expect(inner.b_parent_id).toBe(outer.b_id)
|
||||
|
||||
function mountPanel() {
|
||||
return mount(ModuleTableEditPanel, {
|
||||
props: {
|
||||
moduleId: 'm1',
|
||||
editConfig: editConfig.map((r) => ({ ...r })),
|
||||
listConfig: [],
|
||||
queryConfig: [],
|
||||
fields,
|
||||
groups: groups.map((g) => ({ ...g })),
|
||||
editMode: 'table',
|
||||
},
|
||||
global: { stubs: { draggable: StubDraggable } },
|
||||
})
|
||||
}
|
||||
|
||||
// 按标题文本定位树节点行(分组树整树展开);专用组件节点行类名 .mst-node
|
||||
function treeRow(wrapper, title) {
|
||||
const row = wrapper.findAll('.mst-node').find((w) => w.text().includes(title))
|
||||
expect(row, `未找到树节点:${title}`).toBeTruthy()
|
||||
return row.element
|
||||
}
|
||||
|
||||
/* VTU 的 trigger 会把 dragover/drop 构造为 DragEvent(clientY 只有 getter),
|
||||
自定义坐标会被静默丢弃;手工构造事件并用 defineProperty 注入坐标。 */
|
||||
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)
|
||||
}
|
||||
|
||||
// 同理 pointerdown/pointermove 映射为 MouseEvent(clientX 只读),手工注入坐标
|
||||
function firePointer(el, type, { clientX = 0, pointerId = 1 } = {}) {
|
||||
const ev = new Event(type, { bubbles: true, cancelable: true })
|
||||
Object.defineProperty(ev, 'clientX', { value: clientX })
|
||||
Object.defineProperty(ev, 'pointerId', { value: pointerId })
|
||||
el.dispatchEvent(ev)
|
||||
}
|
||||
|
||||
/* ---- 内部拖拽改为 vuedraggable(SortableJS force-fallback)驱动,与主表设计器一致 ----
|
||||
* jsdom 不实现 elementFromPoint(命中检测),测试里 mock 它返回预先指定的目标行元素;
|
||||
* onSortWindowMove 据此 closest('.mst-node') 定位落点。
|
||||
* vuedraggable 组件用 StubDraggable 替换,测试通过 emit start/end 来模拟拖拽生命周期。 */
|
||||
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>
|
||||
`,
|
||||
}
|
||||
|
||||
let hitElement = null
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 在源节点 grip 上开始拖拽(emit start 到 draggable 组件)
|
||||
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 } },
|
||||
})
|
||||
}
|
||||
|
||||
// 移动鼠标到目标节点行(clientY 决定落点 before/inside/after)
|
||||
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 })
|
||||
}
|
||||
|
||||
// 松手提交(emit end 到 draggable 组件)
|
||||
function endMouseDrag(wrapper) {
|
||||
wrapper.findComponent({ name: 'draggable' }).vm.$emit('end')
|
||||
}
|
||||
|
||||
function lastUpdate(wrapper) {
|
||||
const updates = wrapper.emitted('update')
|
||||
expect(updates?.length).toBeGreaterThan(0)
|
||||
return updates.at(-1)[0]
|
||||
}
|
||||
|
||||
describe('ModuleTableEditPanel 分组树拖拽语义', () => {
|
||||
beforeEach(() => {
|
||||
// 面板(数据视图)内嵌 FmsTable,其主题读取 app store
|
||||
setActivePinia(createPinia())
|
||||
// jsdom 无命中检测,mock elementFromPoint 返回测试指定的目标行元素
|
||||
document.elementFromPoint = () => hitElement
|
||||
// 行高 40:clientY 5 → before,20 → inside,35 → after
|
||||
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()
|
||||
const back = editorToSchema('view', rows, groups, schema)
|
||||
const outerNode = back.columns.find((n) => n.type === 'group')
|
||||
expect(outerNode.title).toBe('基本信息')
|
||||
expect(outerNode.children.map((n) => n.field ?? n.title)).toEqual(['age', '扩展'])
|
||||
// 列宽等已知属性保留
|
||||
expect(back.columns.find((n) => n.field === 'name').width).toBe(120)
|
||||
})
|
||||
|
||||
it('列拖到列上(中部 inside)直接换位:插到目标列后并沿用其分组,不做入组嵌套', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 年龄(c:2,属 g1) 拖到 姓名(c:1,未分组) 行中部
|
||||
startMouseDrag(wrapper, '年龄')
|
||||
moveMouseTo(wrapper, '姓名', { clientY: 20 })
|
||||
endMouseDrag(wrapper)
|
||||
it('edit:span/required/readonly 与行属性互转,默认值不额外写出', () => {
|
||||
const schema = {
|
||||
schemaVersion: 1,
|
||||
children: [
|
||||
{ field: 'name', span: 12, required: true },
|
||||
{ field: 'age' },
|
||||
],
|
||||
}
|
||||
const { rows } = schemaToEditor('edit', schema, 'm1')
|
||||
expect(rows[0].b_colspan).toBe(12)
|
||||
expect(rows[0].b_required).toBe(1)
|
||||
expect(rows[1].b_colspan).toBe(24)
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '2', '3', '4'])
|
||||
expect(rows.map((r) => r.b_xh)).toEqual([10, 20, 30, 40])
|
||||
// 年龄跟随目标列分组(未分组),而不是嵌套进任何分组
|
||||
expect(rows.find((r) => r.b_field_id === '2').b_group_id).toBeNull()
|
||||
const back = editorToSchema('edit', rows, [], schema)
|
||||
expect(back.children[0]).toMatchObject({ field: 'name', span: 12, required: true })
|
||||
// span=24 且原节点未显式给出 → 不写出 span
|
||||
expect(back.children[1].span).toBeUndefined()
|
||||
})
|
||||
|
||||
it('列拖到列上(上缘 before)换到目标列前,跨组移动时沿用目标列分组', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 状态(c:3,属 g1) 拖到 备注(c:4,属 g2) 行上缘
|
||||
startMouseDrag(wrapper, '状态')
|
||||
moveMouseTo(wrapper, '备注', { clientY: 5 })
|
||||
endMouseDrag(wrapper)
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '2', '3', '4'])
|
||||
expect(rows.find((r) => r.b_field_id === '3').b_group_id).toBe('g2')
|
||||
})
|
||||
|
||||
it('列拖到分组节点(中部 inside):移入该分组直属列末尾', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 姓名(c:1,未分组) 拖到分组「扩展信息」(g2) 行中部 → 加入 g2 直属列末尾(备注后)
|
||||
startMouseDrag(wrapper, '姓名')
|
||||
moveMouseTo(wrapper, '扩展信息', { clientY: 20 })
|
||||
endMouseDrag(wrapper)
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.find((r) => r.b_field_id === '1').b_group_id).toBe('g2')
|
||||
// g2 列:备注 → 姓名;整体前序:年龄/状态(g1) → 备注/姓名(g2)
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['2', '3', '4', '1'])
|
||||
})
|
||||
|
||||
it('列拖到「未分组」收纳节点:移出分组并追加到未分组区', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 年龄(c:2,属 g1) 拖到「未分组」节点 → b_group_id 置空,追加到未分组区末尾(姓名后)
|
||||
startMouseDrag(wrapper, '年龄')
|
||||
moveMouseTo(wrapper, '未分组', { clientY: 20 })
|
||||
endMouseDrag(wrapper)
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.find((r) => r.b_field_id === '2').b_group_id).toBeNull()
|
||||
// 未分组:姓名 → 年龄;其后 g1(状态)、g2(备注)
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '2', '3', '4'])
|
||||
})
|
||||
|
||||
it('字段库字段拖入分组带(inside):加入该分组直属列末尾', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 字段库「金额」(未加入) 拖到分组「基本信息」行中部
|
||||
const libItem = wrapper
|
||||
.findAll('.mm-fdesign__lib-item')
|
||||
.find((w) => w.attributes('data-field-id') === '5')
|
||||
expect(libItem).toBeTruthy()
|
||||
const dt = new FakeDataTransfer()
|
||||
fireDrag(libItem.element, 'dragstart', { dataTransfer: dt })
|
||||
fireDrag(treeRow(wrapper, '基本信息'), 'dragover', { dataTransfer: dt, clientY: 20 })
|
||||
fireDrag(treeRow(wrapper, '基本信息'), 'drop', { dataTransfer: dt })
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '2', '3', '5', '4'])
|
||||
expect(rows.find((r) => r.b_field_id === '5').b_group_id).toBe('g1')
|
||||
expect(rows.map((r) => r.b_xh)).toEqual([10, 20, 30, 40, 50])
|
||||
})
|
||||
|
||||
it('字段库已加入字段拖到分组带:跨组移动(复用列拖拽语义)', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 字段库「备注」(已加入 g2) 拖到分组「基本信息」行中部
|
||||
const libItem = wrapper
|
||||
.findAll('.mm-fdesign__lib-item')
|
||||
.find((w) => w.attributes('data-field-id') === '4')
|
||||
const dt = new FakeDataTransfer()
|
||||
fireDrag(libItem.element, 'dragstart', { dataTransfer: dt })
|
||||
fireDrag(treeRow(wrapper, '基本信息'), 'dragover', { dataTransfer: dt, clientY: 20 })
|
||||
fireDrag(treeRow(wrapper, '基本信息'), 'drop', { dataTransfer: dt })
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '2', '3', '4'])
|
||||
expect(rows.find((r) => r.b_field_id === '4').b_group_id).toBe('g1')
|
||||
})
|
||||
|
||||
it('分组拖到列上(inside)直接换位:变为该列所属分组的后置兄弟,不嵌套', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 分组「基本信息」(g1) 拖到 备注(c:4,属 g2) 行中部 → g1 移到 g2 之后(同级换位)
|
||||
startMouseDrag(wrapper, '基本信息')
|
||||
moveMouseTo(wrapper, '备注', { clientY: 20 })
|
||||
endMouseDrag(wrapper)
|
||||
|
||||
// 分组重排:g2 在前(10)、g1 在后(20),g1 仍是顶级分组
|
||||
const groupUpdates = wrapper.emitted('updateGroups')
|
||||
expect(groupUpdates?.length).toBeGreaterThan(0)
|
||||
const nextGroups = groupUpdates.at(-1)[0]
|
||||
const g1 = nextGroups.find((g) => g.b_id === 'g1')
|
||||
const g2 = nextGroups.find((g) => g.b_id === 'g2')
|
||||
expect(g1.b_parent_id).toBeNull()
|
||||
expect(g1.b_xh).toBe(20)
|
||||
expect(g2.b_xh).toBe(10)
|
||||
// 列序跟随分组树前序:姓名(未分组) → 备注(g2) → 年龄/状态(g1)
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '4', '2', '3'])
|
||||
})
|
||||
|
||||
it('分组拖到分组带上(inside)仍为嵌套语义', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 分组「扩展信息」(g2) 拖到分组「基本信息」(g1) 行中部 → 成为 g1 子分组
|
||||
startMouseDrag(wrapper, '扩展信息')
|
||||
moveMouseTo(wrapper, '基本信息', { clientY: 20 })
|
||||
endMouseDrag(wrapper)
|
||||
|
||||
const groupUpdates = wrapper.emitted('updateGroups')
|
||||
const nextGroups = groupUpdates.at(-1)[0]
|
||||
expect(nextGroups.find((g) => g.b_id === 'g2').b_parent_id).toBe('g1')
|
||||
})
|
||||
|
||||
it('预览表列头右缘拖拽仅调列宽,松手才提交一次', () => {
|
||||
const wrapper = mountPanel()
|
||||
const resizer = wrapper
|
||||
.findAll('th.mm-tdesign__cell')
|
||||
.find((th) => th.attributes('data-field-id') === '2')
|
||||
.find('.mm-tdesign__resizer')
|
||||
expect(resizer).toBeTruthy()
|
||||
|
||||
firePointer(resizer.element, 'pointerdown', { clientX: 200 })
|
||||
firePointer(resizer.element, 'pointermove', { clientX: 230 })
|
||||
// 拖拽过程中不提交
|
||||
expect(wrapper.emitted('update')).toBeUndefined()
|
||||
firePointer(resizer.element, 'pointerup')
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
// 年龄初始宽 100,右移 30px → 130
|
||||
expect(rows.find((r) => r.b_field_id === '2').b_width).toBe(130)
|
||||
})
|
||||
|
||||
it('预览表列头拖拽:列落列换位并沿用目标列分组', () => {
|
||||
const wrapper = mountPanel()
|
||||
const cell = (fid) =>
|
||||
wrapper
|
||||
.findAll('th.mm-tdesign__cell')
|
||||
.find((th) => th.attributes('data-field-id') === fid)
|
||||
const src = cell('2') // 年龄(g1)
|
||||
const target = cell('4') // 备注(g2)
|
||||
expect(src).toBeTruthy()
|
||||
expect(target).toBeTruthy()
|
||||
const dt = new FakeDataTransfer()
|
||||
// 拖到目标列左半区(before)
|
||||
fireDrag(src.element, 'dragstart', { dataTransfer: dt, clientX: 10, clientY: 20 })
|
||||
fireDrag(target.element, 'dragover', { dataTransfer: dt, clientX: 5, clientY: 20 })
|
||||
fireDrag(target.element, 'drop', { dataTransfer: dt, clientX: 5, clientY: 20 })
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
// 年龄移到备注前并进入 g2;顺序:姓名(未分组) → 状态(g1) → 年龄/备注(g2)
|
||||
expect(rows.find((r) => r.b_field_id === '2').b_group_id).toBe('g2')
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '3', '2', '4'])
|
||||
})
|
||||
|
||||
it('预览表列头拖拽:拖到分组带移入该分组直属列末尾', () => {
|
||||
const wrapper = mountPanel()
|
||||
const src = wrapper
|
||||
.findAll('th.mm-tdesign__cell')
|
||||
.find((th) => th.attributes('data-field-id') === '1') // 姓名(未分组)
|
||||
const band = wrapper
|
||||
.findAll('th.mm-tdesign__band')
|
||||
.find((th) => th.attributes('data-band-id') === 'g2')
|
||||
expect(src).toBeTruthy()
|
||||
expect(band).toBeTruthy()
|
||||
const dt = new FakeDataTransfer()
|
||||
fireDrag(src.element, 'dragstart', { dataTransfer: dt, clientX: 10, clientY: 20 })
|
||||
fireDrag(band.element, 'dragover', { dataTransfer: dt, clientX: 50, clientY: 20 })
|
||||
fireDrag(band.element, 'drop', { dataTransfer: dt, clientX: 50, clientY: 20 })
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.find((r) => r.b_field_id === '1').b_group_id).toBe('g2')
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['2', '3', '4', '1'])
|
||||
})
|
||||
|
||||
it('预览表:全部分组时,拖到「未分组」落点把列拖出分组', async () => {
|
||||
// 4 列全部在 g1(无未分组列)
|
||||
const rows = [
|
||||
{ b_id: 'e1', b_module_id: 'm1', b_field_id: '1', b_group_id: 'g1', b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 120, b_xh: 10 },
|
||||
{ b_id: 'e2', b_module_id: 'm1', b_field_id: '2', b_group_id: 'g1', b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 120, b_xh: 20 },
|
||||
{ b_id: 'e3', b_module_id: 'm1', b_field_id: '3', b_group_id: 'g1', b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 120, b_xh: 30 },
|
||||
{ b_id: 'e4', b_module_id: 'm1', b_field_id: '4', b_group_id: 'g1', b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 120, b_xh: 40 },
|
||||
]
|
||||
const wrapper = mount(ModuleTableEditPanel, {
|
||||
props: {
|
||||
moduleId: 'm1',
|
||||
editConfig: rows.map((r) => ({ ...r })),
|
||||
listConfig: [],
|
||||
queryConfig: [],
|
||||
fields,
|
||||
groups: [groups[0]].map((g) => ({ ...g })),
|
||||
editMode: 'table',
|
||||
},
|
||||
global: { stubs: { draggable: StubDraggable } },
|
||||
})
|
||||
const src = wrapper
|
||||
.findAll('th.mm-tdesign__cell')
|
||||
.find((th) => th.attributes('data-field-id') === '1')
|
||||
expect(src).toBeTruthy()
|
||||
// 无未分组列 → 拖拽开始后才出现「未分组」落点
|
||||
expect(wrapper.find('.mm-tdesign__ungrouped-drop').exists()).toBe(false)
|
||||
const dt = new FakeDataTransfer()
|
||||
fireDrag(src.element, 'dragstart', { dataTransfer: dt, clientX: 10, clientY: 20 })
|
||||
await nextTick()
|
||||
const dropZone = wrapper.find('.mm-tdesign__ungrouped-drop')
|
||||
expect(dropZone.exists()).toBe(true)
|
||||
fireDrag(dropZone.element, 'dragover', { dataTransfer: dt, clientX: 10, clientY: 20 })
|
||||
fireDrag(dropZone.element, 'drop', { dataTransfer: dt, clientX: 10, clientY: 20 })
|
||||
|
||||
const rowsOut = lastUpdate(wrapper)
|
||||
expect(rowsOut.find((r) => r.b_field_id === '1').b_group_id).toBeNull()
|
||||
expect(rowsOut.map((r) => r.b_field_id)).toEqual(['1', '2', '3', '4'])
|
||||
})
|
||||
|
||||
it('悬停亮指示,松手才提交列拖拽', () => {
|
||||
// 悬停阶段仅亮落点指示,mouseup 时提交一次变更,不再重复
|
||||
const wrapper = mountPanel()
|
||||
// 年龄拖到姓名上(after)
|
||||
startMouseDrag(wrapper, '年龄')
|
||||
moveMouseTo(wrapper, '姓名', { clientY: 20 })
|
||||
// mousemove 不触发换位:update 未发出
|
||||
expect(wrapper.emitted('update')).toBeUndefined()
|
||||
// mouseup 提交一次
|
||||
endMouseDrag(wrapper)
|
||||
expect(wrapper.emitted('update')?.length).toBe(1)
|
||||
})
|
||||
|
||||
it('预览表头:未分组列头在 band 行贯穿整段高度,不留空白', () => {
|
||||
// 4 列:2 列分组(g1),2 列未分组
|
||||
const rows = [
|
||||
{ b_id: 'e1', b_module_id: 'm1', b_field_id: '1', b_group_id: null, b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 120, b_xh: 10 },
|
||||
{ b_id: 'e2', b_module_id: 'm1', b_field_id: '2', b_group_id: null, b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 120, b_xh: 20 },
|
||||
{ b_id: 'e3', b_module_id: 'm1', b_field_id: '3', b_group_id: 'g1', b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 120, b_xh: 30 },
|
||||
{ b_id: 'e4', b_module_id: 'm1', b_field_id: '4', b_group_id: 'g1', b_visible: 1, b_required: 0, b_readonly: 0, b_disabled: 0, b_width: 120, b_xh: 40 },
|
||||
]
|
||||
const wrapper = mount(ModuleTableEditPanel, {
|
||||
props: {
|
||||
moduleId: 'm1',
|
||||
editConfig: rows.map((r) => ({ ...r })),
|
||||
listConfig: [],
|
||||
queryConfig: [],
|
||||
fields,
|
||||
// 仅一个分组 g1(基本信息),避免空分组 g2 产生额外占位列
|
||||
groups: [groups[0]].map((g) => ({ ...g })),
|
||||
editMode: 'table',
|
||||
},
|
||||
global: { stubs: { draggable: StubDraggable } },
|
||||
it('未知节点扩展属性在往返中保留', () => {
|
||||
const schema = {
|
||||
schemaVersion: 1,
|
||||
columns: [{ field: 'name', width: 120, fixed: 'left', format: 'money' }],
|
||||
}
|
||||
const { rows, groups } = schemaToEditor('view', schema, 'm1')
|
||||
const back = editorToSchema('view', rows, groups, schema)
|
||||
expect(back.columns[0]).toMatchObject({ field: 'name', fixed: 'left', format: 'money' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('schemaModel:字段参与节点操作', () => {
|
||||
it('view:追加 / 移除字段节点', () => {
|
||||
let schema = { schemaVersion: 1, columns: [] }
|
||||
expect(hasSchemaField('view', schema, 'name')).toBe(false)
|
||||
schema = addSchemaField('view', schema, { b_field: 'name', b_type: 'input' })
|
||||
expect(hasSchemaField('view', schema, 'name')).toBe(true)
|
||||
expect(schema.columns[0].width).toBe(120)
|
||||
schema = removeSchemaField('view', schema, 'name')
|
||||
expect(hasSchemaField('view', schema, 'name')).toBe(false)
|
||||
})
|
||||
|
||||
it('edit:切换 visible 保留节点', () => {
|
||||
let schema = { schemaVersion: 1, children: [{ field: 'name', span: 24 }] }
|
||||
expect(hasSchemaField('edit', schema, 'name', { requireVisible: true })).toBe(true)
|
||||
schema = setSchemaFieldVisible('edit', schema, 'name', false)
|
||||
expect(schema.children[0].visible).toBe(false)
|
||||
expect(hasSchemaField('edit', schema, 'name')).toBe(true)
|
||||
expect(hasSchemaField('edit', schema, 'name', { requireVisible: true })).toBe(false)
|
||||
})
|
||||
|
||||
it('query:条件增删与默认组件', () => {
|
||||
let schema = { schemaVersion: 1, quick: [], groups: [] }
|
||||
expect(hasQueryCondition(schema, 'amount')).toBe(false)
|
||||
expect(defaultQueryCondition({ b_field: 'amount', b_type: 'money' })).toMatchObject({
|
||||
component: 'number',
|
||||
operator: 'eq',
|
||||
})
|
||||
|
||||
// 未分组列头在 band 行渲染:2 个 cell,rowspan 贯穿全部表头行(band 行 + 列头行)
|
||||
const ungroupedCells = wrapper.findAll('th.mm-tdesign__cell--ungrouped')
|
||||
expect(ungroupedCells).toHaveLength(2)
|
||||
ungroupedCells.forEach((th) => expect(th.attributes('rowspan')).toBe('2'))
|
||||
expect(ungroupedCells[0].attributes('data-field-id')).toBe('1')
|
||||
expect(ungroupedCells[1].attributes('data-field-id')).toBe('2')
|
||||
// 未分组列头不是空带:渲染了字段名
|
||||
expect(ungroupedCells[0].text()).toContain('姓名')
|
||||
expect(ungroupedCells[1].text()).toContain('年龄')
|
||||
|
||||
// 分组带正常渲染
|
||||
const bandCells = wrapper.findAll('th.mm-tdesign__band')
|
||||
expect(bandCells).toHaveLength(1)
|
||||
expect(bandCells[0].text()).toContain('基本信息')
|
||||
|
||||
// 列头行只渲染分组列(未分组列已在 band 行)
|
||||
// 注意:只查预览表(.mm-tdesign__table),数据视图的 FmsTable 经 v-show 仍驻留 DOM
|
||||
const theadRows = wrapper.findAll('.mm-tdesign__table thead tr')
|
||||
const headerRow = theadRows.at(-1)
|
||||
const headerCells = headerRow.findAll('th.mm-tdesign__cell')
|
||||
expect(headerCells.map((th) => th.attributes('data-field-id'))).toEqual(['3', '4'])
|
||||
// 总列数不变(colgroup 仍为 4 列)
|
||||
expect(wrapper.findAll('colgroup col')).toHaveLength(4)
|
||||
schema = addQueryCondition(schema, { b_field: 'amount', b_type: 'money' })
|
||||
expect(hasQueryCondition(schema, 'amount')).toBe(true)
|
||||
expect(schema.groups[0].conditions[0].field).toBe('amount')
|
||||
schema = removeQueryCondition(schema, 'amount')
|
||||
expect(hasQueryCondition(schema, 'amount')).toBe(false)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user