11import { afterEach , describe , expect , it , vi } from "vitest" ;
2- import { cleanup , fireEvent , render , screen , waitFor } from "@testing-library/react" ;
2+ import { act , cleanup , fireEvent , render , screen , waitFor } from "@testing-library/react" ;
33import { RouterContainer } from "./router" ;
44import { AppShellConfigContext , AppShellDataContext } from "@/contexts/appshell-context" ;
55import { Link , Outlet , useNavigate } from "react-router" ;
@@ -27,6 +27,7 @@ const renderWithConfig = ({
2727 contextData = { } ,
2828 authClient,
2929 autoLogin,
30+ guardComponent,
3031} : {
3132 modules ?: Array < Module > ;
3233 basePath ?: string ;
@@ -35,6 +36,7 @@ const renderWithConfig = ({
3536 contextData ?: ContextData ;
3637 authClient ?: EnhancedAuthClient ;
3738 autoLogin ?: boolean ;
39+ guardComponent ?: ( ) => React . ReactNode ;
3840} ) => {
3941 const configurations = {
4042 modules,
@@ -58,7 +60,7 @@ const renderWithConfig = ({
5860
5961 render (
6062 authClient ? (
61- < AuthProvider client = { authClient } autoLogin = { autoLogin } >
63+ < AuthProvider client = { authClient } autoLogin = { autoLogin } guardComponent = { guardComponent } >
6264 { tree }
6365 </ AuthProvider >
6466 ) : (
@@ -492,4 +494,129 @@ describe("RouterContainer with AuthProvider", () => {
492494 await screen . findByText ( "Home" ) ;
493495 expect ( mockLogin ) . not . toHaveBeenCalled ( ) ;
494496 } ) ;
497+
498+ it ( "shows guard component when not ready" , async ( ) => {
499+ const authClient = createMockAuthClient ( {
500+ isAuthenticated : false ,
501+ error : null ,
502+ isReady : false ,
503+ } ) ;
504+
505+ renderWithConfig ( {
506+ modules : [ ] ,
507+ rootComponent : ( ) => < div > Home</ div > ,
508+ initialEntries : [ "/" ] ,
509+ authClient,
510+ guardComponent : ( ) => < div > Loading...</ div > ,
511+ } ) ;
512+
513+ expect ( await screen . findByText ( "Loading..." ) ) . toBeDefined ( ) ;
514+ expect ( screen . queryByText ( "Home" ) ) . toBeNull ( ) ;
515+ } ) ;
516+
517+ it ( "shows guard component when not authenticated" , async ( ) => {
518+ const authClient = createMockAuthClient ( {
519+ isAuthenticated : false ,
520+ error : null ,
521+ isReady : true ,
522+ } ) ;
523+
524+ renderWithConfig ( {
525+ modules : [ ] ,
526+ rootComponent : ( ) => < div > Home</ div > ,
527+ initialEntries : [ "/" ] ,
528+ authClient,
529+ guardComponent : ( ) => < div > Please log in</ div > ,
530+ } ) ;
531+
532+ expect ( await screen . findByText ( "Please log in" ) ) . toBeDefined ( ) ;
533+ expect ( screen . queryByText ( "Home" ) ) . toBeNull ( ) ;
534+ } ) ;
535+
536+ it ( "shows children when authenticated with guardComponent" , async ( ) => {
537+ const mockCheckAuthStatus = vi . fn ( ) . mockResolvedValue ( {
538+ isAuthenticated : true ,
539+ error : null ,
540+ isReady : true ,
541+ } ) ;
542+ const authClient = createMockAuthClient (
543+ { isAuthenticated : true , error : null , isReady : true } ,
544+ { checkAuthStatus : mockCheckAuthStatus } ,
545+ ) ;
546+
547+ renderWithConfig ( {
548+ modules : [ ] ,
549+ rootComponent : ( ) => < div > Home</ div > ,
550+ initialEntries : [ "/" ] ,
551+ authClient,
552+ guardComponent : ( ) => < div > Please log in</ div > ,
553+ } ) ;
554+
555+ expect ( await screen . findByText ( "Home" ) ) . toBeDefined ( ) ;
556+ expect ( screen . queryByText ( "Please log in" ) ) . toBeNull ( ) ;
557+ } ) ;
558+
559+ it ( "transitions from guard to children when auth state changes" , async ( ) => {
560+ // Mutable snapshot; initially not ready, not authenticated.
561+ // The loader calls checkAuthStatus, but the mock does NOT update the
562+ // snapshot during the loader — so the guard is shown on first render.
563+ let snapshot = {
564+ isAuthenticated : false ,
565+ error : null as string | null ,
566+ isReady : false ,
567+ } ;
568+
569+ // Collect listeners so we can trigger state change notifications
570+ const listeners : Array < ( event : { type : string } ) => void > = [ ] ;
571+
572+ // checkAuthStatus resolves without updating snapshot, mimicking a
573+ // still-pending auth check from the guard's perspective.
574+ const mockCheckAuthStatus = vi . fn ( ) . mockResolvedValue ( {
575+ isAuthenticated : false ,
576+ error : null ,
577+ isReady : true ,
578+ } ) ;
579+
580+ const authClient = createMockAuthClient ( snapshot , {
581+ checkAuthStatus : mockCheckAuthStatus ,
582+ // Return the same reference until we reassign snapshot (required by useSyncExternalStore)
583+ getState : vi . fn ( ( ) => snapshot ) ,
584+ addEventListener : vi . fn ( ( listener ) => {
585+ listeners . push ( listener ) ;
586+ return ( ) => {
587+ const idx = listeners . indexOf ( listener ) ;
588+ if ( idx >= 0 ) listeners . splice ( idx , 1 ) ;
589+ } ;
590+ } ) ,
591+ } ) ;
592+
593+ renderWithConfig ( {
594+ modules : [ ] ,
595+ rootComponent : ( ) => < div > Home</ div > ,
596+ initialEntries : [ "/" ] ,
597+ authClient,
598+ guardComponent : ( ) => < div > Loading...</ div > ,
599+ } ) ;
600+
601+ // Initially the guard should be shown
602+ expect ( await screen . findByText ( "Loading..." ) ) . toBeDefined ( ) ;
603+ expect ( screen . queryByText ( "Home" ) ) . toBeNull ( ) ;
604+
605+ // Simulate auth state becoming ready and authenticated (e.g. token refresh
606+ // or login flow completing outside the loader).
607+ act ( ( ) => {
608+ snapshot = {
609+ isAuthenticated : true ,
610+ error : null ,
611+ isReady : true ,
612+ } ;
613+ for ( const listener of listeners ) {
614+ listener ( { type : "auth_state_changed" } ) ;
615+ }
616+ } ) ;
617+
618+ // After auth state transitions, children should replace the guard
619+ expect ( await screen . findByText ( "Home" ) ) . toBeDefined ( ) ;
620+ expect ( screen . queryByText ( "Loading..." ) ) . toBeNull ( ) ;
621+ } ) ;
495622} ) ;
0 commit comments