// 自绘下拉回归:展开/选中/点别处关闭(替代原生 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() }) })