Files
workspace/code/g3soft-erp/web/tests/views/module/module-management/ModuleAutoCodePanel.spec.ts
T
2026-10-15 17:31:35 +08:00

195 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 自动编码面板(s_autocode):惰性建行 + 实时预览 + 校验。
*
* 关键点:模块从未配置自动编码时草稿保持空数组(打开面板即建行会把「无变更」
* diff 成一条 insert),只有用户首次操作才落行;且 defineModel 写入后不能立刻
* 回读 prop(要下一轮才回传),所以建行后返回的是 reactive 代理行。
*
* 「启用」开关不在本面板内(已上提到页面级面板头),面板通过
* defineExpose 的 toggleEnabled() 拨动、enabled prop 接收状态,
* 测试走同一对口而不是找面板里的 G3Switch。
*/
import { describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import ModuleAutoCodePanel from '@/views/module/module-management/ModuleAutoCodePanel.vue'
import type { ModuleAutoCodeRow } from '@/views/module/module-management/utils'
const FIELDS = [
{ b_field: 'b_id', b_name: '编码', b_type: 'input', b_canuse: 1 },
{ b_field: 'b_no', b_name: '业务单号', b_type: 'input', b_canuse: 1 },
{ b_field: 'b_bz', b_name: '备注', b_type: 'textarea', b_canuse: 1 },
{ b_field: 'b_amount', b_name: '金额', b_type: 'money', b_canuse: 1 },
{ b_field: 'b_off', b_name: '停用字段', b_type: 'input', b_canuse: 0 },
]
/** 造一行规则:只写关心的字段,其余取启用态的常用默认值 */
function row(overrides: Partial<ModuleAutoCodeRow> = {}): ModuleAutoCodeRow {
return {
b_module_id: 'm1',
b_field: 'b_no',
b_prefix: '',
b_dateformat: 'yyyyMM',
b_separator: '-',
b_seqwidth: 5,
b_resettype: 'month',
b_startvalue: 1,
b_currentperiod: null,
b_currentvalue: 0,
b_canuse: 1,
...overrides,
}
}
/**
* 模拟父级 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
}
describe('自动编码面板', () => {
it('首次拨「启用」时惰性建行并写回 b_canuse(defineModel 不能写完立刻回读)', async () => {
const wrapper = mountPanel()
// 模块从未配置自动编码:草稿为空,此时拨开关会走 ensureRow 建行
wrapper.vm.toggleEnabled(true)
await wrapper.vm.$nextTick()
const emitted = wrapper.emitted('update:autocode')
expect(emitted).toBeTruthy()
const created = emitted![0][0] as ModuleAutoCodeRow[]
expect(created[0].b_canuse).toBe(1)
expect(created[0].b_module_id).toBe('m1')
// 业务编号字段自动取第一个可用文本字段(排除主键 b_id、非文本类型与停用字段)
expect(created[0].b_field).toBe('b_no')
})
it('已有一行时就地改,不再新建也不重新 emit 整个数组', async () => {
// 传进去的数组即父级持有的草稿:断言它被就地改写
const draft = [row({ b_canuse: 0, b_prefix: 'A' })]
const wrapper = mountPanel(draft)
wrapper.vm.toggleEnabled(true)
await wrapper.vm.$nextTick()
// 已落草稿:直接改这一行的属性(父级 ref 是深度响应式,改得见)
expect(wrapper.emitted('update:autocode')).toBeUndefined()
expect(draft[0]?.b_canuse).toBe(1)
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')
// 标签左对齐依赖 G3Form 的默认 labelAlign=left;竖排下若回到 right 会被推到整列右边缘
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' }),
])
await wrapper.vm.$nextTick()
const preview = wrapper.find('[data-field="b_preview"] input')
expect(preview.attributes('readonly')).toBeDefined()
expect((preview.element as HTMLInputElement).value).toBe('G3-0001')
})
it('启用态下的合法规则校验通过', async () => {
const wrapper = mountPanel([row()], true)
expect(await wrapper.vm.validate()).toEqual([])
})
it('启用但未选业务编号字段时报错(错误挂在对应 FormItem 上)', async () => {
const wrapper = mountPanel([row({ b_field: null })], true)
const errors = await wrapper.vm.validate()
expect(errors.map((item) => item.name)).toContain('b_field')
expect(errors.find((item) => item.name === 'b_field')?.errors[0]).toBe('请选择业务编号字段')
})
it('位数超出 1-18 时报错', async () => {
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(
'流水号位数必须是 1 到 18 之间的整数',
)
})
it('未启用时不校验(占位行只用于展示)', async () => {
const wrapper = mountPanel()
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([])
})
})