Files
workspace/code/g3soft-libs/packages/ui/e2e/layer.spec.ts
T
2026-10-08 22:38:34 +08:00

122 lines
5.1 KiB
TypeScript

import { expect, test } from '@playwright/test'
/**
* 弹层层级回归 —— 这类问题单测覆盖不到(需要真实层叠上下文与命中测试)。
*
* 曾经踩过的坑:
* 1. 档位顺序写反(popup < modal)→ 弹窗/抽屉内的下拉被遮罩盖住;
* 2. 受控打开 / 挂载即展开的浮层根本没分配 z-index;
* 3. Drawer 的 wrap 是 fixed 但没 z-index → 整个抽屉被自己的遮罩压住。
*
* 注意:playground 里有两个「挂载即展开」的常驻 Popover,
* 所以定位浮层时要用 :has-text / filter 精确命中,不能直接 `.g3-popover__popup`。
*/
/** 读取视口内某点最上层的元素类名(判断「谁盖住谁」的唯一可靠方式) */
async function topElementAt(page: import('@playwright/test').Page, x: number, y: number) {
return page.evaluate(
([px, py]) => {
const el = document.elementFromPoint(px as number, py as number)
if (!el) return null
return typeof el.className === 'string' ? el.className : el.tagName
},
[x, y],
)
}
test.describe('浮层层级', () => {
test('挂载即展开的浮层同样有层级,且同档按打开顺序递增', async ({ page }) => {
await page.goto('/')
const popupA = page.locator('.g3-popover__popup', { hasText: '浮层 A' })
const popupB = page.locator('.g3-popover__popup', { hasText: '浮层 B' })
await expect(popupA).toBeVisible()
await expect(popupB).toBeVisible()
const zA = Number(await popupA.evaluate((el) => (el as HTMLElement).style.zIndex))
const zB = Number(await popupB.evaluate((el) => (el as HTMLElement).style.zIndex))
expect(zA).toBeGreaterThan(3000)
expect(zB).toBeGreaterThan(zA)
})
test('抽屉面板不会被自身遮罩盖住,点遮罩仍可关闭', async ({ page }) => {
await page.goto('/')
await page.getByTestId('open-drawer').click()
const drawer = page.locator('.g3-drawer')
await expect(drawer).toBeVisible()
// 等入场动画结束(transform 归零)再做命中测试,否则面板还在视口外
await page.waitForTimeout(450)
const panelBox = (await drawer.boundingBox())!
const inside = await topElementAt(
page,
panelBox.x + panelBox.width / 2,
panelBox.y + panelBox.height / 2,
)
expect(inside).not.toBeNull()
expect(inside).toContain('g3-drawer')
// 遮罩与 wrap 同级(同一实例共用层级)
const maskZ = Number(await page.$eval('.g3-drawer__mask', (el) => (el as HTMLElement).style.zIndex))
const wrapZ = Number(await page.$eval('.g3-drawer__wrap', (el) => (el as HTMLElement).style.zIndex))
expect(wrapZ).toBe(maskZ)
// 点面板外(遮罩区域)关闭
const maskBox = (await page.locator('.g3-drawer__mask').boundingBox())!
await page.mouse.click(maskBox.x + 40, maskBox.y + maskBox.height / 2)
await expect(drawer).toBeHidden()
})
test('弹窗内的下拉浮层高于遮罩,且可正常点选', async ({ page }) => {
await page.goto('/')
await page.getByTestId('open-modal').click()
await expect(page.locator('.g3-modal')).toBeVisible()
const select = page.getByTestId('modal-select')
await select.locator('.g3-select__trigger').click()
const popup = page.locator('.g3-popover__popup').filter({ has: page.locator('.g3-select__dropdown') })
await expect(popup).toBeVisible()
const modalZ = Number(await page.$eval('.g3-modal__mask', (el) => (el as HTMLElement).style.zIndex))
const popupZ = Number(await popup.evaluate((el) => (el as HTMLElement).style.zIndex))
expect(popupZ).toBeGreaterThan(modalZ)
// 浮层区域最上层必须是选项本体,而不是遮罩
const optionBox = (await page.locator('.g3-select__option').first().boundingBox())!
const hit = await topElementAt(page, optionBox.x + 10, optionBox.y + optionBox.height / 2)
expect(hit).toContain('g3-select__option')
})
test('反馈类提示盖在浮层与弹窗之上', async ({ page }) => {
await page.goto('/')
await page.getByTestId('open-modal').click()
const select = page.getByTestId('modal-select')
await select.locator('.g3-select__trigger').click()
await expect(
page.locator('.g3-popover__popup').filter({ has: page.locator('.g3-select__dropdown') }),
).toBeVisible()
// 触发按钮此时在弹窗遮罩之后:真实用户点不到它(这正是层级正确的表现),
// 所以直接派发 click 到元素上,本用例只验证「提示出现后压在最上层」
await page.getByTestId('toast-default').dispatchEvent('click')
await expect(page.locator('.g3-message').first()).toBeVisible()
const messageZ = Number(
await page.$eval('.g3-message__container', (el) => (el as HTMLElement).style.zIndex),
)
const popupZ = Number(
await page
.locator('.g3-popover__popup')
.filter({ has: page.locator('.g3-select__dropdown') })
.evaluate((el) => (el as HTMLElement).style.zIndex),
)
const modalZ = Number(await page.$eval('.g3-modal__mask', (el) => (el as HTMLElement).style.zIndex))
expect(messageZ).toBeGreaterThan(popupZ)
expect(popupZ).toBeGreaterThan(modalZ)
})
})