Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m28s
470 lines
14 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|
|
});
|