@@ -36,13 +36,13 @@ vi.mock("@src/utils/vscode", () => ({
3636 } ,
3737} ) )
3838
39- // Mock use-sound hook
39+ // Mock use-sound hook - must be defined before any imports that use it
4040const mockPlayFunction = vi . fn ( )
41- vi . mock ( "use-sound" , ( ) => ( {
42- default : vi . fn ( ) . mockImplementation ( ( ) => {
43- return [ mockPlayFunction ]
44- } ) ,
45- } ) )
41+ vi . mock ( "use-sound" , ( ) => {
42+ return {
43+ default : vi . fn ( ( ) => [ mockPlayFunction , { stop : vi . fn ( ) , pause : vi . fn ( ) } ] ) ,
44+ }
45+ } )
4646
4747// Mock components that use ESM dependencies
4848vi . mock ( "../BrowserSessionRow" , ( ) => ( {
@@ -107,6 +107,7 @@ vi.mock("react-i18next", () => ({
107107interface ChatTextAreaProps {
108108 onSend : ( value : string ) => void
109109 inputValue ?: string
110+ setInputValue ?: ( value : string ) => void
110111 sendingDisabled ?: boolean
111112 placeholderText ?: string
112113 selectedImages ?: string [ ]
@@ -123,16 +124,28 @@ vi.mock("../ChatTextArea", () => {
123124 return {
124125 default : mockReact . forwardRef ( function MockChatTextArea (
125126 props : ChatTextAreaProps ,
126- ref : React . ForwardedRef < { focus : ( ) => void } > ,
127+ ref : React . ForwardedRef < HTMLTextAreaElement > ,
127128 ) {
128- // Use useImperativeHandle to expose the mock focus method
129- React . useImperativeHandle ( ref , ( ) => ( {
130- focus : mockFocus ,
131- } ) )
129+ // Create a mock textarea element with focus method
130+ mockReact . useImperativeHandle (
131+ ref ,
132+ ( ) => ( {
133+ focus : mockFocus ,
134+ blur : vi . fn ( ) ,
135+ value : props . inputValue || "" ,
136+ } ) ,
137+ [ props . inputValue ] ,
138+ )
132139
133140 return (
134- < div data-testid = "chat-textarea" >
135- < input ref = { mockInputRef } type = "text" onChange = { ( e ) => props . onSend ( e . target . value ) } />
141+ < div data-testid = "chat-textarea" data-sending-disabled = { props . sendingDisabled } >
142+ < input
143+ ref = { mockInputRef }
144+ type = "text"
145+ value = { props . inputValue || "" }
146+ onChange = { ( e ) => props . setInputValue ?.( e . target . value ) }
147+ disabled = { props . sendingDisabled }
148+ />
136149 </ div >
137150 )
138151 } ) ,
@@ -191,6 +204,8 @@ const mockPostMessage = (state: Partial<ExtensionState>) => {
191204 shouldShowAnnouncement : false ,
192205 allowedCommands : [ ] ,
193206 alwaysAllowExecute : false ,
207+ soundEnabled : true ,
208+ soundVolume : 0.5 ,
194209 ...state ,
195210 } ,
196211 } ,
@@ -216,6 +231,77 @@ const renderChatView = (props: Partial<ChatViewProps> = {}) => {
216231 )
217232}
218233
234+ describe ( "ChatView - Window Focus Tests" , ( ) => {
235+ beforeEach ( ( ) => {
236+ vi . clearAllMocks ( )
237+ // Reset focus mock
238+ mockFocus . mockClear ( )
239+ } )
240+
241+ afterEach ( ( ) => {
242+ // Clean up any event listeners
243+ vi . restoreAllMocks ( )
244+ } )
245+
246+ it ( "should set up and clean up window focus event listener" , ( ) => {
247+ const addEventListenerSpy = vi . spyOn ( window , "addEventListener" )
248+ const removeEventListenerSpy = vi . spyOn ( window , "removeEventListener" )
249+
250+ const { unmount } = renderChatView ( { isHidden : false } )
251+
252+ // Check that the event listener was added
253+ expect ( addEventListenerSpy ) . toHaveBeenCalledWith ( "focus" , expect . any ( Function ) )
254+
255+ // Unmount the component
256+ unmount ( )
257+
258+ // Check that the event listener was removed
259+ expect ( removeEventListenerSpy ) . toHaveBeenCalledWith ( "focus" , expect . any ( Function ) )
260+
261+ addEventListenerSpy . mockRestore ( )
262+ removeEventListenerSpy . mockRestore ( )
263+ } )
264+
265+ it ( "should restore focus when window regains focus and conditions are met" , async ( ) => {
266+ renderChatView ( { isHidden : false } )
267+
268+ // Hydrate state to enable the text area
269+ mockPostMessage ( {
270+ clineMessages : [
271+ {
272+ type : "say" ,
273+ say : "text" ,
274+ ts : Date . now ( ) ,
275+ text : "Hello" ,
276+ } ,
277+ ] ,
278+ } )
279+
280+ // Wait for initial render
281+ await waitFor ( ( ) => {
282+ expect ( document . querySelector ( '[data-testid="chat-textarea"]' ) ) . toBeInTheDocument ( )
283+ } )
284+
285+ // Wait for any initial focus calls to complete
286+ await act ( async ( ) => {
287+ await new Promise ( ( resolve ) => setTimeout ( resolve , 100 ) )
288+ } )
289+
290+ // Clear the mock to test only the window focus event
291+ mockFocus . mockClear ( )
292+
293+ // Simulate window focus event
294+ await act ( async ( ) => {
295+ window . dispatchEvent ( new Event ( "focus" ) )
296+ // Wait for the setTimeout in the focus handler
297+ await new Promise ( ( resolve ) => setTimeout ( resolve , 10 ) )
298+ } )
299+
300+ // The focus should have been called
301+ expect ( mockFocus ) . toHaveBeenCalledTimes ( 1 )
302+ } )
303+ } )
304+
219305describe ( "ChatView - Auto Approval Tests" , ( ) => {
220306 beforeEach ( ( ) => vi . clearAllMocks ( ) )
221307
0 commit comments