import { App, createApp, defineComponent, h, nextTick, reactive } from 'vue' import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest' const ElButtonStub = defineComponent({ name: 'ElButton', emits: ['click'], inheritAttrs: false, setup (_, { slots, emit, attrs }) { return () => h('button', { class: ['el-button', attrs.type === 'primary' ? 'el-button--primary' : ''].filter(Boolean), disabled: attrs.disabled === true || attrs.loading === true, 'data-name': attrs.name, 'data-type': attrs.type, 'data-size': attrs.size, 'data-icon': attrs.icon, 'data-text': String(attrs.text), 'data-plain': String(attrs.plain), onClick: () => emit('click') }, slots.default?.()) } }) const ElDrawerStub = defineComponent({ name: 'ElDrawer', props: { modelValue: { type: Boolean, default: false }, title: { type: String, default: '' }, size: { type: String, default: '' }, appendToBody: { type: Boolean, default: undefined }, closeOnPressEscape: { type: Boolean, default: undefined }, closeOnClickModal: { type: Boolean, default: undefined }, customClass: { type: String, default: '' }, modal: { type: Boolean, default: undefined }, showClose: { type: Boolean, default: undefined }, direction: { type: String, default: '' }, modalAppendToBody: { type: Boolean, default: undefined }, destroyOnClose: { type: Boolean, default: undefined }, withHeader: { type: Boolean, default: true } }, emits: ['open', 'opened', 'close', 'closed'], mounted () { if (this.modelValue) { this.$emit('open') this.$emit('opened') } }, render () { if (!this.modelValue) { return null } return h('div', { class: ['el-drawer', this.customClass], 'aria-label': this.title, 'data-size': this.size, 'data-append-to-body': String(this.appendToBody), 'data-close-on-press-escape': String(this.closeOnPressEscape), 'data-close-on-click-modal': String(this.closeOnClickModal), 'data-modal': String(this.modal), 'data-show-close': String(this.showClose), 'data-direction': this.direction, 'data-modal-append-to-body': String(this.modalAppendToBody), 'data-destroy-on-close': String(this.destroyOnClose), 'data-with-header': String(this.withHeader) }, [ this.withHeader ? h('div', { class: 'el-drawer__header' }, this.title) : null, h('div', { class: 'el-drawer__body' }, this.$slots.default?.()), h('button', { class: 'drawer-close-trigger', onClick: () => this.$emit('close') }, 'close'), h('button', { class: 'drawer-closed-trigger', onClick: () => this.$emit('closed') }, 'closed') ]) } }) vi.mock('element-plus', () => ({ ElDrawer: ElDrawerStub, ElButton: ElButtonStub })) let InfoDrawer: any interface RenderResult { app: App el: HTMLDivElement operates: Record events: { openDrawer: ReturnType openedDrawer: ReturnType closeDrawer: ReturnType confirmDrawer: ReturnType closedDrawer: ReturnType } } const renderedApps: RenderResult[] = [] beforeAll(async () => { InfoDrawer = (await import('../../../components/infoDrawer/index.vue')).default }) const createOperates = (rawOperates: Record = {}) => reactive({ title: 'drawer.title', size: '40%', appendToBody: false, closeOnPressEscape: false, wrapperClosable: false, customClass: 'custom-extra', modal: false, showClose: false, direction: 'ltr', modalAppendToBody: false, destroyOnClose: true, withHeader: false, isShowBtn: true, operation: [], buttonRound: true, tableButtonType: 'primary', text: true, ...rawOperates }) const renderDrawer = async (options: { isShowDrawer?: boolean operates?: Record slotText?: string } = {}): Promise => { const events = { openDrawer: vi.fn(), openedDrawer: vi.fn(), closeDrawer: vi.fn(), confirmDrawer: vi.fn(), closedDrawer: vi.fn() } const el = document.createElement('div') document.body.appendChild(el) const operates = createOperates(options.operates) const isShowDrawer = options.isShowDrawer ?? true const app = createApp({ render () { return h(InfoDrawer, { operates, isShowDrawer, onOpenDrawer: events.openDrawer, onOpenedDrawer: events.openedDrawer, onCloseDrawer: events.closeDrawer, onConfirmDrawer: events.confirmDrawer, onClosedDrawer: events.closedDrawer }, { default: () => h('div', { class: 'slot-content' }, options.slotText || 'drawer slot content') }) } }) app.config.globalProperties.$t = (key: string) => key app.mount(el) await nextTick() const result = { app, el, operates, events } 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/infoDrawer', () => { it('根据 props 挂载抽屉并透传关键配置', async () => { const rendered = await renderDrawer() const drawer = getRoot().querySelector('.el-drawer') as HTMLElement | null expect(drawer).not.toBeNull() expect(drawer?.getAttribute('aria-label')).toBe('drawer.title') expect(drawer?.getAttribute('data-size')).toBe('40%') expect(drawer?.getAttribute('data-direction')).toBe('ltr') expect(drawer?.getAttribute('data-append-to-body')).toBe('true') expect(drawer?.getAttribute('data-close-on-press-escape')).toBe('false') expect(drawer?.getAttribute('data-close-on-click-modal')).toBe('false') expect(drawer?.getAttribute('data-modal')).toBe('false') expect(drawer?.getAttribute('data-show-close')).toBe('false') expect(drawer?.getAttribute('data-modal-append-to-body')).toBe('false') expect(drawer?.getAttribute('data-destroy-on-close')).toBe('true') expect(drawer?.getAttribute('data-with-header')).toBe('false') expect(drawer?.className).toContain('custom-drawer') expect(drawer?.className).toContain('custom-extra') expect(drawer?.className).toContain('has-footer') expect(rendered.events.openDrawer).toHaveBeenCalledTimes(1) expect(rendered.events.openedDrawer).toHaveBeenCalledTimes(1) }) it('isShowDrawer 为 false 时不渲染抽屉', async () => { await renderDrawer({ isShowDrawer: false }) expect(getRoot().querySelector('.el-drawer')).toBeNull() }) it('渲染默认插槽与默认底部按钮,并支持确认和关闭事件', async () => { const rendered = await renderDrawer() expect(getRoot().querySelector('.slot-content')?.textContent).toBe('drawer slot content') const footerButtons = Array.from(getRoot().querySelectorAll('.drawer-footer .el-button')) expect(footerButtons).toHaveLength(2) expect(footerButtons[0].textContent).toBe('cancel') expect(footerButtons[1].textContent).toBe('confirm') await click('.drawer-footer .el-button') expect(rendered.events.closeDrawer).toHaveBeenCalledTimes(1) await click('.drawer-footer .el-button--primary') expect(rendered.events.confirmDrawer).toHaveBeenCalledTimes(1) }) it('支持自定义 operation 按钮渲染及方法调用', async () => { const firstAction = vi.fn() const secondAction = vi.fn() await renderDrawer({ operates: { isShowBtn: false, operation: [ { label: () => 'action.one', method: firstAction, type: 'primary', text: true, size: 'small', icon: 'Edit', loading: () => false, disabled: () => false, plain: true }, { label: () => 'action.two', method: secondAction, type: 'default', text: false, size: 'large', icon: 'Delete', loading: () => true, disabled: () => true, plain: false } ] } }) const buttons = Array.from(getRoot().querySelectorAll('.drawer-footer .el-button')) as HTMLButtonElement[] expect(buttons).toHaveLength(2) expect(buttons[0].textContent).toBe('action.one') expect(buttons[0].getAttribute('data-type')).toBe('primary') expect(buttons[0].getAttribute('data-icon')).toBe('Edit') expect(buttons[0].getAttribute('data-plain')).toBe('true') expect(buttons[1].textContent).toBe('action.two') expect(buttons[1].disabled).toBe(true) await click('.drawer-footer .el-button') expect(firstAction).toHaveBeenCalledTimes(1) expect(secondAction).not.toHaveBeenCalled() }) it('无默认按钮且无 operation 时不追加 footer class 与按钮区域', async () => { await renderDrawer({ operates: { isShowBtn: false, operation: [], customClass: '' } }) const drawer = getRoot().querySelector('.el-drawer') as HTMLElement | null expect(drawer).not.toBeNull() expect(drawer?.className).toContain('custom-drawer') expect(drawer?.className).not.toContain('has-footer') expect(getRoot().querySelector('.drawer-footer')).toBeNull() }) it('响应 drawer close 与 closed 事件', async () => { const rendered = await renderDrawer() await click('.drawer-close-trigger') await click('.drawer-closed-trigger') expect(rendered.events.closeDrawer).toHaveBeenCalledTimes(1) expect(rendered.events.closedDrawer).toHaveBeenCalledTimes(1) }) })