// 自绘下拉回归:展开/选中/点别处关闭(替代原生 select)。
import { describe, it, expect, vi } from 'vitest'
import { createRoot } from 'react-dom/client'
import { act } from 'react-dom/test-utils'
import { Dropdown } from '../src/components/Dropdown'
const OPTIONS = [
{ value: '', label: '跟随全局主题' },
{ value: 'github', label: 'GitHub' },
{ value: 'notion', label: 'Notion' },
]
describe('Dropdown', () => {
it('展开列出选项且标出选中项;点击选项回调并关闭;点别处关闭', async () => {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
const onChange = vi.fn()
await act(async () => {
root.render()
})
const button = container.querySelector('.dropdown-button') as HTMLButtonElement
expect(button.textContent).toContain('GitHub')
await act(async () => { button.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
const menu = document.querySelector('.dropdown-menu')
expect(menu).not.toBeNull()
expect(menu?.querySelector('button.selected')?.textContent).toBe('GitHub')
const notion = [...menu!.querySelectorAll('button')].find((b) => b.textContent === 'Notion')
await act(async () => { notion?.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
expect(onChange).toHaveBeenCalledWith('notion')
expect(document.querySelector('.dropdown-menu')).toBeNull()
// 再次展开,点别处关闭
await act(async () => { button.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
expect(document.querySelector('.dropdown-menu')).not.toBeNull()
await act(async () => { document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })) })
expect(document.querySelector('.dropdown-menu')).toBeNull()
await act(async () => { root.unmount() })
container.remove()
})
it('展开层经 Portal 挂到 body;触发钮靠近窗口底部时向上翻且不溢出', async () => {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
await act(async () => {
root.render( undefined} />)
})
const button = container.querySelector('.dropdown-button') as HTMLButtonElement
// 模拟触发钮贴近窗口底部(jsdom 默认视口高 768):下方只剩 30px
button.getBoundingClientRect = () => ({
top: 700, bottom: 730, left: 100, right: 300, width: 200, height: 30, x: 100, y: 700, toJSON: () => ({}),
}) as DOMRect
await act(async () => { button.dispatchEvent(new MouseEvent('click', { bubbles: true })) })
// Portal:菜单不在组件容器内,而是 body 直属
expect(container.querySelector('.dropdown-menu')).toBeNull()
const menu = document.querySelector('.dropdown-menu') as HTMLElement | null
expect(menu).not.toBeNull()
expect(menu!.style.position).toBe('fixed')
// 3 项估算高 101,下方放不下 → 翻到触发钮上方:top = 700 - 101 - 4
expect(menu!.style.top).toBe('595px')
expect(menu!.style.left).toBe('100px')
expect(menu!.style.width).toBe('200px')
await act(async () => { root.unmount() })
container.remove()
})
})