u
This commit is contained in:
1 parent
b4498e5350
commit
bf9dca19de
47 files changed
+8592
-313
No files matched your search
+71
-8
@@ -4,11 +4,14 @@
|
||||
* 关键点:模块从未配置自动编码时草稿保持空数组(打开面板即建行会把「无变更」
|
||||
* diff 成一条 insert),只有用户首次操作才落行;且 defineModel 写入后不能立刻
|
||||
* 回读 prop(要下一轮才回传),所以建行后返回的是 reactive 代理行。
|
||||
*
|
||||
* 「启用」开关不在本面板内(已上提到页面级面板头),面板通过
|
||||
* defineExpose 的 toggleEnabled() 拨动、enabled prop 接收状态,
|
||||
* 测试走同一对口而不是找面板里的 G3Switch。
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { G3Switch } from '@g3soft/ui'
|
||||
import ModuleAutoCodePanel from '@/views/module/module-management/ModuleAutoCodePanel.vue'
|
||||
import type { ModuleAutoCodeRow } from '@/views/module/module-management/utils'
|
||||
|
||||
@@ -38,14 +41,28 @@ function row(overrides: Partial<ModuleAutoCodeRow> = {}): ModuleAutoCodeRow {
|
||||
}
|
||||
}
|
||||
|
||||
/** 模拟父级 v-model:autocode:emit 回来要显式写回 prop,否则复现不了「立刻回读旧值」的问题 */
|
||||
function mountPanel(autocode: ModuleAutoCodeRow[] = []) {
|
||||
/**
|
||||
* 模拟父级 v-model:autocode + 面板头开关:emit 回来要显式写回 prop,
|
||||
* 否则复现不了「立刻回读旧值」的问题。
|
||||
*
|
||||
* 注意 enabled 用可变对象持有、不在 mount 期间引用 wrapper:
|
||||
* 面板的 update:enabled 是 immediate 的,挂载时就会 emit,
|
||||
* 处理器里访问尚未赋值的 wrapper 会直接抛 TDZ 错误。
|
||||
*/
|
||||
function mountPanel(autocode: ModuleAutoCodeRow[] = [], enabled = false) {
|
||||
const state = { enabled }
|
||||
const wrapper = mount(ModuleAutoCodePanel, {
|
||||
props: {
|
||||
moduleId: 'm1',
|
||||
fields: FIELDS,
|
||||
autocode,
|
||||
get enabled() {
|
||||
return state.enabled
|
||||
},
|
||||
'onUpdate:autocode': (value: ModuleAutoCodeRow[]) => wrapper.setProps({ autocode: value }),
|
||||
'onUpdate:enabled': (value: boolean) => {
|
||||
state.enabled = value
|
||||
},
|
||||
},
|
||||
})
|
||||
return wrapper
|
||||
@@ -56,7 +73,7 @@ describe('自动编码面板', () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
// 模块从未配置自动编码:草稿为空,此时拨开关会走 ensureRow 建行
|
||||
wrapper.findComponent(G3Switch).vm.$emit('update:modelValue', true)
|
||||
wrapper.vm.toggleEnabled(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const emitted = wrapper.emitted('update:autocode')
|
||||
@@ -73,7 +90,7 @@ describe('自动编码面板', () => {
|
||||
const draft = [row({ b_canuse: 0, b_prefix: 'A' })]
|
||||
const wrapper = mountPanel(draft)
|
||||
|
||||
wrapper.findComponent(G3Switch).vm.$emit('update:modelValue', true)
|
||||
wrapper.vm.toggleEnabled(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 已落草稿:直接改这一行的属性(父级 ref 是深度响应式,改得见)
|
||||
@@ -82,6 +99,39 @@ describe('自动编码面板', () => {
|
||||
expect(draft[0]?.b_prefix).toBe('A') // 沿用原行,未被占位行覆盖
|
||||
})
|
||||
|
||||
it('把从未配置过的模块拨开又关掉,不留占位行(否则保存会 diff 出一条 insert)', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
wrapper.vm.toggleEnabled(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
wrapper.vm.toggleEnabled(false)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// 最后一次写回的草稿必须回到空数组
|
||||
const emitted = wrapper.emitted('update:autocode')!
|
||||
expect(emitted[emitted.length - 1][0]).toEqual([])
|
||||
})
|
||||
|
||||
it('停用既有规则时保留该行(那是既有规则的停用,要落库)', async () => {
|
||||
const draft = [row({ b_canuse: 1 })]
|
||||
const wrapper = mountPanel(draft, true)
|
||||
|
||||
wrapper.vm.toggleEnabled(false)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(draft[0]?.b_canuse).toBe(0)
|
||||
// 没有把数组清空重写
|
||||
expect(wrapper.emitted('update:autocode')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('启用态回报给父级(驱动面板头开关)', async () => {
|
||||
const wrapper = mountPanel([row({ b_canuse: 1 })])
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.emitted('update:enabled')).toBeTruthy()
|
||||
expect(wrapper.emitted('update:enabled')![0][0]).toBe(true)
|
||||
})
|
||||
|
||||
it('紧凑网格固定竖排且标签左对齐,不跟随全局横排配置', () => {
|
||||
const wrapper = mountPanel()
|
||||
expect(wrapper.find('form').classes()).toContain('g3-form--vertical')
|
||||
@@ -89,6 +139,12 @@ describe('自动编码面板', () => {
|
||||
expect(wrapper.findAll('.g3-form-item__label.is-label-right')).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('面板内不再自带「启用」开关(已上提到页面级面板头)', () => {
|
||||
const wrapper = mountPanel([row()])
|
||||
|
||||
expect(wrapper.find('[role="switch"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('预览按 前缀 + 日期 + 补位流水号 拼接', async () => {
|
||||
const wrapper = mountPanel([
|
||||
row({ b_prefix: 'G3', b_dateformat: '', b_seqwidth: 4, b_resettype: 'none' }),
|
||||
@@ -101,13 +157,13 @@ describe('自动编码面板', () => {
|
||||
})
|
||||
|
||||
it('启用态下的合法规则校验通过', async () => {
|
||||
const wrapper = mountPanel([row()])
|
||||
const wrapper = mountPanel([row()], true)
|
||||
|
||||
expect(await wrapper.vm.validate()).toEqual([])
|
||||
})
|
||||
|
||||
it('启用但未选业务编号字段时报错(错误挂在对应 FormItem 上)', async () => {
|
||||
const wrapper = mountPanel([row({ b_field: null })])
|
||||
const wrapper = mountPanel([row({ b_field: null })], true)
|
||||
|
||||
const errors = await wrapper.vm.validate()
|
||||
expect(errors.map((item) => item.name)).toContain('b_field')
|
||||
@@ -115,7 +171,7 @@ describe('自动编码面板', () => {
|
||||
})
|
||||
|
||||
it('位数超出 1-18 时报错', async () => {
|
||||
const wrapper = mountPanel([row({ b_seqwidth: 20 })])
|
||||
const wrapper = mountPanel([row({ b_seqwidth: 20 })], true)
|
||||
|
||||
const errors = await wrapper.vm.validate()
|
||||
expect(errors.find((item) => item.name === 'b_seqwidth')?.errors[0]).toBe(
|
||||
@@ -128,4 +184,11 @@ describe('自动编码面板', () => {
|
||||
|
||||
expect(await wrapper.vm.validate()).toEqual([])
|
||||
})
|
||||
|
||||
it('未启用时即使参数残缺也不报错(重置周期为空同样跳过)', async () => {
|
||||
// 占位行 / 已停用的规则都不该拦保存:旧写法里 b_resettype 是硬性 required,会误报
|
||||
const wrapper = mountPanel([row({ b_canuse: 0, b_resettype: '', b_seqwidth: 0 })], false)
|
||||
|
||||
expect(await wrapper.vm.validate()).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* ModuleTreePanel 行尾启用勾选。
|
||||
*
|
||||
* 勾选框不走 G3Tree 内置的 checkable(那是行首、多选语义),而是画在 #title 插槽的
|
||||
* 行尾。自己画就要自己兜住三件事,也正是这里要锁住的行为:
|
||||
* 1. 勾选上报 toggleEnabled(id + 目标启用态),由父级落库;
|
||||
* 2. 点击不冒泡 —— 不能顺带选中这一行(行上挂了 click 选中 / mousedown 武装拖拽);
|
||||
* 3. 未保存草稿不可勾(库里没有这行,改不了它的 b_canuse)。
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import type { VueWrapper } from '@vue/test-utils'
|
||||
import { G3Checkbox, G3Tree } from '@g3soft/ui'
|
||||
import ModuleTreePanel from '@/views/module/module-management/ModuleTreePanel.vue'
|
||||
import type { ModuleNode } from '@/views/module/module-management/utils'
|
||||
import BizTreePanel from '@/components/biz-tree/BizTreePanel.vue'
|
||||
|
||||
const TREE = [
|
||||
{
|
||||
b_id: 'm1',
|
||||
b_name: '启用的模块',
|
||||
b_module_type: 'module',
|
||||
b_canuse: 1,
|
||||
},
|
||||
{
|
||||
b_id: 'm2',
|
||||
b_name: '停用的模块',
|
||||
b_module_type: 'module',
|
||||
b_canuse: 0,
|
||||
},
|
||||
{
|
||||
b_id: '-1',
|
||||
b_name: '未保存草稿',
|
||||
b_module_type: 'module',
|
||||
b_canuse: 1,
|
||||
},
|
||||
] as unknown as ModuleNode[]
|
||||
|
||||
function mountPanel(unsavedIds = new Set<string>()) {
|
||||
return mount(ModuleTreePanel, {
|
||||
props: { treeData: TREE, unsavedIds },
|
||||
})
|
||||
}
|
||||
|
||||
/** 按标题取节点行(BizTreePanel 默认插槽由本组件接管,树节点带 role=treeitem) */
|
||||
const rowOf = (wrapper: VueWrapper, label: string) =>
|
||||
wrapper.findAll('[role="treeitem"]').find((row) => row.text().includes(label))
|
||||
|
||||
/** 取某节点行里的勾选框 */
|
||||
const checkboxOf = (wrapper: VueWrapper, label: string) =>
|
||||
rowOf(wrapper, label)?.findComponent(G3Checkbox)
|
||||
|
||||
describe('ModuleTreePanel 行尾启用勾选', () => {
|
||||
it('每个节点行都带一个勾选框,勾选态反映 b_canuse', () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
expect(checkboxOf(wrapper, '启用的模块')?.props('modelValue')).toBe(true)
|
||||
expect(checkboxOf(wrapper, '停用的模块')?.props('modelValue')).toBe(false)
|
||||
})
|
||||
|
||||
it('勾选上报 toggleEnabled(id + 目标启用态)', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
checkboxOf(wrapper, '停用的模块')?.vm.$emit('update:modelValue', true)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.emitted('toggleEnabled')?.[0]?.[0]).toEqual({ id: 'm2', enabled: true })
|
||||
})
|
||||
|
||||
it('取消勾选同样上报(enabled: false)', async () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
checkboxOf(wrapper, '启用的模块')?.vm.$emit('update:modelValue', false)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.emitted('toggleEnabled')?.[0]?.[0]).toEqual({ id: 'm1', enabled: false })
|
||||
})
|
||||
|
||||
it('点击勾选框不触发行的选中(事件不冒泡)', () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
const row = rowOf(wrapper, '启用的模块')!
|
||||
// 勾选框外层用 @click.stop 拦住冒泡:直接点勾选框本体,行上不应收到 click
|
||||
const box = checkboxOf(wrapper, '启用的模块')!.find('input')
|
||||
void box.trigger('click')
|
||||
|
||||
expect(wrapper.emitted('select')).toBeUndefined()
|
||||
// 行自身仍可正常选中,说明拦的是勾选框那一下、不是把整行点击弄坏了
|
||||
void row.trigger('click')
|
||||
expect(wrapper.emitted('select')?.[0]?.[0]).toBe('m1')
|
||||
})
|
||||
|
||||
it('未保存草稿的勾选框禁用且不上报(库里还没有这行)', async () => {
|
||||
const wrapper = mountPanel(new Set(['-1']))
|
||||
|
||||
const box = checkboxOf(wrapper, '未保存草稿')!
|
||||
expect(box.props('disabled')).toBe(true)
|
||||
|
||||
box.vm.$emit('update:modelValue', false)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.emitted('toggleEnabled')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('已保存节点不受草稿集合影响,仍可勾选', async () => {
|
||||
const wrapper = mountPanel(new Set(['-1']))
|
||||
|
||||
expect(checkboxOf(wrapper, '启用的模块')?.props('disabled')).toBe(false)
|
||||
|
||||
checkboxOf(wrapper, '启用的模块')?.vm.$emit('update:modelValue', false)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.emitted('toggleEnabled')?.[0]?.[0]).toEqual({ id: 'm1', enabled: false })
|
||||
})
|
||||
|
||||
it('勾选框渲染在名称之后的行尾(不是行首的多选位)', () => {
|
||||
const wrapper = mountPanel()
|
||||
const row = rowOf(wrapper, '启用的模块')!
|
||||
|
||||
// 行内文本顺序:名称在前;勾选框的 DOM 位置在名称节点之后
|
||||
const nameEl = row.find('.mm-tree-node__name')
|
||||
const enableEl = row.find('.mm-tree-node__enable')
|
||||
expect(nameEl.exists()).toBe(true)
|
||||
expect(enableEl.exists()).toBe(true)
|
||||
|
||||
// compareDocumentPosition: 4 = 名称之后(DOCUMENT_POSITION_FOLLOWING)
|
||||
const relation = nameEl.element.compareDocumentPosition(enableEl.element)
|
||||
expect(relation & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
||||
})
|
||||
|
||||
it('面板必须开 block-node:否则内容层不撑满,勾选框贴不到行尾', () => {
|
||||
const wrapper = mountPanel()
|
||||
|
||||
// G3Tree 的节点内容层默认 inline-flex 且无 flex:1(宽度=文字宽度),
|
||||
// 只有 blockNode 会给它 flex:1。少了它 .mm-tree-node 的 flex:1 只撑到
|
||||
// 「文字那一小段」,勾选框会紧跟名称而不是贴右边 —— 这条断言把这个前提钉住。
|
||||
//
|
||||
// 断言必须落在 G3Tree 上:BizTreePanel / BizTree 都是 inheritAttrs:false,
|
||||
// 模板写的 'block-node'(kebab)会原样留在 BizTree 的 $attrs 里,
|
||||
// 所以 BizTree.props('blockNode') 是 undefined —— 但往下传给 G3Tree 时
|
||||
// Vue 会把 kebab attr 归一到 camelCase prop,G3Tree 那边是 true。
|
||||
// 在中间层断言会得到假阴性(这条一开始就是这么写错的)。
|
||||
expect(wrapper.findComponent(G3Tree).props('blockNode')).toBe(true)
|
||||
})
|
||||
|
||||
it('block-node 生效:内容层带 is-block(flex:1),勾选才有行尾可贴', () => {
|
||||
const wrapper = mountPanel()
|
||||
const row = rowOf(wrapper, '启用的模块')!
|
||||
|
||||
// 库内样式:.g3-tree-node__content 只有 .is-block 才 flex:1
|
||||
expect(row.find('.g3-tree-node__content').classes()).toContain('is-block')
|
||||
})
|
||||
|
||||
it('面板不启用 G3Tree 内置 checkable(避免行首出现多选勾选框)', () => {
|
||||
const wrapper = mountPanel()
|
||||
const panel = wrapper.findComponent(BizTreePanel)
|
||||
// checkable 未透传:内置勾选框列不渲染
|
||||
expect(panel.attributes('checkable')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user