/** * FmsField 多态控件渲染器测试。 * 覆盖契约:type→控件映射、checkbox 0/1↔boolean 归一、v-model 透传、 * control-props 直达、fallthrough(class/原生事件)透传、change 转发、 * 默认插槽逃生舱、types 注册表覆盖、selectId(FmsSelect) 双绑回显。 */ import { describe, it, expect, vi } from 'vitest' import { mount } from '@vue/test-utils' import { h, markRaw } from 'vue' import FmsField from '@/components/fms-field/FmsField.vue' import { Checkbox } from '@/components/ui' // selectId 分支会挂载 FmsSelect,其依赖 @/services/api;桩掉避免真实请求 vi.mock('@/services/api', () => ({ pageDataApi: vi.fn(async () => ({ data: { rows: [], total: 0 } })), })) describe('FmsField 类型 → 控件映射', () => { it('缺省 type 兜底渲染 Input', () => { const wrapper = mount(FmsField) expect(wrapper.find('.input-wrapper').exists()).toBe(true) expect(wrapper.find('input').exists()).toBe(true) wrapper.unmount() }) it('未知 type 兜底渲染 Input', () => { const wrapper = mount(FmsField, { props: { type: 'not-a-real-type' } }) expect(wrapper.find('.input-wrapper').exists()).toBe(true) wrapper.unmount() }) it.each([ ['textarea', 'textarea'], ['number', 'input[inputmode="decimal"]'], ['money', 'input[inputmode="decimal"]'], ['checkbox', 'label.checkbox'], ['select', '.select-trigger'], ['date', '.fms-date-picker'], ['datetime', '.fms-date-picker'], ['range', '.fms-range-picker'], ])('type=%s 渲染对应控件(%s)', (type, selector) => { const wrapper = mount(FmsField, { props: { type } }) expect(wrapper.find(selector).exists()).toBe(true) wrapper.unmount() }) }) describe('FmsField 值绑定', () => { it('input:v-model 双向透传', async () => { const wrapper = mount(FmsField, { props: { modelValue: 'a' } }) expect(wrapper.find('input').element.value).toBe('a') await wrapper.find('input').setValue('abc') expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual(['abc']) wrapper.unmount() }) it('checkbox:存储 1 → 勾选;取消勾选 → 抛 0', async () => { const wrapper = mount(FmsField, { props: { type: 'checkbox', modelValue: 1 } }) const box = wrapper.find('input.checkbox-input') expect(box.element.checked).toBe(true) await box.setValue(false) expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([0]) wrapper.unmount() }) it('checkbox:存储 0 → 未勾选;勾选 → 抛 1', async () => { const wrapper = mount(FmsField, { props: { type: 'checkbox', modelValue: 0 } }) const box = wrapper.find('input.checkbox-input') expect(box.element.checked).toBe(false) await box.setValue(true) expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([1]) wrapper.unmount() }) it('select multiple:选项切换抛出数组并保持下拉打开', async () => { const wrapper = mount(FmsField, { props: { type: 'select', modelValue: ['a'], controlProps: { multiple: true, options: [ { label: '甲', value: 'a' }, { label: '乙', value: 'b' }, ], }, }, attachTo: document.body, }) await wrapper.find('.select-trigger').trigger('click') await wrapper.vm.$nextTick() const options = document.body.querySelectorAll('.select-option') options[1].click() await wrapper.vm.$nextTick() expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([['a', 'b']]) expect(document.body.querySelector('.select-option')).not.toBeNull() wrapper.unmount() }) }) describe('FmsField 扩展口子', () => { it('control-props 直达内部控件', () => { const wrapper = mount(FmsField, { props: { controlProps: { maxlength: 10 } } }) expect(wrapper.find('input').attributes('maxlength')).toBe('10') wrapper.unmount() }) it('fallthrough class 合并到内部控件根节点', () => { const wrapper = mount(FmsField, { attrs: { class: 'my-field' } }) expect(wrapper.find('.input-wrapper.my-field').exists()).toBe(true) wrapper.unmount() }) it('原生事件透传:@keydown 由内部控件冒泡命中', async () => { const onKeydown = vi.fn() const wrapper = mount(FmsField, { attrs: { onKeydown } }) await wrapper.find('input').trigger('keydown', { key: 'Enter' }) expect(onKeydown).toHaveBeenCalled() wrapper.unmount() }) it('内部控件 change 原样转发', async () => { const wrapper = mount(FmsField) await wrapper.find('input').trigger('change') expect(wrapper.emitted('change')).toBeTruthy() wrapper.unmount() }) it('默认插槽整体接管渲染(忽略 type)', () => { const wrapper = mount(FmsField, { props: { type: 'select' }, slots: { default: () => h('div', { class: 'custom-control' }, '自定义') }, }) expect(wrapper.find('.custom-control').exists()).toBe(true) expect(wrapper.find('.select-trigger').exists()).toBe(false) wrapper.unmount() }) it('types 注册表覆盖内置类型映射', () => { const wrapper = mount(FmsField, { // 组件放入响应式容器需 markRaw,避免 Vue 告警(消费者注册自定义类型时同理) props: { type: 'input', types: { input: markRaw(Checkbox) } }, }) expect(wrapper.find('label.checkbox').exists()).toBe(true) expect(wrapper.find('.input-wrapper').exists()).toBe(false) wrapper.unmount() }) }) describe('FmsField selectId(FmsSelect 双绑)', () => { it('渲染 FmsSelect,并用 label 回显选中名', () => { const wrapper = mount(FmsField, { props: { type: 'selectId', modelValue: '7', label: '客户甲', controlProps: { viewName: 'v_customer' }, }, }) expect(wrapper.find('.fms-select').exists()).toBe(true) expect(wrapper.find('.select-input').element.value).toBe('客户甲') wrapper.unmount() }) })