import { App, ComponentPublicInstance, createApp, h, nextTick, reactive, ref } from 'vue' import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' let InfoOperationPanel: any interface RenderResult { app: App el: HTMLDivElement component: ComponentPublicInstance | null events: { removed: ReturnType } propsState: Record } const renderedApps: RenderResult[] = [] beforeAll(async () => { InfoOperationPanel = (await import('../../../components/infoOperationPanel/index.vue')).default }) const createI18n = () => { const map: Record = { searchConditions: '搜索条件', nameLabel: '名称', createTimeLabel: '创建时间', conditions_LIKE_alias: '包含', conditions_EQUAL_label: '等于' } return (key: string) => map[key] || key } const renderComponent = async (initialProps: Record = {}, slots: Record any> = {}): Promise => { const events = { removed: vi.fn() } const componentRef = ref(null) const propsState = reactive({ height: '60px', showBoxShow: true, fieldGroup: { fields: [] }, columns: [], columnI18n: true, ...initialProps }) const el = document.createElement('div') document.body.appendChild(el) const app = createApp({ setup () { return () => h(InfoOperationPanel, { ...propsState, ref: componentRef, onRemoved: events.removed }, slots) } }) const t = createI18n() window.$t = t app.config.globalProperties.$t = t app.mount(el) await nextTick() const result = { app, el, component: componentRef.value, events, propsState } renderedApps.push(result) return result } const getRoot = () => document.body const getOperationPanel = () => { const panel = getRoot().querySelector('#operationPanel') as HTMLElement | null expect(panel).not.toBeNull() return panel! } const getCloseIcons = () => Array.from(getRoot().querySelectorAll('.el-icon-close.delete')) as HTMLElement[] const getDeleteIcon = () => { const icon = getRoot().querySelector('.el-icon-delete.delete') as HTMLElement | null expect(icon).not.toBeNull() return icon! } afterEach(() => { while (renderedApps.length) { const current = renderedApps.pop()! current.app.unmount() current.el.remove() } document.body.innerHTML = '' }) describe('components/infoOperationPanel', () => { it('渲染默认容器、样式和左右插槽内容', async () => { await renderComponent({ height: '80px', showBoxShow: false }, { left: () => h('div', { class: 'slot-left' }, '左侧操作区'), right: () => h('button', { class: 'slot-right' }, '右侧按钮') }) const panel = getOperationPanel() const operationMain = getRoot().querySelector('.operation-main') as HTMLElement | null expect(panel.classList.contains('boxshow-style')).toBe(false) expect(operationMain?.style.height).toBe('80px') expect(getRoot().querySelector('.slot-left')?.textContent).toBe('左侧操作区') expect(getRoot().querySelector('.slot-right')?.textContent).toBe('右侧按钮') expect(getRoot().querySelector('.field-group')).toBeNull() }) it('根据 fieldGroup 渲染搜索条件摘要,并在删除单个条件后同步派发 removed 事件', async () => { const rendered = await renderComponent({ columns: [ { prop: 'name', label: 'nameLabel' } ] }) rendered.propsState.fieldGroup = { fields: [ { type: 'Field', name: 'name', oper: 'LIKE', value: '%测试%', andOr: 'AND' } ] } await nextTick() expect(getRoot().querySelector('.field-group')?.textContent).toContain('搜索条件') expect(getRoot().querySelector('.field-group')?.textContent).toContain('名称 包含 测试') expect(getCloseIcons()).toHaveLength(1) getCloseIcons()[0].click() await nextTick() expect(rendered.events.removed).toHaveBeenCalledTimes(1) const [updatedGroup, removedList] = rendered.events.removed.mock.calls[0] expect(updatedGroup.fields).toEqual([]) expect(removedList).toEqual([ expect.objectContaining({ level: 1, name: 'name', key: 'name', andOr: '并且', text: '名称 包含 测试' }) ]) expect(getRoot().querySelector('.field-group')).toBeNull() }) it('点击清空按钮时仅保留非 columns 中的字段,并返回被移除的第一层字段', async () => { const rendered = await renderComponent({ columns: [ { prop: 'name', label: 'nameLabel' } ] }) rendered.propsState.fieldGroup = { fields: [ { type: 'Field', name: 'name', oper: 'EQUAL', value: '张三', andOr: 'AND' }, { type: 'Field', name: 'otherField', oper: 'EQUAL', value: '保留项', andOr: 'AND' } ] } await nextTick() expect(getRoot().querySelector('.field-group')?.textContent).toContain('名称 等于 张三') getDeleteIcon().click() await nextTick() expect(rendered.events.removed).toHaveBeenCalledTimes(1) const [finalGroup, removedList] = rendered.events.removed.mock.calls[0] expect(finalGroup.fields).toEqual([ expect.objectContaining({ name: 'otherField', value: '保留项' }) ]) expect(removedList).toEqual([ expect.objectContaining({ name: 'name', value: '张三' }) ]) expect(getRoot().querySelector('.field-group')).toBeNull() }) it('渲染嵌套 FieldGroup 括号,并在删除嵌套唯一字段后清理空分组', async () => { const rendered = await renderComponent({ columns: [ { prop: 'name', label: 'nameLabel' }, { prop: 'createTime', label: 'createTimeLabel' } ] }) rendered.propsState.fieldGroup = { fields: [ { type: 'Field', name: 'name', oper: 'EQUAL', value: '张三', andOr: 'AND' }, { type: 'FieldGroup', andOr: 'OR', fields: [ { type: 'Field', name: 'createTime', oper: 'EQUAL', value: '2026-03-16', andOr: 'AND' } ] } ] } await nextTick() const fieldGroupText = getRoot().querySelector('.field-group')?.textContent || '' expect(fieldGroupText).toContain('名称 等于 张三') expect(fieldGroupText).toContain('或者') expect(fieldGroupText).toContain('(') expect(fieldGroupText).toContain('创建时间 等于 2026-03-16') expect(fieldGroupText).toContain(')') expect(getCloseIcons()).toHaveLength(2) getCloseIcons()[1].click() await nextTick() expect(rendered.events.removed).toHaveBeenCalledTimes(1) const [updatedGroup, removedList] = rendered.events.removed.mock.calls[0] expect(updatedGroup.fields).toEqual([ expect.objectContaining({ type: 'Field', name: 'name', value: '张三' }) ]) expect(updatedGroup.fields).toHaveLength(1) expect(removedList).toEqual([]) const updatedText = getRoot().querySelector('.field-group')?.textContent || '' expect(updatedText).toContain('名称 等于 张三') expect(updatedText).not.toContain('创建时间 等于 2026-03-16') expect(updatedText).not.toContain('或者') expect(updatedText).not.toContain('(') expect(updatedText).not.toContain(')') expect(getCloseIcons()).toHaveLength(1) }) })