@@ -5,7 +5,12 @@ import React from "react"
55
66import SettingsView from "../SettingsView"
77
8- // Mock vscode API
8+ // Mock the vscode utility module (acquireVsCodeApi is only available inside the VS Code webview)
9+ vi . mock ( "@src/utils/vscode" , ( ) => ( {
10+ vscode : { postMessage : vi . fn ( ) , getState : vi . fn ( ) , setState : vi . fn ( ) } ,
11+ } ) )
12+
13+ // Legacy global mock retained for other test setups that may rely on it
914const mockPostMessage = vi . fn ( )
1015const mockVscode = {
1116 postMessage : mockPostMessage ,
@@ -242,6 +247,8 @@ vi.mock("../SettingsSearch", () => ({
242247
243248import { useExtensionState } from "@src/context/ExtensionStateContext"
244249import ApiOptions from "../ApiOptions"
250+ import { ExperimentalSettings } from "../ExperimentalSettings"
251+ import { vscode } from "@src/utils/vscode"
245252
246253describe ( "SettingsView - Unsaved Changes Detection" , ( ) => {
247254 let queryClient : QueryClient
@@ -304,7 +311,7 @@ describe("SettingsView - Unsaved Changes Detection", () => {
304311 includeDiagnosticMessages : false ,
305312 maxDiagnosticMessages : 50 ,
306313 includeTaskHistoryInEnhance : true ,
307- openRouterImageApiKey : undefined ,
314+ openRouterImageApiKeyConfigured : false ,
308315 openRouterImageGenerationSelectedModel : undefined ,
309316 reasoningBlockCollapsed : true ,
310317 }
@@ -316,6 +323,8 @@ describe("SettingsView - Unsaved Changes Detection", () => {
316323 // Don't do anything with props, just render a div
317324 return < div data-testid = "api-options" > ApiOptions</ div >
318325 } )
326+ // Reset ExperimentalSettings to silent default
327+ vi . mocked ( ExperimentalSettings ) . mockImplementation ( ( ) => < div > ExperimentalSettings</ div > )
319328 queryClient = new QueryClient ( {
320329 defaultOptions : {
321330 queries : { retry : false } ,
@@ -603,4 +612,96 @@ describe("SettingsView - Unsaved Changes Detection", () => {
603612 // No dialog should appear
604613 expect ( screen . queryByText ( "settings:unsavedChangesDialog.title" ) ) . not . toBeInTheDocument ( )
605614 } )
615+
616+ describe ( "pending image API key" , ( ) => {
617+ const renderWithApiKeyTrigger = ( ) => {
618+ vi . mocked ( ExperimentalSettings ) . mockImplementation ( ( { setOpenRouterImageApiKey } : any ) => (
619+ < div >
620+ < button data-testid = "set-api-key" onClick = { ( ) => setOpenRouterImageApiKey ?.( "sk-or-new-key" ) } >
621+ Set Key
622+ </ button >
623+ < button data-testid = "clear-api-key" onClick = { ( ) => setOpenRouterImageApiKey ?.( "" ) } >
624+ Clear Key
625+ </ button >
626+ </ div >
627+ ) )
628+ // Render with the experimental tab active so ExperimentalSettings is mounted
629+ return render (
630+ < QueryClientProvider client = { queryClient } >
631+ < SettingsView onDone = { vi . fn ( ) } targetSection = "experimental" />
632+ </ QueryClientProvider > ,
633+ )
634+ }
635+
636+ it ( "typing a new key marks settings as changed and includes key in save payload" , async ( ) => {
637+ renderWithApiKeyTrigger ( )
638+
639+ await waitFor ( ( ) => expect ( screen . getByTestId ( "save-button" ) ) . toBeInTheDocument ( ) )
640+
641+ fireEvent . click ( screen . getByTestId ( "set-api-key" ) )
642+
643+ // Save button should now be enabled
644+ await waitFor ( ( ) => {
645+ expect ( ( screen . getByTestId ( "save-button" ) as HTMLButtonElement ) . disabled ) . toBe ( false )
646+ } )
647+
648+ fireEvent . click ( screen . getByTestId ( "save-button" ) )
649+
650+ await waitFor ( ( ) => {
651+ const calls = vi . mocked ( vscode . postMessage ) . mock . calls
652+ const updateCall = ( calls . find ( ( [ msg ] : any ) => msg ?. type === "updateSettings" ) as any ) ?. [ 0 ]
653+ expect ( updateCall ) . toBeDefined ( )
654+ expect ( updateCall . updatedSettings . openRouterImageApiKey ) . toBe ( "sk-or-new-key" )
655+ } )
656+ } )
657+
658+ it ( "clearing the key sends undefined (triggers deletion) rather than empty string" , async ( ) => {
659+ renderWithApiKeyTrigger ( )
660+
661+ await waitFor ( ( ) => expect ( screen . getByTestId ( "save-button" ) ) . toBeInTheDocument ( ) )
662+
663+ fireEvent . click ( screen . getByTestId ( "clear-api-key" ) )
664+
665+ await waitFor ( ( ) => {
666+ expect ( ( screen . getByTestId ( "save-button" ) as HTMLButtonElement ) . disabled ) . toBe ( false )
667+ } )
668+
669+ fireEvent . click ( screen . getByTestId ( "save-button" ) )
670+
671+ await waitFor ( ( ) => {
672+ const calls = vi . mocked ( vscode . postMessage ) . mock . calls
673+ const updateCall = ( calls . find ( ( [ msg ] : any ) => msg ?. type === "updateSettings" ) as any ) ?. [ 0 ]
674+ expect ( updateCall ) . toBeDefined ( )
675+ // Empty string should become undefined so the host deletes the secret
676+ expect ( updateCall . updatedSettings . openRouterImageApiKey ) . toBeUndefined ( )
677+ } )
678+ } )
679+
680+ it ( "discarding changes resets pending key so save button returns to disabled" , async ( ) => {
681+ renderWithApiKeyTrigger ( )
682+
683+ await waitFor ( ( ) => expect ( screen . getByTestId ( "save-button" ) ) . toBeInTheDocument ( ) )
684+
685+ fireEvent . click ( screen . getByTestId ( "set-api-key" ) )
686+
687+ await waitFor ( ( ) => {
688+ expect ( ( screen . getByTestId ( "save-button" ) as HTMLButtonElement ) . disabled ) . toBe ( false )
689+ } )
690+
691+ // Click Done to trigger the unsaved-changes dialog
692+ fireEvent . click ( screen . getByText ( "settings:common.done" ) )
693+
694+ await waitFor ( ( ) => {
695+ expect ( screen . getByText ( "settings:unsavedChangesDialog.title" ) ) . toBeInTheDocument ( )
696+ } )
697+
698+ // Confirm discard
699+ fireEvent . click ( screen . getByText ( "settings:unsavedChangesDialog.discardButton" ) )
700+
701+ // Save button should be disabled again (pending key cleared)
702+ await waitFor ( ( ) => {
703+ expect ( ( screen . getByTestId ( "save-button" ) as HTMLButtonElement ) . disabled ) . toBe ( true )
704+ } )
705+ } )
706+ } )
606707} )
0 commit comments