@@ -46,6 +46,10 @@ vi.mock("../../../src/app/lib/url", () => ({
4646 openGitHubUrl : vi . fn ( ) ,
4747} ) ) ;
4848
49+ vi . mock ( "../../../src/app/lib/errors" , ( ) => ( {
50+ pushNotification : vi . fn ( ) ,
51+ } ) ) ;
52+
4953// ── Imports after mocks ───────────────────────────────────────────────────────
5054
5155import { render } from "@solidjs/testing-library" ;
@@ -530,53 +534,59 @@ describe("SettingsPage — Theme application", () => {
530534} ) ;
531535
532536describe ( "SettingsPage — Grant more orgs button" , ( ) => {
537+ beforeEach ( ( ) => {
538+ vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
539+ } ) ;
540+ afterEach ( ( ) => {
541+ vi . unstubAllEnvs ( ) ;
542+ } ) ;
543+
533544 it ( "renders 'Grant more orgs' button in Organizations & Repositories section" , ( ) => {
534545 renderSettings ( ) ;
535546 screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
536547 } ) ;
537548
538549 it ( "clicking 'Grant more orgs' opens GitHub app settings via openGitHubUrl" , async ( ) => {
539550 const user = userEvent . setup ( ) ;
540- vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
541551 renderSettings ( ) ;
542552 const btn = screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
543553 await user . click ( btn ) ;
544554 expect ( urlModule . openGitHubUrl ) . toHaveBeenCalledWith ( buildOrgAccessUrl ( ) ) ;
545- vi . unstubAllEnvs ( ) ;
546555 } ) ;
547556
548557 it ( "clicking 'Grant more orgs' registers a focus listener for auto-merge" , async ( ) => {
549558 const user = userEvent . setup ( ) ;
550- vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
551559 const addSpy = vi . spyOn ( window , "addEventListener" ) ;
552560 renderSettings ( ) ;
553561 const btn = screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
554562 await user . click ( btn ) ;
555563 expect ( addSpy ) . toHaveBeenCalledWith ( "focus" , expect . any ( Function ) ) ;
556- vi . unstubAllEnvs ( ) ;
557564 } ) ;
558565
559- it ( "shows 'Syncing...' on button while merging and reverts after" , async ( ) => {
566+ it ( "shows disabled 'Syncing...' button during merge, reverts after" , async ( ) => {
560567 const user = userEvent . setup ( ) ;
561- vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
562568 updateConfig ( { selectedOrgs : [ ] } ) ;
563- vi . mocked ( apiModule . fetchOrgs ) . mockResolvedValue ( [ ] ) ;
569+ let resolveFetch ! : ( v : never [ ] ) => void ;
570+ vi . mocked ( apiModule . fetchOrgs ) . mockReturnValue (
571+ new Promise ( ( r ) => { resolveFetch = r as ( v : never [ ] ) => void ; } )
572+ ) ;
564573 renderSettings ( ) ;
565574 const btn = screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
566575 await user . click ( btn ) ;
567576 window . dispatchEvent ( new Event ( "focus" ) ) ;
568577 await waitFor ( ( ) => {
569- expect ( screen . getByRole ( "button" , { name : "Syncing..." } ) ) . toBeDefined ( ) ;
578+ const syncBtn = screen . getByRole ( "button" , { name : "Syncing..." } ) ;
579+ expect ( syncBtn . hasAttribute ( "disabled" ) ) . toBe ( true ) ;
570580 } ) ;
581+ resolveFetch ( [ ] ) ;
571582 await waitFor ( ( ) => {
572- expect ( screen . getByRole ( "button" , { name : "Grant more orgs" } ) ) . toBeDefined ( ) ;
583+ const restored = screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
584+ expect ( restored . hasAttribute ( "disabled" ) ) . toBe ( false ) ;
573585 } ) ;
574- vi . unstubAllEnvs ( ) ;
575586 } ) ;
576587
577588 it ( "auto-merges new orgs when window regains focus after granting" , async ( ) => {
578589 const user = userEvent . setup ( ) ;
579- vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
580590 updateConfig ( { selectedOrgs : [ "existing-org" ] } ) ;
581591 vi . mocked ( apiModule . fetchOrgs ) . mockResolvedValue ( [
582592 { login : "existing-org" , avatarUrl : "" , type : "org" } ,
@@ -585,7 +595,6 @@ describe("SettingsPage — Grant more orgs button", () => {
585595 renderSettings ( ) ;
586596 const btn = screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
587597 await user . click ( btn ) ;
588- // Simulate user returning from GitHub settings tab
589598 window . dispatchEvent ( new Event ( "focus" ) ) ;
590599 await waitFor ( ( ) => {
591600 expect ( apiModule . fetchOrgs ) . toHaveBeenCalled ( ) ;
@@ -594,12 +603,11 @@ describe("SettingsPage — Grant more orgs button", () => {
594603 expect ( config . selectedOrgs ) . toContain ( "new-org" ) ;
595604 expect ( config . selectedOrgs ) . toContain ( "existing-org" ) ;
596605 } ) ;
597- vi . unstubAllEnvs ( ) ;
598606 } ) ;
599607
600- it ( "silently handles fetchOrgs rejection on focus without breaking" , async ( ) => {
608+ it ( "pushes warning notification on fetchOrgs failure" , async ( ) => {
609+ const { pushNotification } = await import ( "../../../src/app/lib/errors" ) ;
601610 const user = userEvent . setup ( ) ;
602- vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
603611 updateConfig ( { selectedOrgs : [ "existing-org" ] } ) ;
604612 vi . mocked ( apiModule . fetchOrgs ) . mockRejectedValue ( new Error ( "Network error" ) ) ;
605613 renderSettings ( ) ;
@@ -609,55 +617,52 @@ describe("SettingsPage — Grant more orgs button", () => {
609617 await waitFor ( ( ) => {
610618 expect ( apiModule . fetchOrgs ) . toHaveBeenCalled ( ) ;
611619 } ) ;
620+ await waitFor ( ( ) => {
621+ expect ( pushNotification ) . toHaveBeenCalledWith (
622+ "org-sync" ,
623+ expect . stringContaining ( "Failed to sync" ) ,
624+ "warning" ,
625+ ) ;
626+ } ) ;
612627 expect ( config . selectedOrgs ) . toEqual ( [ "existing-org" ] ) ;
613- vi . unstubAllEnvs ( ) ;
614628 } ) ;
615629
616630 it ( "skips merge on focus when getClient returns null" , async ( ) => {
617631 const user = userEvent . setup ( ) ;
618- vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
619632 const github = await import ( "../../../src/app/services/github" ) ;
620633 vi . mocked ( github . getClient ) . mockReturnValueOnce ( null ) ;
621634 renderSettings ( ) ;
622635 const btn = screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
623636 await user . click ( btn ) ;
624637 window . dispatchEvent ( new Event ( "focus" ) ) ;
625- // Give mergeNewOrgs time to bail out
626638 await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
627639 expect ( apiModule . fetchOrgs ) . not . toHaveBeenCalled ( ) ;
628- vi . unstubAllEnvs ( ) ;
629640 } ) ;
630641
631642 it ( "rapid double-click deduplicates focus listeners" , async ( ) => {
632643 const user = userEvent . setup ( ) ;
633- vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
634644 const removeSpy = vi . spyOn ( window , "removeEventListener" ) ;
635645 renderSettings ( ) ;
636646 const btn = screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
637647 await user . click ( btn ) ;
638648 await user . click ( btn ) ;
639- // Second click removes the first listener before adding a new one
640649 const focusRemoves = removeSpy . mock . calls . filter ( ( [ evt ] ) => evt === "focus" ) ;
641650 expect ( focusRemoves . length ) . toBeGreaterThanOrEqual ( 1 ) ;
642- vi . unstubAllEnvs ( ) ;
643651 } ) ;
644652
645653 it ( "cleans up pending focus listener on component unmount" , async ( ) => {
646654 const user = userEvent . setup ( ) ;
647- vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
648655 const removeSpy = vi . spyOn ( window , "removeEventListener" ) ;
649656 const { unmount } = renderSettings ( ) ;
650657 const btn = screen . getByRole ( "button" , { name : "Grant more orgs" } ) ;
651658 await user . click ( btn ) ;
652659 unmount ( ) ;
653660 const focusRemoves = removeSpy . mock . calls . filter ( ( [ evt ] ) => evt === "focus" ) ;
654661 expect ( focusRemoves . length ) . toBeGreaterThanOrEqual ( 1 ) ;
655- vi . unstubAllEnvs ( ) ;
656662 } ) ;
657663
658664 it ( "focus listener self-removes — second focus does not re-trigger merge" , async ( ) => {
659665 const user = userEvent . setup ( ) ;
660- vi . stubEnv ( "VITE_GITHUB_CLIENT_ID" , "test-client-id" ) ;
661666 updateConfig ( { selectedOrgs : [ ] } ) ;
662667 vi . mocked ( apiModule . fetchOrgs ) . mockResolvedValue ( [ ] ) ;
663668 renderSettings ( ) ;
@@ -667,10 +672,8 @@ describe("SettingsPage — Grant more orgs button", () => {
667672 await waitFor ( ( ) => {
668673 expect ( apiModule . fetchOrgs ) . toHaveBeenCalledTimes ( 1 ) ;
669674 } ) ;
670- // Second focus should NOT trigger another merge
671675 window . dispatchEvent ( new Event ( "focus" ) ) ;
672676 await new Promise ( ( r ) => setTimeout ( r , 50 ) ) ;
673677 expect ( apiModule . fetchOrgs ) . toHaveBeenCalledTimes ( 1 ) ;
674- vi . unstubAllEnvs ( ) ;
675678 } ) ;
676679} ) ;
0 commit comments