import { App, createApp, defineComponent, h, nextTick } from 'vue' import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' const ElDescriptionsStub = defineComponent({ name: 'ElDescriptions', props: { title: { type: String, default: '' }, column: { type: Number, default: 1 }, direction: { type: String, default: 'horizontal' } }, setup (props, { slots }) { return () => h('section', { class: 'el-descriptions', 'data-column': String(props.column), 'data-direction': props.direction }, [ h('header', { class: 'el-descriptions__header' }, [ h('div', { class: 'el-descriptions__title' }, slots.title ? slots.title() : props.title), h('div', { class: 'el-descriptions__extra' }, slots.extra?.()) ]), h('div', { class: 'el-descriptions__body' }, slots.default?.()) ]) } }) const ElDescriptionsItemStub = defineComponent({ name: 'ElDescriptionsItem', props: { label: { type: String, default: '' }, span: { type: Number, default: 1 }, className: { type: String, default: '' }, labelClassName: { type: String, default: '' } }, setup (props, { slots }) { return () => h('div', { class: 'el-descriptions-item', 'data-span': String(props.span) }, [ h('div', { class: ['el-descriptions-item__label', props.labelClassName].filter(Boolean) }, props.label), h('div', { class: ['el-descriptions-item__content', props.className].filter(Boolean) }, slots.default?.()) ]) } }) const ElTooltipStub = defineComponent({ name: 'ElTooltip', props: { content: { type: String, default: '' } }, setup (props, { slots }) { return () => h('div', { class: 'el-tooltip', 'data-content': props.content }, slots.default?.()) } }) const ElButtonStub = defineComponent({ name: 'ElButton', props: { type: { type: String, default: '' } }, emits: ['click'], setup (props, { slots, emit }) { return () => h('button', { class: ['el-button', props.type ? `el-button--${props.type}` : ''].filter(Boolean), onClick: () => emit('click') }, slots.default?.()) } }) const ElDividerStub = defineComponent({ name: 'ElDivider', setup (_, { slots }) { return () => h('div', { class: 'el-divider' }, [ h('span', { class: 'el-divider__text' }, slots.default?.()) ]) } }) vi.mock('element-plus', () => ({ ElDescriptions: ElDescriptionsStub, ElDescriptionsItem: ElDescriptionsItemStub, ElTooltip: ElTooltipStub, ElButton: ElButtonStub, ElDivider: ElDividerStub })) let ListCard: any interface RenderResult { app: App el: HTMLDivElement } const renderedApps: RenderResult[] = [] const today = '2026-03-16' beforeAll(async () => { ListCard = (await import('../../../components/listCard/listCard.vue')).default }) const createProps = (overrides: Record = {}) => { const baseProps = { data: { index: 0, name: '测试', time: today, nested: { label: '嵌套值' } }, config: { map: { index: '序号', name: '名称', time: '时间' }, title: '测试ListCard' }, attr: { column: 1, direction: 'horizontal' }, itemAttr: { operation: { name: { type: 'primary', text: '修改名称', method: vi.fn() } } } } return { ...baseProps, ...overrides, data: { ...baseProps.data, ...overrides.data }, config: { ...baseProps.config, ...overrides.config, map: overrides.config?.map || baseProps.config.map }, attr: { ...baseProps.attr, ...overrides.attr }, itemAttr: { ...baseProps.itemAttr, ...overrides.itemAttr, operation: overrides.itemAttr?.operation === undefined ? baseProps.itemAttr.operation : overrides.itemAttr.operation } } } const renderListCard = async ( props: Record = {}, slots: Record any> = {} ) => { const el = document.createElement('div') document.body.appendChild(el) const app = createApp({ render () { return h(ListCard, createProps(props), slots) } }) app.mount(el) await nextTick() const result = { app, el } 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() } afterEach(() => { while (renderedApps.length) { const current = renderedApps.pop()! current.app.unmount() current.el.remove() } document.body.innerHTML = '' }) describe('components/listCard', () => { it('渲染标题和基础字段内容', async () => { await renderListCard() expect(getRoot().querySelector('.list-card')).not.toBeNull() expect(getRoot().querySelector('.el-descriptions__title')?.textContent).toContain('测试ListCard') const labels = Array.from(getRoot().querySelectorAll('.el-descriptions-item__label')).map(item => item.textContent) const contents = Array.from(getRoot().querySelectorAll('.el-descriptions-item__content')).map(item => item.textContent) expect(labels).toEqual(['序号', '名称', '时间']) expect(contents.join('|')).toContain('0') expect(contents.join('|')).toContain('测试') expect(contents.join('|')).toContain(today) }) it('渲染 extra 插槽和具名字段插槽', async () => { await renderListCard({ config: { map: { index: '序号', name: '名称', time: '时间' }, title: '测试ListCard', slots: ['name'] } }, { extra: () => h('div', { class: 'extra-slot' }, '操作区'), name: () => h('div', { class: 'name-slot' }, '自定义名称内容') }) expect(getRoot().querySelector('.extra-slot')?.textContent).toBe('操作区') expect(getRoot().querySelector('.name-slot')?.textContent).toBe('自定义名称内容') const contents = Array.from(getRoot().querySelectorAll('.el-descriptions-item__content')).map(item => item.textContent) expect(contents).toContain('自定义名称内容') expect(contents).not.toContain('测试修改名称') }) it('存在 divider 配置时拆分分组并渲染分组标题', async () => { await renderListCard({ config: { map: { index: '序号', divider1: '基础信息', name: '名称', time: '时间' }, title: '测试ListCard' } }) expect(getRoot().querySelectorAll('.el-descriptions')).toHaveLength(2) expect(getRoot().querySelector('.el-divider__text')?.textContent).toBe('基础信息') }) it('tips 命中时走 tooltip 分支并支持 attr.split 读取嵌套字段', async () => { await renderListCard({ config: { map: { 'nested.label': '嵌套名称' }, title: '测试ListCard', tips: ['nested.label'] }, attr: { column: 1, direction: 'horizontal', split: 1 }, itemAttr: {} }) const tooltip = getRoot().querySelector('.el-tooltip') as HTMLElement | null expect(tooltip).not.toBeNull() expect(tooltip?.getAttribute('data-content')).toBe('嵌套值') expect(getRoot().querySelector('.tip-text')?.textContent).toBe('嵌套值') expect(getRoot().querySelector('.el-descriptions-item__label')?.textContent).toBe('嵌套名称') }) it('默认显示 operation 按钮并在点击时调用 method', async () => { const method = vi.fn() await renderListCard({ itemAttr: { operation: { name: { type: 'primary', text: '修改名称', method } } } }) const button = getRoot().querySelector('.el-button--primary') as HTMLButtonElement | null expect(button).not.toBeNull() expect(button?.textContent).toContain('修改名称') await click('.el-button--primary') expect(method).toHaveBeenCalledTimes(1) }) it('operation.show 返回 false 时隐藏按钮,并透传 className、labelClassName 与 span', async () => { await renderListCard({ itemAttr: { className: 'custom-content-class', labelClassName: 'custom-label-class', span: { name: 2 }, operation: { name: { type: 'primary', text: '修改名称', show: () => false, method: vi.fn() } } } }) const labels = Array.from(getRoot().querySelectorAll('.el-descriptions-item__label')) const contents = Array.from(getRoot().querySelectorAll('.el-descriptions-item__content')) const nameItem = Array.from(getRoot().querySelectorAll('.el-descriptions-item')).find(item => item.textContent?.includes('测试')) as HTMLElement | undefined expect(getRoot().querySelector('.el-button')).toBeNull() expect(labels.some(item => item.classList.contains('custom-label-class'))).toBe(true) expect(contents.some(item => item.classList.contains('custom-content-class'))).toBe(true) expect(nameItem?.getAttribute('data-span')).toBe('2') }) })