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.
210 lines
13 KiB
TypeScript
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/);
|
|
});
|