@@ -33,7 +33,7 @@ vi.mock("../../../src/app/stores/cache", () => ({
3333} ) ) ;
3434
3535vi . mock ( "../../../src/app/services/github" , ( ) => ( {
36- getClient : ( ) => ( { } ) ,
36+ getClient : vi . fn ( ( ) => ( { } ) ) ,
3737} ) ) ;
3838
3939vi . mock ( "../../../src/app/services/api" , ( ) => ( {
@@ -143,7 +143,6 @@ describe("SettingsPage — rendering", () => {
143143 it ( "renders a back to dashboard link" , ( ) => {
144144 renderSettings ( ) ;
145145 const backLink = screen . getByRole ( "link" , { name : / b a c k t o d a s h b o a r d / i } ) ;
146- expect ( backLink ) . toBeDefined ( ) ;
147146 expect ( backLink . getAttribute ( "href" ) ) . toBe ( "/dashboard" ) ;
148147 } ) ;
149148
@@ -193,8 +192,7 @@ describe("SettingsPage — rendering", () => {
193192describe ( "SettingsPage — Refresh interval" , ( ) => {
194193 it ( "shows current refresh interval value" , ( ) => {
195194 renderSettings ( ) ;
196- const select = screen . getByDisplayValue ( "5 minutes (default)" ) ;
197- expect ( select ) . toBeDefined ( ) ;
195+ screen . getByDisplayValue ( "5 minutes (default)" ) ;
198196 } ) ;
199197
200198 it ( "changing refresh interval calls updateConfig" , async ( ) => {
@@ -284,6 +282,8 @@ describe("SettingsPage — GitHub Actions", () => {
284282 expect ( workflowInput ) . toBeDefined ( ) ;
285283 } ) ;
286284
285+ // NumberInput uses onInput — fireEvent.input sets the value atomically, while
286+ // userEvent.type fires per-keystroke triggering intermediate valid values.
287287 it ( "changing max workflows per repo updates config" , ( ) => {
288288 renderSettings ( ) ;
289289 const inputs = screen . getAllByRole ( "spinbutton" ) ;
@@ -557,14 +557,18 @@ describe("SettingsPage — Re-auth button", () => {
557557 vi . unstubAllEnvs ( ) ;
558558 } ) ;
559559
560- it ( "'Grant more orgs' button is disabled after click (reentrancy guard)" , async ( ) => {
561- const user = userEvent . setup ( ) ;
560+ it ( "'Grant more orgs' button is disabled after click and re-enables after timeout" , async ( ) => {
561+ vi . useFakeTimers ( ) ;
562+ const user = userEvent . setup ( { advanceTimers : vi . advanceTimersByTime } ) ;
562563 vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
563564 renderSettings ( ) ;
564565 const btn = screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
565566 await user . click ( btn ) ;
566567 expect ( btn . hasAttribute ( "disabled" ) ) . toBe ( true ) ;
568+ vi . advanceTimersByTime ( 3000 ) ;
569+ expect ( btn . hasAttribute ( "disabled" ) ) . toBe ( false ) ;
567570 vi . unstubAllEnvs ( ) ;
571+ vi . useRealTimers ( ) ;
568572 } ) ;
569573} ) ;
570574
@@ -615,4 +619,28 @@ describe("SettingsPage — Auto-merge orgs on mount", () => {
615619 renderSettings ( ) ;
616620 expect ( apiModule . fetchOrgs ) . not . toHaveBeenCalled ( ) ;
617621 } ) ;
622+
623+ it ( "silently handles fetchOrgs rejection without breaking" , async ( ) => {
624+ sessionStorage . setItem ( MERGE_ORGS_KEY , "true" ) ;
625+ updateConfig ( { selectedOrgs : [ "existing-org" ] } ) ;
626+ vi . mocked ( apiModule . fetchOrgs ) . mockRejectedValue ( new Error ( "Network error" ) ) ;
627+ renderSettings ( ) ;
628+ await waitFor ( ( ) => {
629+ expect ( apiModule . fetchOrgs ) . toHaveBeenCalled ( ) ;
630+ } ) ;
631+ // Config unchanged — error was swallowed
632+ expect ( config . selectedOrgs ) . toEqual ( [ "existing-org" ] ) ;
633+ } ) ;
634+
635+ it ( "skips merge when getClient returns null" , async ( ) => {
636+ sessionStorage . setItem ( MERGE_ORGS_KEY , "true" ) ;
637+ const github = await import ( "../../../src/app/services/github" ) ;
638+ vi . mocked ( github . getClient ) . mockReturnValueOnce ( null ) ;
639+ renderSettings ( ) ;
640+ // MERGE_ORGS_KEY still removed synchronously before mergeNewOrgs
641+ await waitFor ( ( ) => {
642+ expect ( sessionStorage . getItem ( MERGE_ORGS_KEY ) ) . toBeNull ( ) ;
643+ } ) ;
644+ expect ( apiModule . fetchOrgs ) . not . toHaveBeenCalled ( ) ;
645+ } ) ;
618646} ) ;
0 commit comments