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