Files
flyer-crawler.projectium.com/src/pages/ShoppingListsPage.test.tsx
Torben Sorensen c78323275b
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m28s
more unit tests - done for now
2026-01-29 16:21:48 -08:00

470 lines
14 KiB
TypeScript

// src/pages/ShoppingListsPage.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { ShoppingListsPage } from './ShoppingListsPage';
import { useAuth } from '../hooks/useAuth';
import { useShoppingLists } from '../hooks/useShoppingLists';
import type { ShoppingList, ShoppingListItem, User, UserProfile } from '../types';
import {
createMockUser,
createMockUserProfile,
createMockShoppingList,
createMockShoppingListItem,
} from '../tests/utils/mockFactories';
// Mock the hooks used by ShoppingListsPage
vi.mock('../hooks/useAuth');
vi.mock('../hooks/useShoppingLists');
// Mock the ShoppingListComponent to isolate ShoppingListsPage logic
vi.mock('../features/shopping/ShoppingList', () => ({
ShoppingListComponent: vi.fn(
({
user,
lists,
activeListId,
onSelectList,
onCreateList,
onDeleteList,
onAddItem,
onUpdateItem,
onRemoveItem,
}: {
user: User | null;
lists: ShoppingList[];
activeListId: number | null;
onSelectList: (listId: number) => void;
onCreateList: (name: string) => Promise<void>;
onDeleteList: (listId: number) => Promise<void>;
onAddItem: (item: { customItemName: string }) => Promise<void>;
onUpdateItem: (itemId: number, updates: Partial<ShoppingListItem>) => Promise<void>;
onRemoveItem: (itemId: number) => Promise<void>;
}) => (
<div data-testid="shopping-list-component">
<span data-testid="user-status">{user ? 'authenticated' : 'not-authenticated'}</span>
<span data-testid="lists-count">{lists.length}</span>
<span data-testid="active-list-id">{activeListId ?? 'none'}</span>
<button data-testid="select-list-btn" onClick={() => onSelectList(999)}>
Select List
</button>
<button data-testid="create-list-btn" onClick={() => onCreateList('New List')}>
Create List
</button>
<button data-testid="delete-list-btn" onClick={() => onDeleteList(1)}>
Delete List
</button>
<button
data-testid="add-item-btn"
onClick={() => onAddItem({ customItemName: 'Test Item' })}
>
Add Item
</button>
<button
data-testid="update-item-btn"
onClick={() => onUpdateItem(10, { is_purchased: true })}
>
Update Item
</button>
<button data-testid="remove-item-btn" onClick={() => onRemoveItem(10)}>
Remove Item
</button>
</div>
),
),
}));
const mockedUseAuth = vi.mocked(useAuth);
const mockedUseShoppingLists = vi.mocked(useShoppingLists);
describe('ShoppingListsPage', () => {
const mockUser: User = createMockUser({ user_id: 'user-123', email: 'test@example.com' });
const mockUserProfile: UserProfile = createMockUserProfile({ user: mockUser });
const mockShoppingLists: ShoppingList[] = [
createMockShoppingList({
shopping_list_id: 1,
name: 'Groceries',
user_id: 'user-123',
items: [
createMockShoppingListItem({
shopping_list_item_id: 101,
shopping_list_id: 1,
custom_item_name: 'Apples',
}),
],
}),
createMockShoppingList({
shopping_list_id: 2,
name: 'Hardware',
user_id: 'user-123',
items: [],
}),
];
// Mock functions from useShoppingLists
const mockSetActiveListId = vi.fn();
const mockCreateList = vi.fn();
const mockDeleteList = vi.fn();
const mockAddItemToList = vi.fn();
const mockUpdateItemInList = vi.fn();
const mockRemoveItemFromList = vi.fn();
const defaultUseShoppingListsReturn = {
shoppingLists: mockShoppingLists,
activeListId: 1,
setActiveListId: mockSetActiveListId,
createList: mockCreateList,
deleteList: mockDeleteList,
addItemToList: mockAddItemToList,
updateItemInList: mockUpdateItemInList,
removeItemFromList: mockRemoveItemFromList,
isCreatingList: false,
isDeletingList: false,
isAddingItem: false,
isUpdatingItem: false,
isRemovingItem: false,
error: null,
};
beforeEach(() => {
vi.resetAllMocks();
// Default authenticated user
mockedUseAuth.mockReturnValue({
userProfile: mockUserProfile,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
// Default shopping lists state
mockedUseShoppingLists.mockReturnValue(defaultUseShoppingListsReturn);
});
describe('Rendering', () => {
it('should render the page title', () => {
render(<ShoppingListsPage />);
expect(screen.getByRole('heading', { name: 'Shopping Lists' })).toBeInTheDocument();
});
it('should render the ShoppingListComponent', () => {
render(<ShoppingListsPage />);
expect(screen.getByTestId('shopping-list-component')).toBeInTheDocument();
});
it('should pass the correct user to ShoppingListComponent when authenticated', () => {
render(<ShoppingListsPage />);
expect(screen.getByTestId('user-status')).toHaveTextContent('authenticated');
});
it('should pass null user to ShoppingListComponent when not authenticated', () => {
mockedUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
render(<ShoppingListsPage />);
expect(screen.getByTestId('user-status')).toHaveTextContent('not-authenticated');
});
it('should pass the shopping lists to ShoppingListComponent', () => {
render(<ShoppingListsPage />);
expect(screen.getByTestId('lists-count')).toHaveTextContent('2');
});
it('should pass the active list ID to ShoppingListComponent', () => {
render(<ShoppingListsPage />);
expect(screen.getByTestId('active-list-id')).toHaveTextContent('1');
});
it('should handle empty shopping lists', () => {
mockedUseShoppingLists.mockReturnValue({
...defaultUseShoppingListsReturn,
shoppingLists: [],
activeListId: null,
});
render(<ShoppingListsPage />);
expect(screen.getByTestId('lists-count')).toHaveTextContent('0');
expect(screen.getByTestId('active-list-id')).toHaveTextContent('none');
});
});
describe('User State', () => {
it('should extract user from userProfile when available', () => {
render(<ShoppingListsPage />);
// The component should pass the user object to ShoppingListComponent
expect(screen.getByTestId('user-status')).toHaveTextContent('authenticated');
});
it('should pass null user when userProfile is null', () => {
mockedUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
render(<ShoppingListsPage />);
expect(screen.getByTestId('user-status')).toHaveTextContent('not-authenticated');
});
it('should pass null user when userProfile has no user property', () => {
mockedUseAuth.mockReturnValue({
userProfile: { ...mockUserProfile, user: undefined as unknown as User },
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
render(<ShoppingListsPage />);
// When userProfile.user is undefined, the nullish coalescing should return null
expect(screen.getByTestId('user-status')).toHaveTextContent('not-authenticated');
});
});
describe('Callback Props', () => {
it('should pass setActiveListId to ShoppingListComponent', async () => {
render(<ShoppingListsPage />);
const selectButton = screen.getByTestId('select-list-btn');
selectButton.click();
expect(mockSetActiveListId).toHaveBeenCalledWith(999);
});
it('should pass createList to ShoppingListComponent', async () => {
render(<ShoppingListsPage />);
const createButton = screen.getByTestId('create-list-btn');
createButton.click();
expect(mockCreateList).toHaveBeenCalledWith('New List');
});
it('should pass deleteList to ShoppingListComponent', async () => {
render(<ShoppingListsPage />);
const deleteButton = screen.getByTestId('delete-list-btn');
deleteButton.click();
expect(mockDeleteList).toHaveBeenCalledWith(1);
});
it('should pass updateItemInList to ShoppingListComponent', async () => {
render(<ShoppingListsPage />);
const updateButton = screen.getByTestId('update-item-btn');
updateButton.click();
expect(mockUpdateItemInList).toHaveBeenCalledWith(10, { is_purchased: true });
});
it('should pass removeItemFromList to ShoppingListComponent', async () => {
render(<ShoppingListsPage />);
const removeButton = screen.getByTestId('remove-item-btn');
removeButton.click();
expect(mockRemoveItemFromList).toHaveBeenCalledWith(10);
});
});
describe('handleAddItemToShoppingList', () => {
it('should call addItemToList with activeListId when adding an item', async () => {
mockAddItemToList.mockResolvedValue(undefined);
render(<ShoppingListsPage />);
const addButton = screen.getByTestId('add-item-btn');
addButton.click();
await waitFor(() => {
expect(mockAddItemToList).toHaveBeenCalledWith(1, { customItemName: 'Test Item' });
});
});
it('should not call addItemToList when activeListId is null', async () => {
mockedUseShoppingLists.mockReturnValue({
...defaultUseShoppingListsReturn,
activeListId: null,
});
render(<ShoppingListsPage />);
const addButton = screen.getByTestId('add-item-btn');
addButton.click();
// Wait a tick to ensure any async operations would have completed
await waitFor(() => {
expect(mockAddItemToList).not.toHaveBeenCalled();
});
});
it('should handle addItemToList with masterItemId', async () => {
// Re-mock ShoppingListComponent to test with masterItemId
const ShoppingListComponent = vi.mocked(
await import('../features/shopping/ShoppingList'),
).ShoppingListComponent;
// Get the onAddItem prop from the last render call
const lastCallProps = (ShoppingListComponent as unknown as Mock).mock.calls[0]?.[0];
if (lastCallProps?.onAddItem) {
await lastCallProps.onAddItem({ masterItemId: 42 });
expect(mockAddItemToList).toHaveBeenCalledWith(1, { masterItemId: 42 });
}
});
});
describe('Integration with useShoppingLists', () => {
it('should use the correct hooks', () => {
render(<ShoppingListsPage />);
expect(mockedUseAuth).toHaveBeenCalled();
expect(mockedUseShoppingLists).toHaveBeenCalled();
});
it('should reflect changes when shoppingLists updates', () => {
const { rerender } = render(<ShoppingListsPage />);
expect(screen.getByTestId('lists-count')).toHaveTextContent('2');
// Simulate adding a new list
mockedUseShoppingLists.mockReturnValue({
...defaultUseShoppingListsReturn,
shoppingLists: [
...mockShoppingLists,
createMockShoppingList({
shopping_list_id: 3,
name: 'New List',
user_id: 'user-123',
}),
],
});
rerender(<ShoppingListsPage />);
expect(screen.getByTestId('lists-count')).toHaveTextContent('3');
});
it('should reflect changes when activeListId updates', () => {
const { rerender } = render(<ShoppingListsPage />);
expect(screen.getByTestId('active-list-id')).toHaveTextContent('1');
mockedUseShoppingLists.mockReturnValue({
...defaultUseShoppingListsReturn,
activeListId: 2,
});
rerender(<ShoppingListsPage />);
expect(screen.getByTestId('active-list-id')).toHaveTextContent('2');
});
});
describe('Page Structure', () => {
it('should have correct CSS classes for layout', () => {
const { container } = render(<ShoppingListsPage />);
const pageContainer = container.firstChild as HTMLElement;
expect(pageContainer).toHaveClass('max-w-4xl', 'mx-auto', 'p-4', 'space-y-6');
});
it('should have correctly styled heading', () => {
render(<ShoppingListsPage />);
const heading = screen.getByRole('heading', { name: 'Shopping Lists' });
expect(heading).toHaveClass('text-3xl', 'font-bold', 'text-gray-900');
});
});
describe('Edge Cases', () => {
it('should handle auth loading state gracefully', () => {
mockedUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'Determining...',
isLoading: true,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
render(<ShoppingListsPage />);
// Page should still render even during auth loading
expect(screen.getByRole('heading', { name: 'Shopping Lists' })).toBeInTheDocument();
expect(screen.getByTestId('user-status')).toHaveTextContent('not-authenticated');
});
it('should handle shopping lists with items correctly', () => {
const listsWithItems = [
createMockShoppingList({
shopping_list_id: 1,
name: 'With Items',
items: [
createMockShoppingListItem({
shopping_list_item_id: 1,
custom_item_name: 'Item 1',
is_purchased: false,
}),
createMockShoppingListItem({
shopping_list_item_id: 2,
custom_item_name: 'Item 2',
is_purchased: true,
}),
],
}),
];
mockedUseShoppingLists.mockReturnValue({
...defaultUseShoppingListsReturn,
shoppingLists: listsWithItems,
});
render(<ShoppingListsPage />);
expect(screen.getByTestId('lists-count')).toHaveTextContent('1');
});
it('should handle async callback errors gracefully', async () => {
// The useShoppingLists hook catches errors internally and logs them,
// so we mock it to resolve (the real error handling is tested in useShoppingLists.test.tsx)
mockAddItemToList.mockResolvedValue(undefined);
render(<ShoppingListsPage />);
const addButton = screen.getByTestId('add-item-btn');
// Should not throw when clicked
expect(() => addButton.click()).not.toThrow();
await waitFor(() => {
expect(mockAddItemToList).toHaveBeenCalled();
});
});
});
});