import { App, ComponentPublicInstance, createApp, defineComponent, h, nextTick, ref } from 'vue' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' const ElInputStub = defineComponent({ name: 'ElInput', props: { modelValue: { type: String, default: '' }, placeholder: { type: String, default: '' } }, emits: ['update:modelValue', 'input', 'keyup', 'keyupEnter'], setup (props, { emit }) { return () => h('input', { value: props.modelValue, placeholder: props.placeholder, onInput: (event: Event) => { const value = (event.target as HTMLInputElement).value emit('update:modelValue', value) emit('input', value) }, onKeyup: (event: KeyboardEvent) => { emit('keyup', event) if (event.key === 'Enter') { emit('keyupEnter', event) } } }) } }) vi.mock('element-plus', () => ({ ElInput: ElInputStub })) vi.mock('@element-plus/icons-vue', () => ({ Search: { name: 'Search' } })) vi.mock('../../../script/util', () => ({ getFieldValue: (obj: Record, filed: string) => { return filed.split('.').reduce((current, key) => current?.[key], obj) } })) let FuzzyMatching: any interface RenderResult { app: App el: HTMLDivElement component: any events: { getfuzzyMatching: ReturnType } } const renderedApps: RenderResult[] = [] beforeAll(async () => { FuzzyMatching = (await import('../../../components/fuzzyMatching/fuzzyMatching.vue')).default }) const renderComponent = async (props: Record = {}): Promise => { const events = { getfuzzyMatching: vi.fn() } const componentRef = ref(null) const el = document.createElement('div') document.body.appendChild(el) const app = createApp({ setup () { return () => h(FuzzyMatching, { ...props, ref: componentRef, onGetfuzzyMatching: events.getfuzzyMatching }) } }) app.config.globalProperties.$t = (key: string) => { if (key === 'pleaseInput') { return '请输入内容' } return key } app.mount(el) await nextTick() const result = { app, el, component: componentRef.value, events } renderedApps.push(result) return result } const getInput = () => { const input = document.body.querySelector('input') as HTMLInputElement | null expect(input).not.toBeNull() return input! } const setComponentKeyword = async (rendered: RenderResult, value: string) => { rendered.component.keyword = value await nextTick() } const emitInput = async (value: string) => { const input = getInput() input.value = value input.dispatchEvent(new Event('input', { bubbles: true })) await nextTick() } const pressEnter = async () => { getInput().dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', bubbles: true })) await nextTick() } describe('components/fuzzyMatching', () => { const vueWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) beforeEach(() => { vi.useFakeTimers() }) afterEach(() => { while (renderedApps.length) { const current = renderedApps.pop()! current.app.unmount() current.el.remove() } document.body.innerHTML = '' vi.clearAllMocks() vi.useRealTimers() }) afterAll(() => { vueWarnSpy.mockRestore() }) it('渲染输入框并使用默认 props', async () => { const rendered = await renderComponent() expect(getInput().getAttribute('placeholder')).toBe('请输入内容') expect(rendered.component.keyword).toBe('') expect(rendered.component.filterResult()).toEqual([]) }) it('默认使用 name 字段进行过滤,并在输入后派发过滤结果和关键字', async () => { const totalData = [ { name: 'search term' }, { name: 'search22' }, { name: 'other value' } ] const rendered = await renderComponent({ totalData }) await setComponentKeyword(rendered, 'search') rendered.component.fuzzyMatching() vi.advanceTimersByTime(499) expect(rendered.events.getfuzzyMatching).not.toHaveBeenCalled() vi.advanceTimersByTime(1) expect(rendered.events.getfuzzyMatching).toHaveBeenCalledTimes(1) expect(rendered.events.getfuzzyMatching).toHaveBeenCalledWith( [totalData[0], totalData[1]], 'search' ) }) it('支持通过 field 配置多个字段过滤', async () => { const totalData = [ { name: 'alpha', code: 'A-001' }, { name: 'beta', code: 'B-002' }, { name: 'gamma', code: 'A-003' } ] const rendered = await renderComponent({ totalData, field: 'name,code' }) await setComponentKeyword(rendered, 'A-') expect(rendered.component.filterResult()).toEqual([totalData[0], totalData[2]]) }) it('支持通过嵌套字段过滤数据', async () => { const totalData = [ { meta: { label: '可匹配项' }, id: 1 }, { meta: { label: '其他项' }, id: 2 } ] const rendered = await renderComponent({ totalData, field: 'meta.label' }) await setComponentKeyword(rendered, '可匹配') expect(rendered.component.filterResult()).toEqual([totalData[0]]) }) it('keyup.enter 会触发同样的模糊匹配逻辑', async () => { const totalData = [ { name: 'enter-target' }, { name: 'other' } ] const rendered = await renderComponent({ totalData }) await emitInput('enter-target') await pressEnter() vi.runOnlyPendingTimers() expect(rendered.events.getfuzzyMatching).toHaveBeenCalledTimes(1) expect(rendered.events.getfuzzyMatching).toHaveBeenLastCalledWith( [totalData[0]], 'enter-target' ) }) it('连续输入时只保留最后一次防抖结果', async () => { const totalData = [ { name: 'search term' }, { name: 'final value' } ] const rendered = await renderComponent({ totalData }) await setComponentKeyword(rendered, 'search') rendered.component.fuzzyMatching() vi.advanceTimersByTime(300) await setComponentKeyword(rendered, 'final') rendered.component.fuzzyMatching() vi.advanceTimersByTime(499) expect(rendered.events.getfuzzyMatching).not.toHaveBeenCalled() vi.advanceTimersByTime(1) expect(rendered.events.getfuzzyMatching).toHaveBeenCalledTimes(1) expect(rendered.events.getfuzzyMatching).toHaveBeenCalledWith( [totalData[1]], 'final' ) }) })