@@ -36,13 +36,23 @@ describe('ChatHeaderButton', () => {
3636 mockChatService = {
3737 sendMessage : jest . fn ( ) ,
3838 newThread : jest . fn ( ) ,
39+ isWindowOpen : jest . fn ( ) . mockReturnValue ( false ) ,
40+ getWindowMode : jest . fn ( ) . mockReturnValue ( 'sidecar' ) ,
41+ setWindowState : jest . fn ( ) ,
42+ setChatWindowRef : jest . fn ( ) ,
43+ clearChatWindowRef : jest . fn ( ) ,
44+ onWindowStateChange : jest . fn ( ) . mockReturnValue ( ( ) => { } ) ,
45+ onWindowOpenRequest : jest . fn ( ) . mockReturnValue ( ( ) => { } ) ,
46+ onWindowCloseRequest : jest . fn ( ) . mockReturnValue ( ( ) => { } ) ,
3947 } as any ;
4048 mockContextProvider = { } ;
4149
42- // Mock sidecar
43- mockCore . overlays . sidecar . open = jest . fn ( ) . mockReturnValue ( {
50+ // Mock sidecar with complete SidecarRef
51+ const mockSidecarRef = {
4452 close : jest . fn ( ) ,
45- } ) ;
53+ onClose : Promise . resolve ( ) ,
54+ } as any ;
55+ mockCore . overlays . sidecar . open = jest . fn ( ) . mockReturnValue ( mockSidecarRef ) ;
4656 mockCore . overlays . sidecar . setSidecarConfig = jest . fn ( ) ;
4757 } ) ;
4858
@@ -85,4 +95,186 @@ describe('ChatHeaderButton', () => {
8595 } ) ;
8696 } ) ;
8797 } ) ;
98+
99+ describe ( 'initialization' , ( ) => {
100+ it ( 'should initialize with window closed state from ChatService' , ( ) => {
101+ mockChatService . isWindowOpen . mockReturnValue ( false ) ;
102+
103+ render (
104+ < ChatHeaderButton
105+ core = { mockCore }
106+ chatService = { mockChatService }
107+ contextProvider = { mockContextProvider }
108+ />
109+ ) ;
110+
111+ expect ( mockChatService . isWindowOpen ) . toHaveBeenCalled ( ) ;
112+ expect ( mockChatService . getWindowMode ) . toHaveBeenCalled ( ) ;
113+ } ) ;
114+
115+ it ( 'should initialize with window open state from ChatService' , ( ) => {
116+ mockChatService . isWindowOpen . mockReturnValue ( true ) ;
117+
118+ render (
119+ < ChatHeaderButton
120+ core = { mockCore }
121+ chatService = { mockChatService }
122+ contextProvider = { mockContextProvider }
123+ />
124+ ) ;
125+
126+ expect ( mockChatService . isWindowOpen ) . toHaveBeenCalled ( ) ;
127+ } ) ;
128+
129+ it ( 'should register ChatWindow ref with ChatService' , ( ) => {
130+ render (
131+ < ChatHeaderButton
132+ core = { mockCore }
133+ chatService = { mockChatService }
134+ contextProvider = { mockContextProvider }
135+ />
136+ ) ;
137+
138+ expect ( mockChatService . setChatWindowRef ) . toHaveBeenCalled ( ) ;
139+ } ) ;
140+
141+ it ( 'should subscribe to ChatService state changes' , ( ) => {
142+ render (
143+ < ChatHeaderButton
144+ core = { mockCore }
145+ chatService = { mockChatService }
146+ contextProvider = { mockContextProvider }
147+ />
148+ ) ;
149+
150+ expect ( mockChatService . onWindowStateChange ) . toHaveBeenCalled ( ) ;
151+ expect ( mockChatService . onWindowOpenRequest ) . toHaveBeenCalled ( ) ;
152+ expect ( mockChatService . onWindowCloseRequest ) . toHaveBeenCalled ( ) ;
153+ } ) ;
154+ } ) ;
155+
156+ describe ( 'window state synchronization' , ( ) => {
157+ it ( 'should respond to ChatService window open request' , ( ) => {
158+ let openRequestCallback : ( ) => void ;
159+ mockChatService . onWindowOpenRequest . mockImplementation ( ( cb ) => {
160+ openRequestCallback = cb ;
161+ return jest . fn ( ) ;
162+ } ) ;
163+
164+ render (
165+ < ChatHeaderButton
166+ core = { mockCore }
167+ chatService = { mockChatService }
168+ contextProvider = { mockContextProvider }
169+ />
170+ ) ;
171+
172+ // Trigger the open request
173+ openRequestCallback ! ( ) ;
174+
175+ expect ( mockCore . overlays . sidecar . open ) . toHaveBeenCalled ( ) ;
176+ } ) ;
177+
178+ it ( 'should respond to ChatService window close request when window is open' , async ( ) => {
179+ let closeRequestCallback : ( ) => void ;
180+ const mockClose = jest . fn ( ) ;
181+ const mockSidecarRef = {
182+ close : mockClose ,
183+ onClose : Promise . resolve ( ) ,
184+ } as any ;
185+ mockChatService . onWindowCloseRequest . mockImplementation ( ( cb ) => {
186+ closeRequestCallback = cb ;
187+ return jest . fn ( ) ;
188+ } ) ;
189+ mockCore . overlays . sidecar . open . mockReturnValue ( mockSidecarRef ) ;
190+
191+ const { container } = render (
192+ < ChatHeaderButton
193+ core = { mockCore }
194+ chatService = { mockChatService }
195+ contextProvider = { mockContextProvider }
196+ />
197+ ) ;
198+
199+ // First open the sidecar by clicking the button
200+ const button = container . querySelector ( '[aria-label="Toggle chat assistant"]' ) as HTMLElement ;
201+ button ?. click ( ) ;
202+
203+ // Wait for the sidecar to be opened
204+ await waitFor ( ( ) => {
205+ expect ( mockCore . overlays . sidecar . open ) . toHaveBeenCalled ( ) ;
206+ } ) ;
207+
208+ // Then trigger the close request
209+ closeRequestCallback ! ( ) ;
210+
211+ // Verify close was called
212+ await waitFor ( ( ) => {
213+ expect ( mockClose ) . toHaveBeenCalled ( ) ;
214+ } ) ;
215+ } ) ;
216+
217+ it ( 'should sync local state when ChatService state changes' , ( ) => {
218+ let stateChangeCallback : ( isOpen : boolean ) => void ;
219+ mockChatService . onWindowStateChange . mockImplementation ( ( cb ) => {
220+ stateChangeCallback = cb ;
221+ return jest . fn ( ) ;
222+ } ) ;
223+
224+ render (
225+ < ChatHeaderButton
226+ core = { mockCore }
227+ chatService = { mockChatService }
228+ contextProvider = { mockContextProvider }
229+ />
230+ ) ;
231+
232+ // Trigger state change to open
233+ stateChangeCallback ! ( true ) ;
234+
235+ // Verify the component reflects the new state (button color should change)
236+ const button = document . querySelector ( '[aria-label="Toggle chat assistant"]' ) ;
237+ expect ( button ) . toBeTruthy ( ) ;
238+ } ) ;
239+ } ) ;
240+
241+ describe ( 'cleanup' , ( ) => {
242+ it ( 'should clear ChatWindow ref on unmount' , ( ) => {
243+ const { unmount } = render (
244+ < ChatHeaderButton
245+ core = { mockCore }
246+ chatService = { mockChatService }
247+ contextProvider = { mockContextProvider }
248+ />
249+ ) ;
250+
251+ unmount ( ) ;
252+
253+ expect ( mockChatService . clearChatWindowRef ) . toHaveBeenCalled ( ) ;
254+ } ) ;
255+
256+ it ( 'should close sidecar on unmount if open' , ( ) => {
257+ const mockClose = jest . fn ( ) ;
258+ mockCore . overlays . sidecar . open . mockReturnValue ( {
259+ close : mockClose ,
260+ onClose : Promise . resolve ( ) ,
261+ } as any ) ;
262+
263+ const { unmount } = render (
264+ < ChatHeaderButton
265+ core = { mockCore }
266+ chatService = { mockChatService }
267+ contextProvider = { mockContextProvider }
268+ />
269+ ) ;
270+
271+ // Open the sidecar
272+ const button = document . querySelector ( '[aria-label="Toggle chat assistant"]' ) ;
273+ button ?. dispatchEvent ( new MouseEvent ( 'click' , { bubbles : true } ) ) ;
274+
275+ unmount ( ) ;
276+
277+ expect ( mockClose ) . toHaveBeenCalled ( ) ;
278+ } ) ;
279+ } ) ;
88280} ) ;
0 commit comments