Files
flyer-crawler.projectium.com/src/hooks/useShoppingLists.test.tsx
Torben Sorensen 46c1e56b14
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 46s
progress enforcing adr-0005
2026-01-08 21:40:20 -08:00

416 lines
13 KiB
TypeScript

// src/hooks/useShoppingLists.test.tsx
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useShoppingLists } from './useShoppingLists';
import { useAuth } from '../hooks/useAuth';
import { useUserData } from '../hooks/useUserData';
import {
useCreateShoppingListMutation,
useDeleteShoppingListMutation,
useAddShoppingListItemMutation,
useUpdateShoppingListItemMutation,
useRemoveShoppingListItemMutation,
} from './mutations';
import type { User } from '../types';
import {
createMockShoppingList,
createMockUser,
createMockUserProfile,
} from '../tests/utils/mockFactories';
// Mock the hooks that useShoppingLists depends on
vi.mock('../hooks/useAuth');
vi.mock('../hooks/useUserData');
vi.mock('./mutations', () => ({
useCreateShoppingListMutation: vi.fn(),
useDeleteShoppingListMutation: vi.fn(),
useAddShoppingListItemMutation: vi.fn(),
useUpdateShoppingListItemMutation: vi.fn(),
useRemoveShoppingListItemMutation: vi.fn(),
}));
const mockedUseAuth = vi.mocked(useAuth);
const mockedUseUserData = vi.mocked(useUserData);
const mockedUseCreateShoppingListMutation = vi.mocked(useCreateShoppingListMutation);
const mockedUseDeleteShoppingListMutation = vi.mocked(useDeleteShoppingListMutation);
const mockedUseAddShoppingListItemMutation = vi.mocked(useAddShoppingListItemMutation);
const mockedUseUpdateShoppingListItemMutation = vi.mocked(useUpdateShoppingListItemMutation);
const mockedUseRemoveShoppingListItemMutation = vi.mocked(useRemoveShoppingListItemMutation);
const mockUser: User = createMockUser({ user_id: 'user-123', email: 'test@example.com' });
const mockLists = [
createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' }),
createMockShoppingList({ shopping_list_id: 2, name: 'Hardware', user_id: 'user-123' }),
];
describe('useShoppingLists Hook', () => {
const mockMutateAsync = vi.fn();
const createBaseMutation = () => ({
mutateAsync: mockMutateAsync,
mutate: vi.fn(),
isPending: false,
error: null,
isError: false,
isSuccess: false,
isIdle: true,
});
const mockCreateMutation = createBaseMutation();
const mockDeleteMutation = createBaseMutation();
const mockAddItemMutation = createBaseMutation();
const mockUpdateItemMutation = createBaseMutation();
const mockRemoveItemMutation = createBaseMutation();
beforeEach(() => {
vi.resetAllMocks();
// Mock all TanStack Query mutation hooks
mockedUseCreateShoppingListMutation.mockReturnValue(mockCreateMutation as any);
mockedUseDeleteShoppingListMutation.mockReturnValue(mockDeleteMutation as any);
mockedUseAddShoppingListItemMutation.mockReturnValue(mockAddItemMutation as any);
mockedUseUpdateShoppingListItemMutation.mockReturnValue(mockUpdateItemMutation as any);
mockedUseRemoveShoppingListItemMutation.mockReturnValue(mockRemoveItemMutation 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({
shoppingLists: [],
watchedItems: [],
isLoading: false,
error: null,
});
});
it('should initialize with no active list when there are no lists', () => {
const { result } = renderHook(() => useShoppingLists());
expect(result.current.shoppingLists).toEqual([]);
expect(result.current.activeListId).toBeNull();
});
it('should set the first list as active when lists exist', () => {
mockedUseUserData.mockReturnValue({
shoppingLists: mockLists,
watchedItems: [],
isLoading: false,
error: null,
});
const { result } = renderHook(() => useShoppingLists());
expect(result.current.activeListId).toBe(1);
});
it('should use TanStack Query mutation hooks', () => {
renderHook(() => useShoppingLists());
// Verify that all mutation hooks were called
expect(mockedUseCreateShoppingListMutation).toHaveBeenCalled();
expect(mockedUseDeleteShoppingListMutation).toHaveBeenCalled();
expect(mockedUseAddShoppingListItemMutation).toHaveBeenCalled();
expect(mockedUseUpdateShoppingListItemMutation).toHaveBeenCalled();
expect(mockedUseRemoveShoppingListItemMutation).toHaveBeenCalled();
});
it('should expose loading states from mutations', () => {
const loadingCreateMutation = { ...mockCreateMutation, isPending: true };
mockedUseCreateShoppingListMutation.mockReturnValue(loadingCreateMutation as any);
const { result } = renderHook(() => useShoppingLists());
expect(result.current.isCreatingList).toBe(true);
});
describe('createList', () => {
it('should call the mutation with correct parameters', async () => {
mockMutateAsync.mockResolvedValue({});
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.createList('New List');
});
expect(mockMutateAsync).toHaveBeenCalledWith({ name: 'New List' });
});
it('should handle mutation errors gracefully', async () => {
mockMutateAsync.mockRejectedValue(new Error('Failed to create'));
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.createList('Failing List');
});
// Should not throw - error is caught and logged
expect(mockMutateAsync).toHaveBeenCalled();
});
});
describe('deleteList', () => {
it('should call the mutation with correct parameters', async () => {
mockMutateAsync.mockResolvedValue({});
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.deleteList(1);
});
expect(mockMutateAsync).toHaveBeenCalledWith({ listId: 1 });
});
it('should handle mutation errors gracefully', async () => {
mockMutateAsync.mockRejectedValue(new Error('Failed to delete'));
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.deleteList(999);
});
// Should not throw - error is caught and logged
expect(mockMutateAsync).toHaveBeenCalled();
});
});
describe('addItemToList', () => {
it('should call the mutation with correct parameters for master item', async () => {
mockMutateAsync.mockResolvedValue({});
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.addItemToList(1, { masterItemId: 42 });
});
expect(mockMutateAsync).toHaveBeenCalledWith({
listId: 1,
item: { masterItemId: 42 },
});
});
it('should call the mutation with correct parameters for custom item', async () => {
mockMutateAsync.mockResolvedValue({});
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.addItemToList(1, { customItemName: 'Special Item' });
});
expect(mockMutateAsync).toHaveBeenCalledWith({
listId: 1,
item: { customItemName: 'Special Item' },
});
});
it('should handle mutation errors gracefully', async () => {
mockMutateAsync.mockRejectedValue(new Error('Failed to add item'));
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.addItemToList(1, { masterItemId: 42 });
});
// Should not throw - error is caught and logged
expect(mockMutateAsync).toHaveBeenCalled();
});
});
describe('updateItemInList', () => {
it('should call the mutation with correct parameters', async () => {
mockMutateAsync.mockResolvedValue({});
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.updateItemInList(10, { is_purchased: true });
});
expect(mockMutateAsync).toHaveBeenCalledWith({
itemId: 10,
updates: { is_purchased: true },
});
});
it('should handle mutation errors gracefully', async () => {
mockMutateAsync.mockRejectedValue(new Error('Failed to update'));
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.updateItemInList(10, { quantity: 5 });
});
// Should not throw - error is caught and logged
expect(mockMutateAsync).toHaveBeenCalled();
});
});
describe('removeItemFromList', () => {
it('should call the mutation with correct parameters', async () => {
mockMutateAsync.mockResolvedValue({});
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.removeItemFromList(10);
});
expect(mockMutateAsync).toHaveBeenCalledWith({ itemId: 10 });
});
it('should handle mutation errors gracefully', async () => {
mockMutateAsync.mockRejectedValue(new Error('Failed to remove'));
const { result } = renderHook(() => useShoppingLists());
await act(async () => {
await result.current.removeItemFromList(999);
});
// Should not throw - error is caught and logged
expect(mockMutateAsync).toHaveBeenCalled();
});
});
describe('error handling', () => {
it('should expose error from any mutation', () => {
const errorMutation = {
...mockAddItemMutation,
error: new Error('Add item failed'),
};
mockedUseAddShoppingListItemMutation.mockReturnValue(errorMutation as any);
const { result } = renderHook(() => useShoppingLists());
expect(result.current.error).toBe('Add item failed');
});
it('should consolidate errors from multiple mutations', () => {
const createError = { ...mockCreateMutation, error: new Error('Create failed') };
const deleteError = { ...mockDeleteMutation, error: new Error('Delete failed') };
mockedUseCreateShoppingListMutation.mockReturnValue(createError as any);
mockedUseDeleteShoppingListMutation.mockReturnValue(deleteError as any);
const { result } = renderHook(() => useShoppingLists());
// Should return the first error found
expect(result.current.error).toBeTruthy();
});
});
describe('activeListId management', () => {
it('should allow setting active list manually', () => {
mockedUseUserData.mockReturnValue({
shoppingLists: mockLists,
watchedItems: [],
isLoading: false,
error: null,
});
const { result } = renderHook(() => useShoppingLists());
act(() => {
result.current.setActiveListId(2);
});
expect(result.current.activeListId).toBe(2);
});
it('should reset active list when all lists are deleted', () => {
// Start with lists
mockedUseUserData.mockReturnValue({
shoppingLists: mockLists,
watchedItems: [],
isLoading: false,
error: null,
});
const { result, rerender } = renderHook(() => useShoppingLists());
expect(result.current.activeListId).toBe(1);
// Update to no lists
mockedUseUserData.mockReturnValue({
shoppingLists: [],
watchedItems: [],
isLoading: false,
error: null,
});
rerender();
expect(result.current.activeListId).toBeNull();
});
it('should select first list when active list is deleted', () => {
// Start with 2 lists, second one active
mockedUseUserData.mockReturnValue({
shoppingLists: mockLists,
watchedItems: [],
isLoading: false,
error: null,
});
const { result, rerender } = renderHook(() => useShoppingLists());
act(() => {
result.current.setActiveListId(2);
});
expect(result.current.activeListId).toBe(2);
// Remove second list (only first remains)
mockedUseUserData.mockReturnValue({
shoppingLists: [mockLists[0]],
watchedItems: [],
isLoading: false,
error: null,
});
rerender();
// Should auto-select the first (and only) list
expect(result.current.activeListId).toBe(1);
});
});
it('should not perform actions if 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(() => useShoppingLists());
await act(async () => {
await result.current.createList('Test');
await result.current.deleteList(1);
await result.current.addItemToList(1, { masterItemId: 1 });
await result.current.updateItemInList(1, { quantity: 1 });
await result.current.removeItemFromList(1);
});
// Mutations should not be called when user is not authenticated
expect(mockMutateAsync).not.toHaveBeenCalled();
});
});