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/); });