11import { render , screen , fireEvent } from "@testing-library/react" ;
22import "@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" ;
412import { createElement } from "react" ;
513import { TreeNodeContainerProps } from "../../../../typings/TreeNodeProps" ;
614import { 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