Files
govoplan-core/webui/conformance/tests/data-grid-layout.spec.ts
T
zemion 6d37aa527f fix(ui): unify heading help, table sizing and navigation contracts
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:16 +02:00

210 lines
13 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
const actionCell = (page: Page) => page.locator('.data-grid-body-cell[data-column-id="actions"]');
const header = (page: Page, id: string) => page.locator(`.data-grid-header-cell[data-column-id="${id}"]`);
const width = (page: Page, id: string) => header(page, id).evaluate((element) => element.getBoundingClientRect().width);
async function expectActionsUnclipped(page: Page) {
await expect.poll(async () => actionCell(page).evaluate((cell) => {
const bounds = cell.getBoundingClientRect();
const scroller = cell.closest(".data-grid-scroll-region")!;
const viewport = scroller.getBoundingClientRect();
return Array.from(cell.querySelectorAll("button")).every((button) => {
const rect = button.getBoundingClientRect();
return rect.width >= 35 && rect.left >= bounds.left && rect.right <= bounds.right
&& rect.top >= bounds.top && rect.bottom <= bounds.bottom
&& rect.left >= viewport.left && rect.right <= viewport.right;
});
})).toBe(true);
}
test("undersized action tracks fit real controls and stay visible through narrow horizontal scrolling", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
await page.goto("/?data-grid-layout");
await expect.poll(() => width(page, "actions")).toBeGreaterThanOrEqual(181);
await expectActionsUnclipped(page);
await page.getByRole("button", { name: "Narrow grid", exact: true }).click();
await expect.poll(() => width(page, "actions")).toBeLessThanOrEqual(160);
const scroller = page.locator(".data-grid-scroll-region");
await expectActionsUnclipped(page);
await scroller.evaluate((element) => { element.scrollLeft = element.scrollWidth / 2; });
await expectActionsUnclipped(page);
await scroller.evaluate((element) => { element.scrollLeft = element.scrollWidth; });
await expectActionsUnclipped(page);
await page.getByRole("button", { name: "Inspect Alpha", exact: true }).click();
await expect(page.getByTestId("clicked-action")).toHaveText("Inspect Alpha");
await scroller.focus();
await expect(scroller).toBeFocused();
// The application shell's supported viewport floor is 320px; its padded
// content area is narrower and must still contain the complete action set.
await page.setViewportSize({ width: 320, height: 720 });
await expectActionsUnclipped(page);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
expect(errors).toEqual([]);
});
test("pointer and keyboard resizing persists, cancels safely, and adapts to container resize", async ({ page }) => {
await page.goto("/?data-grid-layout");
await expectActionsUnclipped(page);
const handle = header(page, "name").getByRole("separator");
await expect(handle).toHaveAttribute("aria-orientation", "vertical");
const initial = await width(page, "name");
await handle.focus();
await handle.press("Shift+ArrowRight");
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 40, 0);
const box = (await handle.boundingBox())!;
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 + 70, box.y + box.height / 2, { steps: 5 });
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 110, 0);
await page.keyboard.press("Escape");
await page.mouse.up();
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 40, 0);
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 40, 0);
await page.getByRole("button", { name: "Narrow grid", exact: true }).click();
await expect.poll(() => width(page, "name")).toBeLessThan(initial + 40);
await expectActionsUnclipped(page);
await page.getByRole("button", { name: "Wide grid", exact: true }).click();
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 40, 0);
await handle.press("Enter");
await expect.poll(() => width(page, "name")).toBeCloseTo(initial, 0);
const resetBox = (await handle.boundingBox())!;
await page.mouse.move(resetBox.x + resetBox.width / 2, resetBox.y + resetBox.height / 2);
await page.mouse.down();
await page.mouse.move(resetBox.x + resetBox.width / 2 + 50, resetBox.y + resetBox.height / 2, { steps: 5 });
await page.mouse.up();
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 50, 0);
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 50, 0);
});
test("free content tracks retain explicit user resizing on remount", async ({ page }) => {
await page.goto("/?data-grid-layout&mode=free");
const handle = header(page, "name").getByRole("separator");
await expectActionsUnclipped(page);
const initial = await width(page, "name");
await handle.press("ArrowRight");
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 10, 0);
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 10, 0);
});
test("empty and changing action sets reserve the same complete slots", async ({ page }) => {
await page.goto("/?data-grid-layout");
await expectActionsUnclipped(page);
const initial = await width(page, "actions");
await page.getByRole("button", { name: "Toggle empty rows", exact: true }).click();
await expectActionsUnclipped(page);
await expect.poll(() => width(page, "actions")).toBeCloseTo(initial, 0);
await page.getByRole("button", { name: "Toggle empty rows", exact: true }).click();
await page.getByRole("button", { name: "Toggle extra action", exact: true }).click();
await expect.poll(() => width(page, "actions")).toBeGreaterThan(initial + 35);
await expectActionsUnclipped(page);
});
test("constrained resizing redistributes tracks without introducing overflow", async ({ page }) => {
await page.goto("/?data-grid-layout&mode=constrained");
await expectActionsUnclipped(page);
const initial = await width(page, "name");
await header(page, "name").getByRole("separator").press("Shift+ArrowRight");
await expect.poll(() => width(page, "name")).toBeCloseTo(initial + 40, 0);
expect(await page.locator(".data-grid-scroll-region").evaluate((element) => element.scrollWidth - element.clientWidth)).toBeLessThanOrEqual(1);
await expectActionsUnclipped(page);
});
for (const mode of ["cover", "free", "constrained"] as const) {
test(`${mode} resizing crosses a fixed intermediate column without changing that column`, async ({ page }) => {
await page.goto(`/?data-grid-layout&mixed-columns&mode=${mode}`);
const handle = header(page, "name").getByRole("separator");
await expect.poll(() => width(page, "actions")).toBeGreaterThanOrEqual(mode === "free" ? 180 : 181);
if (mode !== "free") await expectActionsUnclipped(page);
const initialName = await width(page, "name");
const initialDetail = await width(page, "detail");
const initialFixed = await width(page, "fixed");
const initialGrid = await page.locator(".data-grid").evaluate((element) => element.getBoundingClientRect().width);
const box = (await handle.boundingBox())!;
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 + 80, box.y + box.height / 2, { steps: 6 });
await expect.poll(() => width(page, "name")).toBeCloseTo(initialName + 80, 0);
await expect.poll(() => width(page, "fixed")).toBeCloseTo(initialFixed, 0);
await expect.poll(() => width(page, "detail")).toBeCloseTo(initialDetail - (mode === "constrained" ? 80 : 0), 0);
await page.mouse.up();
await expect.poll(() => width(page, "name")).toBeCloseTo(initialName + 80, 0);
await expect.poll(() => page.locator(".data-grid").evaluate((element) => element.getBoundingClientRect().width))
.toBeCloseTo(initialGrid + (mode === "constrained" ? 0 : 80), 0);
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await expect.poll(() => width(page, "name")).toBeCloseTo(initialName + 80, 0);
await handle.press("Shift+ArrowLeft");
await expect.poll(() => width(page, "name")).toBeCloseTo(initialName + 40, 0);
await expect.poll(() => width(page, "fixed")).toBeCloseTo(initialFixed, 0);
await handle.press("Shift+ArrowRight");
await expect.poll(() => width(page, "name")).toBeCloseTo(initialName + 80, 0);
await expect.poll(() => width(page, "fixed")).toBeCloseTo(initialFixed, 0);
});
}
test("preferred caps permit two-way resizing across fixed peers at the right scroll boundary", async ({ page }) => {
await page.goto("/?data-grid-layout&mixed-columns&preferred-limits");
await expectActionsUnclipped(page);
const initialName = await width(page, "name");
const initialFixed = await width(page, "fixed");
const nameHandle = header(page, "name").getByRole("separator");
for (let step = 0; step < 10; step += 1) await nameHandle.press("Shift+ArrowRight");
await expect.poll(() => width(page, "name")).toBeCloseTo(initialName + 400, 0);
const scroller = page.locator(".data-grid-scroll-region");
await scroller.evaluate((element) => { element.scrollLeft = element.scrollWidth; });
const detailBeforeShrink = await width(page, "detail");
const scrollBeforeShrink = await scroller.evaluate((element) => element.scrollLeft);
const handleBeforeShrink = (await nameHandle.boundingBox())!;
await page.mouse.move(handleBeforeShrink.x + handleBeforeShrink.width / 2, handleBeforeShrink.y + handleBeforeShrink.height / 2);
await page.mouse.down();
await page.mouse.move(handleBeforeShrink.x + handleBeforeShrink.width / 2 - 80, handleBeforeShrink.y + handleBeforeShrink.height / 2, { steps: 6 });
await expect.poll(() => width(page, "name")).toBeCloseTo(initialName + 320, 0);
await expect.poll(() => width(page, "detail")).toBeCloseTo(detailBeforeShrink + 80, 0);
await expect.poll(() => scroller.evaluate((element) => element.scrollLeft)).toBeCloseTo(scrollBeforeShrink, 0);
expect((await nameHandle.boundingBox())!.x).toBeCloseTo(handleBeforeShrink.x - 80, 0);
await page.mouse.up();
await expect.poll(() => width(page, "name")).toBeCloseTo(initialName + 320, 0);
const detailHandle = header(page, "detail").getByRole("separator");
await detailHandle.press("Shift+ArrowRight");
await expect.poll(() => width(page, "detail")).toBeCloseTo(detailBeforeShrink + 120, 0);
await detailHandle.press("Shift+ArrowLeft");
await expect.poll(() => width(page, "detail")).toBeCloseTo(detailBeforeShrink + 80, 0);
await expect.poll(() => width(page, "fixed")).toBeCloseTo(initialFixed, 0);
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await page.getByRole("button", { name: "Toggle grid mount", exact: true }).click();
await expect.poll(() => width(page, "name")).toBeCloseTo(initialName + 320, 0);
await expect.poll(() => width(page, "detail")).toBeCloseTo(detailBeforeShrink + 80, 0);
await expect.poll(() => width(page, "fixed")).toBeCloseTo(initialFixed, 0);
});
test("explicit composite action groups include outer controls in their measured minimum", async ({ page }) => {
await page.goto("/?data-grid-layout&mode=composite");
await expect.poll(() => width(page, "actions")).toBeGreaterThanOrEqual(221);
await expectActionsUnclipped(page);
await page.getByRole("button", { name: "Narrow grid", exact: true }).click();
await expectActionsUnclipped(page);
await page.getByRole("button", { name: "Extra control", exact: true }).click();
await expect(page.getByTestId("clicked-action")).toHaveText("Extra control");
});
test("oversized explicit sticky minima release stickiness instead of obscuring every data column", async ({ page }) => {
await page.goto("/?data-grid-layout&mode=oversized");
await expectActionsUnclipped(page);
await page.getByRole("button", { name: "Narrow grid", exact: true }).click();
await expect(page.locator(".data-grid-shell")).toHaveClass(/data-grid-release-sticky/);
const name = page.locator('.data-grid-body-cell[data-column-id="name"]');
await expect(name).toBeInViewport();
await page.locator(".data-grid-scroll-region").evaluate((element) => { element.scrollLeft = element.scrollWidth; });
await expectActionsUnclipped(page);
await page.getByRole("button", { name: "Wide grid", exact: true }).click();
await expect(page.locator(".data-grid-shell")).not.toHaveClass(/data-grid-release-sticky/);
});