import { afterEach, describe, expect, it } from 'vitest'; import { defineComponent, h } from 'vue'; import { mount } from '@vue/test-utils'; import TransForm from '../src/TransForm.vue'; import HeadlessBetween from '../src/components/HeadlessBetween.vue'; import HeadlessDate from '../src/components/HeadlessDate.vue'; import HeadlessSelect from '../src/components/HeadlessSelect.vue'; import HeadlessTags from '../src/components/HeadlessTags.vue'; import { resetConfig, setConfig } from '../src/config.js'; // 每个用例后恢复全局配置,避免 setConfig 污染其它用例 afterEach(() => { resetConfig(); }); describe('TransForm 操作符下拉(默认 query-option)', () => { it('无 query-option 的字段渲染默认操作符列表(12 项,第一项 EQ)', () => { const wrapper = mount(TransForm, { props: { schema: [{ row: 12, field: 'name', 'field-name': '名称' }] }, }); const opts = wrapper.find('select').findAll('option'); expect(opts).toHaveLength(12); expect(opts[0].text()).toBe('='); expect(opts[0].attributes('value')).toBe('EQ'); expect(opts[11].text()).toBe('IS NOT NULL'); }); it('有 query-option 的字段仍渲染 schema 自己的选项(不被默认覆盖)', () => { const wrapper = mount(TransForm, { props: { schema: [ { row: 12, field: 'status', 'field-name': '状态', 'query-option': [{ value: 'NE', label: '≠' }], }, ], }, }); const opts = wrapper.find('select').findAll('option'); expect(opts).toHaveLength(1); expect(opts[0].text()).toBe('≠'); expect(opts[0].attributes('value')).toBe('NE'); }); it('setConfig 自定义 queryOption 后,无 query-option 字段的下拉随之更新', () => { setConfig({ queryOption: [{ value: 'GT', label: '>' }] }); const wrapper = mount(TransForm, { props: { schema: [{ row: 12, field: 'age', 'field-name': '年龄' }] }, }); const opts = wrapper.find('select').findAll('option'); expect(opts).toHaveLength(1); expect(opts[0].text()).toBe('>'); }); it('有 query-component 但无 query-option 的字段不套默认下拉(操作符由组件自己管理)', () => { const wrapper = mount(TransForm, { props: { schema: [{ row: 12, field: 'custom', 'query-component': 'CustomQuery' }], }, }); // 配置了 query-component 后不再渲染默认 select,未知组件走占位 expect(wrapper.find('select').exists()).toBe(false); expect(wrapper.find('.tf-unknown').exists()).toBe(true); }); }); describe('between-ui / in-ui(DESIGN Q32/Q33)', () => { it('未配置 between-ui 且 query=BETWEEN 时默认渲染双 input(HeadlessBetween)', () => { const wrapper = mount(TransForm, { props: { schema: [ { row: 12, field: 'age', 'field-name': '年龄', 'value-component': 'number', 'default-query': 'BETWEEN', 'query-option': [{ value: 'BETWEEN', label: 'BETWEEN' }], }, ], }, }); expect(wrapper.findComponent(HeadlessBetween).exists()).toBe(true); expect(wrapper.findAll('.tf-between-input')).toHaveLength(2); }); it('未配置 in-ui 且 query=IN 时默认渲染 tags(HeadlessTags)', () => { const wrapper = mount(TransForm, { props: { schema: [ { row: 12, field: 'tags', 'field-name': '标签', 'value-component': 'input', 'default-query': 'IN', 'query-option': [{ value: 'IN', label: 'IN' }], }, ], }, }); expect(wrapper.findComponent(HeadlessTags).exists()).toBe(true); }); it('between-ui 指定 date 时渲染 HeadlessDate', () => { const wrapper = mount(TransForm, { props: { schema: [ { row: 12, field: 'age', 'field-name': '年龄', 'value-component': 'between', 'between-ui': 'date', 'default-query': 'BETWEEN', 'query-option': [{ value: 'BETWEEN', label: 'BETWEEN' }], }, ], }, }); expect(wrapper.findComponent(HeadlessDate).exists()).toBe(true); expect(wrapper.findComponent(HeadlessBetween).exists()).toBe(false); expect(wrapper.find('input[type="date"]').exists()).toBe(true); }); it('in-ui 指定 select 时渲染 HeadlessSelect', () => { const wrapper = mount(TransForm, { props: { schema: [ { row: 12, field: 'status', 'field-name': '状态', 'value-component': 'tags', 'in-ui': 'select', 'default-query': 'IN', options: [ { value: 'a', label: 'A' }, { value: 'b', label: 'B' }, ], 'query-option': [{ value: 'IN', label: 'IN' }], }, ], }, }); expect(wrapper.findComponent(HeadlessSelect).exists()).toBe(true); expect(wrapper.findComponent(HeadlessTags).exists()).toBe(false); }); it('非 BETWEEN/IN 时仍走 value-component,忽略 between-ui/in-ui', () => { const wrapper = mount(TransForm, { props: { schema: [ { row: 12, field: 'age', 'value-component': 'number', 'between-ui': 'date', 'in-ui': 'select', 'default-query': 'EQ', 'query-option': [ { value: 'EQ', label: '=' }, { value: 'BETWEEN', label: 'BETWEEN' }, { value: 'IN', label: 'IN' }, ], }, ], }, }); expect(wrapper.find('input[type="number"]').exists()).toBe(true); expect(wrapper.findComponent(HeadlessDate).exists()).toBe(false); expect(wrapper.findComponent(HeadlessSelect).exists()).toBe(false); }); }); describe('query-component(DESIGN Q17/Q18)', () => { it('无 query-component 时保持默认操作符下拉', () => { const wrapper = mount(TransForm, { props: { schema: [ { row: 12, field: 'name', 'field-name': '名称', 'query-option': [ { value: 'EQ', label: '=' }, { value: 'LIKE', label: 'LIKE' }, ], }, ], }, }); const select = wrapper.find('select'); expect(select.exists()).toBe(true); expect(select.findAll('option')).toHaveLength(2); expect(wrapper.vm.getFilters()[0].query).toBe('EQ'); }); it('配置 query-component 时渲染自定义组件且 v-model 绑定 query', async () => { const CustomQuery = defineComponent({ name: 'CustomQuery', props: { modelValue: { type: String, default: '' }, field: { type: String, default: '' }, fieldName: { type: String, default: '' }, queryOption: { type: Array, default: () => [] }, schema: { type: Object, default: () => ({}) }, disabled: { type: Boolean, default: false }, readOnly: { type: Boolean, default: false }, }, emits: ['update:modelValue'], setup(props, { emit }) { return () => h('button', { class: 'custom-query-btn', type: 'button', disabled: props.disabled, onClick: () => emit('update:modelValue', 'LIKE'), }, `query:${props.modelValue}`); }, }); const wrapper = mount(TransForm, { props: { schema: [ { row: 12, field: 'name', 'field-name': '名称', 'value-component': 'input', 'query-component': CustomQuery, 'default-query': 'EQ', 'query-option': [ { value: 'EQ', label: '=' }, { value: 'LIKE', label: 'LIKE' }, ], }, ], }, }); expect(wrapper.find('select').exists()).toBe(false); const btn = wrapper.find('.custom-query-btn'); expect(btn.exists()).toBe(true); expect(btn.text()).toBe('query:EQ'); expect(btn.attributes('disabled')).toBeUndefined(); await btn.trigger('click'); expect(wrapper.vm.getFilters()[0].query).toBe('LIKE'); expect(wrapper.find('.custom-query-btn').text()).toBe('query:LIKE'); }); it('query-component 字符串名可通过 componentMap 注册', async () => { const CustomQuery = defineComponent({ name: 'MappedQuery', inheritAttrs: false, props: { modelValue: { type: String, default: '' }, options: { type: Array, default: () => [] }, }, emits: ['update:modelValue'], setup(props, { emit }) { return () => h('select', { class: 'mapped-query', value: props.modelValue, onChange: (e) => emit('update:modelValue', e.target.value), }, [ h('option', { value: 'EQ' }, '='), h('option', { value: 'GT' }, '>'), ]); }, }); setConfig({ componentMap: { MappedQuery: CustomQuery } }); const wrapper = mount(TransForm, { props: { schema: [ { row: 12, field: 'age', 'query-component': 'MappedQuery', 'default-query': 'EQ', 'query-option': [ { value: 'EQ', label: '=' }, { value: 'GT', label: '>' }, ], }, ], }, }); const mapped = wrapper.find('.mapped-query'); expect(mapped.exists()).toBe(true); await mapped.setValue('GT'); expect(wrapper.vm.getFilters()[0].query).toBe('GT'); }); });