From 9fd15f3a50e9c6b187f137bd80e58f045f13e11f Mon Sep 17 00:00:00 2001 From: Torben Sorensen Date: Fri, 2 Jan 2026 11:33:05 -0800 Subject: [PATCH] unit test auto-provider refactor --- src/components/AppGuard.test.tsx | 4 +- src/components/FlyerCorrectionTool.test.tsx | 13 +-- src/components/Leaderboard.test.tsx | 11 +- src/components/RecipeSuggester.test.tsx | 23 ++-- src/hooks/useFlyerItems.test.ts | 8 +- src/hooks/useShoppingLists.test.tsx | 1 - src/hooks/useWatchedItems.test.tsx | 1 - src/pages/MyDealsPage.test.tsx | 11 +- src/pages/ResetPasswordPage.test.tsx | 8 +- src/pages/UserProfilePage.test.tsx | 14 +-- src/pages/VoiceLabPage.test.tsx | 15 +-- src/pages/admin/FlyerReviewPage.test.tsx | 25 ++--- .../admin/components/CorrectionRow.test.tsx | 11 +- .../admin/components/ProfileManager.test.tsx | 19 +--- .../admin/components/SystemCheck.test.tsx | 41 +------ src/providers/ApiProvider.test.tsx | 10 +- src/providers/AuthProvider.test.tsx | 6 +- src/tests/setup/globalApiMock.ts | 65 +++++++++++ src/tests/setup/tests-setup-unit.ts | 61 ----------- src/types/exif-parser.d.ts | 102 +++++++++++++++++- src/types/pdf-poppler.d.ts | 80 +++++++++++++- vite.config.ts | 8 +- 22 files changed, 306 insertions(+), 231 deletions(-) create mode 100644 src/tests/setup/globalApiMock.ts diff --git a/src/components/AppGuard.test.tsx b/src/components/AppGuard.test.tsx index 9650da2d..d2882375 100644 --- a/src/components/AppGuard.test.tsx +++ b/src/components/AppGuard.test.tsx @@ -4,13 +4,14 @@ import { screen, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { AppGuard } from './AppGuard'; import { useAppInitialization } from '../hooks/useAppInitialization'; +import * as apiClient from '../services/apiClient'; import { useModal } from '../hooks/useModal'; import { renderWithProviders } from '../tests/utils/renderWithProviders'; // Mock dependencies +// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`. vi.mock('../hooks/useAppInitialization'); vi.mock('../hooks/useModal'); -vi.mock('../services/apiClient'); vi.mock('./WhatsNewModal', () => ({ WhatsNewModal: ({ isOpen }: { isOpen: boolean }) => isOpen ?
: null, @@ -21,6 +22,7 @@ vi.mock('../config', () => ({ }, })); +const mockedApiClient = vi.mocked(apiClient); const mockedUseAppInitialization = vi.mocked(useAppInitialization); const mockedUseModal = vi.mocked(useModal); diff --git a/src/components/FlyerCorrectionTool.test.tsx b/src/components/FlyerCorrectionTool.test.tsx index 370ad30c..5ee38ec1 100644 --- a/src/components/FlyerCorrectionTool.test.tsx +++ b/src/components/FlyerCorrectionTool.test.tsx @@ -10,16 +10,9 @@ import { renderWithProviders } from '../tests/utils/renderWithProviders'; // Unmock the component to test the real implementation vi.unmock('./FlyerCorrectionTool'); -// Mock dependencies -vi.mock('../services/aiApiClient'); -vi.mock('../services/notificationService'); -vi.mock('../services/logger', () => ({ - logger: { - error: vi.fn(), - }, -})); - -const mockedAiApiClient = aiApiClient as Mocked; +// The aiApiClient, notificationService, and logger are mocked globally. +// We can get a typed reference to the aiApiClient for individual test overrides. +const mockedAiApiClient = vi.mocked(aiApiClient); const mockedNotifySuccess = notifySuccess as Mocked; const mockedNotifyError = notifyError as Mocked; diff --git a/src/components/Leaderboard.test.tsx b/src/components/Leaderboard.test.tsx index 5df1ec37..7b70c0ee 100644 --- a/src/components/Leaderboard.test.tsx +++ b/src/components/Leaderboard.test.tsx @@ -9,14 +9,9 @@ import { createMockLeaderboardUser } from '../tests/utils/mockFactories'; import { createMockLogger } from '../tests/utils/mockLogger'; import { renderWithProviders } from '../tests/utils/renderWithProviders'; -// Mock the apiClient -vi.mock('../services/apiClient'); // This was correct -const mockedApiClient = apiClient as Mocked; - -// Mock the logger -vi.mock('../services/logger', () => ({ - logger: createMockLogger(), -})); +// The apiClient and logger are mocked globally. +// We can get a typed reference to the apiClient for individual test overrides. +const mockedApiClient = vi.mocked(apiClient); // Mock lucide-react icons to prevent rendering errors in the test environment vi.mock('lucide-react', () => ({ diff --git a/src/components/RecipeSuggester.test.tsx b/src/components/RecipeSuggester.test.tsx index 7b346e9d..1aa8d3e2 100644 --- a/src/components/RecipeSuggester.test.tsx +++ b/src/components/RecipeSuggester.test.tsx @@ -2,16 +2,15 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { RecipeSuggester } from './RecipeSuggester'; -import { suggestRecipe } from '../services/apiClient'; +import { RecipeSuggester } from './RecipeSuggester'; // This should be after mocks +import * as apiClient from '../services/apiClient'; import { logger } from '../services/logger.client'; import { renderWithProviders } from '../tests/utils/renderWithProviders'; import '@testing-library/jest-dom'; -// Mock the API client -vi.mock('../services/apiClient', () => ({ - suggestRecipe: vi.fn(), -})); +// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`. +// We can get a typed reference to it for individual test overrides. +const mockedApiClient = vi.mocked(apiClient); // Mock the logger vi.mock('../services/logger.client', () => ({ @@ -45,7 +44,7 @@ describe('RecipeSuggester Component', () => { await user.click(button); expect(await screen.findByText('Please enter at least one ingredient.')).toBeInTheDocument(); - expect(suggestRecipe).not.toHaveBeenCalled(); + expect(mockedApiClient.suggestRecipe).not.toHaveBeenCalled(); console.log('TEST: Validation error displayed correctly'); }); @@ -60,7 +59,7 @@ describe('RecipeSuggester Component', () => { // Mock successful API response const mockSuggestion = 'Here is a nice Chicken and Rice recipe...'; // Add a delay to ensure the loading state is visible during the test - vi.mocked(suggestRecipe).mockImplementation(async () => { + mockedApiClient.suggestRecipe.mockImplementation(async () => { await new Promise((resolve) => setTimeout(resolve, 50)); return { ok: true, json: async () => ({ suggestion: mockSuggestion }) } as Response; }); @@ -76,7 +75,7 @@ describe('RecipeSuggester Component', () => { expect(screen.getByText(mockSuggestion)).toBeInTheDocument(); }); - expect(suggestRecipe).toHaveBeenCalledWith(['chicken', 'rice']); + expect(mockedApiClient.suggestRecipe).toHaveBeenCalledWith(['chicken', 'rice']); console.log('TEST: Suggestion displayed and API called with correct args'); }); @@ -90,7 +89,7 @@ describe('RecipeSuggester Component', () => { // Mock API failure response const errorMessage = 'Invalid ingredients provided.'; - vi.mocked(suggestRecipe).mockResolvedValue({ + mockedApiClient.suggestRecipe.mockResolvedValue({ ok: false, json: async () => ({ message: errorMessage }), } as Response); @@ -117,7 +116,7 @@ describe('RecipeSuggester Component', () => { // Mock network error const networkError = new Error('Network Error'); - vi.mocked(suggestRecipe).mockRejectedValue(networkError); + mockedApiClient.suggestRecipe.mockRejectedValue(networkError); const button = screen.getByRole('button', { name: /Suggest a Recipe/i }); await user.click(button); @@ -148,7 +147,7 @@ describe('RecipeSuggester Component', () => { await user.type(input, 'tofu'); // Mock success for the second click - vi.mocked(suggestRecipe).mockResolvedValue({ + mockedApiClient.suggestRecipe.mockResolvedValue({ ok: true, json: async () => ({ suggestion: 'Tofu Stir Fry' }), } as Response); diff --git a/src/hooks/useFlyerItems.test.ts b/src/hooks/useFlyerItems.test.ts index 43ea71f2..3cb5d2db 100644 --- a/src/hooks/useFlyerItems.test.ts +++ b/src/hooks/useFlyerItems.test.ts @@ -4,11 +4,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { useFlyerItems } from './useFlyerItems'; import { useApiOnMount } from './useApiOnMount'; 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. vi.mock('./useApiOnMount'); -vi.mock('../services/apiClient'); const mockedUseApiOnMount = vi.mocked(useApiOnMount); @@ -61,7 +59,6 @@ describe('useFlyerItems Hook', () => { expect(result.current.flyerItems).toEqual([]); expect(result.current.isLoading).toBe(false); expect(result.current.error).toBeNull(); - // Assert: Check that useApiOnMount was called with `enabled: false`. expect(mockedUseApiOnMount).toHaveBeenCalledWith( expect.any(Function), // the wrapped fetcher function @@ -172,7 +169,7 @@ describe('useFlyerItems Hook', () => { const wrappedFetcher = mockedUseApiOnMount.mock.calls[0][0]; const mockResponse = new Response(); vi.mocked(apiClient.fetchFlyerItems).mockResolvedValue(mockResponse); - + //FIX: Missing apiClient import here const response = await wrappedFetcher(123); expect(apiClient.fetchFlyerItems).toHaveBeenCalledWith(123); @@ -180,3 +177,6 @@ describe('useFlyerItems Hook', () => { }); }); }); +import * as apiClient from '../services/apiClient'; + +const mockedApiClient = vi.mocked(apiClient); diff --git a/src/hooks/useShoppingLists.test.tsx b/src/hooks/useShoppingLists.test.tsx index 108fea71..749a2697 100644 --- a/src/hooks/useShoppingLists.test.tsx +++ b/src/hooks/useShoppingLists.test.tsx @@ -29,7 +29,6 @@ type MockApiResult = { vi.mock('./useApi'); vi.mock('../hooks/useAuth'); vi.mock('../hooks/useUserData'); -vi.mock('../services/apiClient'); // The apiClient is globally mocked in our test setup, so we just need to cast it const mockedUseApi = vi.mocked(useApi); diff --git a/src/hooks/useWatchedItems.test.tsx b/src/hooks/useWatchedItems.test.tsx index 77f5210d..97dd173b 100644 --- a/src/hooks/useWatchedItems.test.tsx +++ b/src/hooks/useWatchedItems.test.tsx @@ -17,7 +17,6 @@ import { vi.mock('./useApi'); vi.mock('../hooks/useAuth'); vi.mock('../hooks/useUserData'); -vi.mock('../services/apiClient'); // The apiClient is globally mocked in our test setup, so we just need to cast it const mockedUseApi = vi.mocked(useApi); diff --git a/src/pages/MyDealsPage.test.tsx b/src/pages/MyDealsPage.test.tsx index 47019d29..b0af6120 100644 --- a/src/pages/MyDealsPage.test.tsx +++ b/src/pages/MyDealsPage.test.tsx @@ -1,18 +1,15 @@ -// src/components/MyDealsPage.test.tsx +// src/pages/MyDealsPage.test.tsx import React from 'react'; import { render, screen, waitFor } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest'; import MyDealsPage from './MyDealsPage'; import * as apiClient from '../services/apiClient'; -import { WatchedItemDeal } from '../types'; +import type { WatchedItemDeal } from '../types'; import { logger } from '../services/logger.client'; import { createMockWatchedItemDeal } from '../tests/utils/mockFactories'; -// Mock the apiClient. The component now directly uses `fetchBestSalePrices`. -// By mocking the entire module, we can control the behavior of `fetchBestSalePrices` -// for our tests. -vi.mock('../services/apiClient'); -const mockedApiClient = apiClient as Mocked; +// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`. +const mockedApiClient = vi.mocked(apiClient); // Mock the logger vi.mock('../services/logger.client', () => ({ diff --git a/src/pages/ResetPasswordPage.test.tsx b/src/pages/ResetPasswordPage.test.tsx index 8bd2043d..7e17a81a 100644 --- a/src/pages/ResetPasswordPage.test.tsx +++ b/src/pages/ResetPasswordPage.test.tsx @@ -10,13 +10,7 @@ import { logger } from '../services/logger.client'; // The apiClient and logger are now mocked globally. const mockedApiClient = vi.mocked(apiClient); -vi.mock('../services/logger.client', () => ({ - logger: { - info: vi.fn(), - error: vi.fn(), - }, -})); - +// The logger is mocked globally. // Helper function to render the component within a router context const renderWithRouter = (token: string) => { return render( diff --git a/src/pages/UserProfilePage.test.tsx b/src/pages/UserProfilePage.test.tsx index b92becd6..d4ae9f36 100644 --- a/src/pages/UserProfilePage.test.tsx +++ b/src/pages/UserProfilePage.test.tsx @@ -11,16 +11,8 @@ import { createMockUser, } from '../tests/utils/mockFactories'; -// Mock dependencies -vi.mock('../services/apiClient'); // This was correct -vi.mock('../services/logger.client', () => ({ - logger: { - info: vi.fn(), - error: vi.fn(), - }, -})); -vi.mock('../services/notificationService'); -vi.mock('../services/aiApiClient'); // Mock aiApiClient as it's used in the component +// The apiClient, logger, notificationService, and aiApiClient are all mocked globally. +// We can get a typed reference to the notificationService for individual test overrides. const mockedNotificationService = vi.mocked(await import('../services/notificationService')); vi.mock('../components/AchievementsList', () => ({ AchievementsList: ({ achievements }: { achievements: (UserAchievement & Achievement)[] }) => ( @@ -28,7 +20,7 @@ vi.mock('../components/AchievementsList', () => ({ ), })); -const mockedApiClient = apiClient as Mocked; +const mockedApiClient = vi.mocked(apiClient); // --- Mock Data --- const mockProfile: UserProfile = createMockUserProfile({ diff --git a/src/pages/VoiceLabPage.test.tsx b/src/pages/VoiceLabPage.test.tsx index d0745494..84f8c3f5 100644 --- a/src/pages/VoiceLabPage.test.tsx +++ b/src/pages/VoiceLabPage.test.tsx @@ -10,21 +10,10 @@ import { logger } from '../services/logger.client'; // Extensive logging for debugging const LOG_PREFIX = '[TEST DEBUG]'; -vi.mock('../services/notificationService'); - -// 1. Mock the module to replace its exports with mock functions. -vi.mock('../services/aiApiClient'); -// 2. Get a typed reference to the mocked module to control its functions in tests. +// The aiApiClient, notificationService, and logger are mocked globally. +// We can get a typed reference to the aiApiClient for individual test overrides. const mockedAiApiClient = vi.mocked(aiApiClient); -// Mock the logger -vi.mock('../services/logger.client', () => ({ - logger: { - info: vi.fn(), - error: vi.fn(), - }, -})); - // Define mock at module level so it can be referenced in the implementation const mockAudioPlay = vi.fn(() => { console.log(`${LOG_PREFIX} mockAudioPlay executed`); diff --git a/src/pages/admin/FlyerReviewPage.test.tsx b/src/pages/admin/FlyerReviewPage.test.tsx index e9c20abc..1bfaff1c 100644 --- a/src/pages/admin/FlyerReviewPage.test.tsx +++ b/src/pages/admin/FlyerReviewPage.test.tsx @@ -6,16 +6,9 @@ import { MemoryRouter } from 'react-router-dom'; import * as apiClient from '../../services/apiClient'; import { logger } from '../../services/logger.client'; -// Mock dependencies -vi.mock('../../services/apiClient', () => ({ - getFlyersForReview: vi.fn(), -})); - -vi.mock('../../services/logger.client', () => ({ - logger: { - error: vi.fn(), - }, -})); +// The apiClient and logger are mocked globally. +// We can get a typed reference to the apiClient for individual test overrides. +const mockedApiClient = vi.mocked(apiClient); // Mock LoadingSpinner to simplify DOM and avoid potential issues vi.mock('../../components/LoadingSpinner', () => ({ @@ -29,7 +22,7 @@ describe('FlyerReviewPage', () => { it('renders loading spinner initially', () => { // Mock a promise that doesn't resolve immediately to check loading state - vi.mocked(apiClient.getFlyersForReview).mockReturnValue(new Promise(() => {})); + mockedApiClient.getFlyersForReview.mockReturnValue(new Promise(() => {})); render( @@ -41,7 +34,7 @@ describe('FlyerReviewPage', () => { }); it('renders empty state when no flyers are returned', async () => { - vi.mocked(apiClient.getFlyersForReview).mockResolvedValue({ + mockedApiClient.getFlyersForReview.mockResolvedValue({ ok: true, json: async () => [], } as Response); @@ -84,7 +77,7 @@ describe('FlyerReviewPage', () => { }, ]; - vi.mocked(apiClient.getFlyersForReview).mockResolvedValue({ + mockedApiClient.getFlyersForReview.mockResolvedValue({ ok: true, json: async () => mockFlyers, } as Response); @@ -114,7 +107,7 @@ describe('FlyerReviewPage', () => { }); it('renders error message when API response is not ok', async () => { - vi.mocked(apiClient.getFlyersForReview).mockResolvedValue({ + mockedApiClient.getFlyersForReview.mockResolvedValue({ ok: false, json: async () => ({ message: 'Server error' }), } as Response); @@ -138,7 +131,7 @@ describe('FlyerReviewPage', () => { it('renders error message when API throws an error', async () => { const networkError = new Error('Network error'); - vi.mocked(apiClient.getFlyersForReview).mockRejectedValue(networkError); + mockedApiClient.getFlyersForReview.mockRejectedValue(networkError); render( @@ -159,7 +152,7 @@ describe('FlyerReviewPage', () => { it('renders a generic error for non-Error rejections', async () => { const nonErrorRejection = { message: 'This is not an Error object' }; - vi.mocked(apiClient.getFlyersForReview).mockRejectedValue(nonErrorRejection); + mockedApiClient.getFlyersForReview.mockRejectedValue(nonErrorRejection); render( diff --git a/src/pages/admin/components/CorrectionRow.test.tsx b/src/pages/admin/components/CorrectionRow.test.tsx index 7eb43353..7cb15993 100644 --- a/src/pages/admin/components/CorrectionRow.test.tsx +++ b/src/pages/admin/components/CorrectionRow.test.tsx @@ -12,14 +12,9 @@ import { } from '../../../tests/utils/mockFactories'; import { renderWithProviders } from '../../../tests/utils/renderWithProviders'; -// 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. -const mockedApiClient = apiClient as Mocked; - -// Mock the logger -vi.mock('../../../services/logger', () => ({ - logger: { info: vi.fn(), error: vi.fn() }, -})); +// The apiClient and logger are mocked globally. +// We can get a typed reference to the apiClient for individual test overrides. +const mockedApiClient = vi.mocked(apiClient); // Mock the ConfirmationModal to test its props and interactions // The ConfirmationModal is now in a different directory. diff --git a/src/pages/admin/components/ProfileManager.test.tsx b/src/pages/admin/components/ProfileManager.test.tsx index 865153ca..975ea302 100644 --- a/src/pages/admin/components/ProfileManager.test.tsx +++ b/src/pages/admin/components/ProfileManager.test.tsx @@ -21,25 +21,10 @@ vi.mock('../../../components/PasswordInput', () => ({ PasswordInput: (props: any) => , })); +// The apiClient, notificationService, react-hot-toast, and logger are all mocked globally. +// We can get a typed reference to the apiClient for individual test overrides. const mockedApiClient = vi.mocked(apiClient, true); -vi.mock('../../../services/notificationService'); -vi.mock('react-hot-toast', () => ({ - __esModule: true, - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); -vi.mock('../../../services/logger.client', () => ({ - logger: { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, -})); - const mockOnClose = vi.fn(); const mockOnLoginSuccess = vi.fn(); const mockOnSignOut = vi.fn(); diff --git a/src/pages/admin/components/SystemCheck.test.tsx b/src/pages/admin/components/SystemCheck.test.tsx index 8f2da8c7..949ee1ba 100644 --- a/src/pages/admin/components/SystemCheck.test.tsx +++ b/src/pages/admin/components/SystemCheck.test.tsx @@ -8,46 +8,11 @@ import toast from 'react-hot-toast'; import { createMockUser } from '../../../tests/utils/mockFactories'; import { renderWithProviders } from '../../../tests/utils/renderWithProviders'; -// Mock the entire apiClient module to ensure all exports are defined. -// This is the primary fix for the error: [vitest] No "..." export is defined on the mock. -vi.mock('../../../services/apiClient', () => ({ - // Mocks for providers used by renderWithProviders - fetchFlyers: vi.fn(), - fetchMasterItems: vi.fn(), - fetchWatchedItems: vi.fn(), - fetchShoppingLists: vi.fn(), - getAuthenticatedUserProfile: vi.fn(), - pingBackend: vi.fn(), - checkStorage: vi.fn(), - checkDbPoolHealth: vi.fn(), - checkPm2Status: vi.fn(), - checkRedisHealth: vi.fn(), - checkDbSchema: vi.fn(), - loginUser: vi.fn(), - triggerFailingJob: vi.fn(), - clearGeocodeCache: vi.fn(), -})); -// Get a type-safe mocked version of the apiClient module. +// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`. +// We can get a type-safe mocked version of the module to override functions for specific tests. const mockedApiClient = vi.mocked(apiClient); -// Correct the relative path to the logger module. -vi.mock('../../../services/logger', () => ({ - logger: { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }, -})); - -// Mock toast to check for notifications -vi.mock('react-hot-toast', () => ({ - __esModule: true, - default: { - success: vi.fn(), - error: vi.fn(), - }, -})); +// The logger and react-hot-toast are mocked globally. describe('SystemCheck', () => { // Store original env variable diff --git a/src/providers/ApiProvider.test.tsx b/src/providers/ApiProvider.test.tsx index abd194c0..34132adf 100644 --- a/src/providers/ApiProvider.test.tsx +++ b/src/providers/ApiProvider.test.tsx @@ -6,14 +6,8 @@ import { ApiProvider } from './ApiProvider'; import { ApiContext } from '../contexts/ApiContext'; import * as apiClient from '../services/apiClient'; -// Mock the apiClient module. -// Since ApiProvider and ApiContext import * as apiClient, mocking it ensures -// we control the reference identity and can verify it's being passed correctly. -vi.mock('../services/apiClient', () => ({ - fetchFlyers: vi.fn(), - fetchMasterItems: vi.fn(), - // Add other mocked methods as needed for the shape to be valid-ish -})); +// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`. +// This test verifies that the ApiProvider correctly provides this mocked module. describe('ApiProvider & ApiContext', () => { const TestConsumer = () => { diff --git a/src/providers/AuthProvider.test.tsx b/src/providers/AuthProvider.test.tsx index 559bde83..a0fbd7bf 100644 --- a/src/providers/AuthProvider.test.tsx +++ b/src/providers/AuthProvider.test.tsx @@ -4,12 +4,12 @@ import { render, screen, waitFor, fireEvent, act } from '@testing-library/react' import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest'; import { AuthProvider } from './AuthProvider'; import { AuthContext } from '../contexts/AuthContext'; -import * as apiClient from '../services/apiClient'; import * as tokenStorage from '../services/tokenStorage'; import { createMockUserProfile } from '../tests/utils/mockFactories'; +import * as apiClient from '../services/apiClient'; // Mocks -vi.mock('../services/apiClient'); +// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`. vi.mock('../services/tokenStorage'); vi.mock('../services/logger.client', () => ({ logger: { @@ -20,7 +20,7 @@ vi.mock('../services/logger.client', () => ({ }, })); -const mockedApiClient = apiClient as Mocked; +const mockedApiClient = vi.mocked(apiClient); const mockedTokenStorage = tokenStorage as Mocked; const mockProfile = createMockUserProfile({ diff --git a/src/tests/setup/globalApiMock.ts b/src/tests/setup/globalApiMock.ts new file mode 100644 index 00000000..712d39b7 --- /dev/null +++ b/src/tests/setup/globalApiMock.ts @@ -0,0 +1,65 @@ +// src/tests/setup/globalApiMock.ts +import { vi } from 'vitest'; + +/** + * Mocks the entire apiClient module. + * This global mock is loaded for all tests via the `setupFiles` config in vitest.config.ts. + * It prevents test failures in components that use providers (like FlyersProvider, AuthProvider) + * which make API calls on mount when using `renderWithProviders`. + * + * Individual tests can override specific functions as needed, for example: + * + * import { vi } from 'vitest'; + * import * as apiClient from '../services/apiClient'; + * + * const mockedApiClient = vi.mocked(apiClient); + * + * it('should test something', () => { + * mockedApiClient.someFunction.mockResolvedValue({ ... }); + * // ... rest of the test + * }); + */ +vi.mock('../../services/apiClient', () => ({ + // --- Provider Mocks (with default successful responses) --- + // These are essential for any test using renderWithProviders, as AppProviders + // will mount all these data providers. + fetchFlyers: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ flyers: [], hasMore: false })))), + fetchMasterItems: vi.fn(() => Promise.resolve(new Response(JSON.stringify([])))), + fetchWatchedItems: vi.fn(() => Promise.resolve(new Response(JSON.stringify([])))), + fetchShoppingLists: vi.fn(() => Promise.resolve(new Response(JSON.stringify([])))), + getAuthenticatedUserProfile: vi.fn(() => Promise.resolve(new Response(JSON.stringify(null)))), + fetchCategories: vi.fn(() => Promise.resolve(new Response(JSON.stringify([])))), // For CorrectionsPage + fetchAllBrands: vi.fn(() => Promise.resolve(new Response(JSON.stringify([])))), // For AdminBrandManager + + // --- General Mocks (return empty vi.fn() by default) --- + // These functions are commonly used and can be implemented in specific tests. + suggestRecipe: vi.fn(), + getApplicationStats: vi.fn(), + getSuggestedCorrections: vi.fn(), + approveCorrection: vi.fn(), + rejectCorrection: vi.fn(), + updateSuggestedCorrection: vi.fn(), + pingBackend: vi.fn(), + checkStorage: vi.fn(), + checkDbPoolHealth: vi.fn(), + checkPm2Status: vi.fn(), + checkRedisHealth: vi.fn(), + checkDbSchema: vi.fn(), + loginUser: vi.fn(), + registerUser: vi.fn(), + requestPasswordReset: vi.fn(), + triggerFailingJob: vi.fn(), + clearGeocodeCache: vi.fn(), + uploadBrandLogo: vi.fn(), + fetchActivityLog: vi.fn(), + updateUserProfile: vi.fn(), + updateUserPassword: vi.fn(), + updateUserPreferences: vi.fn(), + exportUserData: vi.fn(), + deleteUserAccount: vi.fn(), + getUserAddress: vi.fn(), + updateUserAddress: vi.fn(), + geocodeAddress: vi.fn(), + getFlyersForReview: vi.fn(), + fetchLeaderboard: vi.fn(), +})); \ No newline at end of file diff --git a/src/tests/setup/tests-setup-unit.ts b/src/tests/setup/tests-setup-unit.ts index b99646ad..c1144ad9 100644 --- a/src/tests/setup/tests-setup-unit.ts +++ b/src/tests/setup/tests-setup-unit.ts @@ -257,67 +257,6 @@ vi.mock('@google/genai', () => { }; }); -/** - * Mocks the entire apiClient module. - * This ensures that all test files that import from apiClient will get this mocked version. - */ -vi.mock('../../services/apiClient', () => ({ - // --- Auth --- - registerUser: vi.fn(), - loginUser: vi.fn(), - getAuthenticatedUserProfile: vi.fn(), - requestPasswordReset: vi.fn(), - resetPassword: vi.fn(), - updateUserPassword: vi.fn(), - deleteUserAccount: vi.fn(), - updateUserPreferences: vi.fn(), - updateUserProfile: vi.fn(), - // --- Data Fetching & Manipulation --- - fetchFlyers: vi.fn(), - fetchFlyerItems: vi.fn(), - // Provide a default implementation that returns a valid Response object to prevent timeouts. - fetchFlyerItemsForFlyers: vi.fn(() => Promise.resolve(new Response(JSON.stringify([])))), - countFlyerItemsForFlyers: vi.fn(() => - Promise.resolve(new Response(JSON.stringify({ count: 0 }))), - ), - fetchMasterItems: vi.fn(), - fetchWatchedItems: vi.fn(), - addWatchedItem: vi.fn(), - removeWatchedItem: vi.fn(), - fetchShoppingLists: vi.fn(), - createShoppingList: vi.fn(), - deleteShoppingList: vi.fn(), - addShoppingListItem: vi.fn(), - updateShoppingListItem: vi.fn(), - removeShoppingListItem: vi.fn(), - fetchHistoricalPriceData: vi.fn(), - processFlyerFile: vi.fn(), - uploadLogoAndUpdateStore: vi.fn(), - exportUserData: vi.fn(), - // --- Address --- - getUserAddress: vi.fn(), - updateUserAddress: vi.fn(), - geocodeAddress: vi.fn(() => Promise.resolve(new Response(JSON.stringify({ lat: 0, lng: 0 })))), - // --- Admin --- - getSuggestedCorrections: vi.fn(), - fetchCategories: vi.fn(), - approveCorrection: vi.fn(), - rejectCorrection: vi.fn(), - updateSuggestedCorrection: vi.fn(), - getApplicationStats: vi.fn(), - fetchActivityLog: vi.fn(), - fetchAllBrands: vi.fn(), - uploadBrandLogo: vi.fn(), - // --- System --- - pingBackend: vi.fn(), - checkDbSchema: vi.fn(), - checkStorage: vi.fn(), - checkDbPoolHealth: vi.fn(), - checkRedisHealth: vi.fn(), - checkPm2Status: vi.fn(), - fetchLeaderboard: vi.fn(), -})); - // FIX: Mock the aiApiClient module as well, which is used by AnalysisPanel vi.mock('../../services/aiApiClient', () => ({ // Provide a default implementation that returns a valid Response object to prevent timeouts. diff --git a/src/types/exif-parser.d.ts b/src/types/exif-parser.d.ts index e4fadad4..9ce1eacb 100644 --- a/src/types/exif-parser.d.ts +++ b/src/types/exif-parser.d.ts @@ -5,4 +5,104 @@ * which does not ship with its own TypeScript types. This allows TypeScript * to recognize it as a module and avoids "implicit any" errors. */ -declare module 'exif-parser'; \ No newline at end of file +declare module 'exif-parser' { + /** + * Represents the size of the image. + */ + export interface ImageSize { + width: number; + height: number; + } + + /** + * Represents thumbnail data if available. + */ + export interface Thumbnail { + format: string; + width: number; + height: number; + offset: number; + size: number; + buffer: Buffer; + } + + /** + * Represents GPS information if available. + */ + export interface GPS { + latitude: number; + longitude: number; + altitude: number; + latitudeRef: string; + longitudeRef: string; + altitudeRef: number; + GPSDateStamp: string; + GPSTimeStamp: number[]; // [hour, minute, second] + } + + /** + * Represents the parsed EXIF data structure. + * This includes common tags and derived properties. + */ + export interface ExifData { + /** + * A dictionary of raw EXIF tags. Keys are tag names (e.g., 'Make', 'Model', 'DateTimeOriginal'). + * Values can be of various types (string, number, Date, etc.). + */ + tags: { + Make?: string; + Model?: string; + Orientation?: number; + XResolution?: number; + YResolution?: number; + ResolutionUnit?: number; + DateTimeOriginal?: Date; // Parsed into a Date object + DateTimeDigitized?: Date; + ExposureTime?: number; + FNumber?: number; + ISOSpeedRatings?: number; + ShutterSpeedValue?: number; + ApertureValue?: number; + BrightnessValue?: number; + ExposureBiasValue?: number; + MaxApertureValue?: number; + MeteringMode?: number; + LightSource?: number; + Flash?: number; + FocalLength?: number; + ColorSpace?: number; + ExifImageWidth?: number; + ExifImageHeight?: number; + ExposureMode?: number; + WhiteBalance?: number; + DigitalZoomRatio?: number; + FocalLengthIn35mmFilm?: number; + SceneCaptureType?: number; + GainControl?: number; + Contrast?: number; + Saturation?: number; + Sharpness?: number; + SubjectDistanceRange?: number; + GPSVersionID?: number[]; + GPSLatitudeRef?: string; + GPSLatitude?: number[]; + GPSLongitudeRef?: string; + GPSLongitude?: number[]; + GPSAltitudeRef?: number; + GPSAltitude?: number; + GPSTimeStamp?: number[]; + GPSDateStamp?: string; + [key: string]: any; // Allow for other, less common tags + }; + imageSize: ImageSize; + thumbnail?: Thumbnail; + gps?: GPS; + } + + export class ExifParser { + static create(buffer: Buffer): ExifParser; + parse(): ExifData; + } + + export default ExifParser; +} \ No newline at end of file diff --git a/src/types/pdf-poppler.d.ts b/src/types/pdf-poppler.d.ts index 98397225..3f732959 100644 --- a/src/types/pdf-poppler.d.ts +++ b/src/types/pdf-poppler.d.ts @@ -7,37 +7,115 @@ * structure, preventing import errors and enabling type checking. */ declare module 'pdf-poppler' { + /** + * Defines the options available for the main `convert` method. + * This appears to be a simplified wrapper around pdftocairo. + */ + export interface ConvertOptions { + /** + * The output image format. + */ + format?: 'jpeg' | 'png' | 'tiff'; + /** + * The directory where output images will be saved. + */ + out_dir?: string; + /** + * The prefix for the output image files. + */ + out_prefix?: string; + /** + * Specify a page number to convert a specific page, or null to convert all pages. + */ + page?: number | null; + /** + * Specifies the resolution, in DPI. The default is 72 DPI. + */ + resolution?: number; + /** + * Scales each page to fit in scale-to x scale-to pixel square. + */ + scale_to?: number; + } + /** * Defines the options available for the pdfToCairo conversion method. - * This interface can be expanded as more options are used. + * These options correspond to the command-line arguments for the `pdftocairo` utility. */ export interface PopplerOptions { antialias?: 'default' | 'gray' | 'none' | 'subpixel'; cropBox?: boolean; cropHeight?: number; cropWidth?: number; + cropSize?: number; cropX?: number; cropY?: number; + duplex?: boolean; + epsFile?: boolean; + expand?: boolean; firstPage?: number; + grayFile?: boolean; lastPage?: number; jpegFile?: boolean; jpegOptions?: string; + level2?: boolean; + level3?: boolean; + monoFile?: boolean; + noCenter?: boolean; noCrop?: boolean; noRotate?: boolean; + noShrink?: boolean; ownerPassword?: string; paperHeight?: number; paperWidth?: number; paperSize?: 'letter' | 'legal' | 'A4' | 'A3' | 'match'; pngFile?: boolean; + psFile?: boolean; + pdfFile?: boolean; resolution?: number; + scaleTo?: number; + scaleToX?: number; + scaleToY?: number; svgFile?: boolean; + tiffFile?: boolean; userPassword?: string; } + /** + * Defines the structure of the PDF information object returned by `pdfInfo`. + */ + export interface PdfInfo { + // Based on common pdfinfo output + title: string; + author: string; + creator: string; + producer: string; + creationDate: string; + modDate: string; + tagged: boolean; + form: string; + pages: number; + encrypted: boolean; + pageSize: string; + fileSize: string; + optimized: boolean; + pdfVersion: string; + } + export class Poppler { constructor(binPath?: string); pdfToCairo(file: string, outputFilePrefix?: string, options?: PopplerOptions): Promise; + pdfInfo(file: string, options?: { ownerPassword?: string; userPassword?: string }): Promise; + pdfToPs(file: string, outputFile: string, options?: any): Promise; + pdfToText(file: string, outputFile: string, options?: any): Promise; } + /** + * Converts a PDF file to images. This seems to be a convenience function provided by the library. + * @param pdfPath The path to the PDF file. + * @param options The conversion options. + */ + export function convert(pdfPath: string, options?: ConvertOptions): Promise; + export default Poppler; } diff --git a/vite.config.ts b/vite.config.ts index 0632a21f..f1c654b9 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -41,12 +41,14 @@ export default defineConfig({ // By default, Vitest does not suppress console logs. // The onConsoleLog hook is only needed if you want to conditionally filter specific logs. // Keeping the default behavior is often safer to avoid missing important warnings. - environment: 'jsdom', - // Explicitly point Vitest to the correct tsconfig and enable globals. globals: true, // tsconfig is auto-detected, so the explicit property is not needed and causes an error. globalSetup: './src/tests/setup/global-setup.ts', - setupFiles: ['./src/tests/setup/tests-setup-unit.ts'], + // The globalApiMock MUST come first to ensure it's applied before other mocks that might depend on it. + setupFiles: [ + './src/tests/setup/globalApiMock.ts', + './src/tests/setup/tests-setup-unit.ts', + ], // Explicitly include only test files. // We remove 'src/vite-env.d.ts' which was causing it to be run as a test. include: ['src/**/*.test.{ts,tsx}'],