// src/hooks/useWatchedItems.test.tsx import { renderHook, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useWatchedItems } from './useWatchedItems'; import { useAuth } from '../hooks/useAuth'; import { useUserData } from '../hooks/useUserData'; import { useAddWatchedItemMutation, useRemoveWatchedItemMutation } from './mutations'; import type { User } from '../types'; import { createMockMasterGroceryItem, createMockUser, createMockUserProfile, } from '../tests/utils/mockFactories'; // Mock the hooks that useWatchedItems depends on vi.mock('../hooks/useAuth'); vi.mock('../hooks/useUserData'); vi.mock('./mutations', () => ({ useAddWatchedItemMutation: vi.fn(), useRemoveWatchedItemMutation: vi.fn(), })); const mockedUseAuth = vi.mocked(useAuth); const mockedUseUserData = vi.mocked(useUserData); const mockedUseAddWatchedItemMutation = vi.mocked(useAddWatchedItemMutation); const mockedUseRemoveWatchedItemMutation = vi.mocked(useRemoveWatchedItemMutation); const mockUser: User = createMockUser({ user_id: 'user-123', email: 'test@example.com' }); const mockInitialItems = [ createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' }), createMockMasterGroceryItem({ master_grocery_item_id: 2, name: 'Bread' }), ]; describe('useWatchedItems Hook', () => { const mockMutateAsync = vi.fn(); const mockAddMutation = { mutateAsync: mockMutateAsync, mutate: vi.fn(), isPending: false, error: null, isError: false, isSuccess: false, isIdle: true, }; const mockRemoveMutation = { mutateAsync: mockMutateAsync, mutate: vi.fn(), isPending: false, error: null, isError: false, isSuccess: false, isIdle: true, }; beforeEach(() => { vi.resetAllMocks(); // Mock TanStack Query mutation hooks mockedUseAddWatchedItemMutation.mockReturnValue(mockAddMutation as any); mockedUseRemoveWatchedItemMutation.mockReturnValue(mockRemoveMutation as any); // Provide default implementation for auth mockedUseAuth.mockReturnValue({ userProfile: createMockUserProfile({ user: mockUser }), authStatus: 'AUTHENTICATED', isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(), }); // Provide default implementation for user data (no more setters!) mockedUseUserData.mockReturnValue({ watchedItems: mockInitialItems, shoppingLists: [], isLoading: false, error: null, }); }); it('should initialize with the watched items from useData', () => { const { result } = renderHook(() => useWatchedItems()); expect(result.current.watchedItems).toEqual(mockInitialItems); expect(result.current.error).toBeNull(); }); it('should use TanStack Query mutation hooks', () => { renderHook(() => useWatchedItems()); // Verify that the mutation hooks were called expect(mockedUseAddWatchedItemMutation).toHaveBeenCalled(); expect(mockedUseRemoveWatchedItemMutation).toHaveBeenCalled(); }); describe('addWatchedItem', () => { it('should call the mutation with correct parameters', async () => { mockMutateAsync.mockResolvedValue({}); const { result } = renderHook(() => useWatchedItems()); await act(async () => { await result.current.addWatchedItem('Cheese', 'Dairy'); }); // Verify mutation was called with correct parameters expect(mockMutateAsync).toHaveBeenCalledWith({ itemName: 'Cheese', category: 'Dairy', }); }); it('should expose error from mutation', () => { const errorMutation = { ...mockAddMutation, error: new Error('API Error'), }; mockedUseAddWatchedItemMutation.mockReturnValue(errorMutation as any); const { result } = renderHook(() => useWatchedItems()); expect(result.current.error).toBe('API Error'); }); it('should handle mutation errors gracefully', async () => { mockMutateAsync.mockRejectedValue(new Error('Failed to add')); const { result } = renderHook(() => useWatchedItems()); await act(async () => { await result.current.addWatchedItem('Failing Item', 'Error'); }); // Should not throw - error is caught and logged expect(mockMutateAsync).toHaveBeenCalled(); }); }); describe('removeWatchedItem', () => { it('should call the mutation with correct parameters', async () => { mockMutateAsync.mockResolvedValue({}); const { result } = renderHook(() => useWatchedItems()); await act(async () => { await result.current.removeWatchedItem(1); }); // Verify mutation was called with correct parameters expect(mockMutateAsync).toHaveBeenCalledWith({ masterItemId: 1, }); }); it('should expose error from remove mutation', () => { const errorMutation = { ...mockRemoveMutation, error: new Error('Deletion Failed'), }; mockedUseRemoveWatchedItemMutation.mockReturnValue(errorMutation as any); const { result } = renderHook(() => useWatchedItems()); expect(result.current.error).toBe('Deletion Failed'); }); it('should handle mutation errors gracefully', async () => { mockMutateAsync.mockRejectedValue(new Error('Failed to remove')); const { result } = renderHook(() => useWatchedItems()); await act(async () => { await result.current.removeWatchedItem(999); }); // Should not throw - error is caught and logged expect(mockMutateAsync).toHaveBeenCalled(); }); }); it('should not perform actions if the user is not authenticated', async () => { mockedUseAuth.mockReturnValue({ userProfile: null, authStatus: 'SIGNED_OUT', isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(), }); const { result } = renderHook(() => useWatchedItems()); await act(async () => { await result.current.addWatchedItem('Test', 'Category'); await result.current.removeWatchedItem(1); }); // Mutations should not be called when user is not authenticated expect(mockMutateAsync).not.toHaveBeenCalled(); }); });