20260914223013

This commit is contained in:
oneao committed 2026-09-14 22:30:13 +08:00
1 parent 549c8bd2eb
commit 63514476d7
60 files changed
+6478 -2004

No files matched your search

@@ -0,0 +1,108 @@
/**
* 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()
})
})
@@ -0,0 +1,92 @@
/**
* Space 间距组件:容器方向 / 间距 gap / 对齐 / 换行 / 分隔符 / 空子节点。
* 只断言渲染出的 class、内联 gap 与包裹结构,不涉及真实布局尺寸。
*/
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Space from '@/components/ui/space/space.vue'
/** 便捷挂载:默认塞两个子元素 */
function mountSpace(props = {}, slots = {}) {
return mount(Space, {
props,
slots: { default: '<span>A</span><span>B</span>', ...slots },
})
}
describe('Space 组件', () => {
it('默认水平:容器带方向类,每个子元素包一层 .space-item', () => {
const wrapper = mountSpace()
expect(wrapper.classes()).toContain('space')
expect(wrapper.classes()).toContain('space-horizontal')
expect(wrapper.findAll('.space-item')).toHaveLength(2)
expect(wrapper.attributes('data-slot')).toBe('space')
})
it('水平默认对齐 center;显式 align 覆盖', () => {
expect(mountSpace().classes()).toContain('space-align-center')
expect(mountSpace({ align: 'start' }).classes()).toContain('space-align-start')
expect(mountSpace({ align: 'baseline' }).classes()).toContain('space-align-baseline')
})
it('size 预设映射为对应 px 的 gap', () => {
const style = mountSpace({ size: 'middle' }).attributes('style')
expect(style).toContain('column-gap: 16px')
expect(style).toContain('row-gap: 16px')
})
it('size 数字按 px 处理', () => {
const style = mountSpace({ size: 20 }).attributes('style')
expect(style).toContain('column-gap: 20px')
expect(style).toContain('row-gap: 20px')
})
it('size 数组 [水平, 垂直] 分别设 column-gap / row-gap', () => {
const style = mountSpace({ size: [12, 24] }).attributes('style')
expect(style).toContain('column-gap: 12px')
expect(style).toContain('row-gap: 24px')
})
it('vertical 简写切换为纵向且不设默认对齐', () => {
const wrapper = mountSpace({ vertical: true })
expect(wrapper.classes()).toContain('space-vertical')
expect(wrapper.classes()).not.toContain('space-align-center')
})
it('orientation 优先于 vertical', () => {
const wrapper = mountSpace({ orientation: 'horizontal', vertical: true })
expect(wrapper.classes()).toContain('space-horizontal')
expect(wrapper.classes()).not.toContain('space-vertical')
})
it('wrap 仅水平方向生效', () => {
expect(mountSpace({ wrap: true }).classes()).toContain('space-wrap')
expect(mountSpace({ wrap: true, vertical: true }).classes()).not.toContain('space-wrap')
})
it('separator 插槽:分隔符数量 = 子元素数 - 1,末尾不加', () => {
const wrapper = mountSpace(
{},
{ default: '<span>A</span><span>B</span><span>C</span>', separator: '|' },
)
expect(wrapper.findAll('.space-item')).toHaveLength(3)
expect(wrapper.findAll('.space-separator')).toHaveLength(2)
expect(wrapper.text()).toContain('|')
})
it('无 separator 插槽时不渲染分隔符', () => {
expect(mountSpace().findAll('.space-separator')).toHaveLength(0)
})
it('无有效子元素时不渲染容器', () => {
const wrapper = mount(Space, { slots: { default: '' } })
expect(wrapper.find('.space').exists()).toBe(false)
})
it('过滤子元素之间的空白文本节点', () => {
const wrapper = mount(Space, {
slots: { default: '<span>A</span>\n <span>B</span>\n' },
})
expect(wrapper.findAll('.space-item')).toHaveLength(2)
})
})