@@ -3,7 +3,7 @@ import { render, screen, userEvent } from '@mongodb-js/testing-library-compass';
3
3
import { expect } from 'chai' ;
4
4
import sinon from 'sinon' ;
5
5
import HadronDocument from 'hadron-document' ;
6
- import { HadronElement } from './element' ;
6
+ import { HadronElement , getNestedKeyPathForElement } from './element' ;
7
7
import type { Element } from 'hadron-document' ;
8
8
9
9
describe ( 'HadronElement' , function ( ) {
@@ -26,6 +26,77 @@ describe('HadronElement', function () {
26
26
clipboardWriteTextStub . restore ( ) ;
27
27
} ) ;
28
28
29
+ it ( 'can add to query and then remove from query' , function ( ) {
30
+ const nestedDoc = new HadronDocument ( { user : { name : 'John' } } ) ;
31
+ const nestedElement = nestedDoc . get ( 'user' ) ! . get ( 'name' ) ! ;
32
+
33
+ // Mock onUpdateQuery callback
34
+ const mockonUpdateQuery = sinon . spy ( ) ;
35
+
36
+ // Start with empty query
37
+ const { rerender } = render (
38
+ < HadronElement
39
+ value = { nestedElement }
40
+ editable = { true }
41
+ editingEnabled = { true }
42
+ lineNumberSize = { 1 }
43
+ onAddElement = { ( ) => { } }
44
+ onUpdateQuery = { mockonUpdateQuery }
45
+ query = { { } }
46
+ />
47
+ ) ;
48
+
49
+ // Open context menu - should show "Add to query"
50
+ const elementNode = screen . getByTestId ( 'hadron-document-element' ) ;
51
+ userEvent . click ( elementNode , { button : 2 } ) ;
52
+
53
+ expect ( screen . getByText ( 'Add to query' ) ) . to . exist ;
54
+ expect ( screen . queryByText ( 'Remove from query' ) ) . to . not . exist ;
55
+
56
+ userEvent . click ( screen . getByText ( 'Add to query' ) , undefined , {
57
+ skipPointerEventsCheck : true ,
58
+ } ) ;
59
+
60
+ expect ( mockonUpdateQuery ) . to . have . been . calledWith (
61
+ 'user.name' ,
62
+ nestedElement . generateObject ( )
63
+ ) ;
64
+
65
+ // Now simulate that the field is in query
66
+ const queryWithField = {
67
+ 'user.name' : nestedElement . generateObject ( ) ,
68
+ } ;
69
+
70
+ // Re-render with updated query state
71
+ rerender (
72
+ < HadronElement
73
+ value = { nestedElement }
74
+ editable = { true }
75
+ editingEnabled = { true }
76
+ lineNumberSize = { 1 }
77
+ onAddElement = { ( ) => { } }
78
+ onUpdateQuery = { mockonUpdateQuery }
79
+ query = { queryWithField }
80
+ />
81
+ ) ;
82
+
83
+ // Open context menu again - should now show "Remove from query"
84
+ userEvent . click ( elementNode , { button : 2 } ) ;
85
+
86
+ expect ( screen . getByText ( 'Remove from query' ) ) . to . exist ;
87
+ expect ( screen . queryByText ( 'Add to query' ) ) . to . not . exist ;
88
+
89
+ userEvent . click ( screen . getByText ( 'Remove from query' ) , undefined , {
90
+ skipPointerEventsCheck : true ,
91
+ } ) ;
92
+
93
+ expect ( mockonUpdateQuery ) . to . have . been . calledTwice ;
94
+ expect ( mockonUpdateQuery . secondCall ) . to . have . been . calledWith (
95
+ 'user.name' ,
96
+ nestedElement . generateObject ( )
97
+ ) ;
98
+ } ) ;
99
+
29
100
it ( 'copies field and value when "Copy field & value" is clicked' , function ( ) {
30
101
render (
31
102
< HadronElement
@@ -117,5 +188,190 @@ describe('HadronElement', function () {
117
188
// Check that the menu item doesn't exist
118
189
expect ( screen . queryByText ( 'Open URL in browser' ) ) . to . not . exist ;
119
190
} ) ;
191
+
192
+ it ( 'does not show "Add to query" when onUpdateQuery is not provided' , function ( ) {
193
+ render (
194
+ < HadronElement
195
+ value = { element }
196
+ editable = { true }
197
+ editingEnabled = { true }
198
+ lineNumberSize = { 1 }
199
+ onAddElement = { ( ) => { } }
200
+ />
201
+ ) ;
202
+ const elementNode = screen . getByTestId ( 'hadron-document-element' ) ;
203
+ userEvent . click ( elementNode , { button : 2 } ) ;
204
+
205
+ expect ( screen . queryByText ( 'Add to query' ) ) . to . not . exist ;
206
+ } ) ;
207
+
208
+ it ( 'calls the correct parameters when "Add to query" is clicked' , function ( ) {
209
+ const nestedDoc = new HadronDocument ( { user : { name : 'John' } } ) ;
210
+ const nestedElement = nestedDoc . get ( 'user' ) ! . get ( 'name' ) ! ;
211
+ const mockonUpdateQuery = sinon . spy ( ) ;
212
+
213
+ render (
214
+ < HadronElement
215
+ value = { nestedElement }
216
+ editable = { true }
217
+ editingEnabled = { true }
218
+ lineNumberSize = { 1 }
219
+ onAddElement = { ( ) => { } }
220
+ onUpdateQuery = { mockonUpdateQuery }
221
+ query = { { } }
222
+ />
223
+ ) ;
224
+
225
+ // Open context menu and click the add to query option
226
+ const elementNode = screen . getByTestId ( 'hadron-document-element' ) ;
227
+ userEvent . click ( elementNode , { button : 2 } ) ;
228
+ userEvent . click ( screen . getByText ( 'Add to query' ) , undefined , {
229
+ skipPointerEventsCheck : true ,
230
+ } ) ;
231
+
232
+ // Verify that toggleQueryFilter was called with the nested field path and element's generated object
233
+ expect ( mockonUpdateQuery ) . to . have . been . calledWith (
234
+ 'user.name' ,
235
+ nestedElement . generateObject ( )
236
+ ) ;
237
+ } ) ;
238
+ } ) ;
239
+
240
+ describe ( 'getNestedKeyPathForElement' , function ( ) {
241
+ it ( 'returns the field name for a top-level field' , function ( ) {
242
+ const doc = new HadronDocument ( { field : 'value' } ) ;
243
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
244
+ const element = doc . elements . at ( 0 ) ! ;
245
+
246
+ const result = getNestedKeyPathForElement ( element ) ;
247
+
248
+ expect ( result ) . to . equal ( 'field' ) ;
249
+ } ) ;
250
+
251
+ it ( 'returns dot notation path for nested object fields' , function ( ) {
252
+ const doc = new HadronDocument ( {
253
+ user : {
254
+ profile : {
255
+ name : 'John' ,
256
+ } ,
257
+ } ,
258
+ } ) ;
259
+ const nameElement = doc . get ( 'user' ) ! . get ( 'profile' ) ! . get ( 'name' ) ! ;
260
+
261
+ const result = getNestedKeyPathForElement ( nameElement ) ;
262
+
263
+ expect ( result ) . to . equal ( 'user.profile.name' ) ;
264
+ } ) ;
265
+
266
+ it ( 'skips array indices in the path' , function ( ) {
267
+ const doc = new HadronDocument ( {
268
+ items : [ { name : 'item1' } , { name : 'item2' } ] ,
269
+ } ) ;
270
+ const nameElement = doc . get ( 'items' ) ! . elements ! . at ( 0 ) ! . get ( 'name' ) ! ;
271
+
272
+ const result = getNestedKeyPathForElement ( nameElement ) ;
273
+
274
+ expect ( result ) . to . equal ( 'items.name' ) ;
275
+ } ) ;
276
+
277
+ it ( 'handles mixed nesting with arrays and objects' , function ( ) {
278
+ const doc = new HadronDocument ( {
279
+ orders : [
280
+ {
281
+ items : [ { product : { name : 'Widget' } } ] ,
282
+ } ,
283
+ ] ,
284
+ } ) ;
285
+ const nameElement = doc
286
+ . get ( 'orders' ) !
287
+ . elements ! . at ( 0 ) !
288
+ . get ( 'items' ) !
289
+ . elements ! . at ( 0 ) !
290
+ . get ( 'product' ) !
291
+ . get ( 'name' ) ! ;
292
+
293
+ const result = getNestedKeyPathForElement ( nameElement ) ;
294
+
295
+ expect ( result ) . to . equal ( 'orders.items.product.name' ) ;
296
+ } ) ;
297
+
298
+ it ( 'handles array elements at the top level' , function ( ) {
299
+ const doc = new HadronDocument ( {
300
+ items : [ { name : 'item1' } , { name : 'item2' } ] ,
301
+ } ) ;
302
+ const nameElement = doc . elements . get ( 'items' ) ! . at ( 0 ) ! . get ( 'name' ) ! ;
303
+
304
+ const result = getNestedKeyPathForElement ( nameElement ) ;
305
+
306
+ expect ( result ) . to . equal ( 'items.name' ) ;
307
+ } ) ;
308
+
309
+ it ( 'handles deeply nested objects' , function ( ) {
310
+ const doc = new HadronDocument ( {
311
+ level1 : {
312
+ level2 : {
313
+ level3 : {
314
+ level4 : {
315
+ value : 'deep' ,
316
+ } ,
317
+ } ,
318
+ } ,
319
+ } ,
320
+ } ) ;
321
+ const valueElement = doc
322
+ . get ( 'level1' ) !
323
+ . get ( 'level2' ) !
324
+ . get ( 'level3' ) !
325
+ . get ( 'level4' ) !
326
+ . get ( 'value' ) ! ;
327
+
328
+ const result = getNestedKeyPathForElement ( valueElement ) ;
329
+
330
+ expect ( result ) . to . equal ( 'level1.level2.level3.level4.value' ) ;
331
+ } ) ;
332
+
333
+ it ( 'handles field names with special characters' , function ( ) {
334
+ const doc = new HadronDocument ( {
335
+ 'field-with-dashes' : {
336
+ field_with_underscores : {
337
+ 'field.with.dots' : 'value' ,
338
+ } ,
339
+ } ,
340
+ } ) ;
341
+ const dotsElement = doc
342
+ . get ( 'field-with-dashes' ) !
343
+ . get ( 'field_with_underscores' ) !
344
+ . get ( 'field.with.dots' ) ! ;
345
+
346
+ const result = getNestedKeyPathForElement ( dotsElement ) ;
347
+
348
+ expect ( result ) . to . equal (
349
+ 'field-with-dashes.field_with_underscores.field.with.dots'
350
+ ) ;
351
+ } ) ;
352
+
353
+ it ( 'handles numeric field names' , function ( ) {
354
+ const doc = new HadronDocument ( {
355
+ 123 : {
356
+ 456 : 'value' ,
357
+ } ,
358
+ } ) ;
359
+ const numericElement = doc . get ( '123' ) ! . get ( '456' ) ! ;
360
+
361
+ const result = getNestedKeyPathForElement ( numericElement ) ;
362
+
363
+ expect ( numericElement . value ) . to . equal ( 'value' ) ;
364
+ expect ( result ) . to . equal ( '123.456' ) ;
365
+ } ) ;
366
+
367
+ it ( 'handles empty object elements' , function ( ) {
368
+ const doc = new HadronDocument ( { emptyObj : { } } ) ;
369
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
370
+ const emptyObjElement = doc . elements . at ( 0 ) ! ;
371
+
372
+ const result = getNestedKeyPathForElement ( emptyObjElement ) ;
373
+
374
+ expect ( result ) . to . equal ( 'emptyObj' ) ;
375
+ } ) ;
120
376
} ) ;
121
377
} ) ;
0 commit comments