1+ import React , { createContext , useContext } from "react"
12import { render , screen } from "@testing-library/react"
23import { describe , it , expect , vi , beforeEach , afterEach } from "vitest"
34import { FollowUpSuggest } from "../FollowUpSuggest"
4- import { ExtensionStateContext , ExtensionStateContextType } from "@src/context/ExtensionStateContext"
55import { TooltipProvider } from "@radix-ui/react-tooltip"
66
77// Mock the translation hook
@@ -23,87 +23,40 @@ vi.mock("@src/i18n/TranslationContext", () => ({
2323 } ) ,
2424} ) )
2525
26- // Mock the extension state
27- const createMockExtensionState = ( overrides ?: Partial < ExtensionStateContextType > ) : ExtensionStateContextType =>
28- ( {
29- version : "1.0.0" ,
30- clineMessages : [ ] ,
31- taskHistory : [ ] ,
32- shouldShowAnnouncement : false ,
33- allowedCommands : [ ] ,
34- soundEnabled : false ,
35- soundVolume : 0.5 ,
36- ttsEnabled : false ,
37- ttsSpeed : 1.0 ,
38- diffEnabled : false ,
39- enableCheckpoints : true ,
40- fuzzyMatchThreshold : 1.0 ,
41- language : "en" ,
42- writeDelayMs : 1000 ,
43- browserViewportSize : "900x600" ,
44- screenshotQuality : 75 ,
45- terminalOutputLineLimit : 500 ,
46- terminalShellIntegrationTimeout : 4000 ,
47- mcpEnabled : true ,
48- enableMcpServerCreation : false ,
49- alwaysApproveResubmit : false ,
50- requestDelaySeconds : 5 ,
51- currentApiConfigName : "default" ,
52- listApiConfigMeta : [ ] ,
53- mode : "code" ,
54- customModePrompts : { } ,
55- customSupportPrompts : { } ,
56- experiments : { } ,
57- enhancementApiConfigId : "" ,
58- condensingApiConfigId : "" ,
59- customCondensingPrompt : "" ,
60- hasOpenedModeSelector : false ,
61- autoApprovalEnabled : true ,
62- alwaysAllowFollowupQuestions : true ,
63- followupAutoApproveTimeoutMs : 3000 , // 3 seconds for testing
64- customModes : [ ] ,
65- maxOpenTabsContext : 20 ,
66- maxWorkspaceFiles : 200 ,
67- cwd : "" ,
68- browserToolEnabled : true ,
69- telemetrySetting : "unset" ,
70- showRooIgnoredFiles : true ,
71- renderContext : "sidebar" ,
72- maxReadFileLine : - 1 ,
73- pinnedApiConfigs : { } ,
74- didHydrateState : true ,
75- showWelcome : false ,
76- theme : { } ,
77- mcpServers : [ ] ,
78- filePaths : [ ] ,
79- openedTabs : [ ] ,
80- organizationAllowList : { type : "all" } ,
81- cloudIsAuthenticated : false ,
82- sharingEnabled : false ,
83- mdmCompliant : true ,
84- autoCondenseContext : false ,
85- autoCondenseContextPercent : 50 ,
86- setHasOpenedModeSelector : vi . fn ( ) ,
87- setAlwaysAllowFollowupQuestions : vi . fn ( ) ,
88- setFollowupAutoApproveTimeoutMs : vi . fn ( ) ,
89- setCondensingApiConfigId : vi . fn ( ) ,
90- setCustomCondensingPrompt : vi . fn ( ) ,
91- setPinnedApiConfigs : vi . fn ( ) ,
92- togglePinnedApiConfig : vi . fn ( ) ,
93- setTerminalCompressProgressBar : vi . fn ( ) ,
94- setHistoryPreviewCollapsed : vi . fn ( ) ,
95- setAutoCondenseContext : vi . fn ( ) ,
96- setAutoCondenseContextPercent : vi . fn ( ) ,
97- ...overrides ,
98- } ) as ExtensionStateContextType
99-
100- const renderWithProviders = ( component : React . ReactElement , stateOverrides ?: Partial < ExtensionStateContextType > ) => {
101- const mockState = createMockExtensionState ( stateOverrides )
26+ // Test-specific extension state context that only provides the values needed by FollowUpSuggest
27+ interface TestExtensionState {
28+ autoApprovalEnabled : boolean
29+ alwaysAllowFollowupQuestions : boolean
30+ followupAutoApproveTimeoutMs : number
31+ }
32+
33+ const TestExtensionStateContext = createContext < TestExtensionState | undefined > ( undefined )
10234
35+ // Mock the useExtensionState hook to use our test context
36+ vi . mock ( "@src/context/ExtensionStateContext" , ( ) => ( {
37+ useExtensionState : ( ) => {
38+ const context = useContext ( TestExtensionStateContext )
39+ if ( ! context ) {
40+ throw new Error ( "useExtensionState must be used within TestExtensionStateProvider" )
41+ }
42+ return context
43+ } ,
44+ } ) )
45+
46+ // Test provider that only provides the specific values needed by FollowUpSuggest
47+ const TestExtensionStateProvider : React . FC < {
48+ children : React . ReactNode
49+ value : TestExtensionState
50+ } > = ( { children, value } ) => {
51+ return < TestExtensionStateContext . Provider value = { value } > { children } </ TestExtensionStateContext . Provider >
52+ }
53+
54+ // Helper function to render component with test providers
55+ const renderWithTestProviders = ( component : React . ReactElement , extensionState : TestExtensionState ) => {
10356 return render (
104- < ExtensionStateContext . Provider value = { mockState } >
57+ < TestExtensionStateProvider value = { extensionState } >
10558 < TooltipProvider > { component } </ TooltipProvider >
106- </ ExtensionStateContext . Provider > ,
59+ </ TestExtensionStateProvider > ,
10760 )
10861}
10962
@@ -113,6 +66,13 @@ describe("FollowUpSuggest", () => {
11366 const mockOnSuggestionClick = vi . fn ( )
11467 const mockOnUnmount = vi . fn ( )
11568
69+ // Default test state with auto-approval enabled
70+ const defaultTestState : TestExtensionState = {
71+ autoApprovalEnabled : true ,
72+ alwaysAllowFollowupQuestions : true ,
73+ followupAutoApproveTimeoutMs : 3000 , // 3 seconds for testing
74+ }
75+
11676 beforeEach ( ( ) => {
11777 vi . clearAllMocks ( )
11878 vi . useFakeTimers ( )
@@ -123,42 +83,45 @@ describe("FollowUpSuggest", () => {
12383 } )
12484
12585 it ( "should display countdown timer when auto-approval is enabled" , ( ) => {
126- renderWithProviders (
86+ renderWithTestProviders (
12787 < FollowUpSuggest
12888 suggestions = { mockSuggestions }
12989 onSuggestionClick = { mockOnSuggestionClick }
13090 ts = { 123 }
13191 onUnmount = { mockOnUnmount }
13292 /> ,
93+ defaultTestState ,
13394 )
13495
13596 // Should show initial countdown (3 seconds)
13697 expect ( screen . getByText ( / 3 s / ) ) . toBeInTheDocument ( )
13798 } )
13899
139100 it ( "should not display countdown timer when isAnswered is true" , ( ) => {
140- renderWithProviders (
101+ renderWithTestProviders (
141102 < FollowUpSuggest
142103 suggestions = { mockSuggestions }
143104 onSuggestionClick = { mockOnSuggestionClick }
144105 ts = { 123 }
145106 onUnmount = { mockOnUnmount }
146107 isAnswered = { true }
147108 /> ,
109+ defaultTestState ,
148110 )
149111
150112 // Should not show countdown
151113 expect ( screen . queryByText ( / \d + s / ) ) . not . toBeInTheDocument ( )
152114 } )
153115
154116 it ( "should clear interval and call onUnmount when component unmounts" , ( ) => {
155- const { unmount } = renderWithProviders (
117+ const { unmount } = renderWithTestProviders (
156118 < FollowUpSuggest
157119 suggestions = { mockSuggestions }
158120 onSuggestionClick = { mockOnSuggestionClick }
159121 ts = { 123 }
160122 onUnmount = { mockOnUnmount }
161123 /> ,
124+ defaultTestState ,
162125 )
163126
164127 // Unmount the component
@@ -169,32 +132,158 @@ describe("FollowUpSuggest", () => {
169132 } )
170133
171134 it ( "should not show countdown when auto-approval is disabled" , ( ) => {
172- renderWithProviders (
135+ const testState : TestExtensionState = {
136+ ...defaultTestState ,
137+ autoApprovalEnabled : false ,
138+ }
139+
140+ renderWithTestProviders (
173141 < FollowUpSuggest
174142 suggestions = { mockSuggestions }
175143 onSuggestionClick = { mockOnSuggestionClick }
176144 ts = { 123 }
177145 onUnmount = { mockOnUnmount }
178146 /> ,
179- { autoApprovalEnabled : false } ,
147+ testState ,
180148 )
181149
182150 // Should not show countdown
183151 expect ( screen . queryByText ( / \d + s / ) ) . not . toBeInTheDocument ( )
184152 } )
185153
186154 it ( "should not show countdown when alwaysAllowFollowupQuestions is false" , ( ) => {
187- renderWithProviders (
155+ const testState : TestExtensionState = {
156+ ...defaultTestState ,
157+ alwaysAllowFollowupQuestions : false ,
158+ }
159+
160+ renderWithTestProviders (
161+ < FollowUpSuggest
162+ suggestions = { mockSuggestions }
163+ onSuggestionClick = { mockOnSuggestionClick }
164+ ts = { 123 }
165+ onUnmount = { mockOnUnmount }
166+ /> ,
167+ testState ,
168+ )
169+
170+ // Should not show countdown
171+ expect ( screen . queryByText ( / \d + s / ) ) . not . toBeInTheDocument ( )
172+ } )
173+
174+ it ( "should use custom timeout value from extension state" , ( ) => {
175+ const testState : TestExtensionState = {
176+ ...defaultTestState ,
177+ followupAutoApproveTimeoutMs : 5000 , // 5 seconds
178+ }
179+
180+ renderWithTestProviders (
181+ < FollowUpSuggest
182+ suggestions = { mockSuggestions }
183+ onSuggestionClick = { mockOnSuggestionClick }
184+ ts = { 123 }
185+ onUnmount = { mockOnUnmount }
186+ /> ,
187+ testState ,
188+ )
189+
190+ // Should show initial countdown (5 seconds)
191+ expect ( screen . getByText ( / 5 s / ) ) . toBeInTheDocument ( )
192+ } )
193+
194+ it ( "should render suggestions without countdown when both auto-approval settings are disabled" , ( ) => {
195+ const testState : TestExtensionState = {
196+ autoApprovalEnabled : false ,
197+ alwaysAllowFollowupQuestions : false ,
198+ followupAutoApproveTimeoutMs : 3000 ,
199+ }
200+
201+ renderWithTestProviders (
188202 < FollowUpSuggest
189203 suggestions = { mockSuggestions }
190204 onSuggestionClick = { mockOnSuggestionClick }
191205 ts = { 123 }
192206 onUnmount = { mockOnUnmount }
193207 /> ,
194- { alwaysAllowFollowupQuestions : false } ,
208+ testState ,
195209 )
196210
211+ // Should render suggestions
212+ expect ( screen . getByText ( "First suggestion" ) ) . toBeInTheDocument ( )
213+ expect ( screen . getByText ( "Second suggestion" ) ) . toBeInTheDocument ( )
214+
197215 // Should not show countdown
198216 expect ( screen . queryByText ( / \d + s / ) ) . not . toBeInTheDocument ( )
199217 } )
218+
219+ it ( "should not render when no suggestions are provided" , ( ) => {
220+ const { container } = renderWithTestProviders (
221+ < FollowUpSuggest
222+ suggestions = { [ ] }
223+ onSuggestionClick = { mockOnSuggestionClick }
224+ ts = { 123 }
225+ onUnmount = { mockOnUnmount }
226+ /> ,
227+ defaultTestState ,
228+ )
229+
230+ // Component should not render anything
231+ expect ( container . firstChild ) . toBeNull ( )
232+ } )
233+
234+ it ( "should not render when onSuggestionClick is not provided" , ( ) => {
235+ const { container } = renderWithTestProviders (
236+ < FollowUpSuggest suggestions = { mockSuggestions } ts = { 123 } onUnmount = { mockOnUnmount } /> ,
237+ defaultTestState ,
238+ )
239+
240+ // Component should not render anything
241+ expect ( container . firstChild ) . toBeNull ( )
242+ } )
243+
244+ it ( "should stop countdown when user manually responds (isAnswered becomes true)" , ( ) => {
245+ const { rerender } = renderWithTestProviders (
246+ < FollowUpSuggest
247+ suggestions = { mockSuggestions }
248+ onSuggestionClick = { mockOnSuggestionClick }
249+ ts = { 123 }
250+ onUnmount = { mockOnUnmount }
251+ isAnswered = { false }
252+ /> ,
253+ defaultTestState ,
254+ )
255+
256+ // Initially should show countdown
257+ expect ( screen . getByText ( / 3 s / ) ) . toBeInTheDocument ( )
258+
259+ // Simulate user manually responding by setting isAnswered to true
260+ rerender (
261+ < TestExtensionStateProvider value = { defaultTestState } >
262+ < TooltipProvider >
263+ < FollowUpSuggest
264+ suggestions = { mockSuggestions }
265+ onSuggestionClick = { mockOnSuggestionClick }
266+ ts = { 123 }
267+ onUnmount = { mockOnUnmount }
268+ isAnswered = { true }
269+ />
270+ </ TooltipProvider >
271+ </ TestExtensionStateProvider > ,
272+ )
273+
274+ // Countdown should no longer be visible immediately after isAnswered becomes true
275+ expect ( screen . queryByText ( / \d + s / ) ) . not . toBeInTheDocument ( )
276+
277+ // Advance timer to ensure countdown doesn't restart or continue
278+ vi . advanceTimersByTime ( 5000 )
279+
280+ // onSuggestionClick should not have been called (auto-selection stopped)
281+ expect ( mockOnSuggestionClick ) . not . toHaveBeenCalled ( )
282+
283+ // Countdown should still not be visible
284+ expect ( screen . queryByText ( / \d + s / ) ) . not . toBeInTheDocument ( )
285+
286+ // Verify onUnmount was called when the countdown was stopped
287+ expect ( mockOnUnmount ) . toHaveBeenCalled ( )
288+ } )
200289} )
0 commit comments