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 { 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 ? <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 mockedUseModal = vi.mocked(useModal);

View File

@@ -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<typeof aiApiClient>;
// 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<typeof notifySuccess>;
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 { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock the apiClient
vi.mock('../services/apiClient'); // This was correct
const mockedApiClient = apiClient as Mocked<typeof apiClient>;
// 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', () => ({

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<typeof apiClient>;
// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
const mockedApiClient = vi.mocked(apiClient);
// Mock the logger
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.
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(

View File

@@ -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<typeof apiClient>;
const mockedApiClient = vi.mocked(apiClient);
// --- Mock Data ---
const mockProfile: UserProfile = createMockUserProfile({

View File

@@ -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`);

View File

@@ -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(
<MemoryRouter>
@@ -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(
<MemoryRouter>
@@ -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(
<MemoryRouter>

View File

@@ -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<typeof apiClient>;
// 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.

View File

@@ -21,25 +21,10 @@ vi.mock('../../../components/PasswordInput', () => ({
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);
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();

View File

@@ -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

View File

@@ -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 = () => {

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 { 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<typeof apiClient>;
const mockedApiClient = vi.mocked(apiClient);
const mockedTokenStorage = tokenStorage as Mocked<typeof tokenStorage>;
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
vi.mock('../../services/aiApiClient', () => ({
// 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
* 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.
*/
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<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;
}

View File

@@ -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}'],