Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 46s
416 lines
13 KiB
TypeScript
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();
|
|
});
|
|
});
|