Skip to content

Commit b4a4ada

Browse files
leonardomendixgjulivan
authored andcommitted
test: add new unit tests
1 parent 2407880 commit b4a4ada

2 files changed

Lines changed: 418 additions & 0 deletions

File tree

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { renderHook } from "@testing-library/react";
2+
import { ObjectItem } from "mendix";
3+
import { dynamic, listReference } from "@mendix/widget-plugin-test-utils";
4+
import { TreeNodeState } from "../../../common/TreeNodeState";
5+
import { TreeConfigRef, useIncrementalTreeData } from "../useIncrementalTreeData";
6+
7+
function makeItem(id: string): ObjectItem {
8+
return { id } as ObjectItem;
9+
}
10+
11+
function makeConfig(overrides: Partial<TreeConfigRef> = {}): TreeConfigRef {
12+
return {
13+
headerType: "text",
14+
headerCaption: {
15+
get: jest.fn((item: ObjectItem) => dynamic(String(item.id)))
16+
} as any,
17+
headerContent: undefined,
18+
parentAssociation: listReference(b =>
19+
b.withGet((_item: ObjectItem) => dynamic(undefined as unknown as ObjectItem)).build()
20+
),
21+
startExpanded: false,
22+
...overrides
23+
};
24+
}
25+
26+
function makeConfigWithParentMap(
27+
parentMap: Record<string, string | undefined>,
28+
overrides: Partial<TreeConfigRef> = {}
29+
): TreeConfigRef {
30+
return {
31+
headerType: "text",
32+
headerCaption: {
33+
get: jest.fn((item: ObjectItem) => dynamic(String(item.id)))
34+
} as any,
35+
headerContent: undefined,
36+
parentAssociation: listReference(b =>
37+
b
38+
.withGet((item: ObjectItem) => {
39+
const parentId = parentMap[String(item.id)];
40+
return parentId ? dynamic(makeItem(parentId)) : dynamic(undefined as unknown as ObjectItem);
41+
})
42+
.build()
43+
),
44+
startExpanded: false,
45+
...overrides
46+
};
47+
}
48+
49+
describe("useIncrementalTreeData", () => {
50+
describe("basic tree building", () => {
51+
it("returns empty array when items is undefined", () => {
52+
const config = makeConfig();
53+
const { result } = renderHook(() => useIncrementalTreeData(undefined, config));
54+
expect(result.current).toEqual([]);
55+
});
56+
57+
it("places items without a parent as roots", () => {
58+
const items = [makeItem("a"), makeItem("b"), makeItem("c")];
59+
const config = makeConfig();
60+
const { result } = renderHook(() => useIncrementalTreeData(items, config));
61+
expect(result.current).toHaveLength(3);
62+
expect(result.current.map(n => n.id)).toEqual(["a", "b", "c"]);
63+
});
64+
65+
it("nests children under their parent", () => {
66+
const items = [makeItem("parent"), makeItem("child")];
67+
const config = makeConfigWithParentMap({ parent: undefined, child: "parent" });
68+
const { result } = renderHook(() => useIncrementalTreeData(items, config));
69+
expect(result.current).toHaveLength(1);
70+
expect(result.current[0].id).toBe("parent");
71+
expect(result.current[0].children).toHaveLength(1);
72+
expect(result.current[0].children[0].id).toBe("child");
73+
});
74+
75+
it("assigns COLLAPSED_WITH_JS when startExpanded is false", () => {
76+
const items = [makeItem("a")];
77+
const config = makeConfig({ startExpanded: false });
78+
const { result } = renderHook(() => useIncrementalTreeData(items, config));
79+
expect(result.current[0].treeNodeState).toBe(TreeNodeState.COLLAPSED_WITH_JS);
80+
});
81+
82+
it("assigns EXPANDED when startExpanded is true", () => {
83+
const items = [makeItem("a")];
84+
const config = makeConfig({ startExpanded: true });
85+
const { result } = renderHook(() => useIncrementalTreeData(items, config));
86+
expect(result.current[0].treeNodeState).toBe(TreeNodeState.EXPANDED);
87+
});
88+
});
89+
90+
describe("out-of-order arrival (child before parent)", () => {
91+
it("re-parents orphan node when its parent arrives later", () => {
92+
const child = makeItem("child");
93+
const parent = makeItem("parent");
94+
const config = makeConfigWithParentMap({ child: "parent", parent: undefined });
95+
96+
const { result, rerender } = renderHook(
97+
({ items }: { items: ObjectItem[] }) => useIncrementalTreeData(items, config),
98+
{ initialProps: { items: [child] } }
99+
);
100+
101+
// child has no parent yet — appears as root
102+
expect(result.current).toHaveLength(1);
103+
expect(result.current[0].id).toBe("child");
104+
105+
// parent arrives
106+
rerender({ items: [child, parent] });
107+
108+
expect(result.current).toHaveLength(1);
109+
expect(result.current[0].id).toBe("parent");
110+
expect(result.current[0].children[0].id).toBe("child");
111+
});
112+
});
113+
114+
describe("cycle detection", () => {
115+
it("places self-referencing node as a root instead of looping", () => {
116+
const item = makeItem("self");
117+
const items = [item];
118+
const config = makeConfigWithParentMap({ self: "self" });
119+
const { result } = renderHook(() => useIncrementalTreeData(items, config));
120+
expect(result.current).toHaveLength(1);
121+
expect(result.current[0].id).toBe("self");
122+
expect(result.current[0].children).toHaveLength(0);
123+
});
124+
});
125+
126+
describe("removed items trigger rebuild", () => {
127+
it("removes node when item disappears from the list", () => {
128+
const config = makeConfig();
129+
130+
const { result, rerender } = renderHook(
131+
({ items }: { items: ObjectItem[] }) => useIncrementalTreeData(items, config),
132+
{ initialProps: { items: [makeItem("a"), makeItem("b"), makeItem("c")] } }
133+
);
134+
135+
expect(result.current).toHaveLength(3);
136+
137+
rerender({ items: [makeItem("a"), makeItem("c")] });
138+
139+
expect(result.current).toHaveLength(2);
140+
expect(result.current.map(n => n.id)).toEqual(["a", "c"]);
141+
});
142+
});
143+
144+
describe("incremental updates (items added without full rebuild)", () => {
145+
it("adds new node on next render", () => {
146+
const config = makeConfig();
147+
148+
const { result, rerender } = renderHook(
149+
({ items }: { items: ObjectItem[] }) => useIncrementalTreeData(items, config),
150+
{ initialProps: { items: [makeItem("a")] } }
151+
);
152+
153+
expect(result.current).toHaveLength(1);
154+
155+
rerender({ items: [makeItem("a"), makeItem("b")] });
156+
157+
expect(result.current).toHaveLength(2);
158+
expect(result.current.map(n => n.id)).toEqual(["a", "b"]);
159+
});
160+
161+
it("does not duplicate a child already placed under its parent", () => {
162+
const items = [makeItem("parent"), makeItem("child")];
163+
const config = makeConfigWithParentMap({ parent: undefined, child: "parent" });
164+
165+
const { result, rerender } = renderHook(
166+
({ items }: { items: ObjectItem[] }) => useIncrementalTreeData(items, config),
167+
{ initialProps: { items } }
168+
);
169+
170+
expect(result.current[0].children).toHaveLength(1);
171+
172+
// same items arrive again
173+
rerender({ items: [makeItem("parent"), makeItem("child")] });
174+
175+
expect(result.current[0].children).toHaveLength(1);
176+
});
177+
});
178+
179+
describe("deep nesting", () => {
180+
it("builds a three-level tree correctly", () => {
181+
const grandparent = makeItem("gp");
182+
const parent = makeItem("p");
183+
const child = makeItem("c");
184+
const config = makeConfigWithParentMap({ gp: undefined, p: "gp", c: "p" });
185+
186+
const items = [grandparent, parent, child];
187+
const { result } = renderHook(() => useIncrementalTreeData(items, config));
188+
189+
expect(result.current).toHaveLength(1);
190+
expect(result.current[0].id).toBe("gp");
191+
expect(result.current[0].children[0].id).toBe("p");
192+
expect(result.current[0].children[0].children[0].id).toBe("c");
193+
});
194+
});
195+
196+
describe("config change triggers full rebuild", () => {
197+
it("rebuilds tree when headerType changes", () => {
198+
const items = [makeItem("a")];
199+
const config1 = makeConfig({ headerType: "text" });
200+
const config2 = makeConfig({ headerType: "custom" });
201+
202+
const { result, rerender } = renderHook(
203+
({ config }: { config: TreeConfigRef }) => useIncrementalTreeData(items, config),
204+
{ initialProps: { config: config1 } }
205+
);
206+
207+
expect(result.current).toHaveLength(1);
208+
209+
rerender({ config: config2 });
210+
211+
expect(result.current).toHaveLength(1);
212+
expect(result.current[0].id).toBe("a");
213+
});
214+
215+
it("rebuilds tree when parentAssociation changes", () => {
216+
const items = [makeItem("a")];
217+
const config1 = makeConfig();
218+
const config2 = makeConfigWithParentMap({ a: undefined });
219+
220+
const { result, rerender } = renderHook(
221+
({ config }: { config: TreeConfigRef }) => useIncrementalTreeData(items, config),
222+
{ initialProps: { config: config1 } }
223+
);
224+
225+
expect(result.current).toHaveLength(1);
226+
227+
rerender({ config: config2 });
228+
229+
expect(result.current).toHaveLength(1);
230+
});
231+
});
232+
});

0 commit comments

Comments
 (0)