e2e/computation-sticky-headers.spec.ts
Linked repository file from this build.
import { expect, test } from "@playwright/test";
for (const fontSize of [16, 24]) {
for (const animationDuration of ["off", "short", "moderate", "long"]) {
test(`computation headers follow scroll at ${fontSize}px with ${animationDuration} motion`, async ({
page,
}) => {
await page.setViewportSize({ width: 1280, height: 720 });
await page.addInitScript(
(appearance) => {
localStorage.setItem("qccrm.appearance", JSON.stringify(appearance));
},
{ fontSize, animationDuration },
);
await page.goto("/reviews/aa1635c0-c99d-5f12-8f26-54d3f23bf13d");
await page.getByRole("tab", { name: "Computation", exact: true }).click();
const grid = page.locator('[data-section="computation"]');
await expect(grid.locator("[data-sticky-section]")).toHaveCount(3);
const failures = await grid.evaluate(async (element) => {
const table = element.querySelector("table")!;
const scroll = table.parentElement!;
const header = table.querySelector("thead")!;
const sections = [...table.querySelectorAll("tbody")];
const problems: string[] = [];
const frame = () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
const check = () => {
const pinned = header.getBoundingClientRect();
for (const [index, section] of sections.entries()) {
const row = section.querySelector<HTMLElement>("[data-sticky-section]")!;
const bounds = section.getBoundingClientRect();
const actual = row.getBoundingClientRect();
const expected = Math.max(
bounds.top,
Math.min(pinned.bottom, bounds.bottom - actual.height),
);
if (Math.abs(actual.top - expected) > 1) {
problems.push(
`Section ${index} at scroll ${scroll.scrollTop}: ${actual.top}, expected ${expected}`,
);
}
// The real header must cover data cells at every visible point of the handoff.
const visibleTop = Math.max(actual.top, pinned.bottom);
const visibleBottom = Math.min(actual.bottom, scroll.getBoundingClientRect().bottom);
if (visibleBottom - visibleTop > 2) {
const hit = document.elementFromPoint(
actual.left + 12,
(visibleTop + visibleBottom) / 2,
);
if (!row.contains(hit)) problems.push(`Section ${index} paints behind another cell`);
}
if (row.getAnimations().length)
problems.push(`Section ${index} animates during scroll`);
}
};
// Start during tab entry, then cross each boundary in both directions.
for (const position of [0, 1, 2, 4, 0]) {
scroll.scrollTop = position;
await frame();
check();
}
for (const section of sections.slice(1)) {
const boundary =
section.getBoundingClientRect().top -
header.getBoundingClientRect().bottom +
scroll.scrollTop;
const height = section.querySelector("tr")!.getBoundingClientRect().height;
for (const direction of [1, -1]) {
for (let step = 0; step <= 20; step++) {
const progress = direction === 1 ? step / 20 : 1 - step / 20;
scroll.scrollTop = boundary - height - 8 + progress * (height + 16);
await frame();
check();
}
}
}
return problems;
});
expect(failures).toEqual([]);
});
}
}