95 lines
3.8 KiB
TypeScript
95 lines
3.8 KiB
TypeScript
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/)
|
|
})
|
|
})
|