diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 9fb11587..f6024fc0 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -338,6 +338,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setUser(updatedUser as User); } setIsLoading(false); + }, + (error) => { + console.error('Firestore user sync error:', error); + setIsLoading(false); } ); diff --git a/src/context/__tests__/AuthContext.test.tsx b/src/context/__tests__/AuthContext.test.tsx index a801ece1..79b24769 100644 --- a/src/context/__tests__/AuthContext.test.tsx +++ b/src/context/__tests__/AuthContext.test.tsx @@ -1,10 +1,20 @@ import '@testing-library/jest-dom'; import React from 'react'; -import { render, screen, act } from '@testing-library/react'; +import { render, screen, act, waitFor } from '@testing-library/react'; import { AuthProvider, useAuth } from '../AuthContext'; const mockSignIn = jest.fn(); const mockSignOut = jest.fn(); +const mockOnAuthStateChanged = jest.fn(() => jest.fn()); +const mockOnSnapshot = jest.fn( + ( + _ref: unknown, + cb: (snapshot: { exists: () => boolean; data?: () => Record }) => void + ) => { + cb({ exists: () => false }); + return jest.fn(); + } +); const firebaseMock = { auth: {}, @@ -15,7 +25,7 @@ const firebaseMock = { jest.mock('firebase/auth', () => ({ getAuth: () => ({}), - onAuthStateChanged: jest.fn(() => jest.fn()), + onAuthStateChanged: (...args: unknown[]) => mockOnAuthStateChanged(...args), signInWithEmailAndPassword: (...args: unknown[]) => mockSignIn(...args), signOut: (...args: unknown[]) => mockSignOut(...args), setPersistence: jest.fn(() => Promise.resolve()), @@ -41,12 +51,11 @@ jest.mock('firebase/firestore', () => ({ doc: jest.fn(() => ({})), getDoc: jest.fn(() => Promise.resolve({ exists: () => false })), setDoc: jest.fn(() => Promise.resolve()), - onSnapshot: jest.fn( - (_ref: unknown, cb: (snapshot: { exists: () => boolean }) => void) => { - cb({ exists: () => false }); - return jest.fn(); - } - ), + onSnapshot: ( + ref: unknown, + cb: (snapshot: { exists: () => boolean; data?: () => Record }) => void, + errorCb?: (error: Error) => void + ) => mockOnSnapshot(ref, cb, errorCb), writeBatch: jest.fn(() => ({ commit: jest.fn(), set: jest.fn(), @@ -76,7 +85,7 @@ jest.mock('@/lib/points', () => ({ })); function TestHarness() { - const { user, login, logout } = useAuth(); + const { user, isLoading, login, logout } = useAuth(); const [error, setError] = React.useState(null); const handleLogin = async () => { try { @@ -89,6 +98,7 @@ function TestHarness() {
{user?.email || 'null'} {error || ''} + {isLoading ? 'loading' : 'done'} @@ -110,6 +120,16 @@ function renderWithAuth() { beforeEach(() => { jest.clearAllMocks(); localStorage.clear(); + mockOnAuthStateChanged.mockImplementation(() => jest.fn()); + mockOnSnapshot.mockImplementation( + ( + _ref: unknown, + cb: (snapshot: { exists: () => boolean; data?: () => Record }) => void + ) => { + cb({ exists: () => false }); + return jest.fn(); + } + ); }); describe('AuthContext', () => { @@ -207,4 +227,62 @@ describe('AuthContext', () => { firebaseMock.firebaseAvailable = true; }); }); + + describe('Firestore onSnapshot loading state', () => { + it('sets isLoading to false when Firestore sync succeeds', async () => { + mockOnAuthStateChanged.mockImplementation((_auth: unknown, callback: (user: { uid: string; email: string | null; displayName: string | null } | null) => void) => { + callback({ uid: 'test-uid', email: 'test@example.com', displayName: 'Test User' }); + return jest.fn(); + }); + + mockOnSnapshot.mockImplementation( + ( + _ref: unknown, + cb: (snapshot: { exists: () => boolean; data?: () => Record }) => void + ) => { + cb({ + exists: () => true, + data: () => ({ + role: 'member', + points: 100, + }), + }); + return jest.fn(); + } + ); + + renderWithAuth(); + + await waitFor(() => { + expect(screen.getByTestId('loading-status')).toHaveTextContent('done'); + }); + expect(screen.getByTestId('user-email')).toHaveTextContent('test@example.com'); + }); + + it('sets isLoading to false and does not hang when Firestore sync fails', async () => { + mockOnAuthStateChanged.mockImplementation((_auth: unknown, callback: (user: { uid: string; email: string | null; displayName: string | null } | null) => void) => { + callback({ uid: 'test-uid-error', email: 'test-error@example.com', displayName: 'Test User Error' }); + return jest.fn(); + }); + + mockOnSnapshot.mockImplementation( + ( + _ref: unknown, + _cb: (snapshot: { exists: () => boolean; data?: () => Record }) => void, + errorCb?: (error: Error) => void + ) => { + if (errorCb) { + errorCb(new Error('Mocked Firestore Permission Denied')); + } + return jest.fn(); + } + ); + + renderWithAuth(); + + await waitFor(() => { + expect(screen.getByTestId('loading-status')).toHaveTextContent('done'); + }); + }); + }); });