20260911173133
This commit is contained in:
1 parent
3838ed74b1
commit
f3ad727f1c
60 files changed
+4971
-6621
No files matched your search
@@ -1,32 +1,40 @@
|
||||
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/components/ModuleListConfigPanel.vue'
|
||||
import ModuleListConfigPanel from '@/views/module/module-management/ModuleListConfigPanel.vue'
|
||||
|
||||
/**
|
||||
* 列表配置面板与子表设计共用 useStructureEditing / ModuleStructureTree / TablePreview,
|
||||
* 拖拽语义与 module-table-edit-panel-dnd.spec 同源;本文件聚焦:
|
||||
* 1) 共用语义在列表侧(s_field_view 行)同样成立;
|
||||
* 2) 列表侧 features 差异(无分组启停菜单、列节点无删除按钮);
|
||||
* 3) 分组删除经 update-groups(groups, removedGroupIds) 上抛级联清理。
|
||||
* 列表配置面板(V2:view JSON 节点模型)结构编辑测试。
|
||||
* 面板对外数据模型为 s_module_schema.b_schema_json({ schemaVersion, columns: [...] }),
|
||||
* 列节点为 { field, ... },分组节点为 { type: 'group', title, children: [...] }。
|
||||
* 编辑器内部经 schemaModel 适配复用结构编辑内核;外部只观察 update:modelValue 的 JSON。
|
||||
*/
|
||||
|
||||
const fields = [
|
||||
{ b_id: '1', b_name: '姓名', b_field: 'name', b_type: 'input', b_i18n: 'field.sys.name' },
|
||||
{ b_id: '2', b_name: '年龄', b_field: 'age', b_type: 'number', b_i18n: 'field.sys.age' },
|
||||
{ b_id: '3', b_name: '状态', b_field: 'status', b_type: 'select', b_i18n: 'field.sys.status' },
|
||||
{ b_id: '4', b_name: '备注', b_field: 'remark', b_type: 'textarea', b_i18n: 'field.sys.remark' },
|
||||
]
|
||||
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 listConfig = [
|
||||
{ b_id: 'v1', b_module_id: 'm1', b_field_id: '1', b_group_id: null, b_i18n: null, b_visible: 1, b_width: 120, b_xh: 10 },
|
||||
{ b_id: 'v2', b_module_id: 'm1', b_field_id: '2', b_group_id: 'g1', b_i18n: null, b_visible: 1, b_width: 100, b_xh: 20 },
|
||||
{ b_id: 'v3', b_module_id: 'm1', b_field_id: '3', b_group_id: 'g1', b_i18n: null, b_visible: 1, b_width: 160, b_xh: 30 },
|
||||
{ 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: {
|
||||
@@ -64,10 +72,8 @@ function mountPanel(extraProps = {}) {
|
||||
return mount(ModuleListConfigPanel, {
|
||||
props: {
|
||||
moduleId: 'm1',
|
||||
moduleCode: 'sys_user',
|
||||
listConfig: listConfig.map((r) => ({ ...r })),
|
||||
fields,
|
||||
groups: groups.map((g) => ({ ...g })),
|
||||
modelValue: baseSchema(),
|
||||
...extraProps,
|
||||
},
|
||||
global: { stubs: { draggable: StubDraggable } },
|
||||
@@ -88,7 +94,6 @@ function fireMouse(el, type, { clientX = 0, clientY = 20, button = 0 } = {}) {
|
||||
el.dispatchEvent(ev)
|
||||
}
|
||||
|
||||
/* VTU trigger 会把 dragover/drop 构造为 DragEvent(坐标只有 getter),手工注入 */
|
||||
function fireDrag(el, type, { clientX = 10, clientY = 20, dataTransfer } = {}) {
|
||||
const ev = new Event(type, { bubbles: true, cancelable: true })
|
||||
Object.defineProperty(ev, 'clientX', { value: clientX })
|
||||
@@ -118,13 +123,21 @@ function endMouseDrag(wrapper) {
|
||||
wrapper.findComponent({ name: 'draggable' }).vm.$emit('end')
|
||||
}
|
||||
|
||||
function lastUpdate(wrapper) {
|
||||
const updates = wrapper.emitted('update')
|
||||
function lastSchema(wrapper) {
|
||||
const updates = wrapper.emitted('update:modelValue')
|
||||
expect(updates?.length).toBeGreaterThan(0)
|
||||
return updates.at(-1)[0]
|
||||
}
|
||||
|
||||
describe('ModuleListConfigPanel 结构编辑(与子表设计共用内核)', () => {
|
||||
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
|
||||
@@ -142,150 +155,107 @@ describe('ModuleListConfigPanel 结构编辑(与子表设计共用内核)',
|
||||
Element.prototype.scrollIntoView = vi.fn()
|
||||
})
|
||||
|
||||
it('列拖到分组节点(inside):移入该分组直属列末尾', () => {
|
||||
it('列拖到分组节点(inside):移入该分组子节点末尾', async () => {
|
||||
const wrapper = mountPanel()
|
||||
// 姓名(v1,未分组) 拖到分组「扩展信息」(g2) 行中部
|
||||
startMouseDrag(wrapper, '姓名')
|
||||
moveMouseTo(wrapper, '扩展信息', { clientY: 20 })
|
||||
moveMouseTo(wrapper, '基本信息', { clientY: 20 })
|
||||
endMouseDrag(wrapper)
|
||||
await nextTick()
|
||||
|
||||
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', '1'])
|
||||
const schema = lastSchema(wrapper)
|
||||
const group = groupNode(schema, '基本信息')
|
||||
expect(group.children.map((n) => n.field)).toEqual(['age', 'status', 'name'])
|
||||
expect(topLevelFields(schema)).toEqual([])
|
||||
})
|
||||
|
||||
it('列拖到「未分组」收纳节点:移出分组', () => {
|
||||
it('列拖到「未分组」收纳节点:移出分组回到顶层', async () => {
|
||||
const wrapper = mountPanel()
|
||||
// 年龄(v2,属 g1) 拖到「未分组」
|
||||
startMouseDrag(wrapper, '年龄')
|
||||
moveMouseTo(wrapper, '未分组', { clientY: 20 })
|
||||
endMouseDrag(wrapper)
|
||||
await nextTick()
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.find((r) => r.b_field_id === '2').b_group_id).toBeNull()
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '2', '3'])
|
||||
const schema = lastSchema(wrapper)
|
||||
expect(groupNode(schema, '基本信息').children.map((n) => n.field)).toEqual(['status'])
|
||||
expect(topLevelFields(schema).sort()).toEqual(['age', 'name'])
|
||||
})
|
||||
|
||||
it('分组拖到分组带上(inside):嵌套(b_parent_id 指向目标)', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 分组「扩展信息」(g2) 拖到分组「基本信息」(g1) 行中部
|
||||
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 groupUpdates = wrapper.emitted('updateGroups')
|
||||
expect(groupUpdates?.length).toBeGreaterThan(0)
|
||||
const nextGroups = groupUpdates.at(-1)[0]
|
||||
expect(nextGroups.find((g) => g.b_id === 'g2').b_parent_id).toBe('g1')
|
||||
const schema = lastSchema(wrapper)
|
||||
const outer = groupNode(schema, '基本信息')
|
||||
const nested = outer.children.find((n) => n.type === 'group')
|
||||
expect(nested.title).toBe('扩展信息')
|
||||
})
|
||||
|
||||
it('删除分组:update-groups 带 removedGroupIds,组内行归未分组', () => {
|
||||
it('删除分组:分组节点消失,组内字段回到顶层', async () => {
|
||||
const wrapper = mountPanel()
|
||||
// 树上「基本信息」(g1) 节点常驻删除按钮
|
||||
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()
|
||||
delBtn.trigger('click')
|
||||
|
||||
const groupUpdates = wrapper.emitted('updateGroups')
|
||||
expect(groupUpdates?.length).toBeGreaterThan(0)
|
||||
const [nextGroups, removedIds] = groupUpdates.at(-1)
|
||||
expect(nextGroups.map((g) => g.b_id)).toEqual(['g2'])
|
||||
expect(removedIds).toEqual(['g1'])
|
||||
|
||||
// g1 下的行(年龄/状态)归未分组并重排
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.every((r) => r.b_group_id == null)).toBe(true)
|
||||
expect(rows.map((r) => r.b_xh)).toEqual([10, 20, 30])
|
||||
})
|
||||
|
||||
it('列表侧 features:分组右键无「启停」,列节点有删除按钮(同表格设计)', async () => {
|
||||
const wrapper = mountPanel()
|
||||
// 列节点(姓名)渲染删除按钮
|
||||
const colRow = wrapper.findAll('.mst-node').find((w) => w.text().includes('姓名'))
|
||||
expect(colRow).toBeTruthy()
|
||||
expect(colRow.findAll('button').some((b) => b.attributes('title') === '删除')).toBe(true)
|
||||
// 分组节点也有删除按钮
|
||||
const groupRow = wrapper.findAll('.mst-node').find((w) => w.text().includes('基本信息'))
|
||||
expect(groupRow.findAll('button').some((b) => b.attributes('title') === '删除')).toBe(true)
|
||||
})
|
||||
|
||||
it('树中列节点删除按钮:删除 = 隐藏(b_visible=0),行保留', async () => {
|
||||
const wrapper = mountPanel()
|
||||
const colRow = wrapper.findAll('.mst-node').find((w) => w.text().includes('姓名'))
|
||||
const delBtn = colRow.findAll('button').find((b) => b.attributes('title') === '删除')
|
||||
expect(delBtn).toBeTruthy()
|
||||
await delBtn.trigger('click')
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
// 行不删(启用字段始终有配置行),仅置隐藏;预览该列置灰带「隐藏」标记
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '2', '3'])
|
||||
expect(rows.find((r) => r.b_field_id === '1').b_visible).toBe(0)
|
||||
const schema = lastSchema(wrapper)
|
||||
expect(groupNode(schema, '基本信息')).toBeUndefined()
|
||||
expect(topLevelFields(schema).sort()).toEqual(['age', 'name', 'status'])
|
||||
})
|
||||
|
||||
it('预览表渲染多级表头:band 行含分组标题,列头含字段名', () => {
|
||||
const wrapper = mountPanel()
|
||||
const bands = wrapper.findAll('th.mm-tdesign__band')
|
||||
expect(bands.map((th) => th.text())).toContain('基本信息')
|
||||
// 未分组列(姓名)表头贯穿 band 行
|
||||
const ungroupedCell = wrapper
|
||||
.findAll('th.mm-tdesign__cell--ungrouped')
|
||||
.find((th) => th.attributes('data-field-id') === '1')
|
||||
expect(ungroupedCell).toBeTruthy()
|
||||
expect(ungroupedCell.text()).toContain('姓名')
|
||||
})
|
||||
|
||||
it('字段库字段点击加入:未加入字段点击后生成配置行并选中', async () => {
|
||||
it('字段库点击未加入字段:追加为顶层列节点', async () => {
|
||||
const wrapper = mountPanel()
|
||||
const libItem = wrapper
|
||||
.findAll('.mm-fdesign__lib-item')
|
||||
.find((w) => w.attributes('data-field-id') === '4')
|
||||
.find((w) => w.attributes('data-field-id') === 'remark')
|
||||
expect(libItem).toBeTruthy()
|
||||
await libItem.trigger('click')
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
// 新列归未分组区(未分组在前),备注为 textarea:默认隐藏、列宽 160
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '4', '2', '3'])
|
||||
expect(rows.find((r) => r.b_field_id === '4').b_group_id).toBeNull()
|
||||
expect(rows.find((r) => r.b_field_id === '4').b_visible).toBe(0)
|
||||
expect(rows.find((r) => r.b_field_id === '4').b_width).toBe(160)
|
||||
const schema = lastSchema(wrapper)
|
||||
expect(topLevelFields(schema)).toContain('remark')
|
||||
})
|
||||
|
||||
it('字段库字段拖入分组带(inside):加入该分组直属列末尾', () => {
|
||||
it('字段库字段拖入分组带(inside):加入该分组子节点末尾', async () => {
|
||||
const wrapper = mountPanel()
|
||||
const libItem = wrapper
|
||||
.findAll('.mm-fdesign__lib-item')
|
||||
.find((w) => w.attributes('data-field-id') === '4')
|
||||
expect(libItem).toBeTruthy()
|
||||
.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 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')
|
||||
const schema = lastSchema(wrapper)
|
||||
expect(groupNode(schema, '基本信息').children.map((n) => n.field)).toEqual([
|
||||
'age',
|
||||
'status',
|
||||
'remark',
|
||||
])
|
||||
})
|
||||
|
||||
it('属性面板:选中列显示列属性(无状态开关),删除列 = 隐藏', async () => {
|
||||
it('预览表渲染多级表头:band 行含分组标题', () => {
|
||||
const wrapper = mountPanel()
|
||||
// 树中点击列节点 → Inspector 显示列属性
|
||||
const treeRow = wrapper.findAll('.mst-node').find((w) => w.text().includes('姓名'))
|
||||
expect(treeRow).toBeTruthy()
|
||||
await treeRow.trigger('click')
|
||||
const inspector = wrapper.find('.mm-fdesign__inspector')
|
||||
expect(inspector.exists()).toBe(true)
|
||||
expect(inspector.text()).toContain('姓名')
|
||||
// 列表侧无状态开关(显示/必填等是子表编辑语义)
|
||||
expect(inspector.findAll('.mm-fdesign__status-chip')).toHaveLength(0)
|
||||
// 删除列按钮:点击隐藏该列(行保留)
|
||||
const delBtn = inspector.findAll('button').find((b) => b.text().includes('删除列'))
|
||||
expect(delBtn).toBeTruthy()
|
||||
await delBtn.trigger('click')
|
||||
|
||||
const rows = lastUpdate(wrapper)
|
||||
expect(rows.map((r) => r.b_field_id)).toEqual(['1', '2', '3'])
|
||||
expect(rows.find((r) => r.b_field_id === '1').b_visible).toBe(0)
|
||||
const bands = wrapper.findAll('th.mm-tdesign__band')
|
||||
expect(bands.map((th) => th.text())).toContain('基本信息')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user