Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
);

Expand Down
96 changes: 87 additions & 9 deletions src/context/__tests__/AuthContext.test.tsx
Original file line number Diff line number Diff line change
@@ -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<string, unknown> }) => void
) => {
cb({ exists: () => false });
return jest.fn();
}
);

const firebaseMock = {
auth: {},
Expand All @@ -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()),
Expand All @@ -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<string, unknown> }) => void,
errorCb?: (error: Error) => void
) => mockOnSnapshot(ref, cb, errorCb),
writeBatch: jest.fn(() => ({
commit: jest.fn(),
set: jest.fn(),
Expand Down Expand Up @@ -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<string | null>(null);
const handleLogin = async () => {
try {
Expand All @@ -89,6 +98,7 @@ function TestHarness() {
<div>
<span data-testid="user-email">{user?.email || 'null'}</span>
<span data-testid="error">{error || ''}</span>
<span data-testid="loading-status">{isLoading ? 'loading' : 'done'}</span>
<button data-testid="login-btn" onClick={handleLogin}>
login
</button>
Expand All @@ -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<string, unknown> }) => void
) => {
cb({ exists: () => false });
return jest.fn();
}
);
});

describe('AuthContext', () => {
Expand Down Expand Up @@ -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<string, unknown> }) => 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<string, unknown> }) => 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');
});
});
});
});
Loading