import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' import { App, createApp, defineComponent, h, nextTick, ref, reactive } from 'vue' const serverApiMock = vi.fn() const deepCopyMock = vi.fn((value: any) => JSON.parse(JSON.stringify(value))) const formInstances: Array<{ config: any modelData: any validate: ReturnType resetFields: ReturnType }> = [] vi.mock('../../../script', () => ({ deepCopy: deepCopyMock, serverApi: serverApiMock })) vi.mock('../../../api/index', () => ({ default: { getWhereByFieldGroup: 'getWhereByFieldGroup', getConditionByRouteName: 'getConditionByRouteName', saveCondition: 'saveCondition' } })) vi.mock('lodash-es', () => ({ uniqueId: vi.fn((prefix = '') => `${prefix}mock`) })) vi.mock('../../../components/infoForm/infoForm.vue', () => ({ default: defineComponent({ name: 'InfoFormStub', props: { config: { type: Object, default: () => ({}) }, modelData: { type: Object, default: () => ({}) } }, setup (props, { slots, expose }) { const validate = vi.fn((cb?: (valid: boolean) => void) => cb?.(true)) const resetFields = vi.fn() formInstances.push({ config: props.config, modelData: props.modelData, validate, resetFields }) expose({ validate, resetFields }) return () => h('div', { class: 'info-form-stub', 'data-config-ref': props.config?.ref || '' }, [slots.fieldGroup?.(), slots.default?.()]) } }) })) vi.mock('../../../components/infoDrawer/index.vue', () => ({ default: defineComponent({ name: 'InfoDrawerStub', props: { title: { type: String, default: '' }, operates: { type: Object, default: () => ({}) }, isShowDrawer: { type: Boolean, default: false } }, emits: ['closeDrawer'], setup (props, { slots, emit }) { return () => props.isShowDrawer ? h('div', { class: 'info-drawer-stub', 'data-title': props.title }, [ h('div', { class: 'drawer-actions' }, (props.operates.operation || []).map((item: any, index: number) => h('button', { class: 'drawer-action', 'data-index': String(index), 'data-label': item.label(), onClick: () => item.method() }, item.label()))), slots.default?.(), h('button', { class: 'drawer-close', onClick: () => emit('closeDrawer') }, 'close') ]) : null } }) })) vi.mock('../../../components/infoDialog/index.vue', () => ({ default: defineComponent({ name: 'InfoDialogStub', props: { options: { type: Object, default: () => ({}) } }, emits: ['closeDialog', 'confirmDialog'], setup (props, { slots, emit }) { return () => props.options?.isShowDialog ? h('div', { class: 'info-dialog-stub' }, [ slots.default?.(), h('button', { class: 'dialog-confirm', onClick: () => emit('confirmDialog') }, 'confirm'), h('button', { class: 'dialog-close', onClick: () => emit('closeDialog') }, 'close') ]) : null } }) })) vi.mock('../../../components/infoSearch/fieldGroup/fieldGroup.vue', () => ({ default: defineComponent({ name: 'FieldGroupStub', props: { searchList: { type: Array, default: () => [] }, columnI18n: { type: Boolean, default: true }, modelData: { type: Object, default: () => ({}) } }, emits: ['pullSql'], setup (props, { emit }) { return () => h('div', { class: 'field-group-stub', 'data-search-length': String(props.searchList.length), 'data-column-i18n': String(props.columnI18n) }, [ h('button', { class: 'pull-sql-trigger', onClick: () => emit('pullSql') }, 'pullSql') ]) } }) })) let InfoSearch: any interface RenderResult { app: App el: HTMLDivElement emitted: { search: ReturnType } infoSearchRef: { value: any } } const renderedApps: RenderResult[] = [] beforeAll(async () => { InfoSearch = (await import('../../../components/infoSearch/infoSearch.vue')).default }) const createColumns = () => ([ { label: 'name', prop: 'name', type: 'string' }, { label: 'status', prop: 'status', type: 'enum', filters: false } ]) const createFieldGroup = (oper = 'LIKE', value: any = ['abc']) => ({ key: 'group_root', type: 'FieldGroup', group: true, andOr: 'AND', fields: [ { key: 'field_1', type: 'Field', group: false, andOr: 'AND', name: 'name', oper, value } ] }) const setupServerApi = () => { serverApiMock.mockImplementation(({ interface: api, success }: any) => { if (api === 'getConditionByRouteName') { success?.([ { name: '模板一', fieldGroup: JSON.stringify(createFieldGroup('LIKE', ['%template%'])) } ]) return Promise.resolve(null) } if (api === 'getWhereByFieldGroup') { success?.({ content: 'name LIKE %abc%' }) return Promise.resolve(null) } if (api === 'saveCondition') { success?.({}) return Promise.resolve(null) } return Promise.resolve(null) }) } const renderInfoSearch = async (props: Record = {}): Promise => { const emitted = { search: vi.fn() } const infoSearchRef = ref(null) const el = document.createElement('div') document.body.appendChild(el) const app = createApp(defineComponent({ setup () { const localProps = reactive({ columns: createColumns(), columnI18n: true, ...props }) return () => h(InfoSearch, { ...localProps, ref: (instance: any) => { infoSearchRef.value = instance }, onSearch: emitted.search }) } })) app.config.globalProperties.$t = (key: string) => key app.mount(el) await nextTick() const result = { app, el, emitted, infoSearchRef } renderedApps.push(result) return result } const getRoot = () => document.body const click = async (selector: string) => { const target = getRoot().querySelector(selector) as HTMLElement | null expect(target).not.toBeNull() target!.click() await nextTick() } const flushPromises = async () => Promise.resolve() const enableFakeTimers = () => { vi.useFakeTimers() } describe('components/infoSearch', () => { afterEach(() => { while (renderedApps.length) { const current = renderedApps.pop()! current.app.unmount() current.el.remove() } document.body.innerHTML = '' formInstances.splice(0) serverApiMock.mockReset() deepCopyMock.mockClear() vi.useRealTimers() }) it('调用 showDrawer 后会渲染抽屉、拉取模板,并向 fieldGroup 传递已过滤的字段列表', async () => { enableFakeTimers() setupServerApi() const { infoSearchRef } = await renderInfoSearch() await infoSearchRef.value.showDrawer() await nextTick() expect(getRoot().querySelector('.info-drawer-stub')).not.toBeNull() expect((getRoot().querySelector('.field-group-stub') as HTMLElement | null)?.getAttribute('data-search-length')).toBe('1') expect(serverApiMock).toHaveBeenCalledWith(expect.objectContaining({ interface: 'getConditionByRouteName', params: { routeName: location.pathname } })) }) it('传入已有条件组时会格式化数据并自动生成 where SQL', async () => { enableFakeTimers() setupServerApi() const { infoSearchRef } = await renderInfoSearch() await infoSearchRef.value.showDrawer(createFieldGroup('LIKE', ['%abc%'])) await nextTick() vi.advanceTimersByTime(0) await flushPromises() await nextTick() const searchForm = formInstances.find(item => item.config.ref === 'search') expect(searchForm).toBeTruthy() expect(searchForm?.validate).toHaveBeenCalled() expect(searchForm?.modelData.where).toBe('name LIKE %abc%') const sqlCall = serverApiMock.mock.calls.find(call => call[0].interface === 'getWhereByFieldGroup') expect(sqlCall?.[0].params.fields[0].value).toEqual(['%abc%']) }) it('点击确认后会抛出 search 事件,并把条件值转换为接口所需格式', async () => { enableFakeTimers() setupServerApi() const { infoSearchRef, emitted } = await renderInfoSearch() await infoSearchRef.value.showDrawer(createFieldGroup('LIKE', ['%keyword%'])) await nextTick() await click('[data-label="confirm"]') expect(emitted.search).toHaveBeenCalledTimes(1) expect(emitted.search.mock.calls[0][0].fields[0].value).toEqual(['%keyword%']) const searchForm = formInstances.find(item => item.config.ref === 'search') expect(searchForm?.resetFields).toHaveBeenCalledTimes(1) expect(getRoot().querySelector('.info-drawer-stub')).toBeNull() }) it('点击保存模板后会打开模板弹窗,并调用保存接口提交当前条件', async () => { enableFakeTimers() setupServerApi() const { infoSearchRef } = await renderInfoSearch() await infoSearchRef.value.showDrawer(createFieldGroup('EQUAL', ['saved'])) await nextTick() const searchForm = formInstances.find(item => item.config.ref === 'search') searchForm!.modelData.templateName = '模板A' await click('[data-label="saveTemplate"]') expect(getRoot().querySelector('.info-dialog-stub')).not.toBeNull() const templateForm = formInstances.find(item => item.config.ref === 'template') expect(templateForm?.modelData.name).toBe('模板A') await click('.dialog-confirm') const saveCall = serverApiMock.mock.calls.find(call => call[0].interface === 'saveCondition') expect(saveCall?.[0].params.name).toBe('模板A') expect(saveCall?.[0].params.routeName).toBe(location.pathname) const savedFieldGroup = JSON.parse(saveCall?.[0].params.fieldGroup || '{}') expect(savedFieldGroup.key).toBe('group_root') expect(savedFieldGroup.type).toBe('FieldGroup') expect(savedFieldGroup.group).toBe(true) expect(savedFieldGroup.andOr).toBe('AND') expect(savedFieldGroup.fields).toHaveLength(1) expect(savedFieldGroup.fields[0]).toEqual(expect.objectContaining({ type: 'Field', group: false, andOr: 'AND', name: 'name', oper: 'EQUAL', value: ['saved'] })) expect(serverApiMock.mock.calls.filter(call => call[0].interface === 'getConditionByRouteName')).toHaveLength(2) expect(getRoot().querySelector('.info-dialog-stub')).toBeNull() }) })