unit test auto-provider refactor
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 19m58s

This commit is contained in:
2026-01-02 11:33:05 -08:00
parent e3c876c7be
commit 9fd15f3a50
22 changed files with 306 additions and 231 deletions

View File

@@ -4,13 +4,14 @@ import { screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AppGuard } from './AppGuard'; import { AppGuard } from './AppGuard';
import { useAppInitialization } from '../hooks/useAppInitialization'; import { useAppInitialization } from '../hooks/useAppInitialization';
import * as apiClient from '../services/apiClient';
import { useModal } from '../hooks/useModal'; import { useModal } from '../hooks/useModal';
import { renderWithProviders } from '../tests/utils/renderWithProviders'; import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock dependencies // Mock dependencies
// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
vi.mock('../hooks/useAppInitialization'); vi.mock('../hooks/useAppInitialization');
vi.mock('../hooks/useModal'); vi.mock('../hooks/useModal');
vi.mock('../services/apiClient');
vi.mock('./WhatsNewModal', () => ({ vi.mock('./WhatsNewModal', () => ({
WhatsNewModal: ({ isOpen }: { isOpen: boolean }) => WhatsNewModal: ({ isOpen }: { isOpen: boolean }) =>
isOpen ? <div data-testid="whats-new-modal-mock" /> : null, isOpen ? <div data-testid="whats-new-modal-mock" /> : null,
@@ -21,6 +22,7 @@ vi.mock('../config', () => ({
}, },
})); }));
const mockedApiClient = vi.mocked(apiClient);
const mockedUseAppInitialization = vi.mocked(useAppInitialization); const mockedUseAppInitialization = vi.mocked(useAppInitialization);
const mockedUseModal = vi.mocked(useModal); const mockedUseModal = vi.mocked(useModal);

View File

@@ -10,16 +10,9 @@ import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Unmock the component to test the real implementation // Unmock the component to test the real implementation
vi.unmock('./FlyerCorrectionTool'); vi.unmock('./FlyerCorrectionTool');
// Mock dependencies // The aiApiClient, notificationService, and logger are mocked globally.
vi.mock('../services/aiApiClient'); // We can get a typed reference to the aiApiClient for individual test overrides.
vi.mock('../services/notificationService'); const mockedAiApiClient = vi.mocked(aiApiClient);
vi.mock('../services/logger', () => ({
logger: {
error: vi.fn(),
},
}));
const mockedAiApiClient = aiApiClient as Mocked<typeof aiApiClient>;
const mockedNotifySuccess = notifySuccess as Mocked<typeof notifySuccess>; const mockedNotifySuccess = notifySuccess as Mocked<typeof notifySuccess>;
const mockedNotifyError = notifyError as Mocked<typeof notifyError>; const mockedNotifyError = notifyError as Mocked<typeof notifyError>;

View File

@@ -9,14 +9,9 @@ import { createMockLeaderboardUser } from '../tests/utils/mockFactories';
import { createMockLogger } from '../tests/utils/mockLogger'; import { createMockLogger } from '../tests/utils/mockLogger';
import { renderWithProviders } from '../tests/utils/renderWithProviders'; import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock the apiClient // The apiClient and logger are mocked globally.
vi.mock('../services/apiClient'); // This was correct // We can get a typed reference to the apiClient for individual test overrides.
const mockedApiClient = apiClient as Mocked<typeof apiClient>; const mockedApiClient = vi.mocked(apiClient);
// Mock the logger
vi.mock('../services/logger', () => ({
logger: createMockLogger(),
}));
// Mock lucide-react icons to prevent rendering errors in the test environment // Mock lucide-react icons to prevent rendering errors in the test environment
vi.mock('lucide-react', () => ({ vi.mock('lucide-react', () => ({

View File

@@ -2,16 +2,15 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen, waitFor } from '@testing-library/react'; import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { RecipeSuggester } from './RecipeSuggester'; import { RecipeSuggester } from './RecipeSuggester'; // This should be after mocks
import { suggestRecipe } from '../services/apiClient'; import * as apiClient from '../services/apiClient';
import { logger } from '../services/logger.client'; import { logger } from '../services/logger.client';
import { renderWithProviders } from '../tests/utils/renderWithProviders'; import { renderWithProviders } from '../tests/utils/renderWithProviders';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
// Mock the API client // The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
vi.mock('../services/apiClient', () => ({ // We can get a typed reference to it for individual test overrides.
suggestRecipe: vi.fn(), const mockedApiClient = vi.mocked(apiClient);
}));
// Mock the logger // Mock the logger
vi.mock('../services/logger.client', () => ({ vi.mock('../services/logger.client', () => ({
@@ -45,7 +44,7 @@ describe('RecipeSuggester Component', () => {
await user.click(button); await user.click(button);
expect(await screen.findByText('Please enter at least one ingredient.')).toBeInTheDocument(); 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'); console.log('TEST: Validation error displayed correctly');
}); });
@@ -60,7 +59,7 @@ describe('RecipeSuggester Component', () => {
// Mock successful API response // Mock successful API response
const mockSuggestion = 'Here is a nice Chicken and Rice recipe...'; const mockSuggestion = 'Here is a nice Chicken and Rice recipe...';
// Add a delay to ensure the loading state is visible during the test // 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)); await new Promise((resolve) => setTimeout(resolve, 50));
return { ok: true, json: async () => ({ suggestion: mockSuggestion }) } as Response; return { ok: true, json: async () => ({ suggestion: mockSuggestion }) } as Response;
}); });
@@ -76,7 +75,7 @@ describe('RecipeSuggester Component', () => {
expect(screen.getByText(mockSuggestion)).toBeInTheDocument(); 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'); console.log('TEST: Suggestion displayed and API called with correct args');
}); });
@@ -90,7 +89,7 @@ describe('RecipeSuggester Component', () => {
// Mock API failure response // Mock API failure response
const errorMessage = 'Invalid ingredients provided.'; const errorMessage = 'Invalid ingredients provided.';
vi.mocked(suggestRecipe).mockResolvedValue({ mockedApiClient.suggestRecipe.mockResolvedValue({
ok: false, ok: false,
json: async () => ({ message: errorMessage }), json: async () => ({ message: errorMessage }),
} as Response); } as Response);
@@ -117,7 +116,7 @@ describe('RecipeSuggester Component', () => {
// Mock network error // Mock network error
const networkError = new Error('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 }); const button = screen.getByRole('button', { name: /Suggest a Recipe/i });
await user.click(button); await user.click(button);
@@ -148,7 +147,7 @@ describe('RecipeSuggester Component', () => {
await user.type(input, 'tofu'); await user.type(input, 'tofu');
// Mock success for the second click // Mock success for the second click
vi.mocked(suggestRecipe).mockResolvedValue({ mockedApiClient.suggestRecipe.mockResolvedValue({
ok: true, ok: true,
json: async () => ({ suggestion: 'Tofu Stir Fry' }), json: async () => ({ suggestion: 'Tofu Stir Fry' }),
} as Response); } as Response);

View File

@@ -4,11 +4,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { useFlyerItems } from './useFlyerItems'; import { useFlyerItems } from './useFlyerItems';
import { useApiOnMount } from './useApiOnMount'; import { useApiOnMount } from './useApiOnMount';
import { createMockFlyer, createMockFlyerItem } from '../tests/utils/mockFactories'; 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. // Mock the underlying useApiOnMount hook to isolate the useFlyerItems hook's logic.
vi.mock('./useApiOnMount'); vi.mock('./useApiOnMount');
vi.mock('../services/apiClient');
const mockedUseApiOnMount = vi.mocked(useApiOnMount); const mockedUseApiOnMount = vi.mocked(useApiOnMount);
@@ -61,7 +59,6 @@ describe('useFlyerItems Hook', () => {
expect(result.current.flyerItems).toEqual([]); expect(result.current.flyerItems).toEqual([]);
expect(result.current.isLoading).toBe(false); expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull(); expect(result.current.error).toBeNull();
// Assert: Check that useApiOnMount was called with `enabled: false`. // Assert: Check that useApiOnMount was called with `enabled: false`.
expect(mockedUseApiOnMount).toHaveBeenCalledWith( expect(mockedUseApiOnMount).toHaveBeenCalledWith(
expect.any(Function), // the wrapped fetcher function expect.any(Function), // the wrapped fetcher function
@@ -172,7 +169,7 @@ describe('useFlyerItems Hook', () => {
const wrappedFetcher = mockedUseApiOnMount.mock.calls[0][0]; const wrappedFetcher = mockedUseApiOnMount.mock.calls[0][0];
const mockResponse = new Response(); const mockResponse = new Response();
vi.mocked(apiClient.fetchFlyerItems).mockResolvedValue(mockResponse); vi.mocked(apiClient.fetchFlyerItems).mockResolvedValue(mockResponse);
//FIX: Missing apiClient import here
const response = await wrappedFetcher(123); const response = await wrappedFetcher(123);
expect(apiClient.fetchFlyerItems).toHaveBeenCalledWith(123); expect(apiClient.fetchFlyerItems).toHaveBeenCalledWith(123);
@@ -180,3 +177,6 @@ describe('useFlyerItems Hook', () => {
}); });
}); });
}); });
import * as apiClient from '../services/apiClient';
const mockedApiClient = vi.mocked(apiClient);

View File

@@ -29,7 +29,6 @@ type MockApiResult = {
vi.mock('./useApi'); vi.mock('./useApi');
vi.mock('../hooks/useAuth'); vi.mock('../hooks/useAuth');
vi.mock('../hooks/useUserData'); vi.mock('../hooks/useUserData');
vi.mock('../services/apiClient');
// The apiClient is globally mocked in our test setup, so we just need to cast it // The apiClient is globally mocked in our test setup, so we just need to cast it
const mockedUseApi = vi.mocked(useApi); const mockedUseApi = vi.mocked(useApi);

View File

@@ -17,7 +17,6 @@ import {
vi.mock('./useApi'); vi.mock('./useApi');
vi.mock('../hooks/useAuth'); vi.mock('../hooks/useAuth');
vi.mock('../hooks/useUserData'); vi.mock('../hooks/useUserData');
vi.mock('../services/apiClient');
// The apiClient is globally mocked in our test setup, so we just need to cast it // The apiClient is globally mocked in our test setup, so we just need to cast it
const mockedUseApi = vi.mocked(useApi); const mockedUseApi = vi.mocked(useApi);

View File

@@ -1,18 +1,15 @@
// src/components/MyDealsPage.test.tsx // src/pages/MyDealsPage.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, waitFor } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest'; import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import MyDealsPage from './MyDealsPage'; import MyDealsPage from './MyDealsPage';
import * as apiClient from '../services/apiClient'; import * as apiClient from '../services/apiClient';
import { WatchedItemDeal } from '../types'; import type { WatchedItemDeal } from '../types';
import { logger } from '../services/logger.client'; import { logger } from '../services/logger.client';
import { createMockWatchedItemDeal } from '../tests/utils/mockFactories'; import { createMockWatchedItemDeal } from '../tests/utils/mockFactories';
// Mock the apiClient. The component now directly uses `fetchBestSalePrices`. // The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
// By mocking the entire module, we can control the behavior of `fetchBestSalePrices` const mockedApiClient = vi.mocked(apiClient);
// for our tests.
vi.mock('../services/apiClient');
const mockedApiClient = apiClient as Mocked<typeof apiClient>;
// Mock the logger // Mock the logger
vi.mock('../services/logger.client', () => ({ vi.mock('../services/logger.client', () => ({

View File

@@ -10,13 +10,7 @@ import { logger } from '../services/logger.client';
// The apiClient and logger are now mocked globally. // The apiClient and logger are now mocked globally.
const mockedApiClient = vi.mocked(apiClient); const mockedApiClient = vi.mocked(apiClient);
vi.mock('../services/logger.client', () => ({ // The logger is mocked globally.
logger: {
info: vi.fn(),
error: vi.fn(),
},
}));
// Helper function to render the component within a router context // Helper function to render the component within a router context
const renderWithRouter = (token: string) => { const renderWithRouter = (token: string) => {
return render( return render(

View File

@@ -11,16 +11,8 @@ import {
createMockUser, createMockUser,
} from '../tests/utils/mockFactories'; } from '../tests/utils/mockFactories';
// Mock dependencies // The apiClient, logger, notificationService, and aiApiClient are all mocked globally.
vi.mock('../services/apiClient'); // This was correct // We can get a typed reference to the notificationService for individual test overrides.
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
const mockedNotificationService = vi.mocked(await import('../services/notificationService')); const mockedNotificationService = vi.mocked(await import('../services/notificationService'));
vi.mock('../components/AchievementsList', () => ({ vi.mock('../components/AchievementsList', () => ({
AchievementsList: ({ achievements }: { achievements: (UserAchievement & Achievement)[] }) => ( AchievementsList: ({ achievements }: { achievements: (UserAchievement & Achievement)[] }) => (
@@ -28,7 +20,7 @@ vi.mock('../components/AchievementsList', () => ({
), ),
})); }));
const mockedApiClient = apiClient as Mocked<typeof apiClient>; const mockedApiClient = vi.mocked(apiClient);
// --- Mock Data --- // --- Mock Data ---
const mockProfile: UserProfile = createMockUserProfile({ const mockProfile: UserProfile = createMockUserProfile({

View File

@@ -10,21 +10,10 @@ import { logger } from '../services/logger.client';
// Extensive logging for debugging // Extensive logging for debugging
const LOG_PREFIX = '[TEST DEBUG]'; const LOG_PREFIX = '[TEST DEBUG]';
vi.mock('../services/notificationService'); // The aiApiClient, notificationService, and logger are mocked globally.
// We can get a typed reference to the aiApiClient for individual test overrides.
// 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.
const mockedAiApiClient = vi.mocked(aiApiClient); 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 // Define mock at module level so it can be referenced in the implementation
const mockAudioPlay = vi.fn(() => { const mockAudioPlay = vi.fn(() => {
console.log(`${LOG_PREFIX} mockAudioPlay executed`); console.log(`${LOG_PREFIX} mockAudioPlay executed`);

View File

@@ -6,16 +6,9 @@ import { MemoryRouter } from 'react-router-dom';
import * as apiClient from '../../services/apiClient'; import * as apiClient from '../../services/apiClient';
import { logger } from '../../services/logger.client'; import { logger } from '../../services/logger.client';
// Mock dependencies // The apiClient and logger are mocked globally.
vi.mock('../../services/apiClient', () => ({ // We can get a typed reference to the apiClient for individual test overrides.
getFlyersForReview: vi.fn(), const mockedApiClient = vi.mocked(apiClient);
}));
vi.mock('../../services/logger.client', () => ({
logger: {
error: vi.fn(),
},
}));
// Mock LoadingSpinner to simplify DOM and avoid potential issues // Mock LoadingSpinner to simplify DOM and avoid potential issues
vi.mock('../../components/LoadingSpinner', () => ({ vi.mock('../../components/LoadingSpinner', () => ({
@@ -29,7 +22,7 @@ describe('FlyerReviewPage', () => {
it('renders loading spinner initially', () => { it('renders loading spinner initially', () => {
// Mock a promise that doesn't resolve immediately to check loading state // 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( render(
<MemoryRouter> <MemoryRouter>
@@ -41,7 +34,7 @@ describe('FlyerReviewPage', () => {
}); });
it('renders empty state when no flyers are returned', async () => { it('renders empty state when no flyers are returned', async () => {
vi.mocked(apiClient.getFlyersForReview).mockResolvedValue({ mockedApiClient.getFlyersForReview.mockResolvedValue({
ok: true, ok: true,
json: async () => [], json: async () => [],
} as Response); } as Response);
@@ -84,7 +77,7 @@ describe('FlyerReviewPage', () => {
}, },
]; ];
vi.mocked(apiClient.getFlyersForReview).mockResolvedValue({ mockedApiClient.getFlyersForReview.mockResolvedValue({
ok: true, ok: true,
json: async () => mockFlyers, json: async () => mockFlyers,
} as Response); } as Response);
@@ -114,7 +107,7 @@ describe('FlyerReviewPage', () => {
}); });
it('renders error message when API response is not ok', async () => { it('renders error message when API response is not ok', async () => {
vi.mocked(apiClient.getFlyersForReview).mockResolvedValue({ mockedApiClient.getFlyersForReview.mockResolvedValue({
ok: false, ok: false,
json: async () => ({ message: 'Server error' }), json: async () => ({ message: 'Server error' }),
} as Response); } as Response);
@@ -138,7 +131,7 @@ describe('FlyerReviewPage', () => {
it('renders error message when API throws an error', async () => { it('renders error message when API throws an error', async () => {
const networkError = new Error('Network error'); const networkError = new Error('Network error');
vi.mocked(apiClient.getFlyersForReview).mockRejectedValue(networkError); mockedApiClient.getFlyersForReview.mockRejectedValue(networkError);
render( render(
<MemoryRouter> <MemoryRouter>
@@ -159,7 +152,7 @@ describe('FlyerReviewPage', () => {
it('renders a generic error for non-Error rejections', async () => { it('renders a generic error for non-Error rejections', async () => {
const nonErrorRejection = { message: 'This is not an Error object' }; const nonErrorRejection = { message: 'This is not an Error object' };
vi.mocked(apiClient.getFlyersForReview).mockRejectedValue(nonErrorRejection); mockedApiClient.getFlyersForReview.mockRejectedValue(nonErrorRejection);
render( render(
<MemoryRouter> <MemoryRouter>

View File

@@ -12,14 +12,9 @@ import {
} from '../../../tests/utils/mockFactories'; } from '../../../tests/utils/mockFactories';
import { renderWithProviders } from '../../../tests/utils/renderWithProviders'; import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
// Cast the mocked module to its mocked type to retain type safety and autocompletion. // The apiClient and logger are mocked globally.
// The apiClient is now mocked globally via src/tests/setup/tests-setup-unit.ts. // We can get a typed reference to the apiClient for individual test overrides.
const mockedApiClient = apiClient as Mocked<typeof apiClient>; const mockedApiClient = vi.mocked(apiClient);
// Mock the logger
vi.mock('../../../services/logger', () => ({
logger: { info: vi.fn(), error: vi.fn() },
}));
// Mock the ConfirmationModal to test its props and interactions // Mock the ConfirmationModal to test its props and interactions
// The ConfirmationModal is now in a different directory. // The ConfirmationModal is now in a different directory.

View File

@@ -21,25 +21,10 @@ vi.mock('../../../components/PasswordInput', () => ({
PasswordInput: (props: any) => <input {...props} data-testid="password-input" />, PasswordInput: (props: any) => <input {...props} data-testid="password-input" />,
})); }));
// 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); 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 mockOnClose = vi.fn();
const mockOnLoginSuccess = vi.fn(); const mockOnLoginSuccess = vi.fn();
const mockOnSignOut = vi.fn(); const mockOnSignOut = vi.fn();

View File

@@ -8,46 +8,11 @@ import toast from 'react-hot-toast';
import { createMockUser } from '../../../tests/utils/mockFactories'; import { createMockUser } from '../../../tests/utils/mockFactories';
import { renderWithProviders } from '../../../tests/utils/renderWithProviders'; import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
// Mock the entire apiClient module to ensure all exports are defined. // The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
// This is the primary fix for the error: [vitest] No "..." export is defined on the mock. // We can get a type-safe mocked version of the module to override functions for specific tests.
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.
const mockedApiClient = vi.mocked(apiClient); const mockedApiClient = vi.mocked(apiClient);
// Correct the relative path to the logger module. // The logger and react-hot-toast are mocked globally.
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(),
},
}));
describe('SystemCheck', () => { describe('SystemCheck', () => {
// Store original env variable // Store original env variable

View File

@@ -6,14 +6,8 @@ import { ApiProvider } from './ApiProvider';
import { ApiContext } from '../contexts/ApiContext'; import { ApiContext } from '../contexts/ApiContext';
import * as apiClient from '../services/apiClient'; import * as apiClient from '../services/apiClient';
// Mock the apiClient module. // The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
// Since ApiProvider and ApiContext import * as apiClient, mocking it ensures // This test verifies that the ApiProvider correctly provides this mocked module.
// 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
}));
describe('ApiProvider & ApiContext', () => { describe('ApiProvider & ApiContext', () => {
const TestConsumer = () => { const TestConsumer = () => {

View File

@@ -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 { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
import { AuthProvider } from './AuthProvider'; import { AuthProvider } from './AuthProvider';
import { AuthContext } from '../contexts/AuthContext'; import { AuthContext } from '../contexts/AuthContext';
import * as apiClient from '../services/apiClient';
import * as tokenStorage from '../services/tokenStorage'; import * as tokenStorage from '../services/tokenStorage';
import { createMockUserProfile } from '../tests/utils/mockFactories'; import { createMockUserProfile } from '../tests/utils/mockFactories';
import * as apiClient from '../services/apiClient';
// Mocks // Mocks
vi.mock('../services/apiClient'); // The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
vi.mock('../services/tokenStorage'); vi.mock('../services/tokenStorage');
vi.mock('../services/logger.client', () => ({ vi.mock('../services/logger.client', () => ({
logger: { logger: {
@@ -20,7 +20,7 @@ vi.mock('../services/logger.client', () => ({
}, },
})); }));
const mockedApiClient = apiClient as Mocked<typeof apiClient>; const mockedApiClient = vi.mocked(apiClient);
const mockedTokenStorage = tokenStorage as Mocked<typeof tokenStorage>; const mockedTokenStorage = tokenStorage as Mocked<typeof tokenStorage>;
const mockProfile = createMockUserProfile({ const mockProfile = createMockUserProfile({

View File

@@ -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(),
}));

View File

@@ -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 // FIX: Mock the aiApiClient module as well, which is used by AnalysisPanel
vi.mock('../../services/aiApiClient', () => ({ vi.mock('../../services/aiApiClient', () => ({
// Provide a default implementation that returns a valid Response object to prevent timeouts. // Provide a default implementation that returns a valid Response object to prevent timeouts.

View File

@@ -5,4 +5,104 @@
* which does not ship with its own TypeScript types. This allows TypeScript * which does not ship with its own TypeScript types. This allows TypeScript
* to recognize it as a module and avoids "implicit any" errors. * to recognize it as a module and avoids "implicit any" errors.
*/ */
declare module 'exif-parser'; 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;
}

View File

@@ -7,37 +7,115 @@
* structure, preventing import errors and enabling type checking. * structure, preventing import errors and enabling type checking.
*/ */
declare module 'pdf-poppler' { 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. * 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 { export interface PopplerOptions {
antialias?: 'default' | 'gray' | 'none' | 'subpixel'; antialias?: 'default' | 'gray' | 'none' | 'subpixel';
cropBox?: boolean; cropBox?: boolean;
cropHeight?: number; cropHeight?: number;
cropWidth?: number; cropWidth?: number;
cropSize?: number;
cropX?: number; cropX?: number;
cropY?: number; cropY?: number;
duplex?: boolean;
epsFile?: boolean;
expand?: boolean;
firstPage?: number; firstPage?: number;
grayFile?: boolean;
lastPage?: number; lastPage?: number;
jpegFile?: boolean; jpegFile?: boolean;
jpegOptions?: string; jpegOptions?: string;
level2?: boolean;
level3?: boolean;
monoFile?: boolean;
noCenter?: boolean;
noCrop?: boolean; noCrop?: boolean;
noRotate?: boolean; noRotate?: boolean;
noShrink?: boolean;
ownerPassword?: string; ownerPassword?: string;
paperHeight?: number; paperHeight?: number;
paperWidth?: number; paperWidth?: number;
paperSize?: 'letter' | 'legal' | 'A4' | 'A3' | 'match'; paperSize?: 'letter' | 'legal' | 'A4' | 'A3' | 'match';
pngFile?: boolean; pngFile?: boolean;
psFile?: boolean;
pdfFile?: boolean;
resolution?: number; resolution?: number;
scaleTo?: number;
scaleToX?: number;
scaleToY?: number;
svgFile?: boolean; svgFile?: boolean;
tiffFile?: boolean;
userPassword?: string; 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 { export class Poppler {
constructor(binPath?: string); constructor(binPath?: string);
pdfToCairo(file: string, outputFilePrefix?: string, options?: PopplerOptions): Promise<string>; pdfToCairo(file: string, outputFilePrefix?: string, options?: PopplerOptions): Promise<string>;
pdfInfo(file: string, options?: { ownerPassword?: string; userPassword?: string }): Promise<PdfInfo>;
pdfToPs(file: string, outputFile: string, options?: any): Promise<string>;
pdfToText(file: string, outputFile: string, options?: any): Promise<string>;
} }
/**
* 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<string>;
export default Poppler; export default Poppler;
} }

View File

@@ -41,12 +41,14 @@ export default defineConfig({
// By default, Vitest does not suppress console logs. // By default, Vitest does not suppress console logs.
// The onConsoleLog hook is only needed if you want to conditionally filter specific 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. // Keeping the default behavior is often safer to avoid missing important warnings.
environment: 'jsdom', 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. globals: true, // tsconfig is auto-detected, so the explicit property is not needed and causes an error.
globalSetup: './src/tests/setup/global-setup.ts', 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. // Explicitly include only test files.
// We remove 'src/vite-env.d.ts' which was causing it to be run as a test. // We remove 'src/vite-env.d.ts' which was causing it to be run as a test.
include: ['src/**/*.test.{ts,tsx}'], include: ['src/**/*.test.{ts,tsx}'],