Files
flyer-crawler.projectium.com/src/layouts/MainLayout.test.tsx
Torben Sorensen c6a5f889b4
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 13m12s
unit test fixes
2025-12-27 22:27:39 -08:00

376 lines
13 KiB
TypeScript

// src/layouts/MainLayout.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { MainLayout } from './MainLayout';
// Mock all custom hooks used by the layout
import { useAuth } from '../hooks/useAuth';
import { useFlyers } from '../hooks/useFlyers';
import { useMasterItems } from '../hooks/useMasterItems';
import { useShoppingLists } from '../hooks/useShoppingLists';
import { useWatchedItems } from '../hooks/useWatchedItems';
import { useActiveDeals } from '../hooks/useActiveDeals';
import {
createMockFlyer,
createMockShoppingList,
createMockUser,
createMockUserProfile,
} from '../tests/utils/mockFactories';
import type { ActivityLogItem } from '../types';
// Unmock the component to test the real implementation
vi.unmock('./MainLayout');
// Mock child components to simplify testing and focus on the layout's logic
vi.mock('../features/flyer/FlyerList', () => ({
FlyerList: () => <div data-testid="flyer-list" />,
}));
vi.mock('../features/flyer/FlyerUploader', () => ({
FlyerUploader: () => <div data-testid="flyer-uploader" />,
}));
vi.mock('../features/shopping/ShoppingList', () => ({
ShoppingListComponent: (props: any) => (
<div data-testid="shopping-list">
<button onClick={() => props.onSelectList(2)}>Select List</button>
<button onClick={() => props.onAddItem({ customItemName: 'Test Item' })}>Add Item</button>
</div>
),
}));
vi.mock('../features/shopping/WatchedItemsList', () => ({
WatchedItemsList: (props: any) => (
<div data-testid="watched-items-list">
<button onClick={() => props.onAddItemToList(101)}>Add to List</button>
</div>
),
}));
vi.mock('../features/charts/PriceChart', () => ({
PriceChart: () => <div data-testid="price-chart" />,
}));
vi.mock('../features/charts/PriceHistoryChart', () => ({
PriceHistoryChart: () => <div data-testid="price-history-chart" />,
}));
vi.mock('../components/Leaderboard', () => ({ default: () => <div data-testid="leaderboard" /> }));
vi.mock('../pages/admin/ActivityLog', async () => {
const { createMockActivityLogItem } = await import('../tests/utils/mockFactories');
return {
ActivityLog: (props: { onLogClick: (log: ActivityLogItem) => void }) => (
<div
data-testid="activity-log"
onClick={() =>
props.onLogClick(
createMockActivityLogItem({
action: 'list_shared',
details: { shopping_list_id: 1, list_name: 'test', shared_with_name: 'test' },
}),
)
}
>
<button
data-testid="activity-log-other"
onClick={(e) => {
e.stopPropagation();
props.onLogClick(createMockActivityLogItem({ action: 'other_action' } as any));
}}
/>
</div>
),
};
});
vi.mock('../components/AnonymousUserBanner', () => ({
AnonymousUserBanner: () => <div data-testid="anonymous-banner" />,
}));
vi.mock('../components/ErrorDisplay', () => ({
ErrorDisplay: ({ message }: { message: string }) => (
<div data-testid="error-display">{message}</div>
),
}));
// Mock the hooks
vi.mock('../hooks/useAuth');
vi.mock('../hooks/useFlyers');
vi.mock('../hooks/useMasterItems');
vi.mock('../hooks/useShoppingLists');
vi.mock('../hooks/useWatchedItems');
vi.mock('../hooks/useActiveDeals');
const mockedUseAuth = vi.mocked(useAuth);
const mockedUseFlyers = vi.mocked(useFlyers);
const mockedUseMasterItems = vi.mocked(useMasterItems);
const mockedUseShoppingLists = vi.mocked(useShoppingLists);
const mockedUseWatchedItems = vi.mocked(useWatchedItems);
const mockedUseActiveDeals = vi.mocked(useActiveDeals);
const mockUser = createMockUser({ user_id: 'user-123', email: 'test@example.com' });
// A simple component to render inside the Outlet for context testing
const OutletContent = () => <div>Outlet Content</div>;
const renderWithRouter = (ui: React.ReactElement) => {
return render(
<MemoryRouter initialEntries={['/']}>
<Routes>
<Route path="/" element={ui}>
<Route index element={<OutletContent />} />
</Route>
</Routes>
</MemoryRouter>,
);
};
describe('MainLayout Component', () => {
const mockOnFlyerSelect = vi.fn();
const mockOnOpenProfile = vi.fn();
const mockSetActiveListId = vi.fn();
// Define default mock return values for each hook
const defaultUseAuthReturn = {
userProfile: null,
authStatus: 'SIGNED_OUT' as const,
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
} as const;
const defaultUseFlyersReturn = {
flyers: [createMockFlyer({ flyer_id: 1, file_name: 'flyer.jpg', item_count: 10 })],
isLoadingFlyers: false,
flyersError: null,
refetchFlyers: vi.fn(),
fetchNextFlyersPage: vi.fn(),
hasNextFlyersPage: false,
isRefetchingFlyers: false,
};
const defaultUseMasterItemsReturn = {
masterItems: [],
isLoading: false,
error: null,
};
const defaultUseShoppingListsReturn = {
shoppingLists: [],
activeListId: null,
setActiveListId: mockSetActiveListId,
createList: vi.fn(),
deleteList: vi.fn(),
addItemToList: vi.fn(),
updateItemInList: vi.fn(),
removeItemFromList: vi.fn(),
isCreatingList: false,
isDeletingList: false,
isAddingItem: false,
isUpdatingItem: false,
isRemovingItem: false,
error: null,
};
const defaultUseWatchedItemsReturn = {
watchedItems: [],
addWatchedItem: vi.fn(),
removeWatchedItem: vi.fn(),
error: null,
};
const defaultUseActiveDealsReturn = {
activeDeals: [],
totalActiveItems: 0,
isLoading: false,
error: null,
};
beforeEach(() => {
vi.clearAllMocks();
// Provide default mock implementations for all hooks
mockedUseAuth.mockReturnValue(defaultUseAuthReturn);
mockedUseFlyers.mockReturnValue(defaultUseFlyersReturn);
mockedUseMasterItems.mockReturnValue(defaultUseMasterItemsReturn);
mockedUseShoppingLists.mockReturnValue(defaultUseShoppingListsReturn);
mockedUseWatchedItems.mockReturnValue(defaultUseWatchedItemsReturn);
mockedUseActiveDeals.mockReturnValue(defaultUseActiveDealsReturn);
});
const defaultProps = {
onFlyerSelect: mockOnFlyerSelect,
selectedFlyerId: null,
onOpenProfile: mockOnOpenProfile,
};
it('renders all main sections and the Outlet content', () => {
renderWithRouter(<MainLayout {...defaultProps} />);
expect(screen.getByTestId('flyer-list')).toBeInTheDocument();
expect(screen.getByTestId('flyer-uploader')).toBeInTheDocument();
expect(screen.getByTestId('shopping-list')).toBeInTheDocument();
expect(screen.getByTestId('watched-items-list')).toBeInTheDocument();
expect(screen.getByTestId('price-chart')).toBeInTheDocument();
expect(screen.getByTestId('price-history-chart')).toBeInTheDocument();
expect(screen.getByTestId('leaderboard')).toBeInTheDocument();
expect(screen.getByTestId('activity-log')).toBeInTheDocument();
expect(screen.getByText('Outlet Content')).toBeInTheDocument();
});
describe('for unauthenticated users', () => {
it('shows the AnonymousUserBanner when signed out and flyers exist', () => {
renderWithRouter(<MainLayout {...defaultProps} />);
expect(screen.getByTestId('anonymous-banner')).toBeInTheDocument();
});
it('does not show the AnonymousUserBanner if there are no flyers', () => {
mockedUseFlyers.mockReturnValueOnce({ ...defaultUseFlyersReturn, flyers: [] });
renderWithRouter(<MainLayout {...defaultProps} />);
expect(screen.queryByTestId('anonymous-banner')).not.toBeInTheDocument();
});
});
describe('for authenticated users', () => {
beforeEach(() => {
mockedUseAuth.mockReturnValue({
...defaultUseAuthReturn,
authStatus: 'AUTHENTICATED',
userProfile: createMockUserProfile({ user: mockUser }),
});
});
it('does not show the AnonymousUserBanner', () => {
renderWithRouter(<MainLayout {...defaultProps} />);
expect(screen.queryByTestId('anonymous-banner')).not.toBeInTheDocument();
});
});
describe('Error Handling', () => {
it('displays an error message if useData has an error', () => {
mockedUseFlyers.mockReturnValueOnce({
...defaultUseFlyersReturn,
flyersError: new Error('Data Fetch Failed'),
});
renderWithRouter(<MainLayout {...defaultProps} />);
expect(screen.getByTestId('error-display')).toHaveTextContent('Data Fetch Failed');
});
it('displays an error message if useShoppingLists has an error', () => {
mockedUseShoppingLists.mockReturnValueOnce({
...defaultUseShoppingListsReturn,
error: 'Shopping List Failed',
});
renderWithRouter(<MainLayout {...defaultProps} />);
expect(screen.getByTestId('error-display')).toHaveTextContent('Shopping List Failed');
});
it('displays an error message if useMasterItems has an error', () => {
mockedUseMasterItems.mockReturnValueOnce({
...defaultUseMasterItemsReturn,
error: 'Master Items Failed',
});
renderWithRouter(<MainLayout {...defaultProps} />);
expect(screen.getByTestId('error-display')).toHaveTextContent('Master Items Failed');
});
it('displays an error message if useWatchedItems has an error', () => {
mockedUseWatchedItems.mockReturnValueOnce({
...defaultUseWatchedItemsReturn,
error: 'Watched Items Failed',
});
renderWithRouter(<MainLayout {...defaultProps} />);
expect(screen.getByTestId('error-display')).toHaveTextContent('Watched Items Failed');
});
it('displays an error message if useActiveDeals has an error', () => {
mockedUseActiveDeals.mockReturnValueOnce({
...defaultUseActiveDealsReturn,
error: 'Active Deals Failed',
});
renderWithRouter(<MainLayout {...defaultProps} />);
expect(screen.getByTestId('error-display')).toHaveTextContent('Active Deals Failed');
});
});
describe('Event Handlers', () => {
it('calls setActiveListId when a list is shared via ActivityLog and the list exists', () => {
mockedUseShoppingLists.mockReturnValueOnce({
...defaultUseShoppingListsReturn,
shoppingLists: [
createMockShoppingList({ shopping_list_id: 1, name: 'My List', user_id: 'user-123' }),
],
});
renderWithRouter(<MainLayout {...defaultProps} />);
const activityLog = screen.getByTestId('activity-log');
fireEvent.click(activityLog);
expect(mockSetActiveListId).toHaveBeenCalledWith(1);
});
it('does not call setActiveListId for actions other than list_shared', () => {
renderWithRouter(<MainLayout {...defaultProps} />);
const otherLogAction = screen.getByTestId('activity-log-other');
fireEvent.click(otherLogAction);
expect(mockSetActiveListId).not.toHaveBeenCalled();
});
it('does not call setActiveListId if the shared list does not exist', () => {
renderWithRouter(<MainLayout {...defaultProps} />);
const activityLog = screen.getByTestId('activity-log');
fireEvent.click(activityLog); // Mock click simulates sharing list with id 1
expect(mockSetActiveListId).not.toHaveBeenCalled();
});
it('calls addItemToList when an item is added from ShoppingListComponent and a list is active', () => {
const mockAddItemToList = vi.fn();
mockedUseShoppingLists.mockReturnValueOnce({
...defaultUseShoppingListsReturn,
activeListId: 1,
addItemToList: mockAddItemToList,
});
renderWithRouter(<MainLayout {...defaultProps} />);
fireEvent.click(screen.getByText('Add Item'));
expect(mockAddItemToList).toHaveBeenCalledWith(1, { customItemName: 'Test Item' });
});
it('does not call addItemToList from ShoppingListComponent if no list is active', () => {
const mockAddItemToList = vi.fn();
mockedUseShoppingLists.mockReturnValueOnce({
...defaultUseShoppingListsReturn,
activeListId: null,
addItemToList: mockAddItemToList,
});
renderWithRouter(<MainLayout {...defaultProps} />);
fireEvent.click(screen.getByText('Add Item'));
expect(mockAddItemToList).not.toHaveBeenCalled();
});
it('calls addItemToList when an item is added from WatchedItemsList and a list is active', () => {
const mockAddItemToList = vi.fn();
mockedUseShoppingLists.mockReturnValueOnce({
...defaultUseShoppingListsReturn,
activeListId: 5,
addItemToList: mockAddItemToList,
});
renderWithRouter(<MainLayout {...defaultProps} />);
fireEvent.click(screen.getByText('Add to List'));
expect(mockAddItemToList).toHaveBeenCalledWith(5, { masterItemId: 101 });
});
it('does not call addItemToList from WatchedItemsList if no list is active', () => {
const mockAddItemToList = vi.fn();
mockedUseShoppingLists.mockReturnValueOnce({
...defaultUseShoppingListsReturn,
activeListId: null,
addItemToList: mockAddItemToList,
});
renderWithRouter(<MainLayout {...defaultProps} />);
fireEvent.click(screen.getByText('Add to List'));
expect(mockAddItemToList).not.toHaveBeenCalled();
});
});
});