// src/hooks/useUserData.test.tsx import React, { ReactNode } from 'react'; import { renderHook, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useUserData } from './useUserData'; import { useAuth } from './useAuth'; import { UserDataProvider } from '../providers/UserDataProvider'; import { useWatchedItemsQuery } from './queries/useWatchedItemsQuery'; import { useShoppingListsQuery } from './queries/useShoppingListsQuery'; import type { UserProfile } from '../types'; import { createMockMasterGroceryItem, createMockShoppingList, createMockUserProfile, } from '../tests/utils/mockFactories'; // 1. Mock the hook's dependencies vi.mock('../hooks/useAuth'); vi.mock('./queries/useWatchedItemsQuery'); vi.mock('./queries/useShoppingListsQuery'); // 2. Create typed mocks for type safety and autocompletion const mockedUseAuth = vi.mocked(useAuth); const mockedUseWatchedItemsQuery = vi.mocked(useWatchedItemsQuery); const mockedUseShoppingListsQuery = vi.mocked(useShoppingListsQuery); // 3. A simple wrapper component that renders our provider. // This is necessary because the useUserData hook needs to be a child of UserDataProvider. const wrapper = ({ children }: { children: ReactNode }) => ( {children} ); // No change needed here, just for context // 4. Mock data for testing const mockUser: UserProfile = createMockUserProfile({ full_name: 'Test User', points: 100, user: { user_id: 'user-123', email: 'test@example.com' }, }); const mockWatchedItems = [createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' })]; const mockShoppingLists = [ createMockShoppingList({ shopping_list_id: 1, name: 'Weekly Groceries', user_id: 'user-123' }), ]; describe('useUserData Hook and UserDataProvider', () => { beforeEach(() => { // Clear mock history before each test to ensure test isolation. vi.clearAllMocks(); }); it('should throw an error if useUserData is used outside of UserDataProvider', () => { // Suppress the expected console.error for this test to keep the output clean. const originalError = console.error; console.error = vi.fn(); // Expecting renderHook to throw an error because there's no provider. expect(() => renderHook(() => useUserData())).toThrow( 'useUserData must be used within a UserDataProvider', ); // Restore the original console.error function. console.error = originalError; }); it('should return initial empty state when user is not authenticated', () => { // Arrange: Simulate a logged-out user. mockedUseAuth.mockReturnValue({ userProfile: null, authStatus: 'SIGNED_OUT', isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(), }); // Arrange: Mock the return value of the inner hooks. mockedUseWatchedItemsQuery.mockReturnValue({ data: [], isLoading: false, error: null, } as any); mockedUseShoppingListsQuery.mockReturnValue({ data: [], isLoading: false, error: null, } as any); // Act: Render the hook within the provider wrapper. const { result } = renderHook(() => useUserData(), { wrapper }); // Assert: Check that the context values are in their initial, empty state. expect(result.current.isLoading).toBe(false); expect(result.current.watchedItems).toEqual([]); expect(result.current.shoppingLists).toEqual([]); expect(result.current.error).toBeNull(); // Assert: Check that queries were disabled (called with false) expect(mockedUseWatchedItemsQuery).toHaveBeenCalledWith(false); expect(mockedUseShoppingListsQuery).toHaveBeenCalledWith(false); }); it('should return loading state when user is authenticated and data is fetching', () => { // Arrange: Simulate a logged-in user. mockedUseAuth.mockReturnValue({ userProfile: mockUser, authStatus: 'AUTHENTICATED', isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(), }); // Arrange: Mock one of the inner hooks to be in a loading state. mockedUseWatchedItemsQuery.mockReturnValue({ data: undefined, isLoading: true, error: null, } as any); mockedUseShoppingListsQuery.mockReturnValue({ data: undefined, isLoading: false, error: null, } as any); // Act const { result } = renderHook(() => useUserData(), { wrapper }); // Assert expect(result.current.isLoading).toBe(true); }); it('should return data on successful fetch when user is authenticated', async () => { // Arrange: Simulate a logged-in user. mockedUseAuth.mockReturnValue({ userProfile: mockUser, authStatus: 'AUTHENTICATED', isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(), }); // Arrange: Mock successful data fetches for both inner hooks. mockedUseWatchedItemsQuery.mockReturnValue({ data: mockWatchedItems, isLoading: false, error: null, } as any); mockedUseShoppingListsQuery.mockReturnValue({ data: mockShoppingLists, isLoading: false, error: null, } as any); // Act const { result } = renderHook(() => useUserData(), { wrapper }); // Assert: Use `waitFor` to allow the `useEffect` hook to update the state. await waitFor(() => { expect(result.current.isLoading).toBe(false); expect(result.current.watchedItems).toEqual(mockWatchedItems); expect(result.current.shoppingLists).toEqual(mockShoppingLists); expect(result.current.error).toBeNull(); }); }); it('should return an error state if one of the fetches fails', async () => { // Arrange: Simulate a logged-in user. mockedUseAuth.mockReturnValue({ userProfile: mockUser, authStatus: 'AUTHENTICATED', isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(), }); const mockError = new Error('Failed to fetch watched items'); mockedUseWatchedItemsQuery.mockReturnValue({ data: undefined, isLoading: false, error: mockError, } as any); mockedUseShoppingListsQuery.mockReturnValue({ data: mockShoppingLists, isLoading: false, error: null, } as any); // Act const { result } = renderHook(() => useUserData(), { wrapper }); // Assert await waitFor(() => { expect(result.current.isLoading).toBe(false); expect(result.current.error).toBe('Failed to fetch watched items'); // Data that was fetched successfully should still be populated. expect(result.current.shoppingLists).toEqual(mockShoppingLists); // Data that failed should be empty. expect(result.current.watchedItems).toEqual([]); }); }); it('should clear data when the user logs out', async () => { // Arrange: Start with a logged-in user and data. mockedUseAuth.mockReturnValue({ userProfile: mockUser, authStatus: 'AUTHENTICATED', isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(), }); mockedUseWatchedItemsQuery.mockReturnValue({ data: mockWatchedItems, isLoading: false, error: null, } as any); mockedUseShoppingListsQuery.mockReturnValue({ data: mockShoppingLists, isLoading: false, error: null, } as any); const { result, rerender } = renderHook(() => useUserData(), { wrapper }); await waitFor(() => expect(result.current.watchedItems).not.toEqual([])); // Act: Simulate logout by re-rendering with a null user. mockedUseAuth.mockReturnValue({ userProfile: null, authStatus: 'SIGNED_OUT', isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(), }); // Update mocks to return empty data for the logged out state mockedUseWatchedItemsQuery.mockReturnValue({ data: [], isLoading: false, error: null, } as any); mockedUseShoppingListsQuery.mockReturnValue({ data: [], isLoading: false, error: null, } as any); rerender(); // Assert: The data should now be cleared. expect(result.current.watchedItems).toEqual([]); expect(result.current.shoppingLists).toEqual([]); }); });