@@ -14,12 +14,6 @@ vi.mock('../server/server-functions', () => ({
1414 getSignOutUrl : vi . fn ( ) ,
1515} ) ) ;
1616
17- // Mock TanStack Router hooks to avoid warnings
18- vi . mock ( '@tanstack/react-router' , ( ) => ( {
19- useNavigate : ( ) => vi . fn ( ) ,
20- useLocation : ( ) => ( { pathname : '/' } ) ,
21- } ) ) ;
22-
2317describe ( 'AuthKitProvider' , ( ) => {
2418 const mockUser : User = {
2519 id : 'user_123' ,
@@ -41,6 +35,31 @@ describe('AuthKitProvider', () => {
4135 vi . clearAllMocks ( ) ;
4236 } ) ;
4337
38+ it ( 'renders without router context (no useNavigate SSR warning)' , async ( ) => {
39+ // Regression test for https://github.com/workos/authkit-tanstack-start/issues/57
40+ // AuthKitProvider renders as a Wrap component before RouterProvider exists.
41+ // It must NOT call useNavigate() during render, or it would trigger:
42+ // "useRouter must be used inside a <RouterProvider>"
43+ //
44+ // Since @tanstack /react-router is not mocked here, any unconditional
45+ // useNavigate() call would throw. If this test passes, the provider
46+ // does not call useNavigate during render.
47+ const { getAuthAction } = await import ( '../server/actions' ) ;
48+ vi . mocked ( getAuthAction ) . mockResolvedValue ( { user : null } ) ;
49+
50+ // This will throw if AuthKitProvider calls useNavigate() unconditionally,
51+ // because there is no RouterProvider wrapping the component.
52+ await act ( async ( ) => {
53+ render (
54+ < AuthKitProvider >
55+ < div > Rendered without router</ div >
56+ </ AuthKitProvider > ,
57+ ) ;
58+ } ) ;
59+
60+ expect ( screen . getByText ( 'Rendered without router' ) ) . toBeDefined ( ) ;
61+ } ) ;
62+
4463 it ( 'renders children' , async ( ) => {
4564 const { getAuthAction } = await import ( '../server/actions' ) ;
4665
@@ -455,7 +474,7 @@ describe('AuthKitProvider', () => {
455474 } ) ;
456475 } ) ;
457476
458- it ( 'handles signOut when no session exists (navigates to returnTo)' , async ( ) => {
477+ it ( 'handles signOut when no session exists (navigates to returnTo via location )' , async ( ) => {
459478 const { getAuthAction } = await import ( '../server/actions' ) ;
460479 const { getSignOutUrl } = await import ( '../server/server-functions' ) ;
461480
@@ -464,30 +483,37 @@ describe('AuthKitProvider', () => {
464483 // Mock getSignOutUrl to return null URL (no session to terminate)
465484 vi . mocked ( getSignOutUrl ) . mockResolvedValue ( { url : null } ) ;
466485
467- const mockNavigate = vi . fn ( ) ;
468- // eslint-disable-next-line @typescript-eslint/no-explicit-any
469- ( vi . mocked ( await import ( '@tanstack/react-router' ) ) as any ) . useNavigate = ( ) => mockNavigate ;
486+ // Mock window.location.href
487+ const originalLocation = window . location ;
488+ Object . defineProperty ( window , 'location' , {
489+ value : { ...originalLocation , href : '' } ,
490+ writable : true ,
491+ } ) ;
470492
471- const TestComponent = ( ) => {
472- const { signOut : handleSignOut } = useAuth ( ) ;
473- return < button onClick = { ( ) => handleSignOut ( { returnTo : '/login' } ) } > Sign Out</ button > ;
474- } ;
493+ try {
494+ const TestComponent = ( ) => {
495+ const { signOut : handleSignOut } = useAuth ( ) ;
496+ return < button onClick = { ( ) => handleSignOut ( { returnTo : '/login' } ) } > Sign Out</ button > ;
497+ } ;
475498
476- render (
477- < AuthKitProvider >
478- < TestComponent />
479- </ AuthKitProvider > ,
480- ) ;
499+ render (
500+ < AuthKitProvider >
501+ < TestComponent />
502+ </ AuthKitProvider > ,
503+ ) ;
481504
482- await waitFor ( ( ) => {
483- expect ( screen . getByText ( 'Sign Out' ) ) . toBeDefined ( ) ;
484- } ) ;
505+ await waitFor ( ( ) => {
506+ expect ( screen . getByText ( 'Sign Out' ) ) . toBeDefined ( ) ;
507+ } ) ;
485508
486- await act ( async ( ) => {
487- fireEvent . click ( screen . getByText ( 'Sign Out' ) ) ;
488- } ) ;
509+ await act ( async ( ) => {
510+ fireEvent . click ( screen . getByText ( 'Sign Out' ) ) ;
511+ } ) ;
489512
490- expect ( mockNavigate ) . toHaveBeenCalledWith ( { to : '/login' } ) ;
513+ expect ( window . location . href ) . toBe ( '/login' ) ;
514+ } finally {
515+ Object . defineProperty ( window , 'location' , { value : originalLocation , writable : true } ) ;
516+ }
491517 } ) ;
492518
493519 it ( 'uses default returnTo when signOut called without options' , async ( ) => {
@@ -497,31 +523,38 @@ describe('AuthKitProvider', () => {
497523 vi . mocked ( getAuthAction ) . mockResolvedValue ( { user : mockUser , sessionId : 'session_123' } ) ;
498524 vi . mocked ( getSignOutUrl ) . mockResolvedValue ( { url : null } ) ;
499525
500- const mockNavigate = vi . fn ( ) ;
501- // eslint-disable-next-line @typescript-eslint/no-explicit-any
502- ( vi . mocked ( await import ( '@tanstack/react-router' ) ) as any ) . useNavigate = ( ) => mockNavigate ;
526+ // Mock window.location.href
527+ const originalLocation = window . location ;
528+ Object . defineProperty ( window , 'location' , {
529+ value : { ...originalLocation , href : '' } ,
530+ writable : true ,
531+ } ) ;
503532
504- const TestComponent = ( ) => {
505- const { signOut : handleSignOut } = useAuth ( ) ;
506- return < button onClick = { ( ) => handleSignOut ( ) } > Sign Out</ button > ;
507- } ;
533+ try {
534+ const TestComponent = ( ) => {
535+ const { signOut : handleSignOut } = useAuth ( ) ;
536+ return < button onClick = { ( ) => handleSignOut ( ) } > Sign Out</ button > ;
537+ } ;
508538
509- render (
510- < AuthKitProvider >
511- < TestComponent />
512- </ AuthKitProvider > ,
513- ) ;
539+ render (
540+ < AuthKitProvider >
541+ < TestComponent />
542+ </ AuthKitProvider > ,
543+ ) ;
514544
515- await waitFor ( ( ) => {
516- expect ( screen . getByText ( 'Sign Out' ) ) . toBeDefined ( ) ;
517- } ) ;
545+ await waitFor ( ( ) => {
546+ expect ( screen . getByText ( 'Sign Out' ) ) . toBeDefined ( ) ;
547+ } ) ;
518548
519- await act ( async ( ) => {
520- fireEvent . click ( screen . getByText ( 'Sign Out' ) ) ;
521- } ) ;
549+ await act ( async ( ) => {
550+ fireEvent . click ( screen . getByText ( 'Sign Out' ) ) ;
551+ } ) ;
522552
523- // Default returnTo is '/'
524- expect ( getSignOutUrl ) . toHaveBeenCalledWith ( { data : { returnTo : '/' } } ) ;
525- expect ( mockNavigate ) . toHaveBeenCalledWith ( { to : '/' } ) ;
553+ // Default returnTo is '/'
554+ expect ( getSignOutUrl ) . toHaveBeenCalledWith ( { data : { returnTo : '/' } } ) ;
555+ expect ( window . location . href ) . toBe ( '/' ) ;
556+ } finally {
557+ Object . defineProperty ( window , 'location' , { value : originalLocation , writable : true } ) ;
558+ }
526559 } ) ;
527560} ) ;
0 commit comments