/** * Modal.confirm 命令式确认对话框:开关时机与 onOk / onCancel 语义。 * 覆盖「确定后关闭」「onOk reject 保持打开」「取消」「点遮罩静默关闭(不触发 onCancel)」 * 「destroy 立即卸载」几种路径。 */ import { afterEach, describe, expect, it, vi } from 'vitest' import { flushPromises } from '@vue/test-utils' import { nextTick } from 'vue' import { confirm } from '@/components/ui/modal/modal-confirm.js' /** Modal 用 Teleport 挂到 body,只能从 document 上找面板 */ const panel = () => document.querySelector('.fms-modal') /** footer 按钮顺序:[取消, 确定] */ const footerButtons = () => [...document.querySelectorAll('.fms-modal-footer button')] /** * 关窗要等 Transition 的两帧 rAF 与 after-leave,时序不确定: * 轮询到条件成立或超时为止。 */ async function waitFor(predicate, timeout = 500) { const start = Date.now() while (Date.now() - start < timeout) { await flushPromises() await nextTick() if (predicate()) return true await new Promise((resolve) => setTimeout(resolve, 20)) } return false } let active = null afterEach(async () => { active?.destroy() active = null await waitFor(() => !panel()) }) describe('Modal.confirm', () => { it('确定:onOk resolve 后对话框关闭', async () => { const onOk = vi.fn() active = confirm({ title: '确认', content: '删除后不可恢复', onOk }) expect(panel()).toBeTruthy() expect(document.body.textContent).toContain('删除后不可恢复') footerButtons().at(-1).click() expect(await waitFor(() => !panel())).toBe(true) expect(onOk).toHaveBeenCalledTimes(1) }) it('确定:onOk reject 时保持打开,等待重试或取消', async () => { const onOk = vi.fn(() => Promise.reject(new Error('删除失败'))) active = confirm({ title: '确认', content: '内容', onOk }) footerButtons().at(-1).click() // 等一段确定的时间,确认对话框没有进入关闭流程 await new Promise((resolve) => setTimeout(resolve, 100)) await flushPromises() expect(onOk).toHaveBeenCalledTimes(1) expect(panel()).toBeTruthy() }) it('取消:onCancel 被调用并关闭', async () => { const onCancel = vi.fn() active = confirm({ title: '确认', content: '内容', onCancel }) footerButtons()[0].click() expect(await waitFor(() => !panel())).toBe(true) expect(onCancel).toHaveBeenCalledTimes(1) }) it('点遮罩空白:关闭对话框但不触发 onCancel(静默关闭)', async () => { const onCancel = vi.fn() active = confirm({ title: '确认', content: '内容', onCancel }) const wrap = document.querySelector('.fms-modal-wrap') expect(wrap).toBeTruthy() // 点击 wrap 自身(面板外空白区域):target === currentTarget 才命中遮罩关闭分支。 // 遮罩关闭走 Modal.requestClose → emit('cancel'),但 confirm 未绑 @cancel, // 故只由 v-model:open 关窗,不应触发调用方 onCancel。 wrap.click() expect(await waitFor(() => !panel())).toBe(true) expect(onCancel).not.toHaveBeenCalled() }) it('okButtonProps.danger 映射为危险态确定按钮', () => { active = confirm({ title: '确认', content: '内容', okButtonProps: { danger: true } }) expect(footerButtons().at(-1).className).toContain('button-danger') }) it('destroy 立即卸载容器与遮罩', async () => { active = confirm({ title: '确认', content: '内容' }) expect(panel()).toBeTruthy() active.destroy() active = null await waitFor(() => !panel()) expect(panel()).toBeFalsy() expect(document.querySelector('.fms-modal-wrap')).toBeFalsy() }) })