208 lines
7.3 KiB
JavaScript
208 lines
7.3 KiB
JavaScript
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()
|
||
})
|
||
})
|