Files
homarr/apps/nextjs/src/components/board/mobile/mobile-layout.spec.ts
T

214 lines
9.1 KiB
TypeScript

import { describe, expect, test } from "vitest";
import { CategorySectionMockBuilder } from "../items/actions/test/mocks/category-section-mock";
import { BoardMockBuilder } from "../items/actions/test/mocks/board-mock";
import { DynamicSectionMockBuilder } from "../items/actions/test/mocks/dynamic-section-mock";
import { EmptySectionMockBuilder } from "../items/actions/test/mocks/empty-section-mock";
import { ItemMockBuilder } from "../items/actions/test/mocks/item-mock";
import {
createMobileBoardElements,
createMobileBoardItems,
getMobileRootSection,
getMobileSectionAnchorId,
} from "./mobile-layout";
describe("createMobileBoardItems", () => {
test("selects the first root section in visual order", () => {
const board = new BoardMockBuilder().build();
board.sections.push(
new EmptySectionMockBuilder({ id: "stored-first", xOffset: 0, yOffset: 1 }).build(),
new EmptySectionMockBuilder({ id: "visually-first", xOffset: 1, yOffset: 0 }).build(),
new EmptySectionMockBuilder({ id: "visually-second", xOffset: 2, yOffset: 0 }).build(),
);
expect(getMobileRootSection(board).id).toBe("visually-first");
});
test("flattens categories and dynamic sections in visual order", () => {
const board = new BoardMockBuilder().build();
const desktopLayoutId = board.layouts.at(0)?.id;
if (!desktopLayoutId) throw new Error("Expected a desktop layout");
const rootSection = new EmptySectionMockBuilder({ id: "root", yOffset: 0 }).build();
const category = new CategorySectionMockBuilder({ id: "category", yOffset: 1, collapsed: true }).build();
const dynamicSection = new DynamicSectionMockBuilder({ id: "dynamic" })
.addLayout({ layoutId: desktopLayoutId, parentSectionId: rootSection.id, xOffset: 0, yOffset: 1 })
.build();
const nestedDynamicSection = new DynamicSectionMockBuilder({ id: "nested-dynamic" })
.addLayout({ layoutId: desktopLayoutId, parentSectionId: dynamicSection.id, xOffset: 0, yOffset: 1 })
.build();
board.sections.push(rootSection, category, dynamicSection, nestedDynamicSection);
board.items.push(
new ItemMockBuilder({ id: "second" })
.addLayout({ layoutId: desktopLayoutId, sectionId: rootSection.id, xOffset: 1, yOffset: 0 })
.build(),
new ItemMockBuilder({ id: "first" })
.addLayout({
layoutId: desktopLayoutId,
sectionId: rootSection.id,
xOffset: 0,
yOffset: 0,
width: 8,
height: 6,
})
.build(),
new ItemMockBuilder({ id: "inside-dynamic" })
.addLayout({ layoutId: desktopLayoutId, sectionId: dynamicSection.id })
.build(),
new ItemMockBuilder({ id: "inside-nested-dynamic" })
.addLayout({ layoutId: desktopLayoutId, sectionId: nestedDynamicSection.id })
.build(),
new ItemMockBuilder({ id: "inside-collapsed-category" })
.addLayout({ layoutId: desktopLayoutId, sectionId: category.id })
.build(),
);
const result = createMobileBoardItems(board, desktopLayoutId);
expect(result.map((item) => item.id)).toEqual([
"first",
"second",
"inside-dynamic",
"inside-nested-dynamic",
"inside-collapsed-category",
]);
expect(result[0]).toMatchObject({ width: 2, height: 3, xOffset: 0, yOffset: 0 });
});
test("uses the desktop layout and keeps orphaned items", () => {
const board = new BoardMockBuilder().build();
const legacyLayoutId = board.layouts.at(0)?.id;
if (!legacyLayoutId) throw new Error("Expected a legacy layout");
const desktopLayoutId = "desktop";
board.layouts.push({ id: desktopLayoutId, name: "Large", columnCount: 16, breakpoint: 1200 });
board.sections.push(new EmptySectionMockBuilder({ id: "root" }).build());
board.items.push(
new ItemMockBuilder({ id: "first" })
.addLayout({ layoutId: legacyLayoutId, sectionId: "root", yOffset: 2 })
.addLayout({ layoutId: desktopLayoutId, sectionId: "missing-section", yOffset: 0, width: 0, height: 0 })
.build(),
new ItemMockBuilder({ id: "second" })
.addLayout({ layoutId: legacyLayoutId, sectionId: "root", yOffset: 0 })
.addLayout({ layoutId: desktopLayoutId, sectionId: "root", yOffset: 1 })
.build(),
);
const result = createMobileBoardItems(board, desktopLayoutId);
expect(result.map((item) => item.id)).toEqual(["second", "first"]);
expect(result[1]).toMatchObject({ width: 1, height: 1 });
});
test("emits named category and dynamic section headings in visual order", () => {
const board = new BoardMockBuilder().build();
const desktopLayoutId = board.layouts.at(0)?.id;
if (!desktopLayoutId) throw new Error("Expected a desktop layout");
const category = new CategorySectionMockBuilder({ id: "category", name: " Services ", yOffset: 0 }).build();
const dynamicSection = new DynamicSectionMockBuilder({
id: "dynamic",
options: { title: "Monitoring", borderColor: "", customCssClasses: [] },
})
.addLayout({ layoutId: desktopLayoutId, parentSectionId: category.id, xOffset: 1, yOffset: 0 })
.build();
const nestedDynamicSection = new DynamicSectionMockBuilder({
id: "nested-dynamic",
options: { title: "Details", borderColor: "", customCssClasses: [] },
})
.addLayout({ layoutId: desktopLayoutId, parentSectionId: dynamicSection.id, xOffset: 0, yOffset: 1 })
.build();
board.sections.push(category, dynamicSection, nestedDynamicSection);
board.items.push(
new ItemMockBuilder({ id: "before-dynamic" })
.addLayout({ layoutId: desktopLayoutId, sectionId: category.id, xOffset: 0, yOffset: 0 })
.build(),
new ItemMockBuilder({ id: "inside-dynamic" })
.addLayout({ layoutId: desktopLayoutId, sectionId: dynamicSection.id, xOffset: 0, yOffset: 0 })
.build(),
new ItemMockBuilder({ id: "inside-nested" })
.addLayout({ layoutId: desktopLayoutId, sectionId: nestedDynamicSection.id })
.build(),
);
const result = createMobileBoardElements(board, desktopLayoutId);
expect(
result.map((element) => (element.type === "sectionHeading" ? `heading:${element.title}` : `item:${element.id}`)),
).toEqual([
"heading:Services",
"item:before-dynamic",
"heading:Monitoring",
"item:inside-dynamic",
"heading:Details",
"item:inside-nested",
]);
expect(result[0]).toMatchObject({
type: "sectionHeading",
sectionId: category.id,
anchorId: getMobileSectionAnchorId(category.id),
headingLevel: 2,
});
expect(result[2]).toMatchObject({ type: "sectionHeading", headingLevel: 3 });
expect(result[4]).toMatchObject({ type: "sectionHeading", headingLevel: 4 });
});
test("omits headings for empty and unnamed sections while preserving their items", () => {
const board = new BoardMockBuilder().build();
const desktopLayoutId = board.layouts.at(0)?.id;
if (!desktopLayoutId) throw new Error("Expected a desktop layout");
const emptyNamedCategory = new CategorySectionMockBuilder({
id: "empty-category",
name: "Empty",
yOffset: 0,
}).build();
const unnamedCategory = new CategorySectionMockBuilder({ id: "unnamed-category", name: " ", yOffset: 1 }).build();
const unnamedDynamicSection = new DynamicSectionMockBuilder({ id: "unnamed-dynamic" })
.addLayout({ layoutId: desktopLayoutId, parentSectionId: unnamedCategory.id })
.build();
board.sections.push(emptyNamedCategory, unnamedCategory, unnamedDynamicSection);
board.items.push(
new ItemMockBuilder({ id: "inside-unnamed" })
.addLayout({ layoutId: desktopLayoutId, sectionId: unnamedDynamicSection.id })
.build(),
new ItemMockBuilder({ id: "orphan" })
.addLayout({ layoutId: desktopLayoutId, sectionId: "missing-section" })
.build(),
);
const result = createMobileBoardElements(board, desktopLayoutId);
expect(result.map((element) => element.type)).toEqual(["item", "item"]);
expect(result.map((element) => element.id)).toEqual(["inside-unnamed", "orphan"]);
});
test("does not skip heading levels when unnamed sections are flattened", () => {
const board = new BoardMockBuilder().build();
const desktopLayoutId = board.layouts.at(0)?.id;
if (!desktopLayoutId) throw new Error("Expected a desktop layout");
const unnamedCategory = new CategorySectionMockBuilder({ id: "category", name: " " }).build();
const namedDynamicSection = new DynamicSectionMockBuilder({
id: "dynamic",
options: { title: "Services", borderColor: "", customCssClasses: [] },
})
.addLayout({ layoutId: desktopLayoutId, parentSectionId: unnamedCategory.id })
.build();
board.sections.push(unnamedCategory, namedDynamicSection);
board.items.push(
new ItemMockBuilder({ id: "item" })
.addLayout({ layoutId: desktopLayoutId, sectionId: namedDynamicSection.id })
.build(),
);
expect(createMobileBoardElements(board, desktopLayoutId)[0]).toMatchObject({
type: "sectionHeading",
title: "Services",
headingLevel: 2,
});
});
});