Files
workspace/code/g3soft-libs/packages/ui/e2e/docs-hover-minimal.spec.ts
T
2026-10-10 17:33:59 +08:00

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/)
})
})