11import type { Node } from '../types'
22import { fireEvent , render , screen } from '@testing-library/react'
3- import NodeContextmenu from '../node-contextmenu'
3+ import { NodeContextmenu } from '../node-contextmenu'
44
5- const mockUseClickAway = vi . hoisted ( ( ) => vi . fn ( ) )
65const mockUseNodes = vi . hoisted ( ( ) => vi . fn ( ) )
76const mockUsePanelInteractions = vi . hoisted ( ( ) => vi . fn ( ) )
87const mockUseStore = vi . hoisted ( ( ) => vi . fn ( ) )
9- const mockPanelOperatorPopup = vi . hoisted ( ( ) => vi . fn ( ) )
10-
11- vi . mock ( 'ahooks' , ( ) => ( {
12- useClickAway : ( ...args : unknown [ ] ) => mockUseClickAway ( ...args ) ,
8+ const mockNodeActionsContextMenuContent = vi . hoisted ( ( ) => vi . fn ( ) )
9+ const mockContextMenuContent = vi . hoisted ( ( ) => vi . fn ( ) )
10+
11+ vi . mock ( '@langgenius/dify-ui/context-menu' , ( ) => ( {
12+ ContextMenu : ( { children, onOpenChange } : { children : React . ReactNode , onOpenChange : ( open : boolean ) => void } ) => (
13+ < div >
14+ { children }
15+ < button type = "button" onClick = { ( ) => onOpenChange ( false ) } > close-context-menu</ button >
16+ </ div >
17+ ) ,
18+ ContextMenuContent : ( { children, positionerProps, popupClassName } : { children : React . ReactNode , positionerProps ?: { anchor ?: unknown } , popupClassName ?: string } ) => {
19+ mockContextMenuContent ( { positionerProps, popupClassName } )
20+ return < div > { children } </ div >
21+ } ,
1322} ) )
1423
1524vi . mock ( '@/app/components/workflow/store/workflow/use-nodes' , ( ) => ( {
@@ -22,20 +31,19 @@ vi.mock('@/app/components/workflow/hooks', () => ({
2231} ) )
2332
2433vi . mock ( '@/app/components/workflow/store' , ( ) => ( {
25- useStore : ( selector : ( state : { nodeMenu ?: { nodeId : string , left : number , top : number } } ) => unknown ) => mockUseStore ( selector ) ,
34+ useStore : ( selector : ( state : { nodeMenu ?: { nodeId : string , clientX : number , clientY : number } } ) => unknown ) => mockUseStore ( selector ) ,
2635} ) )
2736
28- vi . mock ( '@/app/components/workflow/nodes/_base/components/panel-operator/panel-operator-popup' , ( ) => ( {
29- __esModule : true ,
30- default : ( props : {
37+ vi . mock ( '@/app/components/workflow/node-actions-menu/context-menu-content' , ( ) => ( {
38+ NodeActionsContextMenuContent : ( props : {
3139 id : string
3240 data : Node [ 'data' ]
3341 showHelpLink : boolean
34- onClosePopup : ( ) => void
42+ onClose : ( ) => void
3543 } ) => {
36- mockPanelOperatorPopup ( props )
44+ mockNodeActionsContextMenuContent ( props )
3745 return (
38- < button type = "button" onClick = { props . onClosePopup } >
46+ < button type = "button" onClick = { props . onClose } >
3947 { props . id }
4048 :
4149 { props . data . title }
@@ -46,9 +54,8 @@ vi.mock('@/app/components/workflow/nodes/_base/components/panel-operator/panel-o
4654
4755describe ( 'NodeContextmenu' , ( ) => {
4856 const mockHandleNodeContextmenuCancel = vi . fn ( )
49- let nodeMenu : { nodeId : string , left : number , top : number } | undefined
57+ let nodeMenu : { nodeId : string , clientX : number , clientY : number } | undefined
5058 let nodes : Node [ ]
51- let clickAwayHandler : ( ( ) => void ) | undefined
5259
5360 beforeEach ( ( ) => {
5461 vi . clearAllMocks ( )
@@ -63,51 +70,50 @@ describe('NodeContextmenu', () => {
6370 type : 'code' as never ,
6471 } ,
6572 } as Node ]
66- clickAwayHandler = undefined
6773
68- mockUseClickAway . mockImplementation ( ( handler : ( ) => void ) => {
69- clickAwayHandler = handler
70- } )
7174 mockUseNodes . mockImplementation ( ( ) => nodes )
7275 mockUsePanelInteractions . mockReturnValue ( {
7376 handleNodeContextmenuCancel : mockHandleNodeContextmenuCancel ,
7477 } )
75- mockUseStore . mockImplementation ( ( selector : ( state : { nodeMenu ?: { nodeId : string , left : number , top : number } } ) => unknown ) => selector ( { nodeMenu } ) )
78+ mockUseStore . mockImplementation ( ( selector : ( state : { nodeMenu ?: { nodeId : string , clientX : number , clientY : number } } ) => unknown ) => selector ( { nodeMenu } ) )
7679 } )
7780
7881 it ( 'should stay hidden when the node menu is absent' , ( ) => {
7982 render ( < NodeContextmenu /> )
8083
8184 expect ( screen . queryByRole ( 'button' ) ) . not . toBeInTheDocument ( )
82- expect ( mockPanelOperatorPopup ) . not . toHaveBeenCalled ( )
85+ expect ( mockNodeActionsContextMenuContent ) . not . toHaveBeenCalled ( )
8386 } )
8487
8588 it ( 'should stay hidden when the referenced node cannot be found' , ( ) => {
86- nodeMenu = { nodeId : 'missing-node' , left : 80 , top : 120 }
89+ nodeMenu = { nodeId : 'missing-node' , clientX : 80 , clientY : 120 }
8790
8891 render ( < NodeContextmenu /> )
8992
9093 expect ( screen . queryByRole ( 'button' ) ) . not . toBeInTheDocument ( )
91- expect ( mockPanelOperatorPopup ) . not . toHaveBeenCalled ( )
94+ expect ( mockNodeActionsContextMenuContent ) . not . toHaveBeenCalled ( )
9295 } )
9396
94- it ( 'should render the popup at the stored position and close on popup/click-away actions' , ( ) => {
95- nodeMenu = { nodeId : 'node-1' , left : 80 , top : 120 }
96- const { container } = render ( < NodeContextmenu /> )
97+ it ( 'should render the context menu at the stored pointer position and close on content/root actions' , ( ) => {
98+ nodeMenu = { nodeId : 'node-1' , clientX : 80 , clientY : 120 }
99+ render ( < NodeContextmenu /> )
97100
98- expect ( screen . getByRole ( 'button' ) ) . toHaveTextContent ( ' node-1:Node 1')
99- expect ( mockPanelOperatorPopup ) . toHaveBeenCalledWith ( expect . objectContaining ( {
101+ expect ( screen . getByText ( ' node-1:Node 1') ) . toBeInTheDocument ( )
102+ expect ( mockNodeActionsContextMenuContent ) . toHaveBeenCalledWith ( expect . objectContaining ( {
100103 id : 'node-1' ,
101104 data : expect . objectContaining ( { title : 'Node 1' } ) ,
102105 showHelpLink : true ,
103106 } ) )
104- expect ( container . firstChild ) . toHaveStyle ( {
105- left : '80px' ,
106- top : '120px' ,
107- } )
107+ expect ( mockContextMenuContent ) . toHaveBeenCalledWith ( expect . objectContaining ( {
108+ popupClassName : 'w-[240px] rounded-lg' ,
109+ } ) )
110+ const anchor = mockContextMenuContent . mock . calls [ 0 ] ! [ 0 ] . positionerProps . anchor as { getBoundingClientRect : ( ) => DOMRect }
111+ const rect = anchor . getBoundingClientRect ( )
112+ expect ( rect . x ) . toBe ( 80 )
113+ expect ( rect . y ) . toBe ( 120 )
108114
109- fireEvent . click ( screen . getByRole ( 'button ') )
110- clickAwayHandler ?. ( )
115+ fireEvent . click ( screen . getByText ( 'node-1:Node 1 ') )
116+ fireEvent . click ( screen . getByText ( 'close-context-menu' ) )
111117
112118 expect ( mockHandleNodeContextmenuCancel ) . toHaveBeenCalledTimes ( 2 )
113119 } )
0 commit comments