mock mock mock !
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 57m50s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 57m50s
This commit is contained in:
101
src/App.test.tsx
101
src/App.test.tsx
@@ -7,43 +7,28 @@ import App from './App';
|
||||
import * as aiApiClient from './services/aiApiClient'; // Import aiApiClient
|
||||
import * as apiClient from './services/apiClient';
|
||||
import { AppProviders } from './providers/AppProviders';
|
||||
import type { Flyer, Profile, User, UserProfile } from './types';
|
||||
import type { Flyer, Profile, User, UserProfile} from './types';
|
||||
import { createMockFlyer, createMockUserProfile, createMockUser, createMockProfile } from './tests/utils/mockFactories';
|
||||
import type { HeaderProps } from './components/Header';
|
||||
import type { ProfileManagerProps } from './pages/admin/components/ProfileManager';
|
||||
import { mockUseAuth, mockUseFlyers, mockUseMasterItems, mockUseUserData, mockUseFlyerItems } from './tests/setup/mockHooks';
|
||||
|
||||
// Mock useAuth to allow overriding the user state in tests
|
||||
const mockUseAuth = vi.fn();
|
||||
vi.mock('./hooks/useAuth', () => ({
|
||||
useAuth: () => mockUseAuth(),
|
||||
}));
|
||||
// --- Mock Implementations for Readability ---
|
||||
|
||||
// Mock the new data hooks
|
||||
const mockUseFlyers = vi.fn();
|
||||
vi.mock('./hooks/useFlyers', () => ({
|
||||
useFlyers: () => mockUseFlyers(),
|
||||
}));
|
||||
const mockUseMasterItems = vi.fn();
|
||||
vi.mock('./hooks/useMasterItems', () => ({
|
||||
useMasterItems: () => mockUseMasterItems(),
|
||||
}));
|
||||
const mockUseUserData = vi.fn();
|
||||
vi.mock('./hooks/useUserData', () => ({ useUserData: () => mockUseUserData() }));
|
||||
// By defining mock components separately, we make the `vi.mock` section cleaner
|
||||
// and the component logic easier to read and maintain. Using imported prop types
|
||||
// ensures that our mocks stay in sync with the real components.
|
||||
|
||||
const MockHeader: React.FC<HeaderProps> = (props) => (
|
||||
<header data-testid="header-mock">
|
||||
<button onClick={props.onOpenProfile}>Open Profile</button>
|
||||
<button onClick={props.onOpenVoiceAssistant}>Open Voice Assistant</button>
|
||||
</header>
|
||||
);
|
||||
|
||||
// --- End Mock Implementations ---
|
||||
|
||||
// Mock top-level components rendered by App's routes
|
||||
vi.mock('./components/Header', () => ({ Header: (props: HeaderProps) => <header data-testid="header-mock"><button onClick={props.onOpenProfile}>Open Profile</button><button onClick={props.onOpenVoiceAssistant}>Open Voice Assistant</button></header> }));
|
||||
vi.mock('./pages/admin/components/ProfileManager', () => ({ ProfileManager: ({ isOpen, onClose, onProfileUpdate, onLoginSuccess }: { isOpen: boolean, onClose: () => void, onProfileUpdate: (p: Profile) => void, onLoginSuccess: (u: User, t: string, r: boolean) => void }) => isOpen ? <div data-testid="profile-manager-mock"><button onClick={onClose}>Close Profile</button><button onClick={() => onProfileUpdate({ user_id: '1', role: 'user', points: 0, full_name: 'Updated' })}>Update Profile</button><button onClick={() => onLoginSuccess({ user_id: '1', email: 'a@b.com' }, 'token', false)}>Login</button></div> : null }));
|
||||
vi.mock('./features/voice-assistant/VoiceAssistant', () => ({ VoiceAssistant: ({ isOpen, onClose }: { isOpen: boolean, onClose: () => void }) => isOpen ? <div data-testid="voice-assistant-mock"><button onClick={onClose}>Close Voice Assistant</button></div> : null }));
|
||||
vi.mock('./pages/admin/AdminPage', () => ({ AdminPage: () => <div data-testid="admin-page-mock">Admin Page</div> }));
|
||||
// In react-router v6, wrapper routes must render an <Outlet /> for nested routes to appear.
|
||||
vi.mock('./components/AdminRoute', () => ({ AdminRoute: ({ children }: { children: React.ReactNode }) => <div data-testid="admin-route-mock">{children || <Outlet />}</div> }));
|
||||
vi.mock('./pages/admin/CorrectionsPage', () => ({ CorrectionsPage: () => <div data-testid="corrections-page-mock">Corrections Page</div> }));
|
||||
vi.mock('./pages/admin/AdminStatsPage', () => ({ AdminStatsPage: () => <div data-testid="admin-stats-page-mock">Admin Stats Page</div> }));
|
||||
vi.mock('./pages/ResetPasswordPage', () => ({ ResetPasswordPage: () => <div data-testid="reset-password-page-mock">Reset Password Page</div> }));
|
||||
vi.mock('./pages/admin/VoiceLabPage', () => ({ VoiceLabPage: () => <div data-testid="voice-lab-page-mock">Voice Lab Page</div> })); // Corrected path
|
||||
vi.mock('./components/WhatsNewModal', () => ({ WhatsNewModal: ({ isOpen, onClose }: { isOpen: boolean, onClose: () => void }) => isOpen ? <div data-testid="whats-new-modal-mock"><button onClick={onClose}>Close What's New</button></div> : null }));
|
||||
vi.mock('./components/FlyerCorrectionTool', () => ({ FlyerCorrectionTool: ({ isOpen, onClose, onDataExtracted }: { isOpen: boolean, onClose: () => void, onDataExtracted: (type: 'store_name' | 'dates', value: string) => void }) => isOpen ? <div data-testid="flyer-correction-tool-mock"><button onClick={onClose}>Close Correction</button><button onClick={() => onDataExtracted('store_name', 'New Store Name')}>Extract Store</button><button onClick={() => onDataExtracted('dates', '2023-01-01')}>Extract Dates</button></div> : null }));
|
||||
// Mock the new layout and page components
|
||||
vi.mock('./layouts/MainLayout', () => ({ MainLayout: () => <div data-testid="main-layout-mock"><Outlet /></div> }));
|
||||
vi.mock('./pages/HomePage', () => ({ HomePage: (props: { onOpenCorrectionTool: () => void }) => <div data-testid="home-page-mock"><button onClick={props.onOpenCorrectionTool}>Open Correction Tool</button></div> }));
|
||||
|
||||
// Mock pdfjs-dist to prevent the "DOMMatrix is not defined" error in JSDOM.
|
||||
// This must be done in any test file that imports App.tsx.
|
||||
@@ -66,14 +51,9 @@ vi.mock('./config', () => ({
|
||||
const mockedAiApiClient = vi.mocked(aiApiClient); // Mock aiApiClient
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
// Mock the useShoppingLists hook
|
||||
vi.mock('./hooks/useShoppingLists', () => ({ useShoppingLists: vi.fn() }));
|
||||
// Mock the useWatchedItems hook
|
||||
vi.mock('./hooks/useWatchedItems', () => ({ useWatchedItems: vi.fn() }));
|
||||
|
||||
const mockFlyers: Flyer[] = [
|
||||
{ flyer_id: 1, file_name: 'flyer1.jpg', image_url: 'url1', item_count: 10, created_at: '2023-01-01', store: { store_id: 1, name: 'Old Store', created_at: '2023-01-01' } },
|
||||
{ flyer_id: 2, file_name: 'flyer2.jpg', image_url: 'url2', item_count: 20, created_at: '2023-01-02', store: { store_id: 2, name: 'Another Store', created_at: '2023-01-01' } },
|
||||
createMockFlyer({ flyer_id: 1, store: { store_id: 1, name: 'Old Store' } }),
|
||||
createMockFlyer({ flyer_id: 2, store: { store_id: 2, name: 'Another Store' } }),
|
||||
];
|
||||
|
||||
describe('App Component', () => {
|
||||
@@ -143,6 +123,10 @@ describe('App Component', () => {
|
||||
setWatchedItems: vi.fn(),
|
||||
setShoppingLists: vi.fn(),
|
||||
});
|
||||
mockUseFlyerItems.mockReturnValue({
|
||||
flyerItems: [],
|
||||
isLoading: false,
|
||||
});
|
||||
// Clear local storage to prevent state from leaking between tests.
|
||||
localStorage.clear();
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock, configurable: true });
|
||||
@@ -199,14 +183,11 @@ describe('App Component', () => {
|
||||
});
|
||||
|
||||
it('should render the BulkImporter for an admin user', async () => {
|
||||
const mockAdminProfile: UserProfile = {
|
||||
const mockAdminProfile: UserProfile = createMockUserProfile({
|
||||
user_id: 'admin-id',
|
||||
user: { user_id: 'admin-id', email: 'admin@example.com' },
|
||||
role: 'admin',
|
||||
full_name: 'Admin',
|
||||
avatar_url: '',
|
||||
points: 0,
|
||||
};
|
||||
});
|
||||
|
||||
// Force the auth hook to return an authenticated admin user
|
||||
mockUseAuth.mockReturnValue({
|
||||
@@ -243,15 +224,11 @@ describe('App Component', () => {
|
||||
});
|
||||
|
||||
it('should render the admin page on the /admin route', async () => {
|
||||
// Mock a logged-in admin user
|
||||
const mockAdminProfile: UserProfile = {
|
||||
const mockAdminProfile: UserProfile = createMockUserProfile({
|
||||
user_id: 'admin-id',
|
||||
user: { user_id: 'admin-id', email: 'admin@example.com' },
|
||||
role: 'admin',
|
||||
full_name: 'Admin',
|
||||
avatar_url: '',
|
||||
points: 0,
|
||||
};
|
||||
});
|
||||
|
||||
// Directly set the auth state via the mocked hook
|
||||
mockUseAuth.mockReturnValue({
|
||||
@@ -277,10 +254,10 @@ describe('App Component', () => {
|
||||
|
||||
describe('Theme and Unit System Synchronization', () => {
|
||||
it('should set dark mode based on user profile preferences', () => {
|
||||
const profileWithDarkMode: UserProfile = {
|
||||
const profileWithDarkMode: UserProfile = createMockUserProfile({
|
||||
user_id: 'user-1', user: { user_id: 'user-1', email: 'dark@mode.com' }, role: 'user', points: 0,
|
||||
preferences: { darkMode: true }
|
||||
};
|
||||
});
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: profileWithDarkMode.user, profile: profileWithDarkMode, authStatus: 'AUTHENTICATED',
|
||||
isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
|
||||
@@ -291,10 +268,10 @@ describe('App Component', () => {
|
||||
});
|
||||
|
||||
it('should set light mode based on user profile preferences', () => {
|
||||
const profileWithLightMode: UserProfile = {
|
||||
const profileWithLightMode: UserProfile = createMockUserProfile({
|
||||
user_id: 'user-1', user: { user_id: 'user-1', email: 'light@mode.com' }, role: 'user', points: 0,
|
||||
preferences: { darkMode: false }
|
||||
};
|
||||
});
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: profileWithLightMode.user, profile: profileWithLightMode, authStatus: 'AUTHENTICATED',
|
||||
isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
|
||||
@@ -316,10 +293,10 @@ describe('App Component', () => {
|
||||
});
|
||||
|
||||
it('should set unit system based on user profile preferences', async () => {
|
||||
const profileWithMetric: UserProfile = {
|
||||
const profileWithMetric: UserProfile = createMockUserProfile({
|
||||
user_id: 'user-1', user: { user_id: 'user-1', email: 'metric@user.com' }, role: 'user', points: 0,
|
||||
preferences: { unitSystem: 'metric' }
|
||||
};
|
||||
});
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: profileWithMetric.user, profile: profileWithMetric, authStatus: 'AUTHENTICATED',
|
||||
isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
|
||||
@@ -454,8 +431,8 @@ describe('App Component', () => {
|
||||
|
||||
it('should open and close the VoiceAssistant modal for authenticated users', async () => {
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: { user_id: '1', email: 'test@test.com' },
|
||||
profile: { user_id: '1', role: 'user', points: 0, user: { user_id: '1', email: 'test@test.com' } },
|
||||
user: createMockUser({ user_id: '1', email: 'test@test.com' }),
|
||||
profile: createMockUserProfile({ user_id: '1', role: 'user' }),
|
||||
authStatus: 'AUTHENTICATED',
|
||||
isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
|
||||
});
|
||||
@@ -497,10 +474,9 @@ describe('App Component', () => {
|
||||
});
|
||||
|
||||
it('should render admin sub-routes correctly', async () => {
|
||||
const mockAdminProfile: UserProfile = {
|
||||
const mockAdminProfile: UserProfile = createMockUserProfile({
|
||||
user_id: 'admin-id', user: { user_id: 'admin-id', email: 'admin@example.com' }, role: 'admin',
|
||||
full_name: 'Admin', avatar_url: '', points: 0,
|
||||
};
|
||||
});
|
||||
mockUseAuth.mockReturnValue({
|
||||
user: mockAdminProfile.user, profile: mockAdminProfile, authStatus: 'AUTHENTICATED',
|
||||
isLoading: false, login: vi.fn(), logout: vi.fn(), updateProfile: vi.fn(),
|
||||
@@ -597,7 +573,8 @@ describe('App Component', () => {
|
||||
const profileManager = await screen.findByTestId('profile-manager-mock');
|
||||
fireEvent.click(within(profileManager).getByText('Update Profile'));
|
||||
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith({ user_id: '1', role: 'user', points: 0, full_name: 'Updated' });
|
||||
// The mock now returns a more complete object
|
||||
expect(mockUpdateProfile).toHaveBeenCalledWith(expect.objectContaining({ full_name: 'Updated' }));
|
||||
});
|
||||
|
||||
it('should set an error state if login fails inside handleLoginSuccess', async () => {
|
||||
|
||||
@@ -3,35 +3,20 @@ import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { AchievementsList } from './AchievementsList';
|
||||
import { Achievement, UserAchievement } from '../types';
|
||||
|
||||
/**
|
||||
* A mock factory for creating achievement data for tests.
|
||||
* This makes the test setup cleaner and more reusable.
|
||||
* @param overrides - Partial data to override the defaults.
|
||||
*/
|
||||
const createMockAchievement = (overrides: Partial<UserAchievement & Achievement>): (UserAchievement & Achievement) => ({
|
||||
achievement_id: 1,
|
||||
user_id: 'user-123',
|
||||
achieved_at: new Date().toISOString(),
|
||||
name: 'Test Achievement',
|
||||
description: 'A default description.',
|
||||
icon: 'heart',
|
||||
points_value: 10,
|
||||
...overrides,
|
||||
});
|
||||
import { createMockUserAchievement } from '../tests/utils/mockFactories';
|
||||
|
||||
describe('AchievementsList', () => {
|
||||
it('should render the list of achievements with correct details', () => {
|
||||
const mockAchievements = [
|
||||
createMockAchievement({
|
||||
createMockUserAchievement({
|
||||
achievement_id: 1,
|
||||
name: 'Recipe Creator',
|
||||
description: 'Create your first recipe.',
|
||||
icon: 'chef-hat',
|
||||
points_value: 25,
|
||||
}),
|
||||
createMockAchievement({ name: 'List Maker', icon: 'list', points_value: 15 }),
|
||||
createMockAchievement({ name: 'Unknown Achievement', icon: 'star' }), // This icon is not in the component's map
|
||||
createMockUserAchievement({ achievement_id: 2, name: 'List Maker', icon: 'list', points_value: 15 }),
|
||||
createMockUserAchievement({ achievement_id: 3, name: 'Unknown Achievement', icon: 'star' }), // This icon is not in the component's map
|
||||
];
|
||||
|
||||
render(<AchievementsList achievements={mockAchievements} />);
|
||||
|
||||
@@ -4,7 +4,11 @@ import { render, screen } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import { AdminRoute } from './AdminRoute';
|
||||
import type { Profile } from '../types'; // Path remains the same
|
||||
import type { Profile } from '../types';
|
||||
import { createMockProfile } from '../tests/utils/mockFactories';
|
||||
|
||||
// Unmock the component to test the real implementation
|
||||
vi.unmock('./AdminRoute');
|
||||
|
||||
const AdminContent = () => <div>Admin Page Content</div>;
|
||||
const HomePage = () => <div>Home Page</div>;
|
||||
@@ -24,7 +28,7 @@ const renderWithRouter = (profile: Profile | null, initialPath: string) => {
|
||||
|
||||
describe('AdminRoute', () => {
|
||||
it('should render the admin content when user has admin role', () => {
|
||||
const adminProfile: Profile = { user_id: '1', role: 'admin', points: 0 };
|
||||
const adminProfile: Profile = createMockProfile({ user_id: '1', role: 'admin' });
|
||||
renderWithRouter(adminProfile, '/admin');
|
||||
|
||||
expect(screen.getByText('Admin Page Content')).toBeInTheDocument();
|
||||
@@ -32,7 +36,7 @@ describe('AdminRoute', () => {
|
||||
});
|
||||
|
||||
it('should redirect to home page when user does not have admin role', () => {
|
||||
const userProfile: Profile = { user_id: '2', role: 'user', points: 0 };
|
||||
const userProfile: Profile = createMockProfile({ user_id: '2', role: 'user' });
|
||||
renderWithRouter(userProfile, '/admin');
|
||||
|
||||
// The user is redirected, so we should see the home page content
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { Navigate, Outlet } from 'react-router-dom';
|
||||
import type { Profile } from '../types';
|
||||
|
||||
interface AdminRouteProps {
|
||||
export interface AdminRouteProps {
|
||||
profile: Profile | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ import { FlyerCorrectionTool } from './FlyerCorrectionTool';
|
||||
import * as aiApiClient from '../services/aiApiClient';
|
||||
import { notifyError, notifySuccess } from '../services/notificationService';
|
||||
|
||||
// Unmock the component to test the real implementation
|
||||
vi.unmock('./FlyerCorrectionTool');
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../services/aiApiClient');
|
||||
vi.mock('../services/notificationService');
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as aiApiClient from '../services/aiApiClient';
|
||||
import { notifyError, notifySuccess } from '../services/notificationService';
|
||||
import { logger } from '../services/logger.client';
|
||||
|
||||
interface FlyerCorrectionToolProps {
|
||||
export interface FlyerCorrectionToolProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
imageUrl: string;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { FlyerCountDisplay } from './FlyerCountDisplay';
|
||||
import { useFlyers } from '../hooks/useFlyers';
|
||||
import type { Flyer } from '../types';
|
||||
import { createMockFlyer } from '../tests/utils/mockFactories';
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('../hooks/useFlyers');
|
||||
@@ -60,10 +61,7 @@ describe('FlyerCountDisplay', () => {
|
||||
|
||||
it('should render the flyer count when data is successfully loaded', () => {
|
||||
// Arrange: Override the mock to return a successful state with some data.
|
||||
const mockFlyers: Flyer[] = [
|
||||
{ flyer_id: 1, file_name: 'flyer1.pdf', image_url: '', item_count: 10, created_at: '' },
|
||||
{ flyer_id: 2, file_name: 'flyer2.pdf', image_url: '', item_count: 20, created_at: '' },
|
||||
];
|
||||
const mockFlyers: Flyer[] = [createMockFlyer(), createMockFlyer()];
|
||||
mockedUseFlyers.mockReturnValue({
|
||||
flyers: mockFlyers,
|
||||
isLoadingFlyers: false,
|
||||
|
||||
@@ -4,11 +4,15 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Header } from './Header';
|
||||
import type { User, Profile } from '../types'; // Path remains the same
|
||||
import type { Profile } from '../types';
|
||||
import { createMockProfile, createMockUser } from '../tests/utils/mockFactories';
|
||||
|
||||
const mockUser: User = { user_id: 'user-123', email: 'test@example.com' };
|
||||
const mockProfile: Profile = { user_id: 'user-123', role: 'user', points: 0 };
|
||||
const mockAdminProfile: Profile = { user_id: 'user-123', role: 'admin', points: 0 };
|
||||
// Unmock the component to test the real implementation
|
||||
vi.unmock('./Header');
|
||||
|
||||
const mockUser = createMockUser({ user_id: 'user-123', email: 'test@example.com' });
|
||||
const mockProfile: Profile = createMockProfile({ user_id: 'user-123', role: 'user', points: 0 });
|
||||
const mockAdminProfile: Profile = createMockProfile({ user_id: 'user-123', role: 'admin', points: 0 });
|
||||
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenVoiceAssistant = vi.fn();
|
||||
|
||||
@@ -5,6 +5,8 @@ import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
||||
import Leaderboard from './Leaderboard';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import { LeaderboardUser } from '../types';
|
||||
import { createMockLeaderboardUser } from '../tests/utils/mockFactories';
|
||||
import { createMockLogger } from '../tests/utils/mockLogger';
|
||||
|
||||
// Mock the apiClient
|
||||
vi.mock('../services/apiClient'); // This was correct
|
||||
@@ -12,9 +14,7 @@ const mockedApiClient = apiClient as Mocked<typeof apiClient>;
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../services/logger', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
},
|
||||
logger: createMockLogger(),
|
||||
}));
|
||||
|
||||
// Mock lucide-react icons to prevent rendering errors in the test environment
|
||||
@@ -25,10 +25,10 @@ vi.mock('lucide-react', () => ({
|
||||
}));
|
||||
|
||||
const mockLeaderboardData: LeaderboardUser[] = [
|
||||
{ user_id: 'user-1', full_name: 'Alice', avatar_url: null, points: 1000, rank: '1' },
|
||||
{ user_id: 'user-2', full_name: 'Bob', avatar_url: 'http://example.com/bob.jpg', points: 950, rank: '2' },
|
||||
{ user_id: 'user-3', full_name: 'Charlie', avatar_url: null, points: 900, rank: '3' },
|
||||
{ user_id: 'user-4', full_name: 'Diana', avatar_url: null, points: 850, rank: '4' },
|
||||
createMockLeaderboardUser({ user_id: 'user-1', full_name: 'Alice', points: 1000, rank: '1' }),
|
||||
createMockLeaderboardUser({ user_id: 'user-2', full_name: 'Bob', avatar_url: 'http://example.com/bob.jpg', points: 950, rank: '2' }),
|
||||
createMockLeaderboardUser({ user_id: 'user-3', full_name: 'Charlie', points: 900, rank: '3' }),
|
||||
createMockLeaderboardUser({ user_id: 'user-4', full_name: 'Diana', points: 850, rank: '4' }),
|
||||
];
|
||||
|
||||
describe('Leaderboard', () => {
|
||||
@@ -112,7 +112,7 @@ describe('Leaderboard', () => {
|
||||
|
||||
it('should handle users with missing names correctly', async () => {
|
||||
const dataWithMissingNames: LeaderboardUser[] = [
|
||||
{ user_id: 'user-anon', full_name: null, avatar_url: null, points: 500, rank: '5' },
|
||||
createMockLeaderboardUser({ user_id: 'user-anon', full_name: null, points: 500, rank: '5' }),
|
||||
];
|
||||
mockedApiClient.fetchLeaderboard.mockResolvedValue(new Response(JSON.stringify(dataWithMissingNames)));
|
||||
render(<Leaderboard />);
|
||||
|
||||
@@ -4,6 +4,9 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { WhatsNewModal } from './WhatsNewModal';
|
||||
|
||||
// Unmock the component to test the real implementation
|
||||
vi.unmock('./WhatsNewModal');
|
||||
|
||||
describe('WhatsNewModal', () => {
|
||||
const mockOnClose = vi.fn();
|
||||
const defaultProps = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import { XCircleIcon } from './icons/XCircleIcon';
|
||||
import { GiftIcon } from './icons/GiftIcon';
|
||||
|
||||
interface WhatsNewModalProps {
|
||||
export interface WhatsNewModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
version: string;
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
import React from 'react';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { AnalysisPanel } from './AnalysisPanel';
|
||||
import { AnalysisPanel, AnalysisTabType } from './AnalysisPanel';
|
||||
import { useFlyerItems } from '../../hooks/useFlyerItems';
|
||||
import type { Flyer, FlyerItem, Store, MasterGroceryItem } from '../../types';
|
||||
import { useUserData } from '../../hooks/useUserData';
|
||||
import { useAiAnalysis } from '../../hooks/useAiAnalysis';
|
||||
import { createMockFlyer, createMockFlyerItem, createMockMasterGroceryItem, createMockStore } from '../../tests/utils/mockFactories';
|
||||
|
||||
// Mock the logger
|
||||
vi.mock('../../services/logger.client', () => ({
|
||||
@@ -39,24 +40,19 @@ vi.mock('../../components/icons/ScaleIcon', () => ({ ScaleIcon: () => <div data-
|
||||
const mockRunAnalysis = vi.fn();
|
||||
const mockGenerateImage = vi.fn();
|
||||
|
||||
const mockFlyerItems: FlyerItem[] = [
|
||||
{ flyer_item_id: 1, item: 'Apples', price_display: '$1.99', price_in_cents: 199, quantity: '1lb', flyer_id: 1, created_at: '', view_count: 0, click_count: 0, updated_at: '' },
|
||||
];
|
||||
const mockFlyerItems: FlyerItem[] = [createMockFlyerItem({ flyer_item_id: 1, item: 'Apples', price_display: '$1.99', price_in_cents: 199, quantity: '1lb', flyer_id: 1 })];
|
||||
|
||||
const mockWatchedItems: MasterGroceryItem[] = [
|
||||
{ master_grocery_item_id: 101, name: 'Bananas', created_at: '' },
|
||||
{ master_grocery_item_id: 102, name: 'Milk', created_at: '' },
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 101, name: 'Bananas' }),
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 102, name: 'Milk' }),
|
||||
];
|
||||
const mockStore: Store = { store_id: 1, name: 'SuperMart', created_at: '' };
|
||||
const mockFlyer: Flyer = {
|
||||
flyer_id: 1,
|
||||
created_at: '2024-01-01',
|
||||
file_name: 'flyer.pdf',
|
||||
image_url: 'http://example.com/flyer.jpg',
|
||||
item_count: 1,
|
||||
valid_from: '2024-01-01',
|
||||
valid_to: '2024-01-07',
|
||||
store: mockStore,
|
||||
};
|
||||
|
||||
const mockStore: Store = createMockStore({ store_id: 1, name: 'SuperMart' });
|
||||
|
||||
const mockFlyer: Flyer = createMockFlyer({
|
||||
flyer_id: 1,
|
||||
store: mockStore,
|
||||
});
|
||||
|
||||
describe('AnalysisPanel', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -23,7 +23,7 @@ interface AnalysisPanelProps {
|
||||
* that the `activeTab` state can only be one of these values, preventing
|
||||
* type errors when indexing into results or loading state objects.
|
||||
*/
|
||||
type AnalysisTabType = Exclude<AnalysisType, AnalysisType.GENERATE_IMAGE>;
|
||||
export type AnalysisTabType = Exclude<AnalysisType, AnalysisType.GENERATE_IMAGE>;
|
||||
|
||||
interface TabButtonProps {
|
||||
label: string;
|
||||
|
||||
@@ -3,8 +3,9 @@ import React from 'react';
|
||||
import { render, screen, fireEvent, within, prettyDOM } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ExtractedDataTable, ExtractedDataTableProps } from './ExtractedDataTable';
|
||||
import type { FlyerItem, MasterGroceryItem, ShoppingList, User } from '../../types';
|
||||
import type { FlyerItem, MasterGroceryItem, ShoppingList } from '../../types';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import { createMockFlyerItem, createMockMasterGroceryItem, createMockShoppingList, createMockShoppingListItem, createMockUser } from '../../tests/utils/mockFactories';
|
||||
import { useUserData } from '../../hooks/useUserData';
|
||||
import { useMasterItems } from '../../hooks/useMasterItems';
|
||||
import { useWatchedItems } from '../../hooks/useWatchedItems';
|
||||
@@ -17,27 +18,27 @@ vi.mock('../../hooks/useMasterItems');
|
||||
vi.mock('../../hooks/useWatchedItems');
|
||||
vi.mock('../../hooks/useShoppingLists');
|
||||
|
||||
const mockUser: User = { user_id: 'user-123', email: 'test@example.com' };
|
||||
const mockUser = createMockUser({ user_id: 'user-123', email: 'test@example.com' });
|
||||
|
||||
const mockMasterItems: MasterGroceryItem[] = [
|
||||
{ master_grocery_item_id: 1, name: 'Apples', category_id: 1, category_name: 'Produce', created_at: '' },
|
||||
{ master_grocery_item_id: 2, name: 'Milk', category_id: 2, category_name: 'Dairy', created_at: '' },
|
||||
{ master_grocery_item_id: 3, name: 'Chicken Breast', category_id: 3, category_name: 'Meat', created_at: '' },
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Apples', category_id: 1, category_name: 'Produce' }),
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 2, name: 'Milk', category_id: 2, category_name: 'Dairy' }),
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 3, name: 'Chicken Breast', category_id: 3, category_name: 'Meat' }),
|
||||
];
|
||||
|
||||
const mockFlyerItems: FlyerItem[] = [
|
||||
{ flyer_item_id: 101, item: 'Gala Apples', price_display: '$1.99/lb', price_in_cents: 199, quantity: 'per lb', unit_price: { value: 1.99, unit: 'lb' }, master_item_id: 1, category_name: 'Produce', flyer_id: 1, created_at: '', view_count: 0, click_count: 0, updated_at: '' },
|
||||
{ flyer_item_id: 102, item: '2% Milk', price_display: '$4.50', price_in_cents: 450, quantity: '4L', unit_price: { value: 1.125, unit: 'L' }, master_item_id: 2, category_name: 'Dairy', flyer_id: 1, created_at: '', view_count: 0, click_count: 0, updated_at: '' },
|
||||
{ flyer_item_id: 103, item: 'Boneless Chicken', price_display: '$8.00/kg', price_in_cents: 800, quantity: 'per kg', unit_price: { value: 8.00, unit: 'kg' }, master_item_id: 3, category_name: 'Meat', flyer_id: 1, created_at: '', view_count: 0, click_count: 0, updated_at: '' },
|
||||
{ flyer_item_id: 104, item: 'Mystery Soda', price_display: '$1.00', price_in_cents: 100, quantity: '1 can', unit_price: { value: 1.00, unit: 'can' }, master_item_id: undefined, category_name: 'Beverages', flyer_id: 1, created_at: '', view_count: 0, click_count: 0, updated_at: '' }, // Unmatched item
|
||||
{ flyer_item_id: 105, item: 'Apples', price_display: '$2.50/lb', price_in_cents: 250, quantity: 'per lb', unit_price: { value: 2.50, unit: 'lb' }, master_item_id: 1, category_name: 'Produce', flyer_id: 1, created_at: '', view_count: 0, click_count: 0, updated_at: '' }, // Item name matches canonical name
|
||||
createMockFlyerItem({ flyer_item_id: 101, item: 'Gala Apples', price_display: '$1.99/lb', price_in_cents: 199, quantity: 'per lb', unit_price: { value: 1.99, unit: 'lb' }, master_item_id: 1, category_name: 'Produce', flyer_id: 1 }),
|
||||
createMockFlyerItem({ flyer_item_id: 102, item: '2% Milk', price_display: '$4.50', price_in_cents: 450, quantity: '4L', unit_price: { value: 1.125, unit: 'L' }, master_item_id: 2, category_name: 'Dairy', flyer_id: 1 }),
|
||||
createMockFlyerItem({ flyer_item_id: 103, item: 'Boneless Chicken', price_display: '$8.00/kg', price_in_cents: 800, quantity: 'per kg', unit_price: { value: 8.00, unit: 'kg' }, master_item_id: 3, category_name: 'Meat', flyer_id: 1 }),
|
||||
createMockFlyerItem({ flyer_item_id: 104, item: 'Mystery Soda', price_display: '$1.00', price_in_cents: 100, quantity: '1 can', unit_price: { value: 1.00, unit: 'can' }, master_item_id: undefined, category_name: 'Beverages', flyer_id: 1 }), // Unmatched item
|
||||
createMockFlyerItem({ flyer_item_id: 105, item: 'Apples', price_display: '$2.50/lb', price_in_cents: 250, quantity: 'per lb', unit_price: { value: 2.50, unit: 'lb' }, master_item_id: 1, category_name: 'Produce', flyer_id: 1 }), // Item name matches canonical name
|
||||
];
|
||||
|
||||
const mockShoppingLists: ShoppingList[] = [
|
||||
{
|
||||
shopping_list_id: 1, name: 'My List', user_id: 'user-123', created_at: '',
|
||||
items: [{ shopping_list_item_id: 1, shopping_list_id: 1, master_item_id: 2, quantity: 1, is_purchased: false, added_at: '' }], // Contains Milk
|
||||
},
|
||||
createMockShoppingList({
|
||||
shopping_list_id: 1, name: 'My List', user_id: 'user-123',
|
||||
items: [createMockShoppingListItem({ shopping_list_item_id: 1, shopping_list_id: 1, master_item_id: 2, quantity: 1, is_purchased: false })], // Contains Milk
|
||||
}),
|
||||
];
|
||||
|
||||
const mockAddWatchedItem = vi.fn();
|
||||
|
||||
@@ -3,14 +3,9 @@ import React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { FlyerDisplay } from './FlyerDisplay';
|
||||
import type { Store } from '../../types';
|
||||
import { createMockStore } from '../../tests/utils/mockFactories';
|
||||
|
||||
const mockStore: Store = {
|
||||
store_id: 1,
|
||||
name: 'SuperMart',
|
||||
logo_url: 'http://example.com/logo.png',
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
const mockStore = createMockStore({ store_id: 1, name: 'SuperMart', logo_url: 'http://example.com/logo.png' });
|
||||
|
||||
const mockOnOpenCorrectionTool = vi.fn();
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
||||
import { FlyerList, formatShortDate } from './FlyerList';
|
||||
import type { Flyer, UserProfile } from '../../types';
|
||||
import { createMockUserProfile } from '../../tests/utils/mockFactories';
|
||||
import { createMockFlyer } from '../../tests/utils/mockFactories';
|
||||
import * as apiClient from '../../services/apiClient';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
@@ -13,37 +14,27 @@ vi.mock('../../services/apiClient');
|
||||
vi.mock('react-hot-toast', () => ({ default: { success: vi.fn(), error: vi.fn() } }));
|
||||
|
||||
const mockFlyers: Flyer[] = [
|
||||
{
|
||||
createMockFlyer({
|
||||
flyer_id: 1,
|
||||
created_at: '2023-10-01T10:00:00Z',
|
||||
file_name: 'metro_flyer_oct_1.pdf',
|
||||
item_count: 50,
|
||||
image_url: 'http://example.com/flyer1.jpg',
|
||||
store: {
|
||||
store_id: 101,
|
||||
name: 'Metro',
|
||||
created_at: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
store: { store_id: 101, name: 'Metro' },
|
||||
valid_from: '2023-10-05',
|
||||
valid_to: '2023-10-11',
|
||||
},
|
||||
{
|
||||
created_at: '2023-10-01T10:00:00Z',
|
||||
}),
|
||||
createMockFlyer({
|
||||
flyer_id: 2,
|
||||
created_at: '2023-10-02T11:00:00Z',
|
||||
file_name: 'walmart_flyer.pdf',
|
||||
item_count: 75,
|
||||
image_url: 'http://example.com/flyer2.jpg',
|
||||
store: {
|
||||
store_id: 102,
|
||||
name: 'Walmart',
|
||||
created_at: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
store: { store_id: 102, name: 'Walmart' },
|
||||
valid_from: '2023-10-06',
|
||||
valid_to: '2023-10-06', // Same day
|
||||
},
|
||||
{
|
||||
}),
|
||||
createMockFlyer({
|
||||
flyer_id: 3,
|
||||
created_at: '2023-10-03T12:00:00Z',
|
||||
file_name: 'no-store-flyer.pdf',
|
||||
item_count: 10,
|
||||
image_url: 'http://example.com/flyer3.jpg',
|
||||
@@ -52,21 +43,18 @@ const mockFlyers: Flyer[] = [
|
||||
valid_from: '2023-10-07',
|
||||
valid_to: '2023-10-08',
|
||||
store_address: '456 Side St, Ottawa',
|
||||
},
|
||||
{
|
||||
created_at: '2023-10-03T12:00:00Z',
|
||||
}),
|
||||
createMockFlyer({
|
||||
flyer_id: 4,
|
||||
created_at: 'invalid-date',
|
||||
file_name: 'bad-date-flyer.pdf',
|
||||
item_count: 5,
|
||||
image_url: 'http://example.com/flyer4.jpg',
|
||||
store: {
|
||||
store_id: 103,
|
||||
name: 'Date Store',
|
||||
created_at: '2023-01-01T00:00:00Z',
|
||||
},
|
||||
store: { store_id: 103, name: 'Date Store' },
|
||||
created_at: 'invalid-date',
|
||||
valid_from: 'invalid-from',
|
||||
valid_to: null,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const mockedApiClient = apiClient as Mocked<typeof apiClient>;
|
||||
|
||||
@@ -6,6 +6,9 @@ import { VoiceAssistant } from './VoiceAssistant';
|
||||
import * as aiApiClient from '../../services/aiApiClient';
|
||||
import { encode } from '../../utils/audioUtils';
|
||||
|
||||
// Unmock the component to test the real implementation
|
||||
vi.unmock('./VoiceAssistant');
|
||||
|
||||
// Mock dependencies to isolate the component
|
||||
vi.mock('../../services/aiApiClient', () => ({
|
||||
startVoiceSession: vi.fn(),
|
||||
|
||||
@@ -8,7 +8,7 @@ import { encode } from '../../utils/audioUtils';
|
||||
import { logger } from '../../services/logger.client';
|
||||
import { XMarkIcon } from '../../components/icons/XMarkIcon';
|
||||
|
||||
interface VoiceAssistantProps {
|
||||
export interface VoiceAssistantProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { renderHook, waitFor, act } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { useActiveDeals } from './useActiveDeals';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import { useFlyers } from '../hooks/useFlyers';
|
||||
import { useUserData } from '../hooks/useUserData';
|
||||
import type { Flyer, MasterGroceryItem, FlyerItem, DealItem } from '../types';
|
||||
import { createMockFlyer, createMockFlyerItem, createMockMasterGroceryItem } from '../tests/utils/mockFactories';
|
||||
import { mockUseFlyers, mockUseUserData } from '../tests/setup/mockHooks';
|
||||
|
||||
// Explicitly mock apiClient to ensure stable spies are used
|
||||
vi.mock('../services/apiClient', () => ({
|
||||
@@ -16,14 +16,6 @@ vi.mock('../services/apiClient', () => ({
|
||||
// The apiClient is globally mocked in our test setup, so we just need to cast it
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
// Mock the new data provider hooks
|
||||
vi.mock('../hooks/useFlyers');
|
||||
vi.mock('../hooks/useUserData');
|
||||
|
||||
// Create typed mocks for easier usage
|
||||
const mockedUseFlyers = vi.mocked(useFlyers);
|
||||
const mockedUseUserData = vi.mocked(useUserData);
|
||||
|
||||
// Mock the logger to prevent console noise
|
||||
vi.mock('../services/logger.client', () => ({
|
||||
logger: {
|
||||
@@ -46,7 +38,7 @@ describe('useActiveDeals Hook', () => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Set up default successful mocks for the new data hooks
|
||||
mockedUseFlyers.mockReturnValue({
|
||||
mockUseFlyers.mockReturnValue({
|
||||
flyers: mockFlyers,
|
||||
isLoadingFlyers: false,
|
||||
flyersError: null,
|
||||
@@ -55,7 +47,7 @@ describe('useActiveDeals Hook', () => {
|
||||
isRefetchingFlyers: false,
|
||||
refetchFlyers: vi.fn(),
|
||||
});
|
||||
mockedUseUserData.mockReturnValue({
|
||||
mockUseUserData.mockReturnValue({
|
||||
watchedItems: mockWatchedItems,
|
||||
shoppingLists: [],
|
||||
setWatchedItems: vi.fn(),
|
||||
@@ -71,23 +63,61 @@ describe('useActiveDeals Hook', () => {
|
||||
|
||||
const mockFlyers: Flyer[] = [
|
||||
// A currently valid flyer
|
||||
{ flyer_id: 1, file_name: 'valid.pdf', image_url: '', item_count: 10, created_at: '', valid_from: '2024-01-10', valid_to: '2024-01-20', store: { store_id: 1, name: 'Valid Store', created_at: '', logo_url: '' } },
|
||||
createMockFlyer({
|
||||
flyer_id: 1,
|
||||
file_name: 'valid.pdf',
|
||||
item_count: 10,
|
||||
valid_from: '2024-01-10',
|
||||
valid_to: '2024-01-20',
|
||||
store: { store_id: 1, name: 'Valid Store' },
|
||||
}),
|
||||
// An expired flyer
|
||||
{ flyer_id: 2, file_name: 'expired.pdf', image_url: '', item_count: 5, created_at: '', valid_from: '2024-01-01', valid_to: '2024-01-05', store: { store_id: 2, name: 'Expired Store', created_at: '', logo_url: '' } },
|
||||
createMockFlyer({
|
||||
flyer_id: 2,
|
||||
file_name: 'expired.pdf',
|
||||
item_count: 5,
|
||||
valid_from: '2024-01-01',
|
||||
valid_to: '2024-01-05',
|
||||
store: { store_id: 2, name: 'Expired Store' },
|
||||
}),
|
||||
// A future flyer
|
||||
{ flyer_id: 3, file_name: 'future.pdf', image_url: '', item_count: 8, created_at: '', valid_from: '2024-02-01', valid_to: '2024-02-10', store: { store_id: 3, name: 'Future Store', created_at: '', logo_url: '' } },
|
||||
createMockFlyer({
|
||||
flyer_id: 3,
|
||||
file_name: 'future.pdf',
|
||||
item_count: 8,
|
||||
valid_from: '2024-02-01',
|
||||
valid_to: '2024-02-10',
|
||||
store: { store_id: 3, name: 'Future Store' },
|
||||
}),
|
||||
];
|
||||
|
||||
const mockWatchedItems: MasterGroceryItem[] = [
|
||||
{ master_grocery_item_id: 101, name: 'Apples', created_at: '' },
|
||||
{ master_grocery_item_id: 102, name: 'Milk', created_at: '' },
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 101, name: 'Apples' }),
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 102, name: 'Milk' }),
|
||||
];
|
||||
|
||||
const mockFlyerItems: FlyerItem[] = [
|
||||
// A deal for a watched item in a valid flyer
|
||||
{ flyer_item_id: 1, flyer_id: 1, item: 'Red Apples', price_display: '$1.99', price_in_cents: 199, quantity: 'lb', master_item_id: 101, master_item_name: 'Apples', created_at: '', view_count: 0, click_count: 0, updated_at: '' },
|
||||
createMockFlyerItem({
|
||||
flyer_item_id: 1,
|
||||
flyer_id: 1,
|
||||
item: 'Red Apples',
|
||||
price_display: '$1.99',
|
||||
price_in_cents: 199,
|
||||
quantity: 'lb',
|
||||
master_item_id: 101,
|
||||
master_item_name: 'Apples',
|
||||
}),
|
||||
// An item that is not a deal
|
||||
{ flyer_item_id: 2, flyer_id: 1, item: 'Oranges', price_display: '$2.49', price_in_cents: 249, quantity: 'lb', master_item_id: 201, created_at: '', view_count: 0, click_count: 0, updated_at: '' },
|
||||
createMockFlyerItem({
|
||||
flyer_item_id: 2,
|
||||
flyer_id: 1,
|
||||
item: 'Oranges',
|
||||
price_display: '$2.49',
|
||||
price_in_cents: 249,
|
||||
quantity: 'lb',
|
||||
master_item_id: 201,
|
||||
}),
|
||||
];
|
||||
|
||||
it('should return loading state initially and then calculated data', async () => {
|
||||
@@ -126,7 +156,7 @@ describe('useActiveDeals Hook', () => {
|
||||
|
||||
it('should not fetch flyer items if there are no watched items', async () => {
|
||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify({ count: 10 })));
|
||||
mockedUseUserData.mockReturnValue({
|
||||
mockUseUserData.mockReturnValue({
|
||||
watchedItems: [],
|
||||
shoppingLists: [],
|
||||
setWatchedItems: vi.fn(),
|
||||
@@ -148,7 +178,7 @@ describe('useActiveDeals Hook', () => {
|
||||
|
||||
it('should handle the case where there are no valid flyers', async () => {
|
||||
// Override flyers mock to only include invalid ones
|
||||
mockedUseFlyers.mockReturnValue({
|
||||
mockUseFlyers.mockReturnValue({
|
||||
flyers: [mockFlyers[1], mockFlyers[2]],
|
||||
isLoadingFlyers: false,
|
||||
flyersError: null,
|
||||
@@ -219,19 +249,26 @@ describe('useActiveDeals Hook', () => {
|
||||
|
||||
it('should use "Unknown Store" as a fallback if flyer has no store or store name', async () => {
|
||||
// Create a flyer with a null store object
|
||||
const flyerWithoutStore: Flyer = {
|
||||
const flyerWithoutStore = createMockFlyer({
|
||||
flyer_id: 4,
|
||||
file_name: 'no-store.pdf',
|
||||
image_url: '',
|
||||
item_count: 1,
|
||||
created_at: '',
|
||||
valid_from: '2024-01-10',
|
||||
valid_to: '2024-01-20',
|
||||
store: null as any, // Explicitly set to null
|
||||
};
|
||||
const itemInFlyerWithoutStore: FlyerItem = { flyer_item_id: 3, flyer_id: 4, item: 'Mystery Item', price_display: '$5.00', price_in_cents: 500, master_item_id: 101, master_item_name: 'Apples', created_at: '', view_count: 0, click_count: 0, updated_at: '' };
|
||||
});
|
||||
(flyerWithoutStore as any).store = null; // Explicitly set to null
|
||||
|
||||
mockedUseFlyers.mockReturnValue({ ...mockedUseFlyers(), flyers: [flyerWithoutStore] });
|
||||
const itemInFlyerWithoutStore = createMockFlyerItem({
|
||||
flyer_item_id: 3,
|
||||
flyer_id: 4,
|
||||
item: 'Mystery Item',
|
||||
price_display: '$5.00',
|
||||
price_in_cents: 500,
|
||||
master_item_id: 101,
|
||||
master_item_name: 'Apples',
|
||||
});
|
||||
|
||||
mockUseFlyers.mockReturnValue({ ...mockUseFlyers(), flyers: [flyerWithoutStore] });
|
||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify({ count: 1 })));
|
||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([itemInFlyerWithoutStore])));
|
||||
|
||||
@@ -249,11 +286,11 @@ describe('useActiveDeals Hook', () => {
|
||||
|
||||
const mixedItems: FlyerItem[] = [
|
||||
// Watched item (Master ID 101 is in mockWatchedItems)
|
||||
{ flyer_item_id: 1, flyer_id: 1, item: 'Watched Item', price_display: '$1.00', price_in_cents: 100, quantity: 'ea', master_item_id: 101, master_item_name: 'Apples', created_at: '', view_count: 0, click_count: 0, updated_at: '' },
|
||||
createMockFlyerItem({ flyer_item_id: 1, flyer_id: 1, item: 'Watched Item', price_display: '$1.00', price_in_cents: 100, quantity: 'ea', master_item_id: 101, master_item_name: 'Apples' }),
|
||||
// Unwatched item (Master ID 999 is NOT in mockWatchedItems)
|
||||
{ flyer_item_id: 2, flyer_id: 1, item: 'Unwatched Item', price_display: '$2.00', price_in_cents: 200, quantity: 'ea', master_item_id: 999, master_item_name: 'Unknown', created_at: '', view_count: 0, click_count: 0, updated_at: '' },
|
||||
createMockFlyerItem({ flyer_item_id: 2, flyer_id: 1, item: 'Unwatched Item', price_display: '$2.00', price_in_cents: 200, quantity: 'ea', master_item_id: 999, master_item_name: 'Unknown' }),
|
||||
// Item with no master ID
|
||||
{ flyer_item_id: 3, flyer_id: 1, item: 'No Master ID', price_display: '$3.00', price_in_cents: 300, quantity: 'ea', master_item_id: undefined, created_at: '', view_count: 0, click_count: 0, updated_at: '' },
|
||||
createMockFlyerItem({ flyer_item_id: 3, flyer_id: 1, item: 'No Master ID', price_display: '$3.00', price_in_cents: 300, quantity: 'ea', master_item_id: undefined }),
|
||||
];
|
||||
|
||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify(mixedItems)));
|
||||
@@ -307,8 +344,8 @@ describe('useActiveDeals Hook', () => {
|
||||
});
|
||||
|
||||
// Change watched items
|
||||
const newWatchedItems = [...mockWatchedItems, { master_grocery_item_id: 103, name: 'Bread', created_at: '' }];
|
||||
mockedUseUserData.mockReturnValue({
|
||||
const newWatchedItems = [...mockWatchedItems, createMockMasterGroceryItem({ master_grocery_item_id: 103, name: 'Bread' })];
|
||||
mockUseUserData.mockReturnValue({
|
||||
watchedItems: newWatchedItems,
|
||||
shoppingLists: [],
|
||||
setWatchedItems: vi.fn(),
|
||||
@@ -330,16 +367,16 @@ describe('useActiveDeals Hook', () => {
|
||||
// TODAY is 2024-01-15T12:00:00.000Z
|
||||
const boundaryFlyers: Flyer[] = [
|
||||
// Ends today
|
||||
{ flyer_id: 10, file_name: 'ends-today.pdf', image_url: '', item_count: 1, created_at: '', valid_from: '2024-01-01', valid_to: '2024-01-15', store: { store_id: 1, name: 'Store A', created_at: '', logo_url: '' } },
|
||||
createMockFlyer({ flyer_id: 10, file_name: 'ends-today.pdf', item_count: 1, valid_from: '2024-01-01', valid_to: '2024-01-15', store: { store_id: 1, name: 'Store A' } }),
|
||||
// Starts today
|
||||
{ flyer_id: 11, file_name: 'starts-today.pdf', image_url: '', item_count: 1, created_at: '', valid_from: '2024-01-15', valid_to: '2024-01-30', store: { store_id: 1, name: 'Store B', created_at: '', logo_url: '' } },
|
||||
createMockFlyer({ flyer_id: 11, file_name: 'starts-today.pdf', item_count: 1, valid_from: '2024-01-15', valid_to: '2024-01-30', store: { store_id: 1, name: 'Store B' } }),
|
||||
// Valid only today
|
||||
{ flyer_id: 12, file_name: 'only-today.pdf', image_url: '', item_count: 1, created_at: '', valid_from: '2024-01-15', valid_to: '2024-01-15', store: { store_id: 1, name: 'Store C', created_at: '', logo_url: '' } },
|
||||
createMockFlyer({ flyer_id: 12, file_name: 'only-today.pdf', item_count: 1, valid_from: '2024-01-15', valid_to: '2024-01-15', store: { store_id: 1, name: 'Store C' } }),
|
||||
// Ends yesterday (invalid)
|
||||
{ flyer_id: 13, file_name: 'ends-yesterday.pdf', image_url: '', item_count: 1, created_at: '', valid_from: '2024-01-01', valid_to: '2024-01-14', store: { store_id: 1, name: 'Store D', created_at: '', logo_url: '' } },
|
||||
createMockFlyer({ flyer_id: 13, file_name: 'ends-yesterday.pdf', item_count: 1, valid_from: '2024-01-01', valid_to: '2024-01-14', store: { store_id: 1, name: 'Store D' } }),
|
||||
];
|
||||
|
||||
mockedUseFlyers.mockReturnValue({
|
||||
mockUseFlyers.mockReturnValue({
|
||||
flyers: boundaryFlyers,
|
||||
isLoadingFlyers: false,
|
||||
flyersError: null,
|
||||
@@ -361,7 +398,7 @@ describe('useActiveDeals Hook', () => {
|
||||
});
|
||||
|
||||
it('should handle missing price_in_cents and quantity in deal items', async () => {
|
||||
const incompleteItem = {
|
||||
const incompleteItem = createMockFlyerItem({
|
||||
flyer_item_id: 99,
|
||||
flyer_id: 1,
|
||||
item: 'Incomplete Item',
|
||||
@@ -369,11 +406,9 @@ describe('useActiveDeals Hook', () => {
|
||||
// price_in_cents and quantity are missing
|
||||
master_item_id: 101,
|
||||
master_item_name: 'Apples',
|
||||
created_at: '',
|
||||
view_count: 0,
|
||||
click_count: 0,
|
||||
updated_at: ''
|
||||
} as FlyerItem;
|
||||
price_in_cents: null,
|
||||
quantity: undefined,
|
||||
});
|
||||
|
||||
mockedApiClient.countFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify({ count: 1 })));
|
||||
mockedApiClient.fetchFlyerItemsForFlyers.mockResolvedValue(new Response(JSON.stringify([incompleteItem])));
|
||||
|
||||
@@ -3,8 +3,8 @@ import React from 'react';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach, Mocked } from 'vitest';
|
||||
import { useAiAnalysis, aiAnalysisReducer } from './useAiAnalysis';
|
||||
import { AnalysisType } from '../types';
|
||||
import type { Flyer, FlyerItem, MasterGroceryItem } from '../types';
|
||||
import { AnalysisType, Flyer, FlyerItem, MasterGroceryItem } from '../types';
|
||||
import { createMockFlyer, createMockFlyerItem, createMockMasterGroceryItem } from '../tests/utils/mockFactories';
|
||||
import { logger } from '../services/logger.client';
|
||||
import { AiAnalysisService } from '../services/aiAnalysisService';
|
||||
|
||||
@@ -22,9 +22,16 @@ vi.mock('../services/logger.client', () => ({
|
||||
}));
|
||||
|
||||
// 2. Mock data
|
||||
const mockFlyerItems: FlyerItem[] = [{ flyer_item_id: 1, item: 'Apples', price_display: '$1.99', price_in_cents: 199, quantity: '1lb', flyer_id: 1, created_at: '', view_count: 0, click_count: 0, updated_at: '' }];
|
||||
const mockWatchedItems: MasterGroceryItem[] = [{ master_grocery_item_id: 101, name: 'Bananas', created_at: '' }];
|
||||
const mockSelectedFlyer: Flyer = { flyer_id: 1, store: { store_id: 1, name: 'SuperMart', created_at: '' } } as Flyer;
|
||||
const mockFlyerItems: FlyerItem[] = [
|
||||
createMockFlyerItem({ flyer_item_id: 1, item: 'Apples', price_display: '$1.99', price_in_cents: 199, quantity: '1lb', flyer_id: 1 }),
|
||||
];
|
||||
const mockWatchedItems: MasterGroceryItem[] = [
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 101, name: 'Bananas' }),
|
||||
];
|
||||
const mockSelectedFlyer: Flyer = createMockFlyer({
|
||||
flyer_id: 1,
|
||||
store: { store_id: 1, name: 'SuperMart' },
|
||||
});
|
||||
|
||||
describe('useAiAnalysis Hook', () => {
|
||||
let mockService: Mocked<AiAnalysisService>;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAuth } from './useAuth';
|
||||
import { AuthProvider } from '../providers/AuthProvider';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import type { User, UserProfile } from '../types';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
|
||||
// Mock the dependencies
|
||||
vi.mock('../services/apiClient', () => ({
|
||||
@@ -24,15 +25,14 @@ vi.mock('../services/logger.client', () => ({
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
|
||||
// Define mock data
|
||||
const mockUser: User = { user_id: 'user-abc-123', email: 'test@example.com' };
|
||||
const mockProfile: UserProfile = {
|
||||
const mockProfile: UserProfile = createMockUserProfile({
|
||||
user_id: 'user-abc-123',
|
||||
full_name: 'Test User',
|
||||
points: 100,
|
||||
role: 'user',
|
||||
user: mockUser,
|
||||
};
|
||||
user: { user_id: 'user-abc-123', email: 'test@example.com' },
|
||||
});
|
||||
const mockUser: User = mockProfile.user;
|
||||
|
||||
// Reusable wrapper for rendering the hook within the provider
|
||||
const wrapper = ({ children }: { children: ReactNode }) => <AuthProvider>{children}</AuthProvider>;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { renderHook } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { useFlyerItems } from './useFlyerItems';
|
||||
import { useApiOnMount } from './useApiOnMount';
|
||||
import type { Flyer, FlyerItem } from '../types';
|
||||
import { createMockFlyer, createMockFlyerItem } from '../tests/utils/mockFactories';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
|
||||
// Mock the underlying useApiOnMount hook to isolate the useFlyerItems hook's logic.
|
||||
@@ -13,7 +13,7 @@ vi.mock('../services/apiClient');
|
||||
const mockedUseApiOnMount = vi.mocked(useApiOnMount);
|
||||
|
||||
describe('useFlyerItems Hook', () => {
|
||||
const mockFlyer: Flyer = {
|
||||
const mockFlyer = createMockFlyer({
|
||||
flyer_id: 123,
|
||||
file_name: 'test-flyer.jpg',
|
||||
image_url: '/test.jpg',
|
||||
@@ -21,17 +21,15 @@ describe('useFlyerItems Hook', () => {
|
||||
checksum: 'abc',
|
||||
valid_from: '2024-01-01',
|
||||
valid_to: '2024-01-07',
|
||||
item_count: 1,
|
||||
store: {
|
||||
store_id: 1,
|
||||
name: 'Test Store',
|
||||
created_at: new Date().toISOString(),
|
||||
},
|
||||
item_count: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const mockFlyerItems: FlyerItem[] = [
|
||||
{ flyer_item_id: 1, flyer_id: 123, item: 'Apples', price_display: '$1.99', price_in_cents: 199, quantity: '1lb', created_at: new Date().toISOString(), view_count: 0, click_count: 0, updated_at: new Date().toISOString() },
|
||||
const mockFlyerItems = [
|
||||
createMockFlyerItem({ flyer_item_id: 1, flyer_id: 123, item: 'Apples', price_display: '$1.99', price_in_cents: 199, quantity: '1lb' }),
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useFlyers } from './useFlyers';
|
||||
import { FlyersProvider } from '../providers/FlyersProvider';
|
||||
import { useInfiniteQuery } from './useInfiniteQuery';
|
||||
import type { Flyer } from '../types';
|
||||
import { createMockFlyer } from '../tests/utils/mockFactories';
|
||||
|
||||
// 1. Mock the useInfiniteQuery hook, which is the dependency of our FlyersProvider.
|
||||
vi.mock('./useInfiniteQuery');
|
||||
@@ -64,7 +65,7 @@ describe('useFlyers Hook and FlyersProvider', () => {
|
||||
it('should return flyers data and hasNextPage on successful fetch', () => {
|
||||
// Arrange: Mock a successful data fetch.
|
||||
const mockFlyers: Flyer[] = [
|
||||
{ flyer_id: 1, file_name: 'flyer1.jpg', image_url: 'url1', item_count: 5, created_at: '2024-01-01' },
|
||||
createMockFlyer({ flyer_id: 1, file_name: 'flyer1.jpg', image_url: 'url1', item_count: 5, created_at: '2024-01-01' }),
|
||||
];
|
||||
mockedUseInfiniteQuery.mockReturnValue({
|
||||
data: mockFlyers,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useMasterItems } from './useMasterItems';
|
||||
import { MasterItemsProvider } from '../providers/MasterItemsProvider';
|
||||
import { useApiOnMount } from './useApiOnMount';
|
||||
import type { MasterGroceryItem } from '../types';
|
||||
import { createMockMasterGroceryItem } from '../tests/utils/mockFactories';
|
||||
|
||||
// 1. Mock the useApiOnMount hook, which is the dependency of our provider.
|
||||
vi.mock('./useApiOnMount');
|
||||
@@ -57,8 +58,8 @@ describe('useMasterItems Hook and MasterItemsProvider', () => {
|
||||
it('should return masterItems on successful fetch', () => {
|
||||
// Arrange: Mock a successful data fetch.
|
||||
const mockItems: MasterGroceryItem[] = [
|
||||
{ master_grocery_item_id: 1, name: 'Milk', category_id: 1, category_name: 'Dairy', created_at: '' },
|
||||
{ master_grocery_item_id: 2, name: 'Bread', category_id: 2, category_name: 'Bakery', created_at: '' },
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk', category_id: 1, category_name: 'Dairy' }),
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 2, name: 'Bread', category_id: 2, category_name: 'Bakery' }),
|
||||
];
|
||||
mockedUseApiOnMount.mockReturnValue({
|
||||
data: mockItems,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import toast from 'react-hot-toast';
|
||||
import { useProfileAddress } from './useProfileAddress';
|
||||
import { useApi } from './useApi';
|
||||
import type { Profile, Address } from '../types';
|
||||
import { createMockAddress, createMockProfile } from '../tests/utils/mockFactories';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('react-hot-toast', () => ({
|
||||
@@ -25,20 +25,18 @@ const mockedUseApi = vi.mocked(useApi);
|
||||
const mockedToast = vi.mocked(toast);
|
||||
|
||||
// Mock data
|
||||
const mockProfile: Profile = {
|
||||
const mockProfile = createMockProfile({
|
||||
user_id: 'user-123',
|
||||
address_id: 1,
|
||||
full_name: 'Test User',
|
||||
role: 'user',
|
||||
points: 0,
|
||||
};
|
||||
});
|
||||
|
||||
const mockProfileNoAddress: Profile = {
|
||||
const mockProfileNoAddress = createMockProfile({
|
||||
...mockProfile,
|
||||
address_id: null,
|
||||
};
|
||||
});
|
||||
|
||||
const mockAddress: Address = {
|
||||
const mockAddress = createMockAddress({
|
||||
address_id: 1,
|
||||
address_line_1: '123 Main St',
|
||||
city: 'Anytown',
|
||||
@@ -47,9 +45,7 @@ const mockAddress: Address = {
|
||||
country: 'USA',
|
||||
latitude: 34.05,
|
||||
longitude: -118.25,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useProfileAddress Hook', () => {
|
||||
let mockGeocode: Mock;
|
||||
|
||||
@@ -6,8 +6,9 @@ import { useApi } from './useApi';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { useUserData } from '../hooks/useUserData';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import type { ShoppingList, ShoppingListItem, User } from '../types';
|
||||
import React from 'react'; // Required for Dispatch/SetStateAction types
|
||||
import { createMockShoppingList, createMockShoppingListItem, createMockUserProfile } 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 = {
|
||||
@@ -30,7 +31,9 @@ const mockedUseApi = vi.mocked(useApi);
|
||||
const mockedUseAuth = vi.mocked(useAuth);
|
||||
const mockedUseUserData = vi.mocked(useUserData);
|
||||
|
||||
const mockUser: User = { user_id: 'user-123', email: 'test@example.com' };
|
||||
// Create a mock User object by extracting it from a mock UserProfile
|
||||
const mockUserProfile = createMockUserProfile({ user_id: 'user-123', user: { user_id: 'user-123', email: 'test@example.com' } });
|
||||
const mockUser: User = mockUserProfile.user;
|
||||
|
||||
describe('useShoppingLists Hook', () => {
|
||||
// Create a mock setter function that we can spy on
|
||||
@@ -96,9 +99,9 @@ describe('useShoppingLists Hook', () => {
|
||||
});
|
||||
|
||||
it('should set the first list as active on initial load if lists exist', async () => {
|
||||
const mockLists: ShoppingList[] = [
|
||||
{ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', created_at: '', items: [] },
|
||||
{ shopping_list_id: 2, name: 'Hardware Store', user_id: 'user-123', created_at: '', items: [] },
|
||||
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({
|
||||
@@ -125,9 +128,7 @@ describe('useShoppingLists Hook', () => {
|
||||
logout: vi.fn(),
|
||||
updateProfile: vi.fn(),
|
||||
});
|
||||
const mockLists: ShoppingList[] = [
|
||||
{ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', created_at: '', items: [] },
|
||||
];
|
||||
const mockLists = [createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' })];
|
||||
mockedUseUserData.mockReturnValue({
|
||||
shoppingLists: mockLists,
|
||||
setShoppingLists: mockSetShoppingLists,
|
||||
@@ -143,7 +144,7 @@ describe('useShoppingLists Hook', () => {
|
||||
});
|
||||
|
||||
it('should set activeListId to null when lists become empty', async () => {
|
||||
const mockLists: ShoppingList[] = [{ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', created_at: '', items: [] }];
|
||||
const mockLists = [createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' })];
|
||||
|
||||
// Initial render with a list
|
||||
mockedUseUserData.mockReturnValue({
|
||||
@@ -221,7 +222,7 @@ describe('useShoppingLists Hook', () => {
|
||||
|
||||
describe('createList', () => {
|
||||
it('should call the API and update state on successful creation', async () => {
|
||||
const newList: ShoppingList = { shopping_list_id: 99, name: 'New List', user_id: 'user-123', created_at: '', items: [] };
|
||||
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.
|
||||
@@ -257,10 +258,10 @@ describe('useShoppingLists Hook', () => {
|
||||
|
||||
describe('deleteList', () => {
|
||||
// Use a function to get a fresh copy for each test run
|
||||
const getMockLists = (): ShoppingList[] => ([
|
||||
{ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', created_at: '', items: [] },
|
||||
{ shopping_list_id: 2, name: 'Hardware Store', user_id: 'user-123', created_at: '', items: [] },
|
||||
]);
|
||||
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[] = [];
|
||||
|
||||
@@ -347,7 +348,7 @@ describe('useShoppingLists Hook', () => {
|
||||
|
||||
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: ShoppingList[] = [{ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', created_at: '', items: [] }];
|
||||
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);
|
||||
@@ -371,7 +372,7 @@ describe('useShoppingLists Hook', () => {
|
||||
|
||||
describe('addItemToList', () => {
|
||||
let currentLists: ShoppingList[] = [];
|
||||
const getMockLists = () => [{ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', created_at: '', items: [] }];
|
||||
const getMockLists = () => [createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123' })];
|
||||
|
||||
beforeEach(() => {
|
||||
currentLists = getMockLists();
|
||||
@@ -389,7 +390,7 @@ describe('useShoppingLists Hook', () => {
|
||||
});
|
||||
|
||||
it('should call API and add item to the correct list', async () => {
|
||||
const newItem: ShoppingListItem = { shopping_list_item_id: 101, shopping_list_id: 1, custom_item_name: 'Milk', is_purchased: false, quantity: 1, added_at: new Date().toISOString() };
|
||||
const newItem = createMockShoppingListItem({ shopping_list_item_id: 101, shopping_list_id: 1, custom_item_name: 'Milk' });
|
||||
mockAddItemApi.mockResolvedValue(newItem);
|
||||
|
||||
const { result, rerender } = renderHook(() => useShoppingLists());
|
||||
@@ -406,11 +407,11 @@ describe('useShoppingLists Hook', () => {
|
||||
|
||||
it('should not add a duplicate item (by master_item_id) to a list', async () => {
|
||||
console.log('TEST: should not add a duplicate item (by master_item_id) to a list');
|
||||
const existingItem: ShoppingListItem = { shopping_list_item_id: 100, shopping_list_id: 1, master_item_id: 5, custom_item_name: 'Milk', is_purchased: false, quantity: 1, added_at: '' };
|
||||
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 = [{ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', created_at: '', items: [existingItem] }];
|
||||
currentLists = [createMockShoppingList({ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', items: [existingItem] })];
|
||||
// This is what the API would return for adding master_item_id 5 again. It has a new shopping_list_item_id.
|
||||
const newItemFromApi: ShoppingListItem = { shopping_list_item_id: 101, shopping_list_id: 1, master_item_id: 5, custom_item_name: 'Milk', is_purchased: false, quantity: 1, added_at: '' };
|
||||
const newItemFromApi = createMockShoppingListItem({ shopping_list_item_id: 101, shopping_list_id: 1, master_item_id: 5, custom_item_name: 'Milk' });
|
||||
|
||||
mockAddItemApi.mockResolvedValue(newItemFromApi);
|
||||
|
||||
@@ -433,17 +434,15 @@ describe('useShoppingLists Hook', () => {
|
||||
});
|
||||
|
||||
describe('updateItemInList', () => {
|
||||
const initialItem: ShoppingListItem = { shopping_list_item_id: 101, shopping_list_id: 1, custom_item_name: 'Milk', is_purchased: false, quantity: 1, added_at: new Date().toISOString() };
|
||||
const mockLists: ShoppingList[] = [{ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', created_at: '', items: [initialItem] }];
|
||||
const otherList: ShoppingList = { shopping_list_id: 2, name: 'Other', user_id: 'user-123', created_at: '', items: [] };
|
||||
const multiLists = [mockLists[0], otherList];
|
||||
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: ShoppingListItem = { ...initialItem, is_purchased: true };
|
||||
const updatedItem = { ...initialItem, is_purchased: true };
|
||||
mockUpdateItemApi.mockResolvedValue(updatedItem);
|
||||
|
||||
const { result } = renderHook(() => useShoppingLists());
|
||||
@@ -457,7 +456,7 @@ describe('useShoppingLists Hook', () => {
|
||||
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(otherList); // Verify other list is unchanged
|
||||
expect(newState[1]).toBe(multiLists[1]); // Verify other list is unchanged
|
||||
});
|
||||
|
||||
it('should not call update API if no list is active', async () => {
|
||||
@@ -482,10 +481,8 @@ describe('useShoppingLists Hook', () => {
|
||||
});
|
||||
|
||||
describe('removeItemFromList', () => {
|
||||
const initialItem: ShoppingListItem = { shopping_list_item_id: 101, shopping_list_id: 1, custom_item_name: 'Milk', is_purchased: false, quantity: 1, added_at: new Date().toISOString() };
|
||||
const mockLists: ShoppingList[] = [{ shopping_list_id: 1, name: 'Groceries', user_id: 'user-123', created_at: '', items: [initialItem] }];
|
||||
const otherList: ShoppingList = { shopping_list_id: 2, name: 'Other', user_id: 'user-123', created_at: '', items: [] };
|
||||
const multiLists = [mockLists[0], otherList];
|
||||
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({
|
||||
@@ -511,7 +508,7 @@ describe('useShoppingLists Hook', () => {
|
||||
const updater = (mockSetShoppingLists as Mock).mock.calls[0][0];
|
||||
const newState = updater(multiLists);
|
||||
expect(newState[0].items).toHaveLength(0);
|
||||
expect(newState[1]).toBe(otherList); // Verify other list is unchanged
|
||||
expect(newState[1]).toBe(multiLists[1]); // Verify other list is unchanged
|
||||
});
|
||||
|
||||
it('should not call remove API if no list is active', async () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAuth } from '../hooks/useAuth';
|
||||
import { UserDataProvider } from '../providers/UserDataProvider';
|
||||
import { useApiOnMount } from './useApiOnMount';
|
||||
import type { MasterGroceryItem, ShoppingList, UserProfile } from '../types';
|
||||
import { createMockMasterGroceryItem, createMockShoppingList, createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
|
||||
// 1. Mock the hook's dependencies
|
||||
vi.mock('../hooks/useAuth');
|
||||
@@ -21,21 +22,16 @@ const mockedUseApiOnMount = vi.mocked(useApiOnMount);
|
||||
const wrapper = ({ children }: { children: ReactNode }) => <UserDataProvider>{children}</UserDataProvider>; // No change needed here, just for context
|
||||
|
||||
// 4. Mock data for testing
|
||||
const mockUser: UserProfile = {
|
||||
const mockUser: UserProfile = createMockUserProfile({
|
||||
user_id: 'user-123',
|
||||
full_name: 'Test User',
|
||||
role: 'user',
|
||||
points: 100,
|
||||
user: { user_id: 'user-123', email: 'test@example.com' },
|
||||
};
|
||||
});
|
||||
|
||||
const mockWatchedItems: MasterGroceryItem[] = [
|
||||
{ master_grocery_item_id: 1, name: 'Milk', created_at: '' },
|
||||
];
|
||||
const mockWatchedItems = [createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' })];
|
||||
|
||||
const mockShoppingLists: ShoppingList[] = [
|
||||
{ shopping_list_id: 1, name: 'Weekly Groceries', user_id: 'user-123', created_at: '', items: [] },
|
||||
];
|
||||
const mockShoppingLists = [createMockShoppingList({ shopping_list_id: 1, name: 'Weekly Groceries', user_id: 'user-123' })];
|
||||
|
||||
describe('useUserData Hook and UserDataProvider', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAuth } from '../hooks/useAuth';
|
||||
import { useUserData } from '../hooks/useUserData';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import type { MasterGroceryItem, User } from '../types';
|
||||
import { createMockMasterGroceryItem } from '../tests/utils/mockFactories';
|
||||
|
||||
// Mock the hooks that useWatchedItems depends on
|
||||
vi.mock('./useApi');
|
||||
@@ -20,9 +21,9 @@ const mockedUseAuth = vi.mocked(useAuth);
|
||||
const mockedUseUserData = vi.mocked(useUserData);
|
||||
|
||||
const mockUser: User = { user_id: 'user-123', email: 'test@example.com' };
|
||||
const mockInitialItems: MasterGroceryItem[] = [
|
||||
{ master_grocery_item_id: 1, name: 'Milk', created_at: '' },
|
||||
{ master_grocery_item_id: 2, name: 'Bread', created_at: '' },
|
||||
const mockInitialItems = [
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' }),
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 2, name: 'Bread' }),
|
||||
];
|
||||
|
||||
describe('useWatchedItems Hook', () => {
|
||||
@@ -97,7 +98,7 @@ describe('useWatchedItems Hook', () => {
|
||||
|
||||
describe('addWatchedItem', () => {
|
||||
it('should call the API and update state on successful addition', async () => {
|
||||
const newItem: MasterGroceryItem = { master_grocery_item_id: 3, name: 'Cheese', created_at: '' };
|
||||
const newItem = createMockMasterGroceryItem({ master_grocery_item_id: 3, name: 'Cheese' });
|
||||
mockAddWatchedItemApi.mockResolvedValue(newItem);
|
||||
|
||||
const { result } = renderHook(() => useWatchedItems());
|
||||
@@ -148,7 +149,7 @@ describe('useWatchedItems Hook', () => {
|
||||
|
||||
it('should not add duplicate items to the state', async () => {
|
||||
// Item ID 1 ('Milk') already exists in mockInitialItems
|
||||
const existingItem: MasterGroceryItem = { master_grocery_item_id: 1, name: 'Milk', created_at: '' };
|
||||
const existingItem = createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Milk' });
|
||||
mockAddWatchedItemApi.mockResolvedValue(existingItem);
|
||||
|
||||
const { result } = renderHook(() => useWatchedItems());
|
||||
@@ -169,12 +170,12 @@ describe('useWatchedItems Hook', () => {
|
||||
});
|
||||
|
||||
it('should sort items alphabetically by name when adding a new item', async () => {
|
||||
const unsortedItems: MasterGroceryItem[] = [
|
||||
{ master_grocery_item_id: 2, name: 'Zucchini', created_at: '' },
|
||||
{ master_grocery_item_id: 1, name: 'Apple', created_at: '' },
|
||||
const unsortedItems = [
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 2, name: 'Zucchini' }),
|
||||
createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Apple' }),
|
||||
];
|
||||
|
||||
const newItem: MasterGroceryItem = { master_grocery_item_id: 3, name: 'Banana', created_at: '' };
|
||||
const newItem = createMockMasterGroceryItem({ master_grocery_item_id: 3, name: 'Banana' });
|
||||
mockAddWatchedItemApi.mockResolvedValue(newItem);
|
||||
|
||||
const { result } = renderHook(() => useWatchedItems());
|
||||
|
||||
@@ -13,8 +13,12 @@ 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, UserProfile } 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" /> }));
|
||||
@@ -70,7 +74,7 @@ const mockedUseShoppingLists = vi.mocked(useShoppingLists);
|
||||
const mockedUseWatchedItems = vi.mocked(useWatchedItems);
|
||||
const mockedUseActiveDeals = vi.mocked(useActiveDeals);
|
||||
|
||||
const mockUser = { user_id: 'user-123', email: 'test@example.com' };
|
||||
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>;
|
||||
@@ -103,13 +107,7 @@ describe('MainLayout Component', () => {
|
||||
updateProfile: vi.fn(),
|
||||
} as const;
|
||||
const defaultUseFlyersReturn = {
|
||||
flyers: [{
|
||||
flyer_id: 1,
|
||||
file_name: 'flyer.jpg',
|
||||
created_at: new Date().toISOString(),
|
||||
image_url: 'http://example.com/flyer.jpg',
|
||||
item_count: 10,
|
||||
}],
|
||||
flyers: [createMockFlyer({ flyer_id: 1, file_name: 'flyer.jpg', item_count: 10 })],
|
||||
isLoadingFlyers: false,
|
||||
flyersError: null,
|
||||
refetchFlyers: vi.fn(),
|
||||
@@ -202,7 +200,7 @@ describe('MainLayout Component', () => {
|
||||
...defaultUseAuthReturn,
|
||||
user: mockUser,
|
||||
authStatus: 'AUTHENTICATED',
|
||||
profile: { user: mockUser } as UserProfile,
|
||||
profile: createMockUserProfile({ user: mockUser }),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,7 +246,7 @@ describe('MainLayout Component', () => {
|
||||
it('calls setActiveListId when a list is shared via ActivityLog and the list exists', () => {
|
||||
mockedUseShoppingLists.mockReturnValueOnce({
|
||||
...defaultUseShoppingListsReturn,
|
||||
shoppingLists: [{ shopping_list_id: 1, name: 'My List', user_id: 'user-123', created_at: '', items: [] }],
|
||||
shoppingLists: [createMockShoppingList({ shopping_list_id: 1, name: 'My List', user_id: 'user-123' })],
|
||||
});
|
||||
|
||||
renderWithRouter(<MainLayout {...defaultProps} />);
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ActivityLog, ActivityLogClickHandler } from '../pages/admin/ActivityLog
|
||||
import { AnonymousUserBanner } from '../pages/admin/components/AnonymousUserBanner';
|
||||
import { ErrorDisplay } from '../components/ErrorDisplay';
|
||||
|
||||
interface MainLayoutProps {
|
||||
export interface MainLayoutProps {
|
||||
onFlyerSelect: (flyer: import('../types').Flyer) => void;
|
||||
selectedFlyerId: number | null;
|
||||
onOpenProfile: () => void;
|
||||
|
||||
@@ -9,6 +9,9 @@ import type { Flyer, FlyerItem } from '../types';
|
||||
import type { FlyerDisplayProps } from '../features/flyer/FlyerDisplay'; // Keep this for FlyerDisplay mock
|
||||
import type { ExtractedDataTableProps } from '../features/flyer/ExtractedDataTable'; // Import the props for ExtractedDataTable
|
||||
|
||||
// Unmock the component to test the real implementation
|
||||
vi.unmock('./HomePage');
|
||||
|
||||
// Mock child components to isolate the HomePage logic
|
||||
vi.mock('../features/flyer/FlyerDisplay', () => ({
|
||||
FlyerDisplay: (props: FlyerDisplayProps) => <div data-testid="flyer-display" data-image-url={props.imageUrl} />,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ExtractedDataTable } from '../features/flyer/ExtractedDataTable';
|
||||
import { AnalysisPanel } from '../features/flyer/AnalysisPanel';
|
||||
import type { Flyer, FlyerItem } from '../types';
|
||||
|
||||
interface HomePageProps {
|
||||
export interface HomePageProps {
|
||||
selectedFlyer: Flyer | null;
|
||||
flyerItems: FlyerItem[];
|
||||
onOpenCorrectionTool: () => void;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
||||
import UserProfilePage from './UserProfilePage';
|
||||
import * as apiClient from '../services/apiClient';
|
||||
import { UserProfile, Achievement, UserAchievement } from '../types';
|
||||
import { createMockUserProfile, createMockUserAchievement } from '../tests/utils/mockFactories';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('../services/apiClient'); // This was correct
|
||||
@@ -28,17 +29,17 @@ vi.mock('../components/AchievementsList', () => ({
|
||||
const mockedApiClient = apiClient as Mocked<typeof apiClient>;
|
||||
|
||||
// --- Mock Data ---
|
||||
const mockProfile: UserProfile = {
|
||||
const mockProfile: UserProfile = createMockUserProfile({
|
||||
user_id: 'user-123',
|
||||
user: { user_id: 'user-123', email: 'test@example.com' },
|
||||
full_name: 'Test User',
|
||||
avatar_url: 'http://example.com/avatar.jpg',
|
||||
points: 150,
|
||||
role: 'user',
|
||||
};
|
||||
});
|
||||
|
||||
const mockAchievements: (UserAchievement & Achievement)[] = [
|
||||
{
|
||||
createMockUserAchievement({
|
||||
achievement_id: 1,
|
||||
user_id: 'user-123',
|
||||
achieved_at: '2024-01-01T00:00:00Z',
|
||||
@@ -46,7 +47,7 @@ const mockAchievements: (UserAchievement & Achievement)[] = [
|
||||
description: 'Uploaded first flyer.',
|
||||
icon: 'upload',
|
||||
points_value: 10,
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
describe('UserProfilePage', () => {
|
||||
|
||||
@@ -5,18 +5,16 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import toast from 'react-hot-toast';
|
||||
import { AdminBrandManager } from './AdminBrandManager';
|
||||
import * as apiClient from '../../../services/apiClient';
|
||||
import type { Brand } from '../../../types';
|
||||
import { createMockBrand } from '../../../tests/utils/mockFactories';
|
||||
|
||||
// After mocking, we can get a type-safe mocked version of the module.
|
||||
// This allows us to use .mockResolvedValue, .mockRejectedValue, etc. on the functions.
|
||||
// The apiClient is now mocked globally via src/tests/setup/tests-setup-unit.ts.
|
||||
const mockedApiClient = vi.mocked(apiClient);
|
||||
const mockedToast = vi.mocked(toast, true);
|
||||
|
||||
|
||||
const mockBrands: Brand[] = [
|
||||
{ brand_id: 1, name: 'No Frills', store_name: 'No Frills', logo_url: null },
|
||||
{ brand_id: 2, name: 'Compliments', store_name: 'Sobeys', logo_url: 'http://example.com/compliments.png' },
|
||||
const mockBrands = [
|
||||
createMockBrand({ brand_id: 1, name: 'No Frills', store_name: 'No Frills', logo_url: null }),
|
||||
createMockBrand({ brand_id: 2, name: 'Compliments', store_name: 'Sobeys', logo_url: 'http://example.com/compliments.png' }),
|
||||
];
|
||||
|
||||
describe('AdminBrandManager', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { render, screen, fireEvent, waitFor, within } from '@testing-library/rea
|
||||
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
|
||||
import { CorrectionRow } from './CorrectionRow';
|
||||
import * as apiClient from '../../../services/apiClient';
|
||||
import type { SuggestedCorrection, MasterGroceryItem, Category } from '../../../types';
|
||||
import { createMockSuggestedCorrection, createMockMasterGroceryItem, createMockCategory } from '../../../tests/utils/mockFactories';
|
||||
|
||||
// Cast the mocked module to its mocked type to retain type safety and autocompletion.
|
||||
// The apiClient is now mocked globally via src/tests/setup/tests-setup-unit.ts.
|
||||
@@ -35,26 +35,20 @@ vi.mock('../../../components/ConfirmationModal', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const mockCorrection: SuggestedCorrection = {
|
||||
const mockCorrection = createMockSuggestedCorrection({
|
||||
suggested_correction_id: 1,
|
||||
flyer_item_id: 101,
|
||||
user_id: 'user-1',
|
||||
correction_type: 'WRONG_PRICE',
|
||||
suggested_value: '250', // $2.50
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString(),
|
||||
flyer_item_name: 'Bananas',
|
||||
flyer_item_price_display: '$1.99',
|
||||
user_email: 'test@example.com',
|
||||
};
|
||||
});
|
||||
|
||||
const mockMasterItems: MasterGroceryItem[] = [
|
||||
{ master_grocery_item_id: 1, name: 'Bananas', created_at: '', category_id: 1, category_name: 'Produce' },
|
||||
];
|
||||
|
||||
const mockCategories: Category[] = [
|
||||
{ category_id: 1, name: 'Produce' },
|
||||
];
|
||||
const mockMasterItems = [createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Bananas', category_id: 1, category_name: 'Produce' })];
|
||||
const mockCategories = [createMockCategory({ category_id: 1, name: 'Produce' })];
|
||||
|
||||
const mockOnProcessed = vi.fn();
|
||||
|
||||
@@ -305,10 +299,7 @@ describe('CorrectionRow', () => {
|
||||
|
||||
it('should save an edited value for INCORRECT_ITEM_LINK', async () => {
|
||||
// Add a temporary item to the master list for this test
|
||||
const localMasterItems = [
|
||||
...mockMasterItems,
|
||||
{ master_grocery_item_id: 2, name: 'Milk', created_at: '', category_id: 2, category_name: 'Dairy' },
|
||||
];
|
||||
const localMasterItems = [...mockMasterItems, createMockMasterGroceryItem({ master_grocery_item_id: 2, name: 'Milk', category_id: 2, category_name: 'Dairy' })];
|
||||
mockedApiClient.updateSuggestedCorrection.mockResolvedValue(new Response(JSON.stringify({ ...mockCorrection, correction_type: 'INCORRECT_ITEM_LINK', suggested_value: '2' })));
|
||||
|
||||
renderInTable({
|
||||
@@ -330,10 +321,7 @@ describe('CorrectionRow', () => {
|
||||
});
|
||||
|
||||
it('should save an edited value for ITEM_IS_MISCATEGORIZED', async () => {
|
||||
const localCategories = [
|
||||
...mockCategories,
|
||||
{ category_id: 2, name: 'Dairy' },
|
||||
];
|
||||
const localCategories = [...mockCategories, createMockCategory({ category_id: 2, name: 'Dairy' })];
|
||||
mockedApiClient.updateSuggestedCorrection.mockResolvedValue(new Response(JSON.stringify({ ...mockCorrection, correction_type: 'ITEM_IS_MISCATEGORIZED', suggested_value: '2' })));
|
||||
|
||||
renderInTable({
|
||||
|
||||
@@ -7,6 +7,11 @@ import * as apiClient from '../../../services/apiClient';
|
||||
import { notifySuccess, notifyError } from '../../../services/notificationService';
|
||||
import toast from 'react-hot-toast';
|
||||
import * as logger from '../../../services/logger.client';
|
||||
import { createMockProfile, createMockAddress, createMockUser } from '../../../tests/utils/mockFactories';
|
||||
import { createMockLogger } from '../../../tests/utils/mockLogger';
|
||||
|
||||
// Unmock the component to test the real implementation
|
||||
vi.unmock('./ProfileManager');
|
||||
|
||||
const mockedApiClient = vi.mocked(apiClient, true);
|
||||
|
||||
@@ -19,12 +24,7 @@ vi.mock('react-hot-toast', () => ({
|
||||
},
|
||||
}));
|
||||
vi.mock('../../../services/logger.client', () => ({
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
logger: createMockLogger(),
|
||||
}));
|
||||
|
||||
const mockOnClose = vi.fn();
|
||||
@@ -33,24 +33,23 @@ const mockOnSignOut = vi.fn();
|
||||
const mockOnProfileUpdate = vi.fn();
|
||||
|
||||
// --- MOCK DATA ---
|
||||
const authenticatedUser = { user_id: 'auth-user-123', email: 'test@example.com' };
|
||||
const authenticatedUser = createMockUser({ user_id: 'auth-user-123', email: 'test@example.com' });
|
||||
const mockAddressId = 123;
|
||||
const authenticatedProfile = {
|
||||
const authenticatedProfile = createMockProfile({
|
||||
user_id: 'auth-user-123',
|
||||
full_name: 'Test User',
|
||||
avatar_url: 'http://example.com/avatar.png',
|
||||
role: 'user' as const,
|
||||
role: 'user',
|
||||
points: 100,
|
||||
preferences: {
|
||||
darkMode: false,
|
||||
unitSystem: 'imperial' as const,
|
||||
unitSystem: 'imperial',
|
||||
},
|
||||
address_id: mockAddressId,
|
||||
};
|
||||
});
|
||||
|
||||
const mockAddress = {
|
||||
const mockAddress = createMockAddress({
|
||||
address_id: mockAddressId,
|
||||
user_id: 'auth-user-123',
|
||||
address_line_1: '123 Main St',
|
||||
city: 'Anytown',
|
||||
province_state: 'ON',
|
||||
@@ -58,9 +57,7 @@ const mockAddress = {
|
||||
country: 'Canada',
|
||||
latitude: 43.0,
|
||||
longitude: -79.0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
const defaultSignedOutProps = {
|
||||
isOpen: true,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { AuthView } from './AuthView';
|
||||
import { AddressForm } from './AddressForm';
|
||||
import { useProfileAddress } from '../../../hooks/useProfileAddress';
|
||||
|
||||
interface ProfileManagerProps {
|
||||
export interface ProfileManagerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
user: User | null; // Can be null for login/register
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import supertest from 'supertest';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import adminRouter from './admin.routes'; // This was a duplicate, fixed.
|
||||
import { createMockUserProfile, createMockAdminUserView } from '../tests/utils/mockFactories';
|
||||
import adminRouter from './admin.routes';
|
||||
import { createMockUserProfile, createMockAdminUserView, createMockProfile } from '../tests/utils/mockFactories';
|
||||
import { UserProfile, Profile } from '../types';
|
||||
import { NotFoundError } from '../services/db/errors.db';
|
||||
import { createTestApp } from '../tests/utils/createTestApp';
|
||||
@@ -118,12 +118,10 @@ describe('Admin User Management Routes (/api/admin/users)', () => {
|
||||
|
||||
describe('PUT /users/:id', () => {
|
||||
it('should update a user role successfully', async () => {
|
||||
// The `updateUserRole` function returns a `Profile` object, not a `User` object.
|
||||
const updatedUser: Profile = {
|
||||
const updatedUser = createMockProfile({
|
||||
user_id: userId,
|
||||
role: 'admin',
|
||||
points: 0,
|
||||
};
|
||||
});
|
||||
vi.mocked(adminRepo.updateUserRole).mockResolvedValue(updatedUser);
|
||||
const response = await supertest(app)
|
||||
.put(`/api/admin/users/${userId}`)
|
||||
|
||||
@@ -44,7 +44,7 @@ vi.mock('passport-local', () => ({
|
||||
|
||||
import * as db from '../services/db/index.db';
|
||||
import { UserProfile } from '../types';
|
||||
import { createMockUserProfile } from '../tests/utils/mockFactories';
|
||||
import { createMockUserProfile, createMockUserWithPasswordHash } from '../tests/utils/mockFactories';
|
||||
import { mockLogger } from '../tests/utils/mockLogger';
|
||||
|
||||
// Mock dependencies before importing the passport configuration
|
||||
@@ -105,15 +105,14 @@ describe('Passport Configuration', () => {
|
||||
|
||||
it('should call done(null, user) on successful authentication', async () => {
|
||||
// Arrange
|
||||
const mockUser = {
|
||||
user_id: 'user-123',
|
||||
email: 'test@test.com',
|
||||
password_hash: 'hashed_password',
|
||||
failed_login_attempts: 0,
|
||||
last_failed_login: null,
|
||||
points: 0,
|
||||
role: 'user' as const
|
||||
};
|
||||
const mockUser = {
|
||||
...createMockUserWithPasswordHash({
|
||||
user_id: 'user-123',
|
||||
email: 'test@test.com',
|
||||
}),
|
||||
points: 0,
|
||||
role: 'user' as const,
|
||||
};
|
||||
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
|
||||
vi.mocked(bcrypt.compare).mockResolvedValue(true as never);
|
||||
|
||||
@@ -140,15 +139,15 @@ describe('Passport Configuration', () => {
|
||||
});
|
||||
|
||||
it('should call done(null, false) and increment failed attempts on password mismatch', async () => {
|
||||
const mockUser = {
|
||||
user_id: 'user-123',
|
||||
email: 'test@test.com',
|
||||
password_hash: 'hashed_password',
|
||||
failed_login_attempts: 1,
|
||||
last_failed_login: null,
|
||||
points: 0,
|
||||
role: 'user' as const
|
||||
};
|
||||
const mockUser = {
|
||||
...createMockUserWithPasswordHash({
|
||||
user_id: 'user-123',
|
||||
email: 'test@test.com',
|
||||
failed_login_attempts: 1,
|
||||
}),
|
||||
points: 0,
|
||||
role: 'user' as const,
|
||||
};
|
||||
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
|
||||
vi.mocked(bcrypt.compare).mockResolvedValue(false as never);
|
||||
|
||||
@@ -162,15 +161,15 @@ describe('Passport Configuration', () => {
|
||||
});
|
||||
|
||||
it('should call done(null, false) for an OAuth user (no password hash)', async () => {
|
||||
const mockUser = {
|
||||
user_id: 'oauth-user',
|
||||
email: 'oauth@test.com',
|
||||
password_hash: null,
|
||||
failed_login_attempts: 0,
|
||||
last_failed_login: null,
|
||||
points: 0,
|
||||
role: 'user' as const
|
||||
};
|
||||
const mockUser = {
|
||||
...createMockUserWithPasswordHash({
|
||||
user_id: 'oauth-user',
|
||||
email: 'oauth@test.com',
|
||||
password_hash: null,
|
||||
}),
|
||||
points: 0,
|
||||
role: 'user' as const,
|
||||
};
|
||||
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
|
||||
|
||||
if (localStrategyCallbackWrapper.callback) {
|
||||
@@ -182,13 +181,14 @@ describe('Passport Configuration', () => {
|
||||
|
||||
it('should call done(null, false) if account is locked', async () => {
|
||||
const mockUser = {
|
||||
user_id: 'locked-user',
|
||||
email: 'locked@test.com',
|
||||
password_hash: 'hashed_password',
|
||||
failed_login_attempts: 5,
|
||||
last_failed_login: new Date().toISOString(),
|
||||
points: 0,
|
||||
role: 'user' as const
|
||||
...createMockUserWithPasswordHash({
|
||||
user_id: 'locked-user',
|
||||
email: 'locked@test.com',
|
||||
failed_login_attempts: 5,
|
||||
last_failed_login: new Date().toISOString(),
|
||||
}),
|
||||
points: 0,
|
||||
role: 'user' as const,
|
||||
};
|
||||
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
|
||||
|
||||
@@ -201,13 +201,14 @@ describe('Passport Configuration', () => {
|
||||
|
||||
it('should allow login if lockout period has expired', async () => {
|
||||
const mockUser = {
|
||||
user_id: 'expired-lock-user',
|
||||
email: 'expired@test.com',
|
||||
password_hash: 'hashed_password',
|
||||
failed_login_attempts: 5,
|
||||
last_failed_login: new Date(Date.now() - 20 * 60 * 1000).toISOString(),
|
||||
points: 0,
|
||||
role: 'user' as const
|
||||
...createMockUserWithPasswordHash({
|
||||
user_id: 'expired-lock-user',
|
||||
email: 'expired@test.com',
|
||||
failed_login_attempts: 5,
|
||||
last_failed_login: new Date(Date.now() - 20 * 60 * 1000).toISOString(),
|
||||
}),
|
||||
points: 0,
|
||||
role: 'user' as const,
|
||||
};
|
||||
vi.mocked(mockedDb.userRepo.findUserWithProfileByEmail).mockResolvedValue(mockUser);
|
||||
vi.mocked(bcrypt.compare).mockResolvedValue(true as never); // Correct password
|
||||
@@ -303,13 +304,7 @@ describe('Passport Configuration', () => {
|
||||
it('should call next() if user has "admin" role', () => {
|
||||
// Arrange
|
||||
const mockReq: Partial<Request> = {
|
||||
// Create a complete, type-safe mock UserProfile object.
|
||||
user: {
|
||||
user_id: 'admin-id',
|
||||
role: 'admin',
|
||||
points: 100,
|
||||
user: { user_id: 'admin-id', email: 'admin@test.com' }
|
||||
},
|
||||
user: createMockUserProfile({ user_id: 'admin-id', role: 'admin', user: { user_id: 'admin-id', email: 'admin@test.com' } }),
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -323,12 +318,7 @@ describe('Passport Configuration', () => {
|
||||
it('should return 403 Forbidden if user does not have "admin" role', () => {
|
||||
// Arrange
|
||||
const mockReq: Partial<Request> = {
|
||||
user: {
|
||||
user_id: 'user-id',
|
||||
role: 'user',
|
||||
points: 50,
|
||||
user: { user_id: 'user-id', email: 'user@test.com' }
|
||||
},
|
||||
user: createMockUserProfile({ user_id: 'user-id', role: 'user', user: { user_id: 'user-id', email: 'user@test.com' } }),
|
||||
};
|
||||
|
||||
// Act
|
||||
|
||||
@@ -4,7 +4,7 @@ import supertest from 'supertest';
|
||||
import express from 'express';
|
||||
// Use * as bcrypt to match the implementation's import style and ensure mocks align.
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import userRouter from './user.routes';
|
||||
import userRouter from './user.routes'; // This was a duplicate, fixed.
|
||||
import { createMockUserProfile, createMockMasterGroceryItem, createMockShoppingList, createMockShoppingListItem, createMockRecipe, createMockNotification, createMockDietaryRestriction, createMockAppliance, createMockUserWithPasswordHash, createMockAddress } from '../tests/utils/mockFactories';
|
||||
import { Appliance, Notification, DietaryRestriction, Address } from '../types';
|
||||
import { ForeignKeyConstraintError, NotFoundError } from '../services/db/errors.db';
|
||||
@@ -460,7 +460,7 @@ describe('User Routes (/api/users)', () => {
|
||||
describe('PUT /profile', () => {
|
||||
it('should update the user profile successfully', async () => {
|
||||
const profileUpdates = { full_name: 'New Name' };
|
||||
const updatedProfile = { ...mockUserProfile, ...profileUpdates };
|
||||
const updatedProfile = createMockUserProfile({ ...mockUserProfile, ...profileUpdates });
|
||||
vi.mocked(db.userRepo.updateUserProfile).mockResolvedValue(updatedProfile);
|
||||
const response = await supertest(app)
|
||||
.put('/api/users/profile')
|
||||
@@ -588,10 +588,10 @@ describe('User Routes (/api/users)', () => {
|
||||
describe('PUT /profile/preferences', () => {
|
||||
it('should update user preferences successfully', async () => {
|
||||
const preferencesUpdate = { darkMode: true, unitSystem: 'metric' as const };
|
||||
const updatedProfile = {
|
||||
...mockUserProfile,
|
||||
preferences: { ...mockUserProfile.preferences, ...preferencesUpdate }
|
||||
};
|
||||
const updatedProfile = createMockUserProfile({
|
||||
...mockUserProfile,
|
||||
preferences: { ...mockUserProfile.preferences, ...preferencesUpdate },
|
||||
});
|
||||
vi.mocked(db.userRepo.updateUserPreferences).mockResolvedValue(updatedProfile);
|
||||
const response = await supertest(app)
|
||||
.put('/api/users/profile/preferences')
|
||||
@@ -928,7 +928,7 @@ describe('User Routes (/api/users)', () => {
|
||||
|
||||
it('PUT /recipes/:recipeId should update a user\'s own recipe', async () => {
|
||||
const updates = { description: 'A new delicious description.' };
|
||||
const mockUpdatedRecipe = { ...createMockRecipe({ recipe_id: 1 }), ...updates };
|
||||
const mockUpdatedRecipe = createMockRecipe({ recipe_id: 1, ...updates });
|
||||
vi.mocked(db.recipeRepo.updateRecipe).mockResolvedValue(mockUpdatedRecipe);
|
||||
|
||||
const response = await supertest(app)
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Logger } from 'pino';
|
||||
import type { MasterGroceryItem } from '../types';
|
||||
// Import the class, not the singleton instance, so we can instantiate it with mocks.
|
||||
import { AIService } from './aiService.server';
|
||||
import { createMockMasterGroceryItem } from '../tests/utils/mockFactories';
|
||||
|
||||
// Mock the logger to prevent the real pino instance from being created, which causes issues with 'pino-pretty' in tests.
|
||||
vi.mock('./logger.server', () => ({
|
||||
@@ -146,7 +147,7 @@ describe('AI Service (Server)', () => {
|
||||
});
|
||||
|
||||
describe('extractCoreDataFromFlyerImage', () => {
|
||||
const mockMasterItems: MasterGroceryItem[] = [{ master_grocery_item_id: 1, name: 'Apples', created_at: '' }];
|
||||
const mockMasterItems: MasterGroceryItem[] = [createMockMasterGroceryItem({ master_grocery_item_id: 1, name: 'Apples' })];
|
||||
|
||||
it('should extract and post-process flyer data correctly', async () => {
|
||||
const mockAiResponse = {
|
||||
|
||||
25
src/tests/setup/mockHooks.ts
Normal file
25
src/tests/setup/mockHooks.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { vi } from 'vitest';
|
||||
|
||||
// Create mock functions for each custom hook
|
||||
export const mockUseAuth = vi.fn();
|
||||
export const mockUseFlyers = vi.fn();
|
||||
export const mockUseMasterItems = vi.fn();
|
||||
export const mockUseUserData = vi.fn();
|
||||
export const mockUseShoppingLists = vi.fn();
|
||||
export const mockUseWatchedItems = vi.fn();
|
||||
export const mockUseActiveDeals = vi.fn();
|
||||
export const mockUseFlyerItems = vi.fn();
|
||||
export const mockUseAiAnalysis = vi.fn();
|
||||
|
||||
// Mock the modules. The factory function returns the mock function.
|
||||
// By placing these in a central file, any test that imports one of these
|
||||
// mock functions will automatically have the corresponding hook mocked.
|
||||
vi.mock('../../hooks/useAuth', () => ({ useAuth: mockUseAuth }));
|
||||
vi.mock('../../hooks/useFlyers', () => ({ useFlyers: mockUseFlyers }));
|
||||
vi.mock('../../hooks/useMasterItems', () => ({ useMasterItems: mockUseMasterItems }));
|
||||
vi.mock('../../hooks/useUserData', () => ({ useUserData: mockUseUserData }));
|
||||
vi.mock('../../hooks/useShoppingLists', () => ({ useShoppingLists: mockUseShoppingLists }));
|
||||
vi.mock('../../hooks/useWatchedItems', () => ({ useWatchedItems: mockUseWatchedItems }));
|
||||
vi.mock('../../hooks/useActiveDeals', () => ({ useActiveDeals: mockUseActiveDeals }));
|
||||
vi.mock('../../hooks/useFlyerItems', () => ({ useFlyerItems: mockUseFlyerItems }));
|
||||
vi.mock('../../hooks/useAiAnalysis', () => ({ useAiAnalysis: mockUseAiAnalysis }));
|
||||
7
src/tests/setup/tests-setup-integration.ts
Normal file
7
src/tests/setup/tests-setup-integration.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// src/tests/setup/tests-setup-integration.ts
|
||||
import { beforeEach } from 'vitest';
|
||||
import { resetMockIds } from '../utils/mockFactories';
|
||||
|
||||
beforeEach(() => {
|
||||
resetMockIds();
|
||||
});
|
||||
@@ -1,8 +1,14 @@
|
||||
// src/tests/setup/tests-setup-unit.ts
|
||||
import { vi, afterEach } from 'vitest';
|
||||
import { vi, afterEach, beforeEach } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
import '@testing-library/jest-dom/vitest';
|
||||
import { resetMockIds } from '../utils/mockFactories';
|
||||
|
||||
// Reset mock ID counter before each test to ensure test isolation.
|
||||
beforeEach(() => {
|
||||
resetMockIds();
|
||||
});
|
||||
|
||||
// Mock the GeolocationPositionError global that exists in browsers but not in JSDOM.
|
||||
// This is necessary for tests that simulate geolocation failures.
|
||||
|
||||
55
src/tests/utils/componentMocks.tsx
Normal file
55
src/tests/utils/componentMocks.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { createMockProfile, createMockUser } from './mockFactories';
|
||||
import type { HeaderProps } from '../../components/Header';
|
||||
import type { ProfileManagerProps } from '../../pages/admin/components/ProfileManager';
|
||||
import type { VoiceAssistantProps } from '../../features/voice-assistant/VoiceAssistant';
|
||||
import type { FlyerCorrectionToolProps } from '../../components/FlyerCorrectionTool';
|
||||
import type { WhatsNewModalProps } from '../../components/WhatsNewModal';
|
||||
import type { AdminRouteProps } from '../../components/AdminRoute';
|
||||
import type { MainLayoutProps } from '../../layouts/MainLayout';
|
||||
import type { HomePageProps } from '../../pages/HomePage';
|
||||
|
||||
export const MockHeader: React.FC<HeaderProps> = (props) => (
|
||||
<header data-testid="header-mock">
|
||||
<button onClick={props.onOpenProfile}>Open Profile</button>
|
||||
<button onClick={props.onOpenVoiceAssistant}>Open Voice Assistant</button>
|
||||
</header>
|
||||
);
|
||||
|
||||
export const MockProfileManager: React.FC<Partial<ProfileManagerProps>> = ({ isOpen, onClose, onProfileUpdate, onLoginSuccess }) => {
|
||||
if (!isOpen) return null;
|
||||
return (
|
||||
<div data-testid="profile-manager-mock">
|
||||
<button onClick={onClose}>Close Profile</button>
|
||||
<button onClick={() => onProfileUpdate?.(createMockProfile({ user_id: '1', role: 'user', points: 0, full_name: 'Updated' }))}>Update Profile</button>
|
||||
<button onClick={() => onLoginSuccess?.(createMockUser({ user_id: '1', email: 'a@b.com' }), 'token', false)}>Login</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MockVoiceAssistant: React.FC<VoiceAssistantProps> = ({ isOpen, onClose }) => (
|
||||
isOpen ? <div data-testid="voice-assistant-mock"><button onClick={onClose}>Close Voice Assistant</button></div> : null
|
||||
);
|
||||
|
||||
export const MockFlyerCorrectionTool: React.FC<Partial<FlyerCorrectionToolProps>> = ({ isOpen, onClose, onDataExtracted }) => (
|
||||
isOpen ? <div data-testid="flyer-correction-tool-mock"><button onClick={onClose}>Close Correction</button><button onClick={() => onDataExtracted?.('store_name', 'New Store Name')}>Extract Store</button><button onClick={() => onDataExtracted?.('dates', '2023-01-01')}>Extract Dates</button></div> : null
|
||||
);
|
||||
|
||||
export const MockWhatsNewModal: React.FC<Partial<WhatsNewModalProps>> = ({ isOpen, onClose }) => (
|
||||
isOpen ? <div data-testid="whats-new-modal-mock"><button onClick={onClose}>Close What's New</button></div> : null
|
||||
);
|
||||
|
||||
// Add mock implementations for the remaining simple page/layout components
|
||||
export const MockAdminPage: React.FC = () => <div data-testid="admin-page-mock">Admin Page</div>;
|
||||
export const MockAdminRoute: React.FC<Partial<AdminRouteProps>> = () => <div data-testid="admin-route-mock"><Outlet /></div>;
|
||||
export const MockCorrectionsPage: React.FC = () => <div data-testid="corrections-page-mock">Corrections Page</div>;
|
||||
export const MockAdminStatsPage: React.FC = () => <div data-testid="admin-stats-page-mock">Admin Stats Page</div>;
|
||||
export const MockResetPasswordPage: React.FC = () => <div data-testid="reset-password-page-mock">Reset Password Page</div>;
|
||||
export const MockVoiceLabPage: React.FC = () => <div data-testid="voice-lab-page-mock">Voice Lab Page</div>;
|
||||
export const MockMainLayout: React.FC<Partial<MainLayoutProps>> = () => <div data-testid="main-layout-mock"><Outlet /></div>;
|
||||
export const MockHomePage: React.FC<Partial<HomePageProps>> = ({ onOpenCorrectionTool }) => (
|
||||
<div data-testid="home-page-mock">
|
||||
<button onClick={onOpenCorrectionTool}>Open Correction Tool</button>
|
||||
</div>
|
||||
);
|
||||
@@ -1,5 +1,43 @@
|
||||
// src/tests/utils/mockFactories.ts
|
||||
import { UserProfile, User, Flyer, Store, SuggestedCorrection, Brand, FlyerItem, MasterGroceryItem, ShoppingList, ShoppingListItem, Achievement, UserAchievement, Budget, SpendingByCategory, Recipe, RecipeComment, ActivityLogItem, DietaryRestriction, Appliance, Notification, UnmatchedFlyerItem, AdminUserView, WatchedItemDeal, LeaderboardUser, UserWithPasswordHash, Profile, Address } from '../../types';
|
||||
import { UserProfile, User, Flyer, Store, SuggestedCorrection, Brand, Category, FlyerItem, MasterGroceryItem, ShoppingList, ShoppingListItem, Achievement, UserAchievement, Budget, SpendingByCategory, Recipe, RecipeIngredient, RecipeComment, ActivityLogItem, DietaryRestriction, UserDietaryRestriction, Appliance, UserAppliance, Notification, UnmatchedFlyerItem, AdminUserView, WatchedItemDeal, LeaderboardUser, UserWithPasswordHash, Profile, Address, MenuPlan, PlannedMeal, PantryItem, Product, ShoppingTrip, ShoppingTripItem, Receipt, ReceiptItem } from '../../types';
|
||||
|
||||
// --- ID Generator for Deterministic Mocking ---
|
||||
let idCounter = 0;
|
||||
|
||||
/**
|
||||
* Generates a sequential number for deterministic IDs in tests.
|
||||
*/
|
||||
const getNextId = () => ++idCounter;
|
||||
|
||||
/**
|
||||
* Resets the ID counter. Call this in `beforeEach` in your test setup
|
||||
* to ensure tests are isolated.
|
||||
*
|
||||
* @example
|
||||
* beforeEach(() => {
|
||||
* resetMockIds();
|
||||
* });
|
||||
*/
|
||||
export const resetMockIds = () => {
|
||||
idCounter = 0;
|
||||
};
|
||||
// --- End ID Generator ---
|
||||
|
||||
/**
|
||||
* Creates a mock User object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe User object.
|
||||
*/
|
||||
export const createMockUser = (overrides: Partial<User> = {}): User => {
|
||||
const userId = overrides.user_id ?? `user-${getNextId()}`;
|
||||
|
||||
const defaultUser: User = {
|
||||
user_id: userId,
|
||||
email: `${userId}@example.com`,
|
||||
};
|
||||
|
||||
return { ...defaultUser, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock UserProfile object for use in tests, ensuring type safety.
|
||||
@@ -10,24 +48,53 @@ import { UserProfile, User, Flyer, Store, SuggestedCorrection, Brand, FlyerItem,
|
||||
* @returns A complete and type-safe UserProfile object.
|
||||
*/
|
||||
export const createMockUserProfile = (overrides: Partial<UserProfile & { user: Partial<User> }> = {}): UserProfile => {
|
||||
const userId = overrides.user_id ?? `user-${Math.random().toString(36).substring(2, 9)}`;
|
||||
// Ensure the user_id is consistent between the profile and the nested user object
|
||||
const userOverrides: Partial<User> = overrides.user || {};
|
||||
if (overrides.user_id && !userOverrides.user_id) {
|
||||
userOverrides.user_id = overrides.user_id;
|
||||
}
|
||||
|
||||
const user = createMockUser(userOverrides);
|
||||
|
||||
const defaultProfile: UserProfile = {
|
||||
user_id: userId,
|
||||
user_id: user.user_id,
|
||||
role: 'user',
|
||||
points: 0,
|
||||
full_name: 'Test User',
|
||||
avatar_url: null,
|
||||
preferences: {},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
created_by: null,
|
||||
address: null,
|
||||
user: {
|
||||
user_id: userId,
|
||||
email: `${userId}@example.com`,
|
||||
...overrides.user, // Apply nested user overrides
|
||||
},
|
||||
user,
|
||||
};
|
||||
delete (defaultProfile as Partial<UserProfile>).address_id;
|
||||
|
||||
// Exclude 'user' from overrides to prevent overwriting the complete user object with a partial one
|
||||
const { user: _, ...profileOverrides } = overrides;
|
||||
|
||||
return { ...defaultProfile, ...profileOverrides, user_id: user.user_id };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Store object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe Store object.
|
||||
*/
|
||||
export const createMockStore = (overrides: Partial<Store> = {}): Store => {
|
||||
const storeId = overrides.store_id ?? getNextId();
|
||||
|
||||
const defaultStore: Store = {
|
||||
store_id: storeId,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
name: 'Mock Store',
|
||||
logo_url: null,
|
||||
created_by: null,
|
||||
};
|
||||
|
||||
return { ...defaultProfile, ...overrides };
|
||||
return { ...defaultStore, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -38,33 +105,52 @@ export const createMockUserProfile = (overrides: Partial<UserProfile & { user: P
|
||||
* e.g., `createMockFlyer({ item_count: 50, store: { name: 'Walmart' } })`
|
||||
* @returns A complete and type-safe Flyer object.
|
||||
*/
|
||||
export const createMockFlyer = (overrides: Partial<Flyer & { store: Partial<Store> }> = {}): Flyer => {
|
||||
const flyerId = overrides.flyer_id ?? Math.floor(Math.random() * 1000);
|
||||
const storeId = overrides.store?.store_id ?? Math.floor(Math.random() * 100);
|
||||
export const createMockFlyer = (overrides: Omit<Partial<Flyer>, 'store'> & { store?: Partial<Store> } = {}): Flyer => {
|
||||
const flyerId = overrides.flyer_id ?? getNextId();
|
||||
|
||||
// Ensure the store_id is consistent between the flyer and the nested store object
|
||||
const storeOverrides = overrides.store || {};
|
||||
if (overrides.store_id && !storeOverrides.store_id) {
|
||||
storeOverrides.store_id = overrides.store_id;
|
||||
}
|
||||
|
||||
const store = createMockStore(storeOverrides);
|
||||
|
||||
// Determine the final file_name to generate dependent properties from.
|
||||
const fileName = overrides.file_name ?? `flyer-${flyerId}.jpg`;
|
||||
|
||||
// A simple hash function for mock checksum generation.
|
||||
const generateMockChecksum = (str: string) => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = (hash << 5) - hash + char;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return `mock-checksum-${Math.abs(hash).toString(16)}`;
|
||||
};
|
||||
|
||||
const defaultFlyer: Flyer = {
|
||||
flyer_id: flyerId,
|
||||
created_at: new Date().toISOString(),
|
||||
file_name: `flyer-${flyerId}.jpg`,
|
||||
image_url: `/flyer-images/flyer-${flyerId}.jpg`,
|
||||
icon_url: `/flyer-images/icons/icon-flyer-${flyerId}.webp`,
|
||||
checksum: `checksum-${flyerId}`,
|
||||
store_id: storeId,
|
||||
updated_at: new Date().toISOString(),
|
||||
file_name: fileName,
|
||||
image_url: `/flyer-images/${fileName}`,
|
||||
icon_url: `/flyer-images/icons/icon-${fileName.replace(/\.[^/.]+$/, ".webp")}`,
|
||||
checksum: generateMockChecksum(fileName),
|
||||
store_id: store.store_id,
|
||||
valid_from: new Date().toISOString().split('T')[0],
|
||||
valid_to: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 7 days from now
|
||||
store_address: '123 Main St, Anytown, USA',
|
||||
item_count: Math.floor(Math.random() * 100) + 10,
|
||||
item_count: 50,
|
||||
uploaded_by: null,
|
||||
store: {
|
||||
store_id: storeId,
|
||||
created_at: new Date().toISOString(),
|
||||
name: 'Mock Store',
|
||||
logo_url: null,
|
||||
},
|
||||
store,
|
||||
};
|
||||
|
||||
// Deep merge the store object and then merge the top-level properties.
|
||||
return { ...defaultFlyer, ...overrides, store: { ...defaultFlyer.store, ...overrides.store } as Store };
|
||||
const { store: _, ...flyerOverrides } = overrides;
|
||||
|
||||
// Apply overrides. If checksum, file_name, etc., are in overrides, they will correctly replace the generated defaults.
|
||||
return { ...defaultFlyer, ...flyerOverrides };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -74,13 +160,14 @@ export const createMockFlyer = (overrides: Partial<Flyer & { store: Partial<Stor
|
||||
*/
|
||||
export const createMockSuggestedCorrection = (overrides: Partial<SuggestedCorrection> = {}): SuggestedCorrection => {
|
||||
const defaultCorrection: SuggestedCorrection = {
|
||||
suggested_correction_id: Math.floor(Math.random() * 1000),
|
||||
flyer_item_id: Math.floor(Math.random() * 10000),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
suggested_correction_id: getNextId(),
|
||||
flyer_item_id: getNextId(),
|
||||
user_id: `user-${getNextId()}`,
|
||||
correction_type: 'price',
|
||||
suggested_value: '$9.99',
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return { ...defaultCorrection, ...overrides };
|
||||
@@ -92,10 +179,12 @@ export const createMockSuggestedCorrection = (overrides: Partial<SuggestedCorrec
|
||||
* @returns A complete and type-safe Brand object.
|
||||
*/
|
||||
export const createMockBrand = (overrides: Partial<Brand> = {}): Brand => {
|
||||
const brandId = overrides.brand_id ?? Math.floor(Math.random() * 100);
|
||||
const brandId = overrides.brand_id ?? getNextId();
|
||||
|
||||
const defaultBrand: Brand = {
|
||||
brand_id: brandId,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
name: `Brand ${brandId}`,
|
||||
logo_url: null,
|
||||
store_id: null,
|
||||
@@ -105,15 +194,62 @@ export const createMockBrand = (overrides: Partial<Brand> = {}): Brand => {
|
||||
return { ...defaultBrand, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Category object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe Category object.
|
||||
*/
|
||||
export const createMockCategory = (overrides: Partial<Category> = {}): Category => {
|
||||
const categoryId = overrides.category_id ?? getNextId();
|
||||
|
||||
const defaultCategory: Category = {
|
||||
category_id: categoryId,
|
||||
name: `Category ${categoryId}`,
|
||||
};
|
||||
|
||||
return { ...defaultCategory, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Product object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* Can optionally include `master_item` and `brand` to link IDs.
|
||||
* @returns A complete and type-safe Product object.
|
||||
*/
|
||||
export const createMockProduct = (overrides: Partial<Product> & { master_item?: Partial<MasterGroceryItem>, brand?: Partial<Brand> } = {}): Product => {
|
||||
const productId = overrides.product_id ?? getNextId();
|
||||
const masterItemId = overrides.master_item_id ?? overrides.master_item?.master_grocery_item_id ?? getNextId();
|
||||
const brandId = overrides.brand_id ?? overrides.brand?.brand_id; // brand is optional
|
||||
|
||||
const defaultProduct: Product = {
|
||||
product_id: productId,
|
||||
master_item_id: masterItemId,
|
||||
brand_id: brandId,
|
||||
name: `Mock Product ${productId}`,
|
||||
description: 'A mock product description.',
|
||||
size: '100g',
|
||||
upc_code: `0000${productId}`.slice(-12),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { master_item: _, brand: __, ...itemOverrides } = overrides;
|
||||
|
||||
return { ...defaultProduct, ...itemOverrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock FlyerItem object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe FlyerItem object.
|
||||
*/
|
||||
export const createMockFlyerItem = (overrides: Partial<FlyerItem> = {}): FlyerItem => {
|
||||
export const createMockFlyerItem = (overrides: Partial<FlyerItem> & { flyer?: Partial<Flyer> } = {}): FlyerItem => {
|
||||
const flyerItemId = overrides.flyer_item_id ?? getNextId();
|
||||
const flyerId = overrides.flyer_id ?? overrides.flyer?.flyer_id ?? getNextId();
|
||||
|
||||
const defaultItem: FlyerItem = {
|
||||
flyer_item_id: Math.floor(Math.random() * 10000),
|
||||
flyer_id: Math.floor(Math.random() * 1000),
|
||||
flyer_item_id: flyerItemId,
|
||||
flyer_id: flyerId,
|
||||
created_at: new Date().toISOString(),
|
||||
item: 'Mock Item',
|
||||
price_display: '$1.99',
|
||||
@@ -124,7 +260,9 @@ export const createMockFlyerItem = (overrides: Partial<FlyerItem> = {}): FlyerIt
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return { ...defaultItem, ...overrides };
|
||||
const { flyer: _, ...itemOverrides } = overrides;
|
||||
|
||||
return { ...defaultItem, ...itemOverrides };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -132,26 +270,71 @@ export const createMockFlyerItem = (overrides: Partial<FlyerItem> = {}): FlyerIt
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe Recipe object.
|
||||
*/
|
||||
export const createMockRecipe = (overrides: Partial<Recipe> = {}): Recipe => {
|
||||
const recipeId = overrides.recipe_id ?? Math.floor(Math.random() * 1000);
|
||||
export const createMockRecipe = (overrides: Omit<Partial<Recipe>, 'comments' | 'ingredients'> & { comments?: Partial<RecipeComment>[], ingredients?: Partial<RecipeIngredient>[] } = {}): Recipe => {
|
||||
const recipeId = overrides.recipe_id ?? getNextId();
|
||||
|
||||
const defaultRecipe: Recipe = {
|
||||
recipe_id: recipeId,
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
user_id: `user-${getNextId()}`,
|
||||
name: `Mock Recipe ${recipeId}`,
|
||||
description: 'A delicious mock recipe.',
|
||||
instructions: '1. Mock the ingredients. 2. Mock the cooking. 3. Enjoy!',
|
||||
avg_rating: Math.random() * 5,
|
||||
rating_count: Math.floor(Math.random() * 100),
|
||||
fork_count: Math.floor(Math.random() * 20),
|
||||
avg_rating: 4.5,
|
||||
rating_count: 50,
|
||||
fork_count: 10,
|
||||
status: 'public',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
prep_time_minutes: 15,
|
||||
cook_time_minutes: 30,
|
||||
servings: 4,
|
||||
};
|
||||
|
||||
return { ...defaultRecipe, ...overrides };
|
||||
const { comments: commentsOverrides, ingredients: ingredientsOverrides, ...recipeOverrides } = overrides;
|
||||
const recipe = { ...defaultRecipe, ...recipeOverrides };
|
||||
|
||||
if (commentsOverrides) {
|
||||
recipe.comments = commentsOverrides.map((comment) =>
|
||||
createMockRecipeComment({
|
||||
recipe_id: recipeId,
|
||||
...comment,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (ingredientsOverrides) {
|
||||
recipe.ingredients = ingredientsOverrides.map((ingredient) =>
|
||||
createMockRecipeIngredient({
|
||||
recipe_id: recipeId,
|
||||
...ingredient,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return recipe;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock RecipeIngredient object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* Can optionally include a `master_item` to link IDs.
|
||||
* @returns A complete and type-safe RecipeIngredient object.
|
||||
*/
|
||||
export const createMockRecipeIngredient = (overrides: Partial<RecipeIngredient> & { master_item?: Partial<MasterGroceryItem> } = {}): RecipeIngredient => {
|
||||
const recipeIngredientId = overrides.recipe_ingredient_id ?? getNextId();
|
||||
const masterItemId = overrides.master_item_id ?? overrides.master_item?.master_grocery_item_id ?? getNextId();
|
||||
|
||||
const defaultIngredient: RecipeIngredient = {
|
||||
recipe_ingredient_id: recipeIngredientId,
|
||||
recipe_id: getNextId(),
|
||||
master_item_id: masterItemId,
|
||||
quantity: 1,
|
||||
unit: 'cup',
|
||||
};
|
||||
|
||||
const { master_item: _, ...itemOverrides } = overrides;
|
||||
|
||||
return { ...defaultIngredient, ...itemOverrides };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -161,9 +344,9 @@ export const createMockRecipe = (overrides: Partial<Recipe> = {}): Recipe => {
|
||||
*/
|
||||
export const createMockRecipeComment = (overrides: Partial<RecipeComment> = {}): RecipeComment => {
|
||||
const defaultComment: RecipeComment = {
|
||||
recipe_comment_id: Math.floor(Math.random() * 10000),
|
||||
recipe_id: Math.floor(Math.random() * 1000),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
recipe_comment_id: getNextId(),
|
||||
recipe_id: getNextId(),
|
||||
user_id: `user-${getNextId()}`,
|
||||
content: 'This is a mock comment.',
|
||||
status: 'visible',
|
||||
created_at: new Date().toISOString(),
|
||||
@@ -174,6 +357,56 @@ export const createMockRecipeComment = (overrides: Partial<RecipeComment> = {}):
|
||||
return { ...defaultComment, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock PlannedMeal object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe PlannedMeal object.
|
||||
*/
|
||||
export const createMockPlannedMeal = (overrides: Partial<PlannedMeal> = {}): PlannedMeal => {
|
||||
const defaultMeal: PlannedMeal = {
|
||||
planned_meal_id: getNextId(),
|
||||
menu_plan_id: getNextId(),
|
||||
recipe_id: getNextId(),
|
||||
plan_date: new Date().toISOString().split('T')[0],
|
||||
meal_type: 'dinner',
|
||||
servings_to_cook: 4,
|
||||
};
|
||||
|
||||
return { ...defaultMeal, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock MenuPlan object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe MenuPlan object.
|
||||
*/
|
||||
export const createMockMenuPlan = (overrides: Omit<Partial<MenuPlan>, 'planned_meals'> & { planned_meals?: Partial<PlannedMeal>[] } = {}): MenuPlan => {
|
||||
const menuPlanId = overrides.menu_plan_id ?? getNextId();
|
||||
|
||||
const defaultPlan: MenuPlan = {
|
||||
menu_plan_id: menuPlanId,
|
||||
user_id: `user-${getNextId()}`,
|
||||
name: 'Weekly Plan',
|
||||
start_date: new Date().toISOString().split('T')[0],
|
||||
end_date: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { planned_meals: mealsOverrides, ...planOverrides } = overrides;
|
||||
const menuPlan = { ...defaultPlan, ...planOverrides };
|
||||
|
||||
if (mealsOverrides) {
|
||||
menuPlan.planned_meals = mealsOverrides.map((meal) =>
|
||||
createMockPlannedMeal({
|
||||
menu_plan_id: menuPlanId,
|
||||
...meal,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return menuPlan;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock ActivityLogItem object for use in tests.
|
||||
* This factory handles the discriminated union nature of the ActivityLogItem type.
|
||||
@@ -188,9 +421,10 @@ export const createMockActivityLogItem = (overrides: Partial<ActivityLogItem> =
|
||||
const action = overrides.action ?? 'flyer_processed';
|
||||
|
||||
const baseLog = {
|
||||
activity_log_id: Math.floor(Math.random() * 10000),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
activity_log_id: getNextId(),
|
||||
user_id: `user-${getNextId()}`,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
let specificLog: ActivityLogItem;
|
||||
@@ -248,11 +482,12 @@ export const createMockActivityLogItem = (overrides: Partial<ActivityLogItem> =
|
||||
*/
|
||||
export const createMockAchievement = (overrides: Partial<Achievement> = {}): Achievement => {
|
||||
const defaultAchievement: Achievement = {
|
||||
achievement_id: Math.floor(Math.random() * 100),
|
||||
achievement_id: getNextId(),
|
||||
name: 'Mock Achievement',
|
||||
description: 'A great accomplishment.',
|
||||
icon: 'star',
|
||||
points_value: 10,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
return { ...defaultAchievement, ...overrides };
|
||||
};
|
||||
@@ -263,9 +498,9 @@ export const createMockAchievement = (overrides: Partial<Achievement> = {}): Ach
|
||||
* @returns A complete and type-safe object representing the joined achievement data.
|
||||
*/
|
||||
export const createMockUserAchievement = (overrides: Partial<UserAchievement & Achievement> = {}): UserAchievement & Achievement => {
|
||||
const achievementId = overrides.achievement_id ?? Math.floor(Math.random() * 100);
|
||||
const achievementId = overrides.achievement_id ?? getNextId();
|
||||
const defaultUserAchievement: UserAchievement & Achievement = {
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
user_id: `user-${getNextId()}`,
|
||||
achievement_id: achievementId,
|
||||
achieved_at: new Date().toISOString(),
|
||||
// from Achievement
|
||||
@@ -273,6 +508,7 @@ export const createMockUserAchievement = (overrides: Partial<UserAchievement & A
|
||||
description: 'An achievement someone earned.',
|
||||
icon: 'award',
|
||||
points_value: 20,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
return { ...defaultUserAchievement, ...overrides };
|
||||
};
|
||||
@@ -284,12 +520,14 @@ export const createMockUserAchievement = (overrides: Partial<UserAchievement & A
|
||||
*/
|
||||
export const createMockBudget = (overrides: Partial<Budget> = {}): Budget => {
|
||||
const defaultBudget: Budget = {
|
||||
budget_id: Math.floor(Math.random() * 100),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
budget_id: getNextId(),
|
||||
user_id: `user-${getNextId()}`,
|
||||
name: 'Monthly Groceries',
|
||||
amount_cents: 50000,
|
||||
period: 'monthly',
|
||||
start_date: new Date().toISOString().split('T')[0],
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
return { ...defaultBudget, ...overrides };
|
||||
};
|
||||
@@ -301,9 +539,9 @@ export const createMockBudget = (overrides: Partial<Budget> = {}): Budget => {
|
||||
*/
|
||||
export const createMockSpendingByCategory = (overrides: Partial<SpendingByCategory> = {}): SpendingByCategory => {
|
||||
const defaultSpending: SpendingByCategory = {
|
||||
category_id: Math.floor(Math.random() * 20) + 1,
|
||||
category_id: getNextId(),
|
||||
category_name: 'Produce',
|
||||
total_spent_cents: Math.floor(Math.random() * 20000) + 1000,
|
||||
total_spent_cents: 15000,
|
||||
};
|
||||
return { ...defaultSpending, ...overrides };
|
||||
};
|
||||
@@ -313,16 +551,55 @@ export const createMockSpendingByCategory = (overrides: Partial<SpendingByCatego
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe MasterGroceryItem object.
|
||||
*/
|
||||
export const createMockMasterGroceryItem = (overrides: Partial<MasterGroceryItem> = {}): MasterGroceryItem => {
|
||||
export const createMockMasterGroceryItem = (overrides: Partial<MasterGroceryItem> & { category?: Partial<Category> } = {}): MasterGroceryItem => {
|
||||
// Ensure category_id is consistent between the item and the nested category object
|
||||
const categoryOverrides = overrides.category || {};
|
||||
if (overrides.category_id && !categoryOverrides.category_id) {
|
||||
categoryOverrides.category_id = overrides.category_id;
|
||||
}
|
||||
|
||||
const category = createMockCategory(categoryOverrides);
|
||||
|
||||
const defaultItem: MasterGroceryItem = {
|
||||
master_grocery_item_id: Math.floor(Math.random() * 10000),
|
||||
master_grocery_item_id: getNextId(),
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
name: 'Mock Master Item',
|
||||
category_id: 1,
|
||||
category_name: 'Pantry & Dry Goods',
|
||||
category_id: category.category_id,
|
||||
category_name: category.name,
|
||||
is_allergen: false,
|
||||
allergy_info: null,
|
||||
created_by: null,
|
||||
};
|
||||
|
||||
return { ...defaultItem, ...overrides };
|
||||
const { category: _, ...itemOverrides } = overrides;
|
||||
|
||||
return { ...defaultItem, ...itemOverrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock PantryItem object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* Can optionally include a `master_item` to link IDs.
|
||||
* @returns A complete and type-safe PantryItem object.
|
||||
*/
|
||||
export const createMockPantryItem = (overrides: Partial<PantryItem> & { master_item?: Partial<MasterGroceryItem> } = {}): PantryItem => {
|
||||
const pantryItemId = overrides.pantry_item_id ?? getNextId();
|
||||
|
||||
const masterItemId = overrides.master_item_id ?? overrides.master_item?.master_grocery_item_id ?? getNextId();
|
||||
|
||||
const defaultItem: PantryItem = {
|
||||
pantry_item_id: pantryItemId,
|
||||
user_id: `user-${getNextId()}`,
|
||||
master_item_id: masterItemId,
|
||||
quantity: 1,
|
||||
unit: 'each',
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const { master_item: _, ...itemOverrides } = overrides;
|
||||
|
||||
return { ...defaultItem, ...itemOverrides };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -330,36 +607,173 @@ export const createMockMasterGroceryItem = (overrides: Partial<MasterGroceryItem
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe ShoppingList object.
|
||||
*/
|
||||
export const createMockShoppingList = (overrides: Partial<ShoppingList> = {}): ShoppingList => {
|
||||
export const createMockShoppingList = (overrides: Omit<Partial<ShoppingList>, 'items'> & { items?: (Partial<ShoppingListItem> & { master_item?: Partial<MasterGroceryItem> | null })[] } = {}): ShoppingList => {
|
||||
const shoppingListId = overrides.shopping_list_id ?? getNextId();
|
||||
|
||||
const defaultList: ShoppingList = {
|
||||
shopping_list_id: Math.floor(Math.random() * 100),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
shopping_list_id: shoppingListId,
|
||||
user_id: `user-${getNextId()}`,
|
||||
name: 'My Mock List',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
items: [],
|
||||
};
|
||||
|
||||
return { ...defaultList, ...overrides };
|
||||
if (overrides.items) {
|
||||
defaultList.items = overrides.items.map((item) =>
|
||||
createMockShoppingListItem({
|
||||
shopping_list_id: shoppingListId,
|
||||
...item,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { items: _, ...listOverrides } = overrides;
|
||||
|
||||
return { ...defaultList, ...listOverrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock ShoppingListItem object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* Can optionally include a `master_item` to link IDs and populate joined data.
|
||||
* @returns A complete and type-safe ShoppingListItem object.
|
||||
*/
|
||||
export const createMockShoppingListItem = (overrides: Partial<ShoppingListItem> = {}): ShoppingListItem => {
|
||||
export const createMockShoppingListItem = (overrides: Partial<ShoppingListItem> & { master_item?: Partial<MasterGroceryItem> | null } = {}): ShoppingListItem => {
|
||||
const shoppingListItemId = overrides.shopping_list_item_id ?? getNextId();
|
||||
const shoppingListId = overrides.shopping_list_id ?? getNextId();
|
||||
|
||||
const masterItemId = overrides.master_item_id ?? overrides.master_item?.master_grocery_item_id;
|
||||
|
||||
const defaultItem: ShoppingListItem = {
|
||||
shopping_list_item_id: Math.floor(Math.random() * 100000),
|
||||
shopping_list_id: Math.floor(Math.random() * 100),
|
||||
shopping_list_item_id: shoppingListItemId,
|
||||
shopping_list_id: shoppingListId,
|
||||
custom_item_name: 'Mock Shopping List Item',
|
||||
quantity: 1,
|
||||
is_purchased: false,
|
||||
added_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (masterItemId) {
|
||||
defaultItem.master_item_id = masterItemId;
|
||||
}
|
||||
|
||||
const { master_item: masterItemOverride, ...itemOverrides } = overrides;
|
||||
const result = { ...defaultItem, ...itemOverrides };
|
||||
|
||||
if (masterItemOverride && !result.master_item && masterItemOverride.name) {
|
||||
result.master_item = { name: masterItemOverride.name };
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock ShoppingTripItem object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe ShoppingTripItem object.
|
||||
*/
|
||||
export const createMockShoppingTripItem = (overrides: Partial<ShoppingTripItem> & { master_item?: Partial<MasterGroceryItem> } = {}): ShoppingTripItem => {
|
||||
const tripItemId = overrides.shopping_trip_item_id ?? getNextId();
|
||||
const masterItemId = overrides.master_item_id ?? overrides.master_item?.master_grocery_item_id;
|
||||
|
||||
const defaultItem: ShoppingTripItem = {
|
||||
shopping_trip_item_id: tripItemId,
|
||||
shopping_trip_id: getNextId(),
|
||||
master_item_id: masterItemId,
|
||||
custom_item_name: masterItemId ? null : 'Custom Trip Item',
|
||||
master_item_name: masterItemId ? (overrides.master_item?.name ?? 'Mock Master Item') : null,
|
||||
quantity: 1,
|
||||
price_paid_cents: 199,
|
||||
};
|
||||
|
||||
const { master_item: _, ...itemOverrides } = overrides;
|
||||
|
||||
return { ...defaultItem, ...itemOverrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock ShoppingTrip object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe ShoppingTrip object.
|
||||
*/
|
||||
export const createMockShoppingTrip = (overrides: Omit<Partial<ShoppingTrip>, 'items'> & { items?: Partial<ShoppingTripItem>[] } = {}): ShoppingTrip => {
|
||||
const tripId = overrides.shopping_trip_id ?? getNextId();
|
||||
|
||||
const defaultTrip: ShoppingTrip = {
|
||||
shopping_trip_id: tripId,
|
||||
user_id: `user-${getNextId()}`,
|
||||
shopping_list_id: null,
|
||||
completed_at: new Date().toISOString(),
|
||||
total_spent_cents: 0,
|
||||
items: [],
|
||||
};
|
||||
|
||||
const { items: itemsOverrides, ...tripOverrides } = overrides;
|
||||
const trip = { ...defaultTrip, ...tripOverrides };
|
||||
|
||||
if (itemsOverrides) {
|
||||
trip.items = itemsOverrides.map((item) => createMockShoppingTripItem({ shopping_trip_id: tripId, ...item }));
|
||||
if (overrides.total_spent_cents === undefined) {
|
||||
trip.total_spent_cents = trip.items.reduce((total, item) => total + (item.price_paid_cents ?? 0), 0);
|
||||
}
|
||||
}
|
||||
|
||||
return trip;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock ReceiptItem object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe ReceiptItem object.
|
||||
*/
|
||||
export const createMockReceiptItem = (overrides: Partial<ReceiptItem> = {}): ReceiptItem => {
|
||||
const defaultItem: ReceiptItem = {
|
||||
receipt_item_id: getNextId(),
|
||||
receipt_id: getNextId(),
|
||||
raw_item_description: 'Mock Receipt Item',
|
||||
quantity: 1,
|
||||
price_paid_cents: 199,
|
||||
master_item_id: null,
|
||||
product_id: null,
|
||||
status: 'unmatched',
|
||||
};
|
||||
|
||||
return { ...defaultItem, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Receipt object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* @returns A complete and type-safe Receipt object.
|
||||
*/
|
||||
export const createMockReceipt = (overrides: Omit<Partial<Receipt>, 'items'> & { items?: Partial<ReceiptItem>[] } = {}): Receipt => {
|
||||
const receiptId = overrides.receipt_id ?? getNextId();
|
||||
|
||||
const defaultReceipt: Receipt = {
|
||||
receipt_id: receiptId,
|
||||
user_id: `user-${getNextId()}`,
|
||||
store_id: null,
|
||||
receipt_image_url: `/receipts/mock-receipt-${receiptId}.jpg`,
|
||||
transaction_date: new Date().toISOString(),
|
||||
total_amount_cents: null,
|
||||
status: 'pending',
|
||||
raw_text: null,
|
||||
created_at: new Date().toISOString(),
|
||||
processed_at: null,
|
||||
};
|
||||
|
||||
const { items: itemsOverrides, ...receiptOverrides } = overrides;
|
||||
const receipt = { ...defaultReceipt, ...receiptOverrides };
|
||||
|
||||
if (itemsOverrides) {
|
||||
receipt.items = itemsOverrides.map((item) => createMockReceiptItem({ receipt_id: receiptId, ...item }));
|
||||
}
|
||||
|
||||
return receipt;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock DietaryRestriction object for testing.
|
||||
* @param overrides - Optional properties to override the defaults.
|
||||
@@ -374,6 +788,46 @@ export const createMockDietaryRestriction = (overrides: Partial<DietaryRestricti
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock UserDietaryRestriction object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* Can optionally include `user` and `restriction` objects to link IDs.
|
||||
* @returns A complete and type-safe UserDietaryRestriction object.
|
||||
*/
|
||||
export const createMockUserDietaryRestriction = (
|
||||
overrides: Partial<UserDietaryRestriction> & { user?: Partial<User>; restriction?: Partial<DietaryRestriction> } = {}
|
||||
): UserDietaryRestriction => {
|
||||
const userId = overrides.user_id ?? overrides.user?.user_id ?? `user-${getNextId()}`;
|
||||
const restrictionId = overrides.restriction_id ?? overrides.restriction?.dietary_restriction_id ?? getNextId();
|
||||
|
||||
const defaultUserRestriction: UserDietaryRestriction = {
|
||||
user_id: userId,
|
||||
restriction_id: restrictionId,
|
||||
};
|
||||
|
||||
return { ...defaultUserRestriction, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock UserAppliance object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
* Can optionally include `user` and `appliance` objects to link IDs.
|
||||
* @returns A complete and type-safe UserAppliance object.
|
||||
*/
|
||||
export const createMockUserAppliance = (
|
||||
overrides: Partial<UserAppliance> & { user?: Partial<User>; appliance?: Partial<Appliance> } = {}
|
||||
): UserAppliance => {
|
||||
const userId = overrides.user_id ?? overrides.user?.user_id ?? `user-${getNextId()}`;
|
||||
const applianceId = overrides.appliance_id ?? overrides.appliance?.appliance_id ?? getNextId();
|
||||
|
||||
const defaultUserAppliance: UserAppliance = {
|
||||
user_id: userId,
|
||||
appliance_id: applianceId,
|
||||
};
|
||||
|
||||
return { ...defaultUserAppliance, ...overrides };
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a mock Address object for use in tests.
|
||||
* @param overrides - An object containing properties to override the default mock values.
|
||||
@@ -381,7 +835,7 @@ export const createMockDietaryRestriction = (overrides: Partial<DietaryRestricti
|
||||
*/
|
||||
export const createMockAddress = (overrides: Partial<Address> = {}): Address => {
|
||||
const defaultAddress: Address = {
|
||||
address_id: Math.floor(Math.random() * 1000),
|
||||
address_id: getNextId(),
|
||||
address_line_1: '123 Mock St',
|
||||
city: 'Mockville',
|
||||
province_state: 'BC',
|
||||
@@ -405,7 +859,7 @@ export const createMockAddress = (overrides: Partial<Address> = {}): Address =>
|
||||
* @returns A complete and type-safe UserWithPasswordHash object.
|
||||
*/
|
||||
export const createMockUserWithPasswordHash = (overrides: Partial<UserWithPasswordHash> = {}): UserWithPasswordHash => {
|
||||
const userId = overrides.user_id ?? `user-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const userId = overrides.user_id ?? `user-${getNextId()}`;
|
||||
|
||||
const defaultUser: UserWithPasswordHash = {
|
||||
user_id: userId,
|
||||
@@ -413,6 +867,9 @@ export const createMockUserWithPasswordHash = (overrides: Partial<UserWithPasswo
|
||||
password_hash: 'hashed_password',
|
||||
failed_login_attempts: 0,
|
||||
last_failed_login: null,
|
||||
last_login_ip: null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return { ...defaultUser, ...overrides };
|
||||
@@ -424,10 +881,11 @@ export const createMockUserWithPasswordHash = (overrides: Partial<UserWithPasswo
|
||||
* @returns A complete and type-safe Profile object.
|
||||
*/
|
||||
export const createMockProfile = (overrides: Partial<Profile> = {}): Profile => {
|
||||
const userId = overrides.user_id ?? `user-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const userId = overrides.user_id ?? `user-${getNextId()}`;
|
||||
|
||||
const defaultProfile: Profile = {
|
||||
user_id: userId,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
full_name: 'Mock Profile User',
|
||||
avatar_url: null,
|
||||
@@ -435,6 +893,8 @@ export const createMockProfile = (overrides: Partial<Profile> = {}): Profile =>
|
||||
points: 0,
|
||||
role: 'user',
|
||||
preferences: {},
|
||||
created_by: null,
|
||||
updated_by: null,
|
||||
};
|
||||
|
||||
return { ...defaultProfile, ...overrides };
|
||||
@@ -447,11 +907,11 @@ export const createMockProfile = (overrides: Partial<Profile> = {}): Profile =>
|
||||
*/
|
||||
export const createMockWatchedItemDeal = (overrides: Partial<WatchedItemDeal> = {}): WatchedItemDeal => {
|
||||
const defaultDeal: WatchedItemDeal = {
|
||||
master_item_id: Math.floor(Math.random() * 1000),
|
||||
master_item_id: getNextId(),
|
||||
item_name: 'Mock Deal Item',
|
||||
best_price_in_cents: Math.floor(Math.random() * 1000) + 100,
|
||||
best_price_in_cents: 599,
|
||||
store_name: 'Mock Store',
|
||||
flyer_id: Math.floor(Math.random() * 100),
|
||||
flyer_id: getNextId(),
|
||||
valid_to: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(), // 5 days from now
|
||||
};
|
||||
|
||||
@@ -464,14 +924,14 @@ export const createMockWatchedItemDeal = (overrides: Partial<WatchedItemDeal> =
|
||||
* @returns A complete and type-safe LeaderboardUser object.
|
||||
*/
|
||||
export const createMockLeaderboardUser = (overrides: Partial<LeaderboardUser> = {}): LeaderboardUser => {
|
||||
const userId = overrides.user_id ?? `user-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const userId = overrides.user_id ?? `user-${getNextId()}`;
|
||||
|
||||
const defaultUser: LeaderboardUser = {
|
||||
user_id: userId,
|
||||
full_name: 'Leaderboard User',
|
||||
avatar_url: null,
|
||||
points: Math.floor(Math.random() * 1000),
|
||||
rank: String(Math.floor(Math.random() * 100) + 1),
|
||||
points: 500,
|
||||
rank: '10',
|
||||
};
|
||||
|
||||
return { ...defaultUser, ...overrides };
|
||||
@@ -484,13 +944,15 @@ export const createMockLeaderboardUser = (overrides: Partial<LeaderboardUser> =
|
||||
*/
|
||||
export const createMockUnmatchedFlyerItem = (overrides: Partial<UnmatchedFlyerItem> = {}): UnmatchedFlyerItem => {
|
||||
const defaultItem: UnmatchedFlyerItem = {
|
||||
unmatched_flyer_item_id: Math.floor(Math.random() * 1000),
|
||||
unmatched_flyer_item_id: getNextId(),
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString(),
|
||||
flyer_item_id: Math.floor(Math.random() * 10000),
|
||||
updated_at: new Date().toISOString(),
|
||||
reviewed_at: null,
|
||||
flyer_item_id: getNextId(),
|
||||
flyer_item_name: 'Mystery Product',
|
||||
price_display: '$?.??',
|
||||
flyer_id: Math.floor(Math.random() * 100),
|
||||
flyer_id: getNextId(),
|
||||
store_name: 'Random Store',
|
||||
};
|
||||
|
||||
@@ -503,7 +965,7 @@ export const createMockUnmatchedFlyerItem = (overrides: Partial<UnmatchedFlyerIt
|
||||
* @returns A complete and type-safe AdminUserView object.
|
||||
*/
|
||||
export const createMockAdminUserView = (overrides: Partial<AdminUserView> = {}): AdminUserView => {
|
||||
const userId = overrides.user_id ?? `user-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const userId = overrides.user_id ?? `user-${getNextId()}`;
|
||||
|
||||
const defaultUserView: AdminUserView = {
|
||||
user_id: userId,
|
||||
@@ -524,12 +986,13 @@ export const createMockAdminUserView = (overrides: Partial<AdminUserView> = {}):
|
||||
*/
|
||||
export const createMockNotification = (overrides: Partial<Notification> = {}): Notification => {
|
||||
const defaultNotification: Notification = {
|
||||
notification_id: Math.floor(Math.random() * 1000),
|
||||
user_id: `user-${Math.random().toString(36).substring(2, 9)}`,
|
||||
notification_id: getNextId(),
|
||||
user_id: `user-${getNextId()}`,
|
||||
content: 'This is a mock notification.',
|
||||
link_url: null,
|
||||
is_read: false,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
return { ...defaultNotification, ...overrides };
|
||||
|
||||
34
src/types.ts
34
src/types.ts
@@ -2,13 +2,16 @@
|
||||
export interface Store {
|
||||
store_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
name: string;
|
||||
logo_url?: string | null;
|
||||
created_by?: string | null;
|
||||
}
|
||||
|
||||
export interface Flyer {
|
||||
flyer_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
file_name: string;
|
||||
image_url: string;
|
||||
icon_url?: string | null; // URL for the 64x64 icon version of the flyer
|
||||
@@ -79,9 +82,13 @@ export interface FlyerItem {
|
||||
export interface MasterGroceryItem {
|
||||
master_grocery_item_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
name: string;
|
||||
category_id?: number | null;
|
||||
category_name?: string | null;
|
||||
is_allergen?: boolean;
|
||||
allergy_info?: any | null; // JSONB
|
||||
created_by?: string | null;
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
@@ -91,6 +98,8 @@ export interface Category {
|
||||
|
||||
export interface Brand {
|
||||
brand_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
name: string;
|
||||
logo_url?: string | null;
|
||||
store_id?: number | null;
|
||||
@@ -99,6 +108,8 @@ export interface Brand {
|
||||
|
||||
export interface Product {
|
||||
product_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
master_item_id: number;
|
||||
brand_id?: number | null;
|
||||
name: string;
|
||||
@@ -131,10 +142,14 @@ export interface UserWithPasswordHash extends User {
|
||||
password_hash: string | null;
|
||||
failed_login_attempts: number;
|
||||
last_failed_login: string | null; // TIMESTAMPTZ
|
||||
last_login_ip?: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
export interface Profile {
|
||||
user_id: string; // UUID
|
||||
updated_at?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
full_name?: string | null;
|
||||
avatar_url?: string | null;
|
||||
address_id?: number | null;
|
||||
@@ -144,6 +159,8 @@ export interface Profile {
|
||||
darkMode?: boolean;
|
||||
unitSystem?: 'metric' | 'imperial';
|
||||
} | null;
|
||||
created_by?: string | null;
|
||||
updated_by?: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +182,7 @@ export interface SuggestedCorrection {
|
||||
suggested_value: string;
|
||||
status: 'pending' | 'approved' | 'rejected';
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
reviewed_at?: string | null;
|
||||
reviewed_notes?: string | null;
|
||||
// Joined data
|
||||
@@ -201,6 +219,7 @@ export interface Notification {
|
||||
link_url?: string | null;
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ShoppingList {
|
||||
@@ -208,6 +227,7 @@ export interface ShoppingList {
|
||||
user_id: string; // UUID
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
items: ShoppingListItem[]; // Nested items
|
||||
}
|
||||
|
||||
@@ -220,6 +240,7 @@ export interface ShoppingListItem {
|
||||
is_purchased: boolean;
|
||||
notes?: string | null;
|
||||
added_at: string;
|
||||
updated_at: string;
|
||||
// Joined data for display
|
||||
master_item?: {
|
||||
name: string;
|
||||
@@ -284,6 +305,9 @@ export interface Recipe {
|
||||
rating_count: number;
|
||||
fork_count: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
comments?: RecipeComment[];
|
||||
ingredients?: RecipeIngredient[];
|
||||
}
|
||||
|
||||
export interface RecipeIngredient {
|
||||
@@ -341,6 +365,7 @@ export interface MenuPlan {
|
||||
start_date: string; // DATE
|
||||
end_date: string; // DATE
|
||||
created_at: string;
|
||||
planned_meals?: PlannedMeal[];
|
||||
}
|
||||
|
||||
export interface SharedMenuPlan {
|
||||
@@ -476,6 +501,7 @@ interface ActivityLogItemBase {
|
||||
action: string;
|
||||
display_text: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
icon?: string | null;
|
||||
}
|
||||
|
||||
@@ -587,6 +613,7 @@ export interface Receipt {
|
||||
raw_text?: string | null;
|
||||
created_at: string;
|
||||
processed_at?: string | null;
|
||||
items?: ReceiptItem[];
|
||||
}
|
||||
|
||||
export interface ReceiptItem {
|
||||
@@ -852,6 +879,8 @@ export interface UnmatchedFlyerItem {
|
||||
unmatched_flyer_item_id: number;
|
||||
status: 'pending' | 'resolved' | 'ignored'; // 'resolved' is used instead of 'reviewed' from the DB for clarity
|
||||
created_at: string; // Date string
|
||||
updated_at: string;
|
||||
reviewed_at?: string | null;
|
||||
flyer_item_id: number;
|
||||
flyer_item_name: string;
|
||||
price_display: string;
|
||||
@@ -869,6 +898,8 @@ export interface Budget {
|
||||
amount_cents: number;
|
||||
period: 'weekly' | 'monthly';
|
||||
start_date: string; // DATE
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -890,6 +921,7 @@ export interface Achievement {
|
||||
description: string;
|
||||
icon?: string | null;
|
||||
points_value: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,11 @@ export default defineConfig({
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
// This setup file is where we can add global test configurations
|
||||
setupFiles: './src/tests/setup/tests-setup-unit.ts',
|
||||
setupFiles: [
|
||||
'./src/tests/setup/tests-setup-unit.ts',
|
||||
'./src/tests/setup/mockHooks.ts',
|
||||
'./src/tests/setup/mockComponents.tsx'
|
||||
],
|
||||
// This line is the key fix: it tells Vitest to include the type definitions
|
||||
include: ['src/**/*.test.tsx'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user