fix: add item does not respect dynamic sections (#2010)

* bug: add item does not respect dynamic sections

* fix: deepsource issue
This commit is contained in:
Meier Lukas authored and GitHub committed 2025-01-21 11:00:29 +01:00
1 parent 7960a4cea1
commit d4f04da709
8 files changed
+460 -76

No files matched your search

@@ -0,0 +1,61 @@
import { describe, expect, test, vi } from "vitest";
import type { Board } from "~/app/[locale]/boards/_types";
import { createItemCallback } from "../create-item";
import * as emptyPosition from "../empty-position";
import { createDynamicSection, createEmptySection, createItem } from "./shared";
describe("item actions create-item", () => {
test("should add it to first section", () => {
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
const input = {
sections: [createEmptySection("1", 2), createEmptySection("2", 0), createEmptySection("3", 1)],
columnCount: 4,
} satisfies Pick<Board, "sections" | "columnCount">;
const result = createItemCallback({
kind: "clock",
})(input as unknown as Board);
const firstSection = result.sections.find((section) => section.id === "2");
const item = firstSection?.items.at(0);
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
expect(spy).toHaveBeenCalledWith([], input.columnCount);
});
test("should correctly pass dynamic section and items to getFirstEmptyPosition", () => {
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
const firstSection = createEmptySection("2", 0);
const expectedItem = createItem({ id: "12", xOffset: 1, yOffset: 2, width: 3, height: 2 });
firstSection.items.push(expectedItem);
const dynamicSectionInFirst = createDynamicSection({
id: "4",
parentSectionId: "2",
yOffset: 0,
xOffset: 0,
width: 2,
height: 2,
});
const input = {
sections: [
createEmptySection("1", 2),
firstSection,
createEmptySection("3", 1),
dynamicSectionInFirst,
createDynamicSection({ id: "5", parentSectionId: "3", yOffset: 1 }),
],
columnCount: 4,
} satisfies Pick<Board, "sections" | "columnCount">;
const result = createItemCallback({
kind: "clock",
})(input as unknown as Board);
const firstSectionResult = result.sections.find((section) => section.id === "2");
const item = firstSectionResult?.items.find((item) => item.id !== "12");
expect(item).toEqual(expect.objectContaining({ kind: "clock", xOffset: 5, yOffset: 5 }));
expect(spy).toHaveBeenCalledWith([expectedItem, dynamicSectionInFirst], input.columnCount);
});
});
@@ -0,0 +1,52 @@
import { describe, expect, test, vi } from "vitest";
import type { Board } from "~/app/[locale]/boards/_types";
import { duplicateItemCallback } from "../duplicate-item";
import * as emptyPosition from "../empty-position";
import { createEmptySection, createItem } from "./shared";
describe("item actions duplicate-item", () => {
test("should copy it in the same section", () => {
const spy = vi.spyOn(emptyPosition, "getFirstEmptyPosition");
spy.mockReturnValue({ xOffset: 5, yOffset: 5 });
const currentSection = createEmptySection("2", 1);
const currentItem = createItem({
id: "1",
xOffset: 1,
yOffset: 3,
width: 3,
height: 2,
kind: "minecraftServerStatus",
integrationIds: ["1"],
options: { address: "localhost" },
advancedOptions: { customCssClasses: ["test"] },
});
const otherItem = createItem({
id: "2",
});
currentSection.items.push(currentItem, otherItem);
const input = {
columnCount: 10,
sections: [createEmptySection("1", 0), currentSection, createEmptySection("3", 2)],
} satisfies Pick<Board, "sections" | "columnCount">;
const result = duplicateItemCallback({ itemId: currentItem.id })(input as unknown as Board);
const section = result.sections.find((section) => section.id === "2");
expect(section?.items.length).toBe(3);
const duplicatedItem = section?.items.find((item) => item.id !== currentItem.id && item.id !== otherItem.id);
expect(duplicatedItem).toEqual(
expect.objectContaining({
kind: "minecraftServerStatus",
xOffset: 5,
yOffset: 5,
width: 3,
height: 2,
integrationIds: ["1"],
options: { address: "localhost" },
advancedOptions: { customCssClasses: ["test"] },
}),
);
});
});
@@ -0,0 +1,138 @@
import { describe, expect, test } from "vitest";
import type { Item } from "~/app/[locale]/boards/_types";
import { getFirstEmptyPosition } from "../empty-position";
describe("get first empty position", () => {
test.each([
[[[" ", " ", " ", " "]], [1, 1], 0, 0],
[[["a", " ", " ", " "]], [1, 1], 1, 0],
[[[" ", "a", " ", " "]], [1, 1], 0, 0],
[
[
["a", "a", " ", " "],
["a", "a", " ", " "],
],
[1, 1],
2,
0,
],
[[["a", "a", "a", "a"]], [1, 1], 0, 1],
[
[
["a", "a", "a", "a"],
["a", "a", "a", "a"],
],
[1, 1],
0,
2,
],
[
[
["a", "a", " ", "b", "b"],
["a", "a", " ", "b", "b"],
],
[1, 2],
2,
0,
],
[
[
["a", "a", " ", " ", "b", "b"],
["a", "a", " ", " ", "b", "b"],
],
[2, 2],
2,
0,
],
[
[
["a", "a", " ", "d", "b", "b"],
["a", "a", "c", "e", "b", "b"],
],
[1, 1],
2,
0,
],
[
[
["a", "a", " ", " ", "b", "b"],
["a", "a", " ", "e", "b", "b"],
],
[2, 2],
0,
2,
],
])("should return the first empty position", (layout, size, expectedX, expectedY) => {
const elements = createElementsFromLayout(layout);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = getFirstEmptyPosition(elements, layout[0]!.length, undefined, { width: size[0]!, height: size[1]! });
expect(result).toEqual({ xOffset: expectedX, yOffset: expectedY });
});
test.each([
[[[" ", " "]], [1, 1], 0, 0, 1],
[[["a", " "]], [1, 1], 1, 0, 1],
[[["a", "a"]], [1, 1], undefined, undefined, 1],
[[["a", "a"]], [1, 1], 0, 1, 2],
[
[
["a", "b", " ", " "],
["a", "c", " ", "d"],
],
[2, 2],
undefined,
undefined,
3,
],
[
[
["a", "b", " ", " "],
["a", "c", " ", "d"],
],
[2, 2],
0,
2,
4,
],
[[["a", "b"]], [2, 1], 0, 1, 2],
])("should return the first empty position with limited rows", (layout, size, expectedX, expectedY, rowCount) => {
const elements = createElementsFromLayout(layout);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = getFirstEmptyPosition(elements, layout[0]!.length, rowCount, { width: size[0]!, height: size[1]! });
expect(result).toEqual(expectedX !== undefined ? { xOffset: expectedX, yOffset: expectedY } : undefined);
});
});
const createElementsFromLayout = (layout: string[][]) => {
const elements: (Pick<Item, "xOffset" | "yOffset" | "width" | "height"> & { char: string })[] = [];
for (let yOffset = 0; yOffset < layout.length; yOffset++) {
const row = layout[yOffset];
if (!row) continue;
for (let xOffset = 0; xOffset < row.length; xOffset++) {
const item = row[xOffset];
if (item === " " || !item) continue;
const existing = elements.find((element) => element.char === item);
if (existing) {
existing.height = yOffset - existing.yOffset + 1;
existing.width = xOffset - existing.xOffset + 1;
continue;
}
elements.push({
yOffset,
xOffset,
width: 1,
height: 1,
char: item,
});
}
}
return elements;
};
@@ -0,0 +1,32 @@
import type { DynamicSection, EmptySection, Item } from "~/app/[locale]/boards/_types";
export const createEmptySection = (id: string, yOffset: number): EmptySection => ({
id,
kind: "empty",
yOffset,
xOffset: 0,
items: [],
});
export const createDynamicSection = (section: Omit<Partial<DynamicSection>, "kind">): DynamicSection => ({
id: section.id ?? "0",
kind: "dynamic",
parentSectionId: section.parentSectionId ?? "0",
height: section.height ?? 1,
width: section.width ?? 1,
yOffset: section.yOffset ?? 0,
xOffset: section.xOffset ?? 0,
items: section.items ?? [],
});
export const createItem = (item: Partial<Item>): Item => ({
id: item.id ?? "0",
width: item.width ?? 1,
height: item.height ?? 1,
yOffset: item.yOffset ?? 0,
xOffset: item.xOffset ?? 0,
kind: item.kind ?? "clock",
integrationIds: item.integrationIds ?? [],
options: item.options ?? {},
advancedOptions: item.advancedOptions ?? { customCssClasses: [] },
});