All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 13m12s
376 lines
13 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|