@@ -3,7 +3,7 @@ import { render, screen, userEvent } from '@mongodb-js/testing-library-compass';
33import { expect } from 'chai' ;
44import sinon from 'sinon' ;
55import HadronDocument from 'hadron-document' ;
6- import { HadronElement } from './element' ;
6+ import { HadronElement , getNestedKeyPathForElement } from './element' ;
77import type { Element } from 'hadron-document' ;
88
99describe ( 'HadronElement' , function ( ) {
@@ -26,6 +26,77 @@ describe('HadronElement', function () {
2626 clipboardWriteTextStub . restore ( ) ;
2727 } ) ;
2828
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+
29100 it ( 'copies field and value when "Copy field & value" is clicked' , function ( ) {
30101 render (
31102 < HadronElement
@@ -117,5 +188,190 @@ describe('HadronElement', function () {
117188 // Check that the menu item doesn't exist
118189 expect ( screen . queryByText ( 'Open URL in browser' ) ) . to . not . exist ;
119190 } ) ;
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+ } ) ;
120376 } ) ;
121377} ) ;
0 commit comments