All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 15m20s
751 lines
26 KiB
TypeScript
751 lines
26 KiB
TypeScript
// src/hooks/useShoppingLists.test.tsx
|
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach, type Mock, test } from 'vitest';
|
|
import { useShoppingLists } from './useShoppingLists';
|
|
import { useApi } from './useApi';
|
|
import { useAuth } from '../hooks/useAuth';
|
|
import { useUserData } from '../hooks/useUserData';
|
|
import * as apiClient from '../services/apiClient';
|
|
import {
|
|
createMockShoppingList,
|
|
createMockShoppingListItem,
|
|
createMockUserProfile,
|
|
createMockUser,
|
|
} from '../tests/utils/mockFactories';
|
|
import React from 'react';
|
|
import type { ShoppingList, User } from '../types'; // Import ShoppingList and User types
|
|
|
|
// Define a type for the mock return value of useApi to ensure type safety in tests
|
|
type MockApiResult = {
|
|
execute: Mock;
|
|
error: Error | null;
|
|
loading: boolean;
|
|
isRefetching: boolean;
|
|
data: any;
|
|
reset: Mock;
|
|
};
|
|
|
|
// Mock the hooks that useShoppingLists depends on
|
|
vi.mock('./useApi');
|
|
vi.mock('../hooks/useAuth');
|
|
vi.mock('../hooks/useUserData');
|
|
vi.mock('../services/apiClient');
|
|
|
|
// The apiClient is globally mocked in our test setup, so we just need to cast it
|
|
const mockedUseApi = vi.mocked(useApi);
|
|
const mockedUseAuth = vi.mocked(useAuth);
|
|
const mockedUseUserData = vi.mocked(useUserData);
|
|
|
|
// Create a mock User object by extracting it from a mock UserProfile
|
|
const mockUserProfile = createMockUserProfile({
|
|
user: createMockUser({ user_id: 'user-123', email: 'test@example.com' }),
|
|
});
|
|
|
|
describe('useShoppingLists Hook', () => {
|
|
// Create a mock setter function that we can spy on
|
|
const mockSetShoppingLists = vi.fn() as unknown as React.Dispatch<
|
|
React.SetStateAction<ShoppingList[]>
|
|
>;
|
|
|
|
// Create mock execute functions for each API operation
|
|
const mockCreateListApi = vi.fn();
|
|
const mockDeleteListApi = vi.fn();
|
|
const mockAddItemApi = vi.fn();
|
|
const mockUpdateItemApi = vi.fn();
|
|
const mockRemoveItemApi = vi.fn();
|
|
|
|
const defaultApiMocks: MockApiResult[] = [
|
|
{
|
|
execute: mockCreateListApi,
|
|
error: null,
|
|
loading: false,
|
|
isRefetching: false,
|
|
data: null,
|
|
reset: vi.fn(),
|
|
},
|
|
{
|
|
execute: mockDeleteListApi,
|
|
error: null,
|
|
loading: false,
|
|
isRefetching: false,
|
|
data: null,
|
|
reset: vi.fn(),
|
|
},
|
|
{
|
|
execute: mockAddItemApi,
|
|
error: null,
|
|
loading: false,
|
|
isRefetching: false,
|
|
data: null,
|
|
reset: vi.fn(),
|
|
},
|
|
{
|
|
execute: mockUpdateItemApi,
|
|
error: null,
|
|
loading: false,
|
|
isRefetching: false,
|
|
data: null,
|
|
reset: vi.fn(),
|
|
},
|
|
{
|
|
execute: mockRemoveItemApi,
|
|
error: null,
|
|
loading: false,
|
|
isRefetching: false,
|
|
data: null,
|
|
reset: vi.fn(),
|
|
},
|
|
];
|
|
|
|
// Helper function to set up the useApi mock for a specific test run
|
|
const setupApiMocks = (mocks: MockApiResult[] = defaultApiMocks) => {
|
|
let callCount = 0;
|
|
mockedUseApi.mockImplementation(() => {
|
|
const mock = mocks[callCount % mocks.length];
|
|
callCount++;
|
|
return mock;
|
|
});
|
|
};
|
|
|
|
beforeEach(() => {
|
|
// Reset all mocks before each test to ensure isolation
|
|
vi.clearAllMocks();
|
|
|
|
// Mock useApi to return a sequence of successful API configurations by default
|
|
setupApiMocks();
|
|
|
|
mockedUseAuth.mockReturnValue({
|
|
userProfile: mockUserProfile,
|
|
authStatus: 'AUTHENTICATED',
|
|
isLoading: false,
|
|
login: vi.fn(),
|
|
logout: vi.fn(),
|
|
updateProfile: vi.fn(),
|
|
});
|
|
|
|
mockedUseUserData.mockReturnValue({
|
|
shoppingLists: [],
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
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 on initial load if lists exist', async () => {
|
|
const mockLists = [
|
|
createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' }),
|
|
createMockShoppingList({ shopping_list_id: 2, name: 'Hardware Store', user_id: 'user-123' }),
|
|
];
|
|
|
|
mockedUseUserData.mockReturnValue({
|
|
shoppingLists: mockLists,
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
|
|
await waitFor(() => expect(result.current.activeListId).toBe(1));
|
|
});
|
|
|
|
it('should not set an active list if the user is not authenticated', () => {
|
|
mockedUseAuth.mockReturnValue({
|
|
userProfile: null,
|
|
authStatus: 'SIGNED_OUT',
|
|
isLoading: false,
|
|
login: vi.fn(),
|
|
logout: vi.fn(),
|
|
updateProfile: vi.fn(),
|
|
});
|
|
const mockLists = [
|
|
createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' }),
|
|
];
|
|
mockedUseUserData.mockReturnValue({
|
|
shoppingLists: mockLists,
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
|
|
expect(result.current.activeListId).toBeNull();
|
|
});
|
|
|
|
it('should set activeListId to null when lists become empty', async () => {
|
|
const mockLists = [
|
|
createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' }),
|
|
];
|
|
|
|
// Initial render with a list
|
|
mockedUseUserData.mockReturnValue({
|
|
shoppingLists: mockLists,
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
const { result, rerender } = renderHook(() => useShoppingLists());
|
|
|
|
await waitFor(() => expect(result.current.activeListId).toBe(1));
|
|
|
|
// Rerender with empty lists
|
|
mockedUseUserData.mockReturnValue({
|
|
shoppingLists: [],
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
rerender();
|
|
|
|
// The effect should update the activeListId to null
|
|
await waitFor(() => expect(result.current.activeListId).toBeNull());
|
|
});
|
|
|
|
it('should expose loading states for API operations', () => {
|
|
// Mock useApi to return loading: true for each specific operation in sequence
|
|
mockedUseApi
|
|
.mockReturnValueOnce({ ...defaultApiMocks[0], loading: true }) // create
|
|
.mockReturnValueOnce({ ...defaultApiMocks[1], loading: true }) // delete
|
|
.mockReturnValueOnce({ ...defaultApiMocks[2], loading: true }) // add item
|
|
.mockReturnValueOnce({ ...defaultApiMocks[3], loading: true }) // update item
|
|
.mockReturnValueOnce({ ...defaultApiMocks[4], loading: true }); // remove item
|
|
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
|
|
expect(result.current.isCreatingList).toBe(true);
|
|
expect(result.current.isDeletingList).toBe(true);
|
|
expect(result.current.isAddingItem).toBe(true);
|
|
expect(result.current.isUpdatingItem).toBe(true);
|
|
expect(result.current.isRemovingItem).toBe(true);
|
|
});
|
|
|
|
it('should configure useApi with the correct apiClient methods', async () => {
|
|
renderHook(() => useShoppingLists());
|
|
|
|
// useApi is called 5 times in the hook in this order:
|
|
// 1. createList, 2. deleteList, 3. addItem, 4. updateItem, 5. removeItem
|
|
const createListApiFn = mockedUseApi.mock.calls[0][0];
|
|
const deleteListApiFn = mockedUseApi.mock.calls[1][0];
|
|
const addItemApiFn = mockedUseApi.mock.calls[2][0];
|
|
const updateItemApiFn = mockedUseApi.mock.calls[3][0];
|
|
const removeItemApiFn = mockedUseApi.mock.calls[4][0];
|
|
|
|
await createListApiFn('New List');
|
|
expect(apiClient.createShoppingList).toHaveBeenCalledWith('New List');
|
|
|
|
await deleteListApiFn(1);
|
|
expect(apiClient.deleteShoppingList).toHaveBeenCalledWith(1);
|
|
|
|
await addItemApiFn(1, { customItemName: 'Item' });
|
|
expect(apiClient.addShoppingListItem).toHaveBeenCalledWith(1, { customItemName: 'Item' });
|
|
|
|
await updateItemApiFn(1, { is_purchased: true });
|
|
expect(apiClient.updateShoppingListItem).toHaveBeenCalledWith(1, { is_purchased: true });
|
|
|
|
await removeItemApiFn(1);
|
|
expect(apiClient.removeShoppingListItem).toHaveBeenCalledWith(1);
|
|
});
|
|
|
|
describe('createList', () => {
|
|
it('should call the API and update state on successful creation', async () => {
|
|
const newList = createMockShoppingList({
|
|
shopping_list_id: 99,
|
|
name: 'New List',
|
|
user_id: 'user-123',
|
|
});
|
|
let currentLists: ShoppingList[] = [];
|
|
|
|
// Mock the implementation of the setter to simulate a real state update.
|
|
// This will cause the hook to re-render with the new list.
|
|
(mockSetShoppingLists as Mock).mockImplementation(
|
|
(updater: React.SetStateAction<ShoppingList[]>) => {
|
|
currentLists = typeof updater === 'function' ? updater(currentLists) : updater;
|
|
},
|
|
);
|
|
|
|
// The hook will now see the updated `currentLists` on re-render.
|
|
mockedUseUserData.mockImplementation(() => ({
|
|
shoppingLists: currentLists,
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
}));
|
|
|
|
mockCreateListApi.mockResolvedValue(newList);
|
|
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
|
|
// `act` ensures that all state updates from the hook are processed before assertions are made
|
|
await act(async () => {
|
|
await result.current.createList('New List');
|
|
});
|
|
|
|
expect(mockCreateListApi).toHaveBeenCalledWith('New List');
|
|
expect(currentLists).toEqual([newList]);
|
|
});
|
|
});
|
|
|
|
describe('deleteList', () => {
|
|
// Use a function to get a fresh copy for each test run
|
|
const getMockLists = () => [
|
|
createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' }),
|
|
createMockShoppingList({ shopping_list_id: 2, name: 'Hardware Store', user_id: 'user-123' }),
|
|
];
|
|
|
|
let currentLists: ShoppingList[] = [];
|
|
|
|
beforeEach(() => {
|
|
// Isolate state for each test in this describe block
|
|
currentLists = getMockLists();
|
|
(mockSetShoppingLists as Mock).mockImplementation((updater) => {
|
|
currentLists = typeof updater === 'function' ? updater(currentLists) : updater;
|
|
});
|
|
mockedUseUserData.mockImplementation(() => ({
|
|
shoppingLists: currentLists,
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
}));
|
|
});
|
|
|
|
it('should call the API and update state on successful deletion', async () => {
|
|
console.log('TEST: should call the API and update state on successful deletion');
|
|
mockDeleteListApi.mockResolvedValue(null); // Successful delete returns null
|
|
|
|
const { result, rerender } = renderHook(() => useShoppingLists());
|
|
console.log(' LOG: Initial lists count:', currentLists.length);
|
|
await act(async () => {
|
|
console.log(' LOG: Deleting list with ID 1.');
|
|
await result.current.deleteList(1);
|
|
rerender();
|
|
});
|
|
|
|
await waitFor(() => expect(mockDeleteListApi).toHaveBeenCalledWith(1));
|
|
console.log(' LOG: Final lists count:', currentLists.length);
|
|
// Check that the global state setter was called with the correctly filtered list
|
|
expect(currentLists).toHaveLength(1);
|
|
expect(currentLists[0].shopping_list_id).toBe(2);
|
|
console.log(' LOG: SUCCESS! State was updated correctly.');
|
|
});
|
|
|
|
it('should update activeListId if the active list is deleted', async () => {
|
|
console.log('TEST: should update activeListId if the active list is deleted');
|
|
mockDeleteListApi.mockResolvedValue(null);
|
|
|
|
// Render the hook and wait for the initial effect to set activeListId
|
|
const { result, rerender } = renderHook(() => useShoppingLists());
|
|
console.log(' LOG: Initial ActiveListId:', result.current.activeListId);
|
|
await waitFor(() => expect(result.current.activeListId).toBe(1));
|
|
console.log(' LOG: Waited for ActiveListId to be 1.');
|
|
|
|
await act(async () => {
|
|
console.log(' LOG: Deleting active list (ID 1).');
|
|
await result.current.deleteList(1);
|
|
rerender();
|
|
});
|
|
|
|
console.log(' LOG: Deletion complete. Checking for new ActiveListId...');
|
|
// After deletion, the hook should select the next available list as active
|
|
await waitFor(() => expect(result.current.activeListId).toBe(2));
|
|
console.log(' LOG: SUCCESS! ActiveListId updated to 2.');
|
|
});
|
|
|
|
it('should not change activeListId if a non-active list is deleted', async () => {
|
|
console.log('TEST: should not change activeListId if a non-active list is deleted');
|
|
mockDeleteListApi.mockResolvedValue(null);
|
|
const { result, rerender } = renderHook(() => useShoppingLists());
|
|
console.log(' LOG: Initial ActiveListId:', result.current.activeListId);
|
|
await waitFor(() => expect(result.current.activeListId).toBe(1)); // Initial active is 1
|
|
console.log(' LOG: Waited for ActiveListId to be 1.');
|
|
|
|
await act(async () => {
|
|
console.log(' LOG: Deleting non-active list (ID 2).');
|
|
await result.current.deleteList(2); // Delete list 2
|
|
rerender();
|
|
});
|
|
|
|
await waitFor(() => expect(mockDeleteListApi).toHaveBeenCalledWith(2));
|
|
console.log(' LOG: Final lists count:', currentLists.length);
|
|
expect(currentLists).toHaveLength(1);
|
|
expect(currentLists[0].shopping_list_id).toBe(1); // Only list 1 remains
|
|
console.log(' LOG: Final ActiveListId:', result.current.activeListId);
|
|
expect(result.current.activeListId).toBe(1); // Active list ID should not change
|
|
console.log(' LOG: SUCCESS! ActiveListId remains 1.');
|
|
});
|
|
|
|
it('should set activeListId to null when the last list is deleted', async () => {
|
|
console.log('TEST: should set activeListId to null when the last list is deleted');
|
|
const singleList = [
|
|
createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' }),
|
|
];
|
|
// Override the state for this specific test
|
|
currentLists = singleList;
|
|
mockDeleteListApi.mockResolvedValue(null);
|
|
|
|
const { result, rerender } = renderHook(() => useShoppingLists());
|
|
console.log(' LOG: Initial render. ActiveListId:', result.current.activeListId);
|
|
await waitFor(() => expect(result.current.activeListId).toBe(1));
|
|
console.log(' LOG: ActiveListId successfully set to 1.');
|
|
await act(async () => {
|
|
console.log(' LOG: Calling deleteList(1).');
|
|
await result.current.deleteList(1);
|
|
console.log(' LOG: deleteList(1) finished. Rerendering component with updated lists.');
|
|
rerender();
|
|
});
|
|
console.log(' LOG: act/rerender complete. Final ActiveListId:', result.current.activeListId);
|
|
await waitFor(() => expect(result.current.activeListId).toBeNull());
|
|
console.log(' LOG: SUCCESS! ActiveListId is null as expected.');
|
|
});
|
|
});
|
|
|
|
describe('addItemToList', () => {
|
|
let currentLists: ShoppingList[] = [];
|
|
const getMockLists = () => [
|
|
createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' }),
|
|
];
|
|
|
|
beforeEach(() => {
|
|
currentLists = getMockLists();
|
|
(mockSetShoppingLists as Mock).mockImplementation((updater) => {
|
|
currentLists = typeof updater === 'function' ? updater(currentLists) : updater;
|
|
});
|
|
mockedUseUserData.mockImplementation(() => ({
|
|
shoppingLists: currentLists,
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
}));
|
|
});
|
|
|
|
it('should call API and add item to the correct list', async () => {
|
|
const newItem = createMockShoppingListItem({
|
|
shopping_list_item_id: 101,
|
|
shopping_list_id: 1,
|
|
custom_item_name: 'Milk',
|
|
});
|
|
mockAddItemApi.mockResolvedValue(newItem);
|
|
|
|
const { result, rerender } = renderHook(() => useShoppingLists());
|
|
|
|
await act(async () => {
|
|
await result.current.addItemToList(1, { customItemName: 'Milk' });
|
|
rerender();
|
|
});
|
|
|
|
expect(mockAddItemApi).toHaveBeenCalledWith(1, { customItemName: 'Milk' });
|
|
expect(currentLists[0].items).toHaveLength(1);
|
|
expect(currentLists[0].items[0]).toEqual(newItem);
|
|
});
|
|
|
|
it('should not call the API if a duplicate item (by master_item_id) is added', async () => {
|
|
console.log('TEST: should not call the API if a duplicate item (by master_item_id) is added');
|
|
const existingItem = createMockShoppingListItem({
|
|
shopping_list_item_id: 100,
|
|
shopping_list_id: 1,
|
|
master_item_id: 5,
|
|
custom_item_name: 'Milk',
|
|
});
|
|
// Override state for this specific test
|
|
currentLists = [
|
|
createMockShoppingList({
|
|
shopping_list_id: 1,
|
|
name: 'Groceries',
|
|
user_id: 'user-123',
|
|
items: [existingItem],
|
|
}),
|
|
];
|
|
|
|
const { result, rerender } = renderHook(() => useShoppingLists());
|
|
console.log(' LOG: Initial item count:', currentLists[0].items.length);
|
|
await act(async () => {
|
|
console.log(' LOG: Attempting to add duplicate masterItemId: 5');
|
|
await result.current.addItemToList(1, { masterItemId: 5 });
|
|
rerender();
|
|
});
|
|
|
|
// The API should not have been called because the duplicate was caught client-side.
|
|
expect(mockAddItemApi).not.toHaveBeenCalled();
|
|
|
|
console.log(' LOG: Final item count:', currentLists[0].items.length);
|
|
expect(currentLists[0].items).toHaveLength(1); // Length should remain 1
|
|
console.log(' LOG: SUCCESS! Duplicate was not added and API was not called.');
|
|
});
|
|
|
|
it('should log an error and not call the API if the listId does not exist', async () => {
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
|
|
await act(async () => {
|
|
// Call with a non-existent list ID (mock lists have IDs 1 and 2)
|
|
await result.current.addItemToList(999, { customItemName: 'Wont be added' });
|
|
});
|
|
|
|
// The API should not have been called because the list was not found.
|
|
expect(mockAddItemApi).not.toHaveBeenCalled();
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith('useShoppingLists: List with ID 999 not found.');
|
|
|
|
consoleErrorSpy.mockRestore();
|
|
});
|
|
});
|
|
|
|
describe('updateItemInList', () => {
|
|
const initialItem = createMockShoppingListItem({
|
|
shopping_list_item_id: 101,
|
|
shopping_list_id: 1,
|
|
custom_item_name: 'Milk',
|
|
is_purchased: false,
|
|
quantity: 1,
|
|
});
|
|
const multiLists = [
|
|
createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', items: [initialItem] }),
|
|
createMockShoppingList({ shopping_list_id: 2, name: 'Other' }),
|
|
];
|
|
|
|
beforeEach(() => {
|
|
mockedUseUserData.mockReturnValue({
|
|
shoppingLists: multiLists,
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
it('should call API and update the correct item, leaving other lists unchanged', async () => {
|
|
const updatedItem = { ...initialItem, is_purchased: true };
|
|
mockUpdateItemApi.mockResolvedValue(updatedItem);
|
|
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
act(() => {
|
|
result.current.setActiveListId(1);
|
|
}); // Set active list
|
|
|
|
await act(async () => {
|
|
await result.current.updateItemInList(101, { is_purchased: true });
|
|
});
|
|
|
|
expect(mockUpdateItemApi).toHaveBeenCalledWith(101, { is_purchased: true });
|
|
const updater = (mockSetShoppingLists as Mock).mock.calls[0][0];
|
|
const newState = updater(multiLists);
|
|
expect(newState[0].items[0].is_purchased).toBe(true);
|
|
expect(newState[1]).toBe(multiLists[1]); // Verify other list is unchanged
|
|
});
|
|
|
|
it('should not call update API if no list is active', async () => {
|
|
console.log('TEST: should not call update API if no list is active');
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
console.log(' LOG: Initial render. ActiveListId:', result.current.activeListId);
|
|
|
|
// Wait for the initial effect to set the active list
|
|
await waitFor(() => expect(result.current.activeListId).toBe(1));
|
|
console.log(' LOG: Initial active list is 1.');
|
|
|
|
act(() => {
|
|
result.current.setActiveListId(null);
|
|
}); // Ensure no active list
|
|
console.log(
|
|
' LOG: Manually set activeListId to null. Current value:',
|
|
result.current.activeListId,
|
|
);
|
|
await act(async () => {
|
|
console.log(' LOG: Calling updateItemInList while activeListId is null.');
|
|
await result.current.updateItemInList(101, { is_purchased: true });
|
|
});
|
|
expect(mockUpdateItemApi).not.toHaveBeenCalled();
|
|
console.log(' LOG: SUCCESS! mockUpdateItemApi was not called.');
|
|
});
|
|
});
|
|
|
|
describe('removeItemFromList', () => {
|
|
const initialItem = createMockShoppingListItem({
|
|
shopping_list_item_id: 101,
|
|
shopping_list_id: 1,
|
|
custom_item_name: 'Milk',
|
|
});
|
|
const multiLists = [
|
|
createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', items: [initialItem] }),
|
|
createMockShoppingList({ shopping_list_id: 2, name: 'Other' }),
|
|
];
|
|
|
|
beforeEach(() => {
|
|
mockedUseUserData.mockReturnValue({
|
|
shoppingLists: multiLists,
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
it('should call API and remove item from the active list, leaving other lists unchanged', async () => {
|
|
mockRemoveItemApi.mockResolvedValue(null);
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
act(() => {
|
|
result.current.setActiveListId(1);
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.removeItemFromList(101);
|
|
});
|
|
|
|
expect(mockRemoveItemApi).toHaveBeenCalledWith(101);
|
|
const updater = (mockSetShoppingLists as Mock).mock.calls[0][0];
|
|
const newState = updater(multiLists);
|
|
expect(newState[0].items).toHaveLength(0);
|
|
expect(newState[1]).toBe(multiLists[1]); // Verify other list is unchanged
|
|
});
|
|
|
|
it('should not call remove API if no list is active', async () => {
|
|
console.log('TEST: should not call remove API if no list is active');
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
console.log(' LOG: Initial render. ActiveListId:', result.current.activeListId);
|
|
|
|
// Wait for the initial effect to set the active list
|
|
await waitFor(() => expect(result.current.activeListId).toBe(1));
|
|
console.log(' LOG: Initial active list is 1.');
|
|
|
|
act(() => {
|
|
result.current.setActiveListId(null);
|
|
}); // Ensure no active list
|
|
console.log(
|
|
' LOG: Manually set activeListId to null. Current value:',
|
|
result.current.activeListId,
|
|
);
|
|
await act(async () => {
|
|
console.log(' LOG: Calling removeItemFromList while activeListId is null.');
|
|
await result.current.removeItemFromList(101);
|
|
});
|
|
expect(mockRemoveItemApi).not.toHaveBeenCalled();
|
|
console.log(' LOG: SUCCESS! mockRemoveItemApi was not called.');
|
|
});
|
|
});
|
|
|
|
describe('API Error Handling', () => {
|
|
test.each([
|
|
{
|
|
name: 'createList',
|
|
action: (hook: any) => hook.createList('New List'),
|
|
apiMock: mockCreateListApi,
|
|
mockIndex: 0,
|
|
errorMessage: 'API Failed',
|
|
},
|
|
{
|
|
name: 'deleteList',
|
|
action: (hook: any) => hook.deleteList(1),
|
|
apiMock: mockDeleteListApi,
|
|
mockIndex: 1,
|
|
errorMessage: 'Deletion failed',
|
|
},
|
|
{
|
|
name: 'addItemToList',
|
|
action: (hook: any) => hook.addItemToList(1, { customItemName: 'Milk' }),
|
|
apiMock: mockAddItemApi,
|
|
mockIndex: 2,
|
|
errorMessage: 'Failed to add item',
|
|
},
|
|
{
|
|
name: 'updateItemInList',
|
|
action: (hook: any) => hook.updateItemInList(101, { is_purchased: true }),
|
|
apiMock: mockUpdateItemApi,
|
|
mockIndex: 3,
|
|
errorMessage: 'Update failed',
|
|
},
|
|
{
|
|
name: 'removeItemFromList',
|
|
action: (hook: any) => hook.removeItemFromList(101),
|
|
apiMock: mockRemoveItemApi,
|
|
mockIndex: 4,
|
|
errorMessage: 'Removal failed',
|
|
},
|
|
])(
|
|
'should set an error for $name if the API call fails',
|
|
async ({ action, apiMock, mockIndex, errorMessage }) => {
|
|
// Setup a default list so activeListId is set automatically
|
|
const mockList = createMockShoppingList({ shopping_list_id: 1, name: 'List 1' });
|
|
mockedUseUserData.mockReturnValue({
|
|
shoppingLists: [mockList],
|
|
setShoppingLists: mockSetShoppingLists,
|
|
watchedItems: [],
|
|
setWatchedItems: vi.fn(),
|
|
isLoading: false,
|
|
error: null,
|
|
});
|
|
|
|
const apiMocksWithError = [...defaultApiMocks];
|
|
apiMocksWithError[mockIndex] = {
|
|
...apiMocksWithError[mockIndex],
|
|
error: new Error(errorMessage),
|
|
};
|
|
setupApiMocks(apiMocksWithError);
|
|
apiMock.mockRejectedValue(new Error(errorMessage));
|
|
|
|
// Spy on console.error to ensure the catch block is executed for logging
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
|
|
const { result } = renderHook(() => useShoppingLists());
|
|
|
|
// Wait for the effect to set the active list ID
|
|
await waitFor(() => expect(result.current.activeListId).toBe(1));
|
|
|
|
await act(async () => {
|
|
await action(result.current);
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(result.current.error).toBe(errorMessage);
|
|
// Verify that our custom logging within the catch block was called
|
|
expect(consoleErrorSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
consoleErrorSpy.mockRestore();
|
|
},
|
|
);
|
|
});
|
|
|
|
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('Should not work');
|
|
});
|
|
|
|
expect(mockCreateListApi).not.toHaveBeenCalled();
|
|
});
|
|
});
|