Merge custom option test fix into widget modernization

This commit is contained in:
Thomas Camlong committed 2026-08-09 18:06:25 +02:00
commit fc8b9178a3
21 files changed
+246 -179

No files matched your search

+72 -52
View File
@@ -6,11 +6,7 @@ import { createId } from "@paralleldrive/cuid2";
import { stringify as stringifySuperJSON } from "superjson";
import { describe, test } from "vitest";
import {
LOGICAL_GRID_CELL_SIZE,
LOGICAL_GRID_GAP,
MIN_ACCESSIBLE_CANVAS_SCALE,
} from "../apps/nextjs/src/components/board/layout/constants";
import { LOGICAL_GRID_CELL_SIZE, LOGICAL_GRID_GAP } from "../apps/nextjs/src/components/board/layout/constants";
import * as sqliteSchema from "../packages/db/schema/sqlite";
import { createHomarrContainer } from "./shared/create-homarr-container";
import { createSqliteDbFileAsync } from "./shared/e2e-db";
@@ -26,7 +22,6 @@ const gridRuntimeMarker = "homarr-dnd-kit-v1";
const dndKitPayloadMarker = "dnd-kit-description";
const logicalCellSize = LOGICAL_GRID_CELL_SIZE;
const logicalCellPitch = LOGICAL_GRID_CELL_SIZE + LOGICAL_GRID_GAP;
const minimumCanvasScale = MIN_ACCESSIBLE_CANVAS_SCALE;
describe("Board grid", () => {
test("supports fixed, nested, rail, pointer, keyboard and reconciliation workflows", async () => {
@@ -100,6 +95,11 @@ describe("Board grid", () => {
await expect(rail).toBeVisible();
await expect(rail).toHaveAttribute("data-board-gutter", "left");
await expect(railRootSection).toHaveAttribute("data-rail-placement", "left");
await expect(rail).toHaveCSS("overflow-x", "hidden");
const initialRailBox = await expectBoundingBoxAsync(rail);
const initialRailRootBox = await expectBoundingBoxAsync(railRootSection);
expect(initialRailRootBox.width).toBeCloseTo(initialRailBox.width, 1);
expect(initialRailRootBox.height).toBeGreaterThanOrEqual(initialRailBox.height - 1);
await expect(railItem).toHaveAttribute("data-grid-x", "0");
await expect(containerSection).toHaveAttribute("data-grid-w", "2");
await expect(containerSection).toHaveAttribute("data-grid-h", "2");
@@ -174,21 +174,15 @@ describe("Board grid", () => {
expect(await readLayoutShiftScoreAsync(page)).toBeLessThan(0.01);
// CSS zoom exercises Chromium's browser-zoom layout path without relying
// on desktop browser chrome. At 200% the scale floor must preserve
// magnification and expose horizontal scrolling instead of shrinking the
// board by the inverse zoom factor.
// on desktop browser chrome. The board must keep fitting without adding
// horizontal scrolling at any effective zoom.
await setDocumentZoomAsync(page, 2);
await expect(canvas).toHaveAttribute("data-canvas-scale", String(minimumCanvasScale));
await expect(canvas).toHaveAttribute("data-canvas-overflow", "true");
const zoomedCanvasScale = await readCanvasScaleAsync(canvas);
expect(zoomedCanvasScale).toBeLessThan(normalCanvasScale);
await expect(canvas).toHaveAttribute("data-canvas-overflow", "false");
await expectFixedLogicalTileAsync(logicalTile);
await expectUniformVisualScaleAsync(logicalTile);
const fittedClockFontSize = await firstItem.locator(".clock-time-text").evaluate((element) => {
return Number.parseFloat(getComputedStyle(element).fontSize);
});
expect(fittedClockFontSize * minimumCanvasScale).toBeGreaterThanOrEqual(25);
const zoomedTileBox = await expectBoundingBoxAsync(logicalTile);
expect(zoomedTileBox.width).toBeGreaterThan(normalZoomTileBox.width * 1.25);
await expectHorizontalOverflowAsync(canvas);
await expect(canvas).toHaveCSS("overflow-x", "clip");
await expectDocumentNotHorizontallyScrollableAsync(page);
const overflowRailY = (await expectBoundingBoxAsync(rail)).y;
await page.evaluate(() => window.scrollTo({ top: 300, behavior: "auto" }));
@@ -251,6 +245,14 @@ describe("Board grid", () => {
await expect(page.locator(`[data-grid-runtime="${gridRuntimeMarker}"]`)).toHaveCount(3);
await expect(page.getByTestId("board-grid-editor-loading")).toHaveCount(0);
await expect(page.getByTestId("board-canvas-row-count-button")).toHaveCount(0);
await expect(rail).toHaveAttribute("data-board-editing", "true");
await expect(rail).toHaveCSS("position", "sticky");
await expect(rail).toHaveCSS("transform", "none");
const railBox = await expectBoundingBoxAsync(rail);
expect(railBox.height + railBox.y).toBeCloseTo(page.viewportSize()?.height ?? 0, 0);
expect(await rail.evaluate((element) => getComputedStyle(element, "::before").content.replaceAll('"', ""))).toBe(
"Left dashboard rail",
);
await expect(logicalTile).toHaveCSS("overflow-x", "hidden");
await expect(logicalTile).toHaveCSS("overflow-y", "auto");
await logicalTile.evaluate((element) => {
@@ -337,8 +339,11 @@ describe("Board grid", () => {
await expect(itemSelectDialog).toHaveCount(0);
const mainGrid = page.locator(`[data-grid-section-id="${fixture.sectionId}"]`);
const editorViewport = page.viewportSize();
if (!editorViewport) throw new Error("Expected a browser viewport");
expect((await expectBoundingBoxAsync(mainGrid)).height).toBeGreaterThanOrEqual(editorViewport.height - 1);
const interactionSnapshot = await readEditorGridSnapshotAsync(mainGrid);
await expectGridBoundaryRejectsOverflowAsync(page, mainGrid, firstItem);
await expectGridGrowsAtBoundaryAsync(page, canvas, mainGrid, firstItem);
await firstItem.locator("[data-grid-item-content]").evaluate((element) => {
const iframe = document.createElement("iframe");
iframe.dataset.e2eDragIframe = "true";
@@ -367,8 +372,8 @@ describe("Board grid", () => {
.evaluateAll((elements) => elements.forEach((element) => element.remove()));
await page.setViewportSize({ width: 600, height: 1200 });
await expect(canvas).toHaveAttribute("data-canvas-scale", String(minimumCanvasScale));
await expectHorizontalOverflowAsync(canvas);
await expect(canvas).toHaveAttribute("data-canvas-overflow", "false");
await expect(canvas).toHaveCSS("overflow-x", "clip");
await expectDocumentNotHorizontallyScrollableAsync(page);
for (const direction of ["n", "ne", "e", "se", "s", "sw", "w", "nw"] as const) {
await expectTargetSizeAsync(firstItem.locator(`:scope > [data-grid-resize-handle="${direction}"]`));
@@ -718,13 +723,12 @@ describe("Board grid", () => {
expect(continuousResizeWidth).toBeGreaterThan(logicalCellSize * canvasScale);
await page.mouse.move(eastResizeStart.x - logicalCellPitch * canvasScale, eastResizeStart.y, { steps: 6 });
await expect(containerSection).toHaveAttribute("data-grid-w", "2");
await expect(containerSection).toHaveAttribute("data-grid-w", "1");
await expect(resizePlaceholder).toHaveAttribute("data-grid-w", "1");
expect((await expectBoundingBoxAsync(resizeOutline)).width).toBeCloseTo(logicalCellSize * canvasScale, 1);
const liveNestedGridBox = await expectBoundingBoxAsync(nestedEditor);
const liveContainerBox = await expectBoundingBoxAsync(containerSection);
expect(liveNestedGridBox.width).toBeCloseTo(originalResizeWidth, 1);
expect(liveNestedGridBox.width).toBeLessThanOrEqual(liveContainerBox.width + 1);
expect(liveContainerBox.width).toBeCloseTo(logicalCellSize * canvasScale, 1);
await expect(containerSection.locator(":scope > .board-grid-content-mount")).toHaveCSS("overflow", "clip");
await page.mouse.up();
await expect(containerSection).toHaveAttribute("data-grid-w", "1");
await expect(resizeOutline).toHaveCount(0);
@@ -759,15 +763,26 @@ describe("Board grid", () => {
await expect(containerSection).toHaveAttribute("data-grid-h", "2");
const southResizeHandle = containerSection.locator(':scope > [data-grid-resize-handle="s"]');
await dragLocatorByAsync(page, southResizeHandle, 0, logicalCellPitch * canvasScale);
const gridHeightBeforeSouthResize = (await expectBoundingBoxAsync(mainGrid)).height;
const southResizeBox = await expectBoundingBoxAsync(southResizeHandle);
const southResizeStart = {
x: southResizeBox.x + southResizeBox.width / 2,
y: southResizeBox.y + southResizeBox.height / 2,
};
await page.mouse.move(southResizeStart.x, southResizeStart.y);
await page.mouse.down();
await page.mouse.move(southResizeStart.x, southResizeStart.y + logicalCellPitch * canvasScale, { steps: 8 });
await expect(containerSection).toHaveAttribute("data-grid-h", "3");
await expect(belowItem).toHaveAttribute("data-grid-y", "3");
await expect
.poll(async () => (await expectBoundingBoxAsync(mainGrid)).height)
.toBeGreaterThanOrEqual(gridHeightBeforeSouthResize);
await page.mouse.up();
expect(
gridLayoutsOverlap(await readGridLayoutAsync(containerSection), await readGridLayoutAsync(belowItem)),
).toBe(false);
// Auto-scroll needs real document overflow. Grow the canvas through an
// explicit resize instead of relying on edit mode to add empty rows.
// Grow the canvas further to exercise sustained document auto-scroll.
await dragLocatorByAsync(page, southResizeHandle, 0, logicalCellPitch * canvasScale * 6);
await expect(containerSection).toHaveAttribute("data-grid-h", "9");
await expect(belowItem).toHaveAttribute("data-grid-y", "9");
@@ -843,14 +858,10 @@ describe("Board grid", () => {
false,
);
const stickyRailY = (await expectBoundingBoxAsync(rail)).y;
await page.evaluate(() => window.scrollTo({ top: 500, behavior: "auto" }));
await expect
.poll(async () => {
const y = (await expectBoundingBoxAsync(rail)).y;
return y >= 0 && y <= stickyRailY + 0.5;
})
.toBe(true);
const stickyRailY = (await expectBoundingBoxAsync(rail)).y;
await page.evaluate(() => window.scrollTo({ top: 600, behavior: "auto" }));
await expect.poll(async () => (await expectBoundingBoxAsync(rail)).y).toBeCloseTo(stickyRailY, 0);
await page.evaluate(() => window.scrollTo({ top: 0, behavior: "auto" }));
await setDocumentZoomAsync(page, 1.25);
@@ -979,6 +990,15 @@ describe("Board grid", () => {
const emptyRightGutter = page.getByRole("complementary", { name: "Right dashboard rail" });
await expect(emptyRightGutter).toHaveCount(1);
expect((await expectBoundingBoxAsync(emptyRightGutter)).width).toBeGreaterThan(0);
await page.getByTestId("board-edit-mode-toggle").click();
await expect(emptyRightGutter).toHaveAttribute("data-board-editing", "true");
expect(
await emptyRightGutter.evaluate((element) => ({
content: getComputedStyle(element, "::before").content.replaceAll('"', ""),
insetInlineStart: getComputedStyle(element, "::before").insetInlineStart,
top: getComputedStyle(element, "::before").top,
})),
).toEqual({ content: "Right dashboard rail", insetInlineStart: "0px", top: "0px" });
expect(pageErrors).toEqual([]);
} finally {
runtimeResources?.stop();
@@ -1713,9 +1733,10 @@ const expectImmediateOutsideReleaseRestoresSnapshotAsync = async (page: Page, gr
await expect.poll(async () => await readEditorGridSnapshotAsync(grid)).toEqual(snapshot);
};
const expectGridBoundaryRejectsOverflowAsync = async (page: Page, grid: Locator, locator: Locator) => {
const expectGridGrowsAtBoundaryAsync = async (page: Page, canvas: Locator, grid: Locator, locator: Locator) => {
const snapshot = await readEditorGridSnapshotAsync(grid);
const gridBox = await expectBoundingBoxAsync(grid);
const canvasBox = await expectBoundingBoxAsync(canvas);
const itemBox = await expectBoundingBoxAsync(locator);
const start = { x: itemBox.x + itemBox.width * 0.63, y: itemBox.y + itemBox.height * 0.42 };
@@ -1723,14 +1744,25 @@ const expectGridBoundaryRejectsOverflowAsync = async (page: Page, grid: Locator,
await page.mouse.down();
await page.mouse.move(start.x + 8, start.y);
await expect(locator).toHaveAttribute("data-dnd-drag-source", "true");
await page.mouse.move(gridBox.x + gridBox.width / 2, gridBox.y + gridBox.height + logicalCellPitch, { steps: 12 });
await expect(page.locator("[data-grid-placeholder-for]")).toHaveCount(0);
const activeGridBox = await expectBoundingBoxAsync(grid);
expect(activeGridBox.height).toBeCloseTo(gridBox.height, 1);
await expect.poll(async () => (await expectBoundingBoxAsync(grid)).height).toBeGreaterThan(gridBox.height);
await page.mouse.move(gridBox.x + gridBox.width / 2, gridBox.y + gridBox.height + logicalCellPitch / 2, {
steps: 12,
});
await expect(page.locator("[data-grid-placeholder-for]")).toHaveCount(1);
const activeGridBox = await expectBoundingBoxAsync(grid);
expect(activeGridBox.height).toBeGreaterThan(gridBox.height);
await page.mouse.move(canvasBox.x + canvasBox.width + 1000, start.y, { steps: 12 });
const overlayBox = await expectBoundingBoxAsync(page.locator(".board-grid-drag-overlay[data-dnd-dragging]"));
expect(overlayBox.x + overlayBox.width).toBeLessThanOrEqual(canvasBox.x + canvasBox.width + 1);
await page.keyboard.press("Escape");
await page.mouse.up();
await expect(page.locator("body")).not.toHaveAttribute("data-board-grid-interacting");
await expect.poll(async () => await readEditorGridSnapshotAsync(grid)).toEqual(snapshot);
await locator.evaluate((element) => element.scrollIntoView({ block: "center", behavior: "instant" }));
await expectGridEntryGeometrySettledAsync(locator);
};
const expectInvalidNestedDropRestoresSnapshotAsync = async (
@@ -2036,18 +2068,6 @@ const expectUniformVisualScaleAsync = async (locator: Locator) => {
expect(scale.horizontal).toBeCloseTo(scale.vertical, 3);
};
const expectHorizontalOverflowAsync = async (locator: Locator) => {
const dimensions = await locator.evaluate((element) => {
const htmlElement = element as HTMLElement;
return {
clientWidth: htmlElement.clientWidth,
scrollWidth: htmlElement.scrollWidth,
};
});
expect(dimensions.scrollWidth).toBeGreaterThan(dimensions.clientWidth);
};
const expectDocumentNotHorizontallyScrollableAsync = async (page: Page) => {
await expect
.poll(