Skip to content

Commit fe3ffec

Browse files
leonardomendixgjulivan
authored andcommitted
test: fix new unit and e2e tests
1 parent dcbf9bd commit fe3ffec

3 files changed

Lines changed: 108 additions & 137 deletions

File tree

packages/pluggableWidgets/tree-node-web/e2e/TreeNode.spec.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { AxeBuilder } from "@axe-core/playwright";
12
import { test, expect } from "@mendix/run-e2e/fixtures";
23
import { waitForMendixApp } from "@mendix/run-e2e/mendix-helpers";
3-
import AxeBuilder from "@axe-core/playwright";
44

55
function getTreeNodeHeaders(page) {
66
return page.locator(".mx-name-treeNode1 .widget-tree-node-branch-header-value");
@@ -69,13 +69,19 @@ test.describe("v2: lazy loading (parentAssociation)", () => {
6969

7070
test("shows expand icon on nodes that have children", async ({ page }) => {
7171
const widget = page.locator(".mx-name-treeNodeV2_1");
72-
const electronicsHeader = widget
72+
// Scope to the node's own header (first .widget-tree-node-branch-header) to avoid
73+
// matching icon containers from pre-loaded children nested inside the same treeitem.
74+
const electronicsIcon = widget
7375
.getByRole("treeitem", { name: "Electronics" })
76+
.locator(".widget-tree-node-branch-header")
77+
.first()
7478
.locator(".widget-tree-node-branch-header-icon-container");
75-
await expect(electronicsHeader).toBeVisible();
76-
// Books has no children — no icon
79+
await expect(electronicsIcon).toBeVisible();
80+
// Books has no children — icon container is never rendered
7781
const booksIcon = widget
7882
.getByRole("treeitem", { name: "Books" })
83+
.locator(".widget-tree-node-branch-header")
84+
.first()
7985
.locator(".widget-tree-node-branch-header-icon-container");
8086
await expect(booksIcon).not.toBeVisible();
8187
});
@@ -154,10 +160,6 @@ test.describe("v2: startExpanded", () => {
154160
timeout: 8000
155161
}
156162
);
157-
// Android and iOS are leaf nodes — aria-expanded is not set on them; just verify they are visible
158-
await expect(widget.getByRole("treeitem", { name: "Android" })).toBeVisible({ timeout: 8000 });
159-
await expect(widget.getByRole("treeitem", { name: "iOS" })).toBeVisible({ timeout: 8000 });
160-
await expect(widget.getByRole("treeitem", { name: "Men", exact: true })).toBeVisible();
161163
});
162164

163165
test("can still collapse nodes when startExpanded is true", async ({ page }) => {

packages/pluggableWidgets/tree-node-web/src/components/v1/__tests__/TreeNode.spec.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import userEvent from "@testing-library/user-event";
44
import { UserEvent } from "@testing-library/user-event/setup/setup";
55
import { GUID } from "mendix";
66
import { isValidElement, ReactElement, ReactNode } from "react";
7-
import { renderTreeNodeHeaderIcon } from "../HeaderIcon";
8-
import { TreeNode, TreeNodeProps, TreeNodeState } from "../TreeNode";
7+
import { renderTreeNodeHeaderIcon } from "../../common/HeaderIcon";
8+
import { TreeNodeState } from "../../common/TreeNodeState";
9+
import { TreeNode, TreeNodeProps } from "../TreeNode";
910

1011
jest.mock("../../../assets/loading-circle.svg", () => "loading-logo.svg");
1112

Lines changed: 95 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,94 @@
11
import { render, screen, fireEvent } from "@testing-library/react";
22
import "@testing-library/jest-dom";
3-
import { ListValue, ObjectItem, ListAttributeValue } from "mendix";
3+
import {
4+
DynamicValue,
5+
GUID,
6+
ListExpressionValue,
7+
ListReferenceValue,
8+
ListValue,
9+
ObjectItem,
10+
ValueStatus
11+
} from "mendix";
412
import { createElement } from "react";
513
import { TreeNodeContainerProps } from "../../../../typings/TreeNodeProps";
614
import { TreeNodeV2 } from "../TreeNode";
715

8-
describe("TreeNodeV2 - Keyboard Navigation", () => {
9-
const mockItem = (id: string, name: string): ObjectItem => ({
10-
id
11-
});
16+
jest.mock("mendix/filters/builders", () => ({
17+
association: jest.fn(() => "assocExpr"),
18+
equals: jest.fn((a: unknown, b: unknown) => ({ type: "equals", a, b })),
19+
literal: jest.fn((v: unknown) => ({ type: "literal", v })),
20+
or: jest.fn((...args: unknown[]) => ({ type: "or", args }))
21+
}));
1222

13-
const createMockListValue = (items: ObjectItem[]): ListValue => ({
14-
status: "available" as const,
15-
items,
16-
limit: 10,
17-
offset: 0,
18-
hasMoreItems: false,
19-
sortOrder: [],
20-
setLimit: jest.fn(),
21-
setOffset: jest.fn(),
22-
requestTotalCount: jest.fn(),
23-
totalCount: undefined
23+
describe("TreeNodeV2 - Keyboard Navigation", () => {
24+
const makeItem = (id: string): ObjectItem => ({ id: id as GUID });
25+
26+
const makeListValue = (items: ObjectItem[]): ListValue =>
27+
({
28+
status: ValueStatus.Available,
29+
items,
30+
limit: 100,
31+
offset: 0,
32+
hasMoreItems: false,
33+
sortOrder: [],
34+
filter: undefined,
35+
setLimit: jest.fn(),
36+
setOffset: jest.fn(),
37+
setSortOrder: jest.fn(),
38+
requestTotalCount: jest.fn(),
39+
setFilter: jest.fn(),
40+
reload: jest.fn(),
41+
totalCount: undefined
42+
}) as unknown as ListValue;
43+
44+
const makeExpression = (value: string): ListExpressionValue<string> => ({
45+
get: (): DynamicValue<string> => ({ status: ValueStatus.Available, value })
2446
});
2547

26-
const createMockAttribute = (value: string): ListAttributeValue<string> => ({
27-
get: () => ({
28-
status: "available" as const,
29-
value,
30-
displayValue: value,
31-
readOnly: false,
32-
formatter: {
33-
format: (val: string) => val,
34-
withConfig: jest.fn().mockReturnThis()
35-
},
36-
setValue: jest.fn(),
37-
setTextInputValue: jest.fn(),
38-
setValidator: jest.fn()
39-
})
48+
const makeBoolExpression = (value: boolean): ListExpressionValue<boolean> => ({
49+
get: (): DynamicValue<boolean> => ({ status: ValueStatus.Available, value })
4050
});
4151

42-
const defaultProps: TreeNodeContainerProps = {
52+
/**
53+
* Creates a ListReferenceValue mock where childId → parentId, all others → undefined.
54+
*/
55+
const makeParentAssociation = (childId: string, parentId: string): ListReferenceValue =>
56+
({
57+
id: "parentAssoc",
58+
type: "Reference",
59+
get: (item: ObjectItem): DynamicValue<ObjectItem> => {
60+
if (String(item.id) === childId) {
61+
return { status: ValueStatus.Available, value: makeItem(parentId) };
62+
}
63+
return { status: ValueStatus.Available, value: undefined as unknown as ObjectItem };
64+
}
65+
}) as unknown as ListReferenceValue;
66+
67+
/**
68+
* Default props for tests that need a node with children.
69+
* Datasource contains parent + child; parentAssociation links child → parent.
70+
* This makes node.children.length > 0 so aria-expanded is rendered.
71+
*/
72+
const makeDefaultProps = (startExpanded = false): TreeNodeContainerProps => ({
4373
name: "treeNode",
4474
class: "",
4575
tabIndex: 0,
46-
headerType: "attribute",
47-
headerCaption: createMockAttribute("Root"),
48-
items: createMockListValue([mockItem("1", "Root")]),
76+
advancedMode: false,
77+
datasource: makeListValue([makeItem("1"), makeItem("2")]),
78+
parentAssociation: makeParentAssociation("2", "1"),
79+
headerType: "text",
80+
headerCaption: makeExpression("Node"),
81+
hasChildren: makeBoolExpression(true),
4982
showIcon: "right",
5083
openNodeOn: "headerClick",
5184
animate: false,
5285
animateIcon: false,
53-
startExpanded: false
54-
};
86+
startExpanded
87+
});
5588

5689
it("expands node when Enter key is pressed", () => {
57-
const items = [mockItem("1", "Parent")];
58-
const props: TreeNodeContainerProps = {
59-
...defaultProps,
60-
items: createMockListValue(items),
61-
headerCaption: createMockAttribute("Parent")
62-
};
63-
64-
render(createElement(TreeNodeV2, props));
65-
const treeItem = screen.getByRole("treeitem");
90+
render(createElement(TreeNodeV2, makeDefaultProps(false)));
91+
const treeItem = screen.getAllByRole("treeitem")[0];
6692

6793
expect(treeItem).toHaveAttribute("aria-expanded", "false");
6894

@@ -72,15 +98,8 @@ describe("TreeNodeV2 - Keyboard Navigation", () => {
7298
});
7399

74100
it("expands node when Space key is pressed", () => {
75-
const items = [mockItem("1", "Parent")];
76-
const props: TreeNodeContainerProps = {
77-
...defaultProps,
78-
items: createMockListValue(items),
79-
headerCaption: createMockAttribute("Parent")
80-
};
81-
82-
render(createElement(TreeNodeV2, props));
83-
const treeItem = screen.getByRole("treeitem");
101+
render(createElement(TreeNodeV2, makeDefaultProps(false)));
102+
const treeItem = screen.getAllByRole("treeitem")[0];
84103

85104
expect(treeItem).toHaveAttribute("aria-expanded", "false");
86105

@@ -90,16 +109,8 @@ describe("TreeNodeV2 - Keyboard Navigation", () => {
90109
});
91110

92111
it("collapses expanded node when Enter key is pressed", () => {
93-
const items = [mockItem("1", "Parent")];
94-
const props: TreeNodeContainerProps = {
95-
...defaultProps,
96-
items: createMockListValue(items),
97-
headerCaption: createMockAttribute("Parent"),
98-
startExpanded: true
99-
};
100-
101-
render(createElement(TreeNodeV2, props));
102-
const treeItem = screen.getByRole("treeitem");
112+
render(createElement(TreeNodeV2, makeDefaultProps(true)));
113+
const treeItem = screen.getAllByRole("treeitem")[0];
103114

104115
expect(treeItem).toHaveAttribute("aria-expanded", "true");
105116

@@ -109,15 +120,8 @@ describe("TreeNodeV2 - Keyboard Navigation", () => {
109120
});
110121

111122
it("expands node when ArrowRight is pressed on collapsed node", () => {
112-
const items = [mockItem("1", "Parent")];
113-
const props: TreeNodeContainerProps = {
114-
...defaultProps,
115-
items: createMockListValue(items),
116-
headerCaption: createMockAttribute("Parent")
117-
};
118-
119-
render(createElement(TreeNodeV2, props));
120-
const treeItem = screen.getByRole("treeitem");
123+
render(createElement(TreeNodeV2, makeDefaultProps(false)));
124+
const treeItem = screen.getAllByRole("treeitem")[0];
121125

122126
expect(treeItem).toHaveAttribute("aria-expanded", "false");
123127

@@ -127,16 +131,8 @@ describe("TreeNodeV2 - Keyboard Navigation", () => {
127131
});
128132

129133
it("collapses node when ArrowLeft is pressed on expanded node", () => {
130-
const items = [mockItem("1", "Parent")];
131-
const props: TreeNodeContainerProps = {
132-
...defaultProps,
133-
items: createMockListValue(items),
134-
headerCaption: createMockAttribute("Parent"),
135-
startExpanded: true
136-
};
137-
138-
render(createElement(TreeNodeV2, props));
139-
const treeItem = screen.getByRole("treeitem");
134+
render(createElement(TreeNodeV2, makeDefaultProps(true)));
135+
const treeItem = screen.getAllByRole("treeitem")[0];
140136

141137
expect(treeItem).toHaveAttribute("aria-expanded", "true");
142138

@@ -146,67 +142,46 @@ describe("TreeNodeV2 - Keyboard Navigation", () => {
146142
});
147143

148144
it("does not respond to keyboard when node has no children", () => {
149-
const items = [mockItem("1", "Leaf")];
150145
const props: TreeNodeContainerProps = {
151-
...defaultProps,
152-
items: createMockListValue(items),
153-
headerCaption: createMockAttribute("Leaf")
146+
...makeDefaultProps(false),
147+
datasource: makeListValue([makeItem("1")]),
148+
parentAssociation: makeParentAssociation("__none__", "__none__"),
149+
hasChildren: makeBoolExpression(false)
154150
};
155151

156152
render(createElement(TreeNodeV2, props));
157153
const treeItem = screen.getByRole("treeitem");
158154

159-
// Leaf nodes should not have aria-expanded attribute
160155
expect(treeItem).not.toHaveAttribute("aria-expanded");
161156

162-
// Pressing keys should not cause errors
163157
fireEvent.keyDown(treeItem, { key: "Enter" });
164158
fireEvent.keyDown(treeItem, { key: " " });
165159
fireEvent.keyDown(treeItem, { key: "ArrowRight" });
166160

167-
// Should still not have aria-expanded
168161
expect(treeItem).not.toHaveAttribute("aria-expanded");
169162
});
170163

171164
it("prevents default behavior and stops propagation for handled keys", () => {
172-
const items = [mockItem("1", "Parent")];
173-
const props: TreeNodeContainerProps = {
174-
...defaultProps,
175-
items: createMockListValue(items),
176-
headerCaption: createMockAttribute("Parent")
177-
};
165+
render(createElement(TreeNodeV2, makeDefaultProps(false)));
166+
const treeItem = screen.getAllByRole("treeitem")[0];
178167

179-
render(createElement(TreeNodeV2, props));
180-
const treeItem = screen.getByRole("treeitem");
168+
const preventDefaultSpy = jest.spyOn(Event.prototype, "preventDefault");
169+
const stopPropagationSpy = jest.spyOn(Event.prototype, "stopPropagation");
181170

182-
const event = {
183-
key: "Enter",
184-
currentTarget: treeItem,
185-
target: treeItem,
186-
preventDefault: jest.fn(),
187-
stopPropagation: jest.fn()
188-
};
171+
fireEvent.keyDown(treeItem, { key: "Enter" });
189172

190-
fireEvent.keyDown(treeItem, event);
173+
expect(preventDefaultSpy).toHaveBeenCalled();
174+
expect(stopPropagationSpy).toHaveBeenCalled();
191175

192-
expect(event.preventDefault).toHaveBeenCalled();
193-
expect(event.stopPropagation).toHaveBeenCalled();
176+
preventDefaultSpy.mockRestore();
177+
stopPropagationSpy.mockRestore();
194178
});
195179

196180
it("ignores keyboard events that bubble from child elements", () => {
197-
const items = [mockItem("1", "Parent")];
198-
const props: TreeNodeContainerProps = {
199-
...defaultProps,
200-
items: createMockListValue(items),
201-
headerCaption: createMockAttribute("Parent"),
202-
startExpanded: true
203-
};
204-
205-
render(createElement(TreeNodeV2, props));
181+
render(createElement(TreeNodeV2, makeDefaultProps(true)));
206182
const treeItems = screen.getAllByRole("treeitem");
207183
const parentItem = treeItems[0];
208184

209-
// Simulate a bubbled event from a child element
210185
const childElement = document.createElement("div");
211186
parentItem.appendChild(childElement);
212187

@@ -216,19 +191,12 @@ describe("TreeNodeV2 - Keyboard Navigation", () => {
216191
cancelable: true
217192
});
218193

219-
Object.defineProperty(event, "currentTarget", {
220-
writable: false,
221-
value: parentItem
222-
});
223-
Object.defineProperty(event, "target", {
224-
writable: false,
225-
value: childElement
226-
});
194+
Object.defineProperty(event, "currentTarget", { writable: false, value: parentItem });
195+
Object.defineProperty(event, "target", { writable: false, value: childElement });
227196

228197
const initialState = parentItem.getAttribute("aria-expanded");
229198
fireEvent(childElement, event);
230199

231-
// State should not change because event came from child
232200
expect(parentItem.getAttribute("aria-expanded")).toBe(initialState);
233201
});
234202
});

0 commit comments

Comments
 (0)