import { expect, test } from '@playwright/test' /** * 回归:**hover 触发**时菜单项高亮「闪一下然后消失」。 * * 最小复现(用户指定):文档站 /ui/overlay/dropdown 的「触发方式 → hover 触发」示例。 * * 根因(已由开关调用栈 + 逐帧采样证实): * hover 触发下触发器与浮层是两棵 DOM(浮层 Teleport 到 body)。指针从触发器移进 * 菜单项时会先后触发: * 1. 触发器 mouseleave(排定关闭定时器,随后被浮层 mouseenter 清掉) * 2. 浮层自己的 mouseenter → setOpen(true) → **再上报一次 open=true** * Dropdown.onOpenChange 曾经「见 true 就 activeIndex = -1」,于是菜单项刚点亮的 * 高亮被这次重复上报清掉;指针已停住、不会再派发 mouseenter → 高亮永久不恢复。 * click 触发时鼠标停在触发器上,没有这次重复上报,所以只有 hover 模式复现。 * * 判定方式刻意避开「指针轨迹」这类不稳定因素: * - 不用逐帧采样(依赖 Playwright 的移动时序,会飘); * - 改为「hover 到项上 → 等足够久 → 断言高亮仍在」。 * 修复前:等 100ms(mouseEnterDelay) 后高亮被清 → 断言失败; * 修复后:高亮一直保持 → 断言通过。 */ test.use({ baseURL: 'http://localhost:3100' }) test.describe('hover 触发:菜单项高亮不得闪断', () => { test('hover 到菜单项并停留,高亮必须持续保持', async ({ page }) => { await page.goto('/ui/overlay/dropdown') const trigger = page.getByRole('button', { name: 'hover 触发' }).first() await expect(trigger).toBeVisible() await trigger.hover() const menu = page.locator('.g3-dropdown__menu').first() await expect(menu).toBeVisible({ timeout: 3000 }) const item = menu.locator('.g3-dropdown__item').first() // hover 到菜单项上 await item.hover() // 立刻应高亮 await expect(item).toHaveClass(/is-item-active/) // 关键等待:越过 mouseEnterDelay(100ms) + 若干帧, // 让「浮层重复上报 open → 清 activeIndex」这条路径有机会发生 await page.waitForTimeout(400) // 修复前这里会失败(高亮已被清掉且不会恢复) await expect(item).toHaveClass(/is-item-active/) // 菜单本身也不该关闭 await expect(menu).toBeVisible() }) test('在两个菜单项之间移动,高亮正确转移且不残留', async ({ page }) => { await page.goto('/ui/overlay/dropdown') const trigger = page.getByRole('button', { name: 'hover 触发' }).first() await trigger.hover() const menu = page.locator('.g3-dropdown__menu').first() await expect(menu).toBeVisible({ timeout: 3000 }) const items = menu.locator('.g3-dropdown__item') await items.nth(0).hover() await expect(items.nth(0)).toHaveClass(/is-item-active/) await items.nth(1).hover() await expect(items.nth(1)).toHaveClass(/is-item-active/) await expect(items.nth(0)).not.toHaveClass(/is-item-active/) // 停留后仍保持 await page.waitForTimeout(400) await expect(items.nth(1)).toHaveClass(/is-item-active/) }) test('click 触发作为对照组', async ({ page }) => { await page.goto('/ui/overlay/dropdown') const clickTrigger = page.getByRole('button', { name: '点击触发' }).first() if ((await clickTrigger.count()) === 0) { test.skip(true, '该页面没有「点击触发」示例按钮') return } await clickTrigger.click() const menu = page.locator('.g3-dropdown__menu').first() await expect(menu).toBeVisible() const item = menu.locator('.g3-dropdown__item').first() await item.hover() await expect(item).toHaveClass(/is-item-active/) await page.waitForTimeout(400) await expect(item).toHaveClass(/is-item-active/) }) })