Files
new-markdown/tests/dropdown.test.tsx
Muse eb27e4ca2d fix(dropdown): 展开层改 Portal + fixed 定位,逃出弹窗 overflow 裁剪
弹窗(.modal 带 overflow-y:auto)里的下拉展开层是 absolute 定位,
会被弹窗滚动盒裁掉下半截(Git 同步设置里实测被遮挡)。展开层改经
Portal 挂到 body、按触发钮 rect 用 fixed 定位:下方空间不足时
向上翻,maxHeight 按可用空间夹取;窗口 resize 时关闭。
useDismiss 泛化为可传多个 ref(触发器与 Portal 菜单两棵子树都算内)。
CSS 相应去掉 .dropdown-menu 的 absolute 定位(几何改由内联给出)。
验证:vitest 124+2(新增翻转几何断言:贴底触发钮翻上展开、
Portal 落点不在组件容器内);无头 Chromium 实测弹窗内下拉菜单
完整落在视口内、8 项全显、已逃出弹窗 DOM。
2026-11-14 20:17:51 +08:00

67 lines
3.3 KiB
TypeScript
Raw Permalink 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.
// 自绘下拉回归:展开/选中/点别处关闭(替代原生 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(<Dropdown value="github" options={OPTIONS} onChange={onChange} />)
})
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(<Dropdown value="github" options={OPTIONS} onChange={() => 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()
})
})