Files
2026-09-10 15:29:45 +01:00

91 lines
3.8 KiB
TypeScript

import { expect, type Locator, type Page } from "@playwright/test";
import { waitForFiniteAnimations } from "./animations";
export async function readMenuBackdrop(menu: Locator) {
return menu.evaluate((element) => {
const positioner = element.parentElement;
if (!positioner?.hasAttribute("data-radix-popper-content-wrapper")) {
throw new Error("Menu has no positioning wrapper");
}
const backdrop = getComputedStyle(positioner, "::before");
return {
content: backdrop.content,
opacity: Number.parseFloat(backdrop.opacity),
background: backdrop.backgroundColor,
blur: backdrop.backdropFilter || backdrop.getPropertyValue("-webkit-backdrop-filter"),
position: backdrop.position,
inset: [backdrop.top, backdrop.right, backdrop.bottom, backdrop.left],
width: Number.parseFloat(backdrop.width),
height: Number.parseFloat(backdrop.height),
zIndex: Number.parseInt(backdrop.zIndex, 10),
pointerEvents: backdrop.pointerEvents,
foregroundFilter: getComputedStyle(element).filter,
wrapperTransform: getComputedStyle(positioner).transform,
wrapperZIndex: Number.parseInt(getComputedStyle(positioner).zIndex, 10),
viewport: { width: window.innerWidth, height: window.innerHeight },
supportsBlur:
CSS.supports("backdrop-filter", "blur(1px)") ||
CSS.supports("-webkit-backdrop-filter", "blur(1px)"),
};
});
}
export async function expectMenuBackdropCount(page: Page, count: number) {
await expect
.poll(() =>
page.locator("[data-radix-popper-content-wrapper]").evaluateAll(
(positioners) =>
positioners.filter((positioner) => {
if (!positioner.querySelector(':scope > [role="menu"]')) return false;
const style = getComputedStyle(positioner, "::before");
return !["none", "normal"].includes(style.content) && style.display !== "none";
}).length,
),
)
.toBe(count);
}
export async function expectMobileMenuBackdrop(menu: Locator) {
await expect(menu).toBeVisible();
await waitForFiniteAnimations(menu);
await expect
.poll(async () => ["none", "normal"].includes((await readMenuBackdrop(menu)).content))
.toBe(false);
const backdrop = await readMenuBackdrop(menu);
expect(backdrop.opacity).toBe(1);
expect(backdrop.background).not.toBe("rgba(0, 0, 0, 0)");
expect(backdrop.position).toBe("fixed");
expect(backdrop.inset).toEqual(["0px", "0px", "0px", "0px"]);
expect(backdrop.width).toBeCloseTo(backdrop.viewport.width, 0);
expect(backdrop.height).toBeCloseTo(backdrop.viewport.height, 0);
expect(backdrop.zIndex).toBeLessThan(0);
expect(backdrop.pointerEvents).toBe("none");
expect(backdrop.foregroundFilter).toMatch(/^(none|blur\(0px\))$/);
expect(backdrop.wrapperTransform).toBe("none");
expect(backdrop.wrapperZIndex).toBeGreaterThan(0);
if (backdrop.supportsBlur) {
expect(
Number.parseFloat(backdrop.blur.match(/blur\(([\d.]+)px\)/)?.[1] ?? "0"),
).toBeGreaterThan(0);
}
return backdrop;
}
export async function expectReachableMenuItem(item: Locator) {
await item.scrollIntoViewIfNeeded();
await expect(item).toBeVisible();
const hit = await item.evaluate(async (element) => {
const menu = element.closest('[role="menu"]');
await Promise.all(
(menu?.getAnimations({ subtree: true }) ?? [])
.filter((animation) => Number.isFinite(animation.effect?.getComputedTiming().iterations))
.map((animation) => animation.finished.catch(() => undefined)),
);
const box = element.getBoundingClientRect();
const target = document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2);
return { reachable: !!target && element.contains(target), height: box.height };
});
expect(hit.reachable).toBe(true);
expect(Math.round(hit.height)).toBeGreaterThanOrEqual(44);
}