51 lines
2.5 KiB
JavaScript
51 lines
2.5 KiB
JavaScript
import { test, expect } from './coverage-fixtures.js'
|
|
|
|
// Regression: opening a row's kebab (ActionMenu) on /app/manage used to snap
|
|
// the page scroll to the top and render the menu detached from its trigger,
|
|
// making it impossible to operate. Two causes: the menu auto-focus scrolled
|
|
// the page (no preventScroll), and the position:fixed popover was rendered
|
|
// inside a row whose hover `transform` re-anchored it. Fix portals the popover
|
|
// to document.body, positions it before paint, and focuses without scrolling.
|
|
test.describe('Manage Page - Action menu positioning', () => {
|
|
test('opening a row menu keeps scroll stable and places the menu by its trigger', async ({ page }) => {
|
|
// Small viewport so the page is scrollable and a scroll jump is observable.
|
|
await page.setViewportSize({ width: 1024, height: 500 })
|
|
await page.goto('/app/manage')
|
|
await expect(page.locator('.table')).toBeVisible({ timeout: 10_000 })
|
|
|
|
const trigger = page.locator('button.action-menu__trigger').first()
|
|
await expect(trigger).toBeVisible()
|
|
|
|
// Bring the trigger into view ourselves first, so the only scroll we then
|
|
// measure is the one the menu would (wrongly) cause - not Playwright's own
|
|
// scroll-into-view before the click.
|
|
await trigger.scrollIntoViewIfNeeded()
|
|
const scrollBefore = await page.evaluate(() => window.scrollY)
|
|
await trigger.click()
|
|
|
|
const menu = page.locator('[role="menu"]')
|
|
await expect(menu).toBeVisible()
|
|
|
|
// Behavioural symptom 1: focusing the menu must not yank the page scroll.
|
|
const scrollAfter = await page.evaluate(() => window.scrollY)
|
|
expect(scrollAfter).toBe(scrollBefore)
|
|
|
|
// Behavioural symptom 2: the menu must sit next to its trigger, not float
|
|
// at the top of the window where it can't be operated.
|
|
const triggerBox = await trigger.boundingBox()
|
|
const menuBox = await menu.boundingBox()
|
|
expect(triggerBox).not.toBeNull()
|
|
expect(menuBox).not.toBeNull()
|
|
// Menu top is within ~24px of the trigger's bottom (below) or above it
|
|
// (flipped) — in all cases it tracks the trigger, never floating at y≈0.
|
|
const tracksTrigger =
|
|
Math.abs(menuBox.y - (triggerBox.y + triggerBox.height)) < 24 ||
|
|
Math.abs((menuBox.y + menuBox.height) - triggerBox.y) < 24
|
|
expect(tracksTrigger).toBe(true)
|
|
|
|
// Mechanism: the popover must be portaled to document.body so position:fixed
|
|
// resolves against the viewport, not a transformed ancestor row.
|
|
await expect(page.locator('body > .popover')).toHaveCount(1)
|
|
})
|
|
})
|