Compare commits

...

12 Commits

Author SHA1 Message Date
Gitea Actions
d1554050bd ci: Bump version to 0.9.0 for production release [skip ci] 2026-01-03 05:50:23 +05:00
Gitea Actions
b1fae270bb ci: Bump version to 0.8.0 for production release [skip ci] 2026-01-03 05:48:40 +05:00
Gitea Actions
c852483e18 ci: Bump version to 0.7.29 [skip ci] 2026-01-03 02:43:54 +05:00
2e01ad5bc9 more test fixin
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 13m50s
2026-01-02 13:43:20 -08:00
Gitea Actions
26763c7183 ci: Bump version to 0.7.28 [skip ci] 2026-01-03 02:04:26 +05:00
f0c5c2c45b more test fixin
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 12m40s
2026-01-02 13:03:25 -08:00
Gitea Actions
034bb60fd5 ci: Bump version to 0.7.27 [skip ci] 2026-01-03 01:31:54 +05:00
d4b389cb79 more test fixin
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 16m39s
2026-01-02 12:31:19 -08:00
Gitea Actions
a71fb81468 ci: Bump version to 0.7.26 [skip ci] 2026-01-03 00:58:34 +05:00
9bee0a013b unit test auto-provider refactor
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 17m8s
2026-01-02 11:58:03 -08:00
Gitea Actions
8bcb4311b3 ci: Bump version to 0.7.25 [skip ci] 2026-01-03 00:34:45 +05:00
9fd15f3a50 unit test auto-provider refactor
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 19m58s
2026-01-02 11:33:11 -08:00
32 changed files with 474 additions and 303 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.7.24",
"version": "0.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.7.24",
"version": "0.9.0",
"dependencies": {
"@bull-board/api": "^6.14.2",
"@bull-board/express": "^6.14.2",

View File

@@ -1,7 +1,7 @@
{
"name": "flyer-crawler",
"private": true,
"version": "0.7.24",
"version": "0.9.0",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",

View File

@@ -2115,6 +2115,61 @@ AS $$
ORDER BY potential_savings_cents DESC;
$$;
-- Function to get a user's spending breakdown by category for a given date range.
DROP FUNCTION IF EXISTS public.get_spending_by_category(UUID, DATE, DATE);
CREATE OR REPLACE FUNCTION public.get_spending_by_category(p_user_id UUID, p_start_date DATE, p_end_date DATE)
RETURNS TABLE (
category_id BIGINT,
category_name TEXT,
total_spent_cents BIGINT
)
LANGUAGE sql
STABLE
SECURITY INVOKER
AS $$
WITH all_purchases AS (
-- CTE 1: Combine purchases from completed shopping trips.
-- We only consider items that have a price paid.
SELECT
sti.master_item_id,
sti.price_paid_cents
FROM public.shopping_trip_items sti
JOIN public.shopping_trips st ON sti.shopping_trip_id = st.shopping_trip_id
WHERE st.user_id = p_user_id
AND st.completed_at::date BETWEEN p_start_date AND p_end_date
AND sti.price_paid_cents IS NOT NULL
UNION ALL
-- CTE 2: Combine purchases from processed receipts.
SELECT
ri.master_item_id,
ri.price_paid_cents
FROM public.receipt_items ri
JOIN public.receipts r ON ri.receipt_id = r.receipt_id
WHERE r.user_id = p_user_id
AND r.transaction_date::date BETWEEN p_start_date AND p_end_date
AND ri.master_item_id IS NOT NULL -- Only include items matched to a master item
)
-- Final Aggregation: Group all combined purchases by category and sum the spending.
SELECT
c.category_id,
c.name AS category_name,
SUM(ap.price_paid_cents)::BIGINT AS total_spent_cents
FROM all_purchases ap
-- Join with master_grocery_items to get the category_id for each purchase.
JOIN public.master_grocery_items mgi ON ap.master_item_id = mgi.master_grocery_item_id
-- Join with categories to get the category name for display.
JOIN public.categories c ON mgi.category_id = c.category_id
GROUP BY
c.category_id, c.name
HAVING
SUM(ap.price_paid_cents) > 0
ORDER BY
total_spent_cents DESC;
$$;
-- Function to approve a suggested correction and apply it.
DROP FUNCTION IF EXISTS public.approve_correction(BIGINT);
@@ -2669,6 +2724,58 @@ CREATE TRIGGER on_new_recipe_collection_share
AFTER INSERT ON public.shared_recipe_collections
FOR EACH ROW EXECUTE FUNCTION public.log_new_recipe_collection_share();
-- 10. Trigger function to geocode a store location's address.
-- This function is designed to be extensible for external geocoding services.
DROP FUNCTION IF EXISTS public.geocode_store_location();
CREATE OR REPLACE FUNCTION public.geocode_store_location()
RETURNS TRIGGER AS $$
DECLARE
full_address TEXT;
BEGIN
-- Only proceed if the address has actually changed.
-- Note: We check against the linked address fields via the NEW.address_id in a real scenario,
-- but for this trigger to work effectively, it usually requires a direct update on the address table
-- or this trigger should be moved to the 'addresses' table.
-- However, based on the provided logic, we are keeping the placeholder structure.
-- Placeholder logic:
IF TG_OP = 'INSERT' THEN
-- Logic to fetch address string based on NEW.address_id and geocode
NULL;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger to call the geocoding function.
DROP TRIGGER IF EXISTS on_store_location_address_change ON public.store_locations;
CREATE TRIGGER on_store_location_address_change
BEFORE INSERT OR UPDATE ON public.store_locations
FOR EACH ROW EXECUTE FUNCTION public.geocode_store_location();
-- 11. Trigger function to increment the fork_count on the original recipe.
DROP FUNCTION IF EXISTS public.increment_recipe_fork_count();
CREATE OR REPLACE FUNCTION public.increment_recipe_fork_count()
RETURNS TRIGGER AS $$
BEGIN
-- Only run if the recipe is a fork (original_recipe_id is not null).
IF NEW.original_recipe_id IS NOT NULL THEN
UPDATE public.recipes SET fork_count = fork_count + 1 WHERE recipe_id = NEW.original_recipe_id;
-- Award 'First Fork' achievement.
PERFORM public.award_achievement(NEW.user_id, 'First Fork');
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS on_recipe_fork ON public.recipes;
CREATE TRIGGER on_recipe_fork
AFTER INSERT ON public.recipes
FOR EACH ROW EXECUTE FUNCTION public.increment_recipe_fork_count();
-- =================================================================
-- Function: get_best_sale_prices_for_all_users()
-- Description: Retrieves the best sale price for every item on every user's watchlist.

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,23 +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(),
}));
// Mock the logger
vi.mock('../services/logger.client', () => ({
logger: {
error: 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);
describe('RecipeSuggester Component', () => {
beforeEach(() => {
@@ -45,7 +37,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 +52,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 +68,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 +82,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 +109,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 +140,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

@@ -12,12 +12,7 @@ import {
} from '../tests/utils/mockFactories';
import { mockUseFlyers, mockUseUserData } from '../tests/setup/mockHooks';
// Explicitly mock apiClient to ensure stable spies are used
vi.mock('../services/apiClient', () => ({
countFlyerItemsForFlyers: vi.fn(),
fetchFlyerItemsForFlyers: vi.fn(),
}));
// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
// Mock the hooks to avoid Missing Context errors
vi.mock('./useFlyers', () => ({
useFlyers: () => mockUseFlyers(),
@@ -30,14 +25,6 @@ vi.mock('../hooks/useUserData', () => ({
// The apiClient is globally mocked in our test setup, so we just need to cast it
const mockedApiClient = vi.mocked(apiClient);
// Mock the logger to prevent console noise
vi.mock('../services/logger.client', () => ({
logger: {
error: vi.fn(),
info: vi.fn(), // Added to prevent crashes on abort logging
},
}));
// Set a consistent "today" for testing flyer validity to make tests deterministic
const TODAY = new Date('2024-01-15T12:00:00.000Z');

View File

@@ -11,21 +11,9 @@ import { createMockUserProfile } from '../tests/utils/mockFactories';
import { logger } from '../services/logger.client';
// Mock the dependencies
vi.mock('../services/apiClient', () => ({
// Mock other functions if needed
getAuthenticatedUserProfile: vi.fn(),
}));
// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
vi.mock('../services/tokenStorage');
// Mock the logger to spy on its methods
vi.mock('../services/logger.client', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
}));
const mockedApiClient = vi.mocked(apiClient);
const mockedTokenStorage = vi.mocked(tokenStorage);

View File

@@ -3,12 +3,11 @@ import { renderHook } from '@testing-library/react';
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';
import { createMockFlyer, createMockFlyerItem } from '../tests/utils/mockFactories';
// 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 +60,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
@@ -171,11 +169,11 @@ describe('useFlyerItems Hook', () => {
const wrappedFetcher = mockedUseApiOnMount.mock.calls[0][0];
const mockResponse = new Response();
vi.mocked(apiClient.fetchFlyerItems).mockResolvedValue(mockResponse);
const mockedApiClient = vi.mocked(apiClient);
mockedApiClient.fetchFlyerItems.mockResolvedValue(mockResponse);
const response = await wrappedFetcher(123);
expect(apiClient.fetchFlyerItems).toHaveBeenCalledWith(123);
expect(mockedApiClient.fetchFlyerItems).toHaveBeenCalledWith(123);
expect(response).toBe(mockResponse);
});
});

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,25 +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>;
// Mock the logger
vi.mock('../services/logger.client', () => ({
logger: {
error: vi.fn(),
},
}));
// The apiClient is mocked globally in `src/tests/setup/globalApiMock.ts`.
const mockedApiClient = vi.mocked(apiClient);
// Mock lucide-react icons to prevent rendering errors in the test environment
vi.mock('lucide-react', () => ({

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();
@@ -982,6 +967,13 @@ describe('ProfileManager', () => {
it('should show error notification when auto-geocoding fails', async () => {
vi.useFakeTimers();
// FIX: Mock getUserAddress to return an address *without* coordinates.
// This is the condition required to trigger the auto-geocoding logic.
const addressWithoutCoords = { ...mockAddress, latitude: undefined, longitude: undefined };
mockedApiClient.getUserAddress.mockResolvedValue(
new Response(JSON.stringify(addressWithoutCoords)),
);
render(<ProfileManager {...defaultAuthenticatedProps} />);
// Wait for initial load

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({
@@ -198,7 +198,7 @@ describe('AuthProvider', () => {
await waitFor(() => {
// The error is now caught and displayed by the TestConsumer
expect(screen.getByTestId('error-display')).toHaveTextContent(
'Login succeeded, but failed to fetch your data: API is down',
'Login succeeded, but failed to fetch your data: Received null or undefined profile from API.',
);
expect(mockedTokenStorage.setToken).toHaveBeenCalledWith('test-token-no-profile');

View File

@@ -94,8 +94,8 @@ export class AIService {
// The fallback list is ordered by preference (speed/cost vs. power).
// We try the fastest models first, then the more powerful 'pro' model as a high-quality fallback,
// and finally the 'lite' model as a last resort.
private readonly models = [ 'gemini-3-flash-preview', 'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemma-3-27b', 'gemma-3-12b'];
private readonly models_lite = ["gemma-3-4b", "gemma-3-2b", "gemma-3-1b"];
private readonly models = [ 'gemini-3-flash-preview','gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite','gemini-2.0-flash-001','gemini-2.0-flash','gemini-2.0-flash-exp','gemini-2.0-flash-lite-001','gemini-2.0-flash-lite', 'gemma-3-27b-it', 'gemma-3-12b-it'];
private readonly models_lite = ["gemma-3-4b-it", "gemma-3-2b-it", "gemma-3-1b-it"];
constructor(logger: Logger, aiClient?: IAiClient, fs?: IFileSystem) {
this.logger = logger;

View File

@@ -103,17 +103,22 @@ export class FlyerRepository {
const result = await this.db.query<Flyer>(query, values);
return result.rows[0];
} catch (error) {
const isChecksumError =
error instanceof Error && error.message.includes('flyers_checksum_check');
const errorMessage = error instanceof Error ? error.message : '';
let checkMsg = 'A database check constraint failed.';
if (errorMessage.includes('flyers_checksum_check')) {
checkMsg =
'The provided checksum is invalid or does not meet format requirements (e.g., must be a 64-character SHA-256 hash).';
} else if (errorMessage.includes('flyers_status_check')) {
checkMsg = 'Invalid status provided for flyer.';
} else if (errorMessage.includes('url_check')) {
checkMsg = 'Invalid URL format provided for image or icon.';
}
handleDbError(error, logger, 'Database error in insertFlyer', { flyerData }, {
uniqueMessage: 'A flyer with this checksum already exists.',
fkMessage: 'The specified user or store for this flyer does not exist.',
// Provide a more specific message for the checksum constraint violation,
// which is a common issue during seeding or testing with placeholder data.
checkMessage: isChecksumError
? 'The provided checksum is invalid or does not meet format requirements (e.g., must be a 64-character SHA-256 hash).'
: 'Invalid status provided for flyer.',
checkMessage: checkMsg,
defaultMessage: 'Failed to insert flyer into database.',
});
}

View File

@@ -83,8 +83,8 @@ describe('FlyerDataTransformer', () => {
// 1. Check flyer data
expect(flyerData).toEqual({
file_name: originalFileName,
image_url: '/flyer-images/flyer-page-1.jpg',
icon_url: '/flyer-images/icons/icon-flyer-page-1.webp',
image_url: `http://localhost:3000/flyer-images/flyer-page-1.jpg`,
icon_url: `http://localhost:3000/flyer-images/icons/icon-flyer-page-1.webp`,
checksum,
store_name: 'Test Store',
valid_from: '2024-01-01',
@@ -167,8 +167,8 @@ describe('FlyerDataTransformer', () => {
expect(itemsForDb).toHaveLength(0);
expect(flyerData).toEqual({
file_name: originalFileName,
image_url: '/flyer-images/another.png',
icon_url: '/flyer-images/icons/icon-another.webp',
image_url: `http://localhost:3000/flyer-images/another.png`,
icon_url: `http://localhost:3000/flyer-images/icons/icon-another.webp`,
checksum,
store_name: 'Unknown Store (auto)', // Should use fallback
valid_from: null,

View File

@@ -23,14 +23,14 @@ export class FlyerDataTransformer {
): FlyerItemInsert {
return {
...item,
// Use logical OR to default falsy values (null, undefined, '') to a fallback.
// The trim is important for cases where the AI returns only whitespace.
item: String(item.item || '').trim() || 'Unknown Item',
// Use nullish coalescing to default only null/undefined to an empty string.
price_display: String(item.price_display ?? ''),
quantity: String(item.quantity ?? ''),
// Use logical OR to default falsy category names (null, undefined, '') to a fallback.
category_name: String(item.category_name || 'Other/Miscellaneous'),
// Use nullish coalescing and trim for robustness.
// An empty or whitespace-only name falls back to 'Unknown Item'.
item: (item.item ?? '').trim() || 'Unknown Item',
// Default null/undefined to an empty string and trim.
price_display: (item.price_display ?? '').trim(),
quantity: (item.quantity ?? '').trim(),
// An empty or whitespace-only category falls back to 'Other/Miscellaneous'.
category_name: (item.category_name ?? '').trim() || 'Other/Miscellaneous',
// Use nullish coalescing to convert null to undefined for the database.
master_item_id: item.master_item_id ?? undefined,
view_count: 0,
@@ -75,10 +75,13 @@ export class FlyerDataTransformer {
logger.warn('AI did not return a store name. Using fallback "Unknown Store (auto)".');
}
// Construct proper URLs including protocol and host to satisfy DB constraints
const baseUrl = process.env.BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
const flyerData: FlyerInsert = {
file_name: originalFileName,
image_url: `/flyer-images/${path.basename(firstImage)}`,
icon_url: `/flyer-images/icons/${iconFileName}`,
image_url: new URL(`/flyer-images/${path.basename(firstImage)}`, baseUrl).href,
icon_url: new URL(`/flyer-images/icons/${iconFileName}`, baseUrl).href,
checksum,
store_name: storeName,
valid_from: extractedData.valid_from,

View File

@@ -240,7 +240,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
console.error('[DEBUG] EXIF test job failed:', jobStatus.failedReason);
}
expect(jobStatus?.state).toBe('completed');
const flyerId = jobStatus?.data?.flyerId;
const flyerId = jobStatus?.returnValue?.flyerId;
expect(flyerId).toBeTypeOf('number');
createdFlyerIds.push(flyerId);
@@ -326,7 +326,7 @@ describe('Flyer Processing Background Job Integration Test', () => {
console.error('[DEBUG] PNG metadata test job failed:', jobStatus.failedReason);
}
expect(jobStatus?.state).toBe('completed');
const flyerId = jobStatus?.data?.flyerId;
const flyerId = jobStatus?.returnValue?.flyerId;
expect(flyerId).toBeTypeOf('number');
createdFlyerIds.push(flyerId);

View File

@@ -0,0 +1,80 @@
// 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(),
// --- Added to fix "No export is defined on the mock" errors ---
fetchFlyerItems: vi.fn(),
createShoppingList: vi.fn(),
deleteShoppingList: vi.fn(),
addShoppingListItem: vi.fn(),
updateShoppingListItem: vi.fn(),
removeShoppingListItem: vi.fn(),
addWatchedItem: vi.fn(),
removeWatchedItem: vi.fn(),
fetchBestSalePrices: vi.fn(),
resetPassword: vi.fn(),
getUserAchievements: vi.fn(),
uploadAvatar: vi.fn(),
countFlyerItemsForFlyers: vi.fn(),
fetchFlyerItemsForFlyers: 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}'],