/** * 自动编码面板(s_autocode):惰性建行 + 实时预览 + 校验。 * * 关键点:模块从未配置自动编码时草稿保持空数组(打开面板即建行会把「无变更」 * diff 成一条 insert),只有用户首次操作才落行;且 defineModel 写入后不能立刻 * 回读 prop(要下一轮才回传),所以建行后返回的是 reactive 代理行。 * * 「启用」开关在表单第一行,测试点面板里真实的 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 { 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, * 否则复现不了「立刻回读旧值」的问题。 */ function mountPanel(autocode: ModuleAutoCodeRow[] = []) { const wrapper = mount(ModuleAutoCodePanel, { props: { moduleId: 'm1', fields: FIELDS, autocode, 'onUpdate:autocode': (value: ModuleAutoCodeRow[]) => wrapper.setProps({ autocode: value }), }, }) return wrapper } describe('自动编码面板', () => { it('首次拨「启用」时惰性建行并写回 b_canuse(defineModel 不能写完立刻回读)', async () => { const wrapper = mountPanel() // 模块从未配置自动编码:草稿为空,此时拨开关会走 ensureRow 建行 await wrapper.find('[role="switch"]').trigger('click') 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) await wrapper.find('[role="switch"]').trigger('click') 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() await wrapper.find('[role="switch"]').trigger('click') await wrapper.vm.$nextTick() await wrapper.find('[role="switch"]').trigger('click') 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) await wrapper.find('[role="switch"]').trigger('click') await wrapper.vm.$nextTick() expect(draft[0]?.b_canuse).toBe(0) // 没有把数组清空重写 expect(wrapper.emitted('update:autocode')).toBeUndefined() }) it('开关的选中态跟随规则行(配过又停用的行显示为关)', async () => { const on = mountPanel([row({ b_canuse: 1 })]) await on.vm.$nextTick() expect(on.find('[role="switch"]').attributes('aria-checked')).toBe('true') const off = mountPanel([row({ b_canuse: 0 })]) await off.vm.$nextTick() expect(off.find('[role="switch"]').attributes('aria-checked')).toBe('false') }) 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() // 标签顺序 = 填写顺序:行内从左到右、行间从上到下 // (启用 → 业务编号字段/重置周期 → 前缀/日期/分隔符/位数/起始值 → 编号预览) const labels = wrapper.findAll('.g3-form-item__label') expect(labels.map((item) => item.text())).toEqual([ '启用', '业务编号字段', '重置周期', '前缀', '日期', '分隔符', '位数', '起始值', '编号预览', ]) // 未配置(草稿为空)时开关也渲染、可拨:它就是配置入口 const switchEl = wrapper.find('[role="switch"]') expect(switchEl.exists()).toBe(true) expect(switchEl.attributes('disabled')).toBeUndefined() expect(switchEl.attributes('aria-checked')).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()]) expect(await wrapper.vm.validate()).toEqual([]) }) it('启用但未选业务编号字段时报错(错误挂在对应 FormItem 上)', async () => { const wrapper = mountPanel([row({ b_field: null })]) 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 })]) 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 })]) expect(await wrapper.vm.validate()).toEqual([]) }) })