Files
workspace/code/fms/fms-vue/tests/components/popover.spec.js
T
2026-09-13 23:16:08 +08:00

208 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick, ref } from 'vue'
import Popover from '@/components/ui/popover/popover.vue'
import Button from '@/components/ui/button/button.vue'
/** 查询 Teleport 到 body 的浮层(Transition + v-if 渲染) */
function bodyPopover() {
return document.body.querySelector('.popover')
}
async function flush() {
await nextTick()
}
describe('Popover 触发器包裹', () => {
it('默认(unwrapped=false)渲染 span.popover-trigger 包裹层', () => {
const wrapper = mount(Popover, {
props: { trigger: 'click' },
slots: {
trigger: '<button class="t-btn">触发</button>',
content: '<div class="t-content">内容</div>',
},
})
expect(wrapper.find('span.popover-trigger').exists()).toBe(true)
expect(wrapper.find('button.t-btn').exists()).toBe(true)
wrapper.unmount()
})
it('unwrapped 单根元素:不渲染包裹层 span,仅保留用户触发器', () => {
const wrapper = mount(Popover, {
props: { trigger: 'click', unwrapped: true },
slots: {
trigger: '<button class="t-btn">触发</button>',
content: '<div class="t-content">内容</div>',
},
})
expect(wrapper.find('span.popover-trigger').exists()).toBe(false)
expect(wrapper.find('button.t-btn').exists()).toBe(true)
wrapper.unmount()
})
it('unwrapped 多根 fragment:自动回退到 span.popover-trigger 包裹层', () => {
const wrapper = mount(Popover, {
props: { trigger: 'click', unwrapped: true },
slots: {
trigger: '<button>A</button><button>B</button>',
content: '<div class="t-content">内容</div>',
},
})
expect(wrapper.find('span.popover-trigger').exists()).toBe(true)
expect(wrapper.findAll('span.popover-trigger > button').length).toBe(2)
wrapper.unmount()
})
it('unwrapped 文本节点:回退到包裹层', () => {
const wrapper = mount(Popover, {
props: { trigger: 'click', unwrapped: true },
slots: {
trigger: '纯文本触发器',
content: '<div class="t-content">内容</div>',
},
})
expect(wrapper.find('span.popover-trigger').exists()).toBe(true)
wrapper.unmount()
})
})
describe('Popover 零包裹交互', () => {
it('unwrapped 点击触发器可开合浮层(事件注入到用户 vnode)', async () => {
const wrapper = mount(Popover, {
props: { trigger: 'click', unwrapped: true },
slots: {
trigger: '<button class="t-btn">触发</button>',
content: '<div class="t-content">内容</div>',
},
})
expect(bodyPopover()).toBeNull()
await wrapper.find('button.t-btn').trigger('click')
await flush()
expect(bodyPopover()).not.toBeNull()
await wrapper.find('button.t-btn').trigger('click')
await flush()
expect(bodyPopover()).toBeNull()
wrapper.unmount()
})
it('unwrapped 浮层有 left/top 定位(证明锚点指令真正挂载:vnode.dirs 字段名)', async () => {
// 回归测试:曾因把指令注入到 vnode.directives 字段(应为 vnode.dirs),
// 导致锚点指令静默失效、triggerEl 永远 null、updatePosition 提前 return、
// 浮层无定位在 body 末尾不可见。jsdom 矩形=0,updatePosition 期望写出 0px。
const wrapper = mount(Popover, {
props: { trigger: 'click', unwrapped: true },
slots: {
trigger: '<button class="t-btn">触发</button>',
content: '<div class="t-content">内容</div>',
},
})
await wrapper.find('button.t-btn').trigger('click')
await flush()
const float = bodyPopover()
expect(float).not.toBeNull()
// 锚点指令必须已挂载并同步 triggerEl,updatePosition 才能写 left/top;
// 若 vnode.dirs 拼成 directives(被静默忽略),style.left 保持空串。
// jsdom 矩形=0,具体值取决于 computePosition 的 shift 防溢出,仅断言非空即可。
expect(float.style.left).not.toBe('')
expect(float.style.top).not.toBe('')
wrapper.unmount()
})
it('事件合并:用户 @click 与内部 toggle 均触发,不互相覆盖', async () => {
const TestHost = {
components: { Popover },
setup() {
const userClicks = ref(0)
return { userClicks }
},
template: `
<div>
<Popover trigger="click" unwrapped>
<template #trigger>
<button class="t-btn" @click="userClicks++">触发</button>
</template>
<template #content><div class="t-content">内容</div></template>
</Popover>
<span class="t-counter">{{ userClicks }}</span>
</div>
`,
}
const wrapper = mount(TestHost)
await wrapper.find('button.t-btn').trigger('click')
await flush()
// 用户 handler 先执行
expect(wrapper.find('.t-counter').text()).toBe('1')
// 内部 toggle 后执行 → 浮层打开
expect(bodyPopover()).not.toBeNull()
wrapper.unmount()
})
it('组件作触发器:事件注入到组件根元素,点击可开合', async () => {
const TestHost = {
components: { Popover, Button },
template: `
<Popover trigger="click" unwrapped>
<template #trigger><Button>点击</Button></template>
<template #content><div class="t-content">内容</div></template>
</Popover>
`,
}
const wrapper = mount(TestHost)
const btn = wrapper.find('button')
expect(btn.exists()).toBe(true)
await btn.trigger('click')
await flush()
expect(bodyPopover()).not.toBeNull()
wrapper.unmount()
})
})
describe('Popover 浮层宽度跟随触发器', () => {
/** jsdom 里 offsetWidth 恒为 0,覆写成真实宽度才能断言下发值 */
function stubTriggerWidth(wrapper, width) {
const trigger = wrapper.find('span.popover-trigger').element
Object.defineProperty(trigger, 'offsetWidth', { value: width, configurable: true })
}
it('开启时触发器宽作为 min-width 下发,而非写死 width(内容更宽仍可撑开)', async () => {
const wrapper = mount(Popover, {
props: { trigger: 'click', followTriggerWidth: true },
slots: {
trigger: '<button class="t-btn">触发</button>',
content: '<div class="t-content">内容</div>',
},
})
stubTriggerWidth(wrapper, 240)
await wrapper.find('button.t-btn').trigger('click')
await flush()
const float = bodyPopover()
expect(float.style.minWidth).toBe('240px')
// 关键:不写死宽度,浮层才能被更宽的内容撑开;内容靠 CSS 铺满浮层
expect(float.style.width).toBe('')
expect(float.classList.contains('popover-follow-trigger-width')).toBe(true)
wrapper.unmount()
})
it('未开启时不下发任何宽度约束(完全由内容撑开)', async () => {
const wrapper = mount(Popover, {
props: { trigger: 'click' },
slots: {
trigger: '<button class="t-btn">触发</button>',
content: '<div class="t-content">内容</div>',
},
})
stubTriggerWidth(wrapper, 240)
await wrapper.find('button.t-btn').trigger('click')
await flush()
const float = bodyPopover()
expect(float.style.minWidth).toBe('')
expect(float.style.width).toBe('')
expect(float.classList.contains('popover-follow-trigger-width')).toBe(false)
wrapper.unmount()
})
})