Files
sendou.ink/app/components/elements/anchor-positioning.browser.test.tsx

191 lines
5.6 KiB
TypeScript

import { afterEach, describe, expect, test, vi } from "vitest";
import { render } from "vitest-browser-react";
import { SendouPopover } from "./Popover";
import { SendouSelect, SendouSelectItem } from "./Select";
const SEASONS = [{ id: 1, name: "Season 1" }];
const MANY_SEASONS = Array.from({ length: 40 }, (_, index) => ({
id: index + 1,
name: `Season ${index + 1}`,
}));
let disablingStyle: HTMLStyleElement | null = null;
afterEach(() => {
disablingStyle?.remove();
disablingStyle = null;
vi.restoreAllMocks();
});
/** The test browser has anchor positioning, so the fallback has to be forced on. */
function disableAnchorPositioning() {
vi.spyOn(CSS, "supports").mockReturnValue(false);
disablingStyle = document.createElement("style");
disablingStyle.textContent = `[popover] {
position-area: none !important;
justify-self: normal !important;
align-self: normal !important;
}`;
document.head.append(disablingStyle);
}
function rectOf(element: Element) {
return element.getBoundingClientRect();
}
describe("useAnchorPositioning", () => {
test("centers the popover under its trigger", async () => {
disableAnchorPositioning();
const screen = await render(
<div style={{ padding: "100px" }}>
<SendouPopover trigger={<button type="button">Filters</button>}>
Filter by season
</SendouPopover>
</div>,
);
const trigger = screen.getByRole("button", { name: "Filters" });
await trigger.click();
const triggerRect = rectOf(trigger.element());
const popoverRect = rectOf(screen.getByRole("dialog").element());
expect(popoverRect.top).toBeGreaterThanOrEqual(triggerRect.bottom);
expect(
Math.abs(
popoverRect.left +
popoverRect.width / 2 -
(triggerRect.left + triggerRect.width / 2),
),
).toBeLessThan(2);
});
test("gives the select popover the width of its trigger", async () => {
disableAnchorPositioning();
const screen = await render(
<div style={{ padding: "100px" }}>
<SendouSelect
label="Season"
items={SEASONS}
placeholder="Pick a season"
>
{({ id, name }: (typeof SEASONS)[number]) => (
<SendouSelectItem key={id} id={id}>
{name}
</SendouSelectItem>
)}
</SendouSelect>
</div>,
);
const trigger = screen.getByRole("button", { name: /Pick a season/ });
await trigger.click();
await expect
.element(screen.getByRole("option", { name: "Season 1" }))
.toBeVisible();
const triggerRect = rectOf(trigger.element());
const popover = document.querySelector("[popover]");
const popoverRect = rectOf(popover as Element);
expect(popoverRect.width).toBeCloseTo(triggerRect.width, 0);
expect(popoverRect.left).toBeCloseTo(triggerRect.left, 0);
expect(popoverRect.top).toBeGreaterThanOrEqual(triggerRect.bottom);
});
test("opens a select upwards when its options do not fit below the trigger", async () => {
const screen = await render(<SelectNearViewportBottom />);
const trigger = screen.getByRole("button", { name: /Pick a season/ });
await trigger.click();
await expect
.element(screen.getByRole("option", { name: "Season 1", exact: true }))
.toBeVisible();
const triggerRect = rectOf(trigger.element());
const popoverRect = rectOf(document.querySelector("[popover]") as Element);
expect(popoverRect.bottom).toBeLessThanOrEqual(triggerRect.top);
expect(popoverRect.top).toBeGreaterThanOrEqual(0);
});
test("caps a long select to the space below its trigger", async () => {
const screen = await render(
<div style={{ padding: "100px" }}>
<SendouSelect
label="Season"
items={MANY_SEASONS}
placeholder="Pick a season"
>
{({ id, name }: (typeof MANY_SEASONS)[number]) => (
<SendouSelectItem key={id} id={id}>
{name}
</SendouSelectItem>
)}
</SendouSelect>
</div>,
);
const trigger = screen.getByRole("button", { name: /Pick a season/ });
await trigger.click();
await expect
.element(screen.getByRole("option", { name: "Season 1", exact: true }))
.toBeVisible();
const popover = document.querySelector("[popover]") as HTMLElement;
const listbox = screen.getByRole("listbox").element();
const spaceBelow =
window.innerHeight -
rectOf(trigger.element()).bottom -
Number.parseFloat(getComputedStyle(popover).marginTop) -
12;
// an explicit cap, as WebKit does not resolve the percentage one in the CSS
expect(Number.parseFloat(popover.style.maxHeight)).toBeCloseTo(
spaceBelow,
0,
);
expect(rectOf(popover).bottom).toBeLessThanOrEqual(window.innerHeight);
expect(listbox.scrollHeight).toBeGreaterThan(listbox.clientHeight);
});
test("keeps the select where it opened when searching shrinks the list", async () => {
const screen = await render(<SelectNearViewportBottom />);
const trigger = screen.getByRole("button", { name: /Pick a season/ });
await trigger.click();
await expect
.element(screen.getByRole("option", { name: "Season 1", exact: true }))
.toBeVisible();
const popover = document.querySelector("[popover]") as Element;
const bottomOnOpen = rectOf(popover).bottom;
await screen.getByRole("combobox").fill("Season 40");
await expect
.element(screen.getByRole("option", { name: "Season 40" }))
.toBeVisible();
expect(rectOf(popover).bottom).toBeCloseTo(bottomOnOpen, 0);
});
});
function SelectNearViewportBottom() {
return (
<div style={{ marginTop: "calc(100vh - 100px)" }}>
<SendouSelect
label="Season"
items={MANY_SEASONS}
placeholder="Pick a season"
search={{ placeholder: "Search seasons..." }}
>
{({ id, name }: (typeof MANY_SEASONS)[number]) => (
<SendouSelectItem key={id} id={id}>
{name}
</SendouSelectItem>
)}
</SendouSelect>
</div>
);
}