fix: address board grid review

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-14 11:07:06 +02:00
1 parent cddae6719e
commit 28ac23e66b
51 files changed
+647 -278

No files matched your search

@@ -92,6 +92,8 @@ describe("fixed grid item behavior", () => {
mocks.editMode = true;
mocks.maxRowCount = null;
getWeatherMock().advancedOptions.title = null;
getClockMock().xOffset = 1;
getClockMock().yOffset = 0;
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
@@ -200,6 +202,21 @@ describe("fixed grid item behavior", () => {
});
});
test("stops keyboard editing when the rendered grid state is invalid", () => {
getClockMock().xOffset = 0;
renderWeather(root);
const entry = getEditorEntry(container);
startKeyboardEditing(entry);
act(() => entry.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowRight", bubbles: true })));
expect(entry.dataset.keyboardEditing).toBe("false");
expect(mocks.commitSectionGrid).not.toHaveBeenCalled();
expect(mocks.announce).toHaveBeenLastCalledWith(
"widget.weather.name, column 1, row 1: item.moveResize.keyboard.stopped",
);
});
test("uses the whole tile as the focusable move surface with a visual drag affordance", () => {
getWeatherMock().advancedOptions.title = " Balcony forecast ";
renderWeather(root);
@@ -286,6 +303,12 @@ const getWeatherMock = () => {
return item;
};
const getClockMock = () => {
const item = mocks.items[1];
if (!item) throw new Error("Expected the clock item mock to exist");
return item;
};
const getEditorEntry = (container: HTMLElement) => {
const entry = container.querySelector<HTMLDivElement>("[data-editor-grid-entry]");
if (!entry) throw new Error("Expected the fixed grid item to render an editor entry");
@@ -0,0 +1,26 @@
import { describe, expect, test } from "vitest";
import { getGridDepth } from "../grid-depth";
describe("grid depth", () => {
test("follows the registered parent chain for deeply nested grids", () => {
const parents = new Map<string, string | null>([
["root", null],
["first", "root"],
["second", "first"],
["third", "second"],
]);
expect(getGridDepth("root", parents)).toBe(0);
expect(getGridDepth("third", parents)).toBe(3);
});
test("stops safely when malformed parent data contains a cycle", () => {
const parents = new Map<string, string | null>([
["first", "second"],
["second", "first"],
]);
expect(getGridDepth("first", parents)).toBe(1);
});
});
@@ -12,7 +12,7 @@ describe("board grid import boundary", () => {
test("keeps every current dnd-kit runtime import inside the edit-only module", () => {
const runtimeImports = getSourceFiles(nextjsSourceDirectory)
.flatMap(getRuntimeImports)
.filter(({ specifier }) => CURRENT_DND_KIT_PACKAGES.has(specifier))
.filter(({ specifier }) => isCurrentDndKitImport(specifier))
.map(({ filePath, kind, specifier }) => ({
file: toSourceRelativePath(filePath),
kind,
@@ -40,6 +40,11 @@ describe("board grid import boundary", () => {
kind: "static",
specifier: "@dnd-kit/dom",
},
{
file: "components/board/sections/grid/grid-editor.tsx",
kind: "static",
specifier: "@dnd-kit/dom/utilities",
},
{
file: "components/board/sections/grid/grid-editor.tsx",
kind: "static",
@@ -104,7 +109,10 @@ describe("board grid import boundary", () => {
});
});
const CURRENT_DND_KIT_PACKAGES = new Set(["@dnd-kit/abstract", "@dnd-kit/collision", "@dnd-kit/dom", "@dnd-kit/react"]);
const CURRENT_DND_KIT_PACKAGES = ["@dnd-kit/abstract", "@dnd-kit/collision", "@dnd-kit/dom", "@dnd-kit/react"];
const isCurrentDndKitImport = (specifier: string) =>
CURRENT_DND_KIT_PACKAGES.some((packageName) => specifier === packageName || specifier.startsWith(`${packageName}/`));
interface RuntimeImport {
filePath: string;
@@ -0,0 +1,90 @@
import { act, createElement, useLayoutEffect } from "react";
import { createRoot } from "react-dom/client";
import type { Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { BoardGridPortalHost, useBoardGridPortalHost } from "../grid-portal-host";
const mocks = vi.hoisted(() => ({
board: {
items: [{ id: "weather", layouts: [{ layoutId: "desktop", sectionId: "root" }] }],
sections: [],
},
}));
vi.mock("@homarr/auth/client", () => ({
useIntegrations: () => undefined,
}));
vi.mock("@homarr/boards/context", () => ({
useCurrentLayout: () => "desktop",
useRequiredBoard: () => mocks.board,
}));
describe("board grid portal host", () => {
let container: HTMLDivElement;
let root: Root;
beforeEach(() => {
mocks.board = {
items: [{ id: "weather", layouts: [{ layoutId: "desktop", sectionId: "root" }] }],
sections: [],
};
container = document.createElement("div");
document.body.append(container);
root = createRoot(container);
});
afterEach(() => {
act(() => root.unmount());
container.remove();
});
test("keeps a portal across relocation and prunes it after permanent deletion", () => {
const acquired: HTMLElement[] = [];
renderHost(root, true, 0, acquired);
expect(getContainerCount(container)).toBe("1");
mocks.board = {
items: [{ id: "weather", layouts: [{ layoutId: "desktop", sectionId: "other" }] }],
sections: [],
};
renderHost(root, true, 1, acquired);
expect(getContainerCount(container)).toBe("1");
expect(new Set(acquired).size).toBe(1);
mocks.board = { items: [], sections: [] };
renderHost(root, false, 2, acquired);
expect(getContainerCount(container)).toBe("0");
});
});
const renderHost = (root: Root, acquire: boolean, revision: number, acquired: HTMLElement[]) => {
act(() => {
root.render(
createElement(BoardGridPortalHost, null, createElement(PortalRegistryProbe, { acquire, revision, acquired })),
);
});
};
const PortalRegistryProbe = ({
acquire,
revision,
acquired,
}: {
acquire: boolean;
revision: number;
acquired: HTMLElement[];
}) => {
const portalHost = useBoardGridPortalHost();
useLayoutEffect(() => {
if (acquire) acquired.push(portalHost.acquireContainer("weather"));
}, [acquire, acquired, portalHost, revision]);
return createElement("span", { "data-container-count": String(portalHost.containers.size) });
};
const getContainerCount = (container: HTMLElement) =>
container.querySelector<HTMLElement>("[data-container-count]")?.dataset.containerCount;