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: '', content: '
内容
', }, }) 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: '', content: '
内容
', }, }) 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: '', content: '
内容
', }, }) 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: '
内容
', }, }) 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: '', content: '
内容
', }, }) 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: '', content: '
内容
', }, }) 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: `
{{ userClicks }}
`, } 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: ` `, } 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: '', content: '
内容
', }, }) 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: '', content: '
内容
', }, }) 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() }) })