// src/App.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 { MemoryRouter, Outlet } from 'react-router-dom'; import App from './App'; import * as apiClient from './services/apiClient'; // Mock useAuth to allow overriding the user state in tests const mockUseAuth = vi.fn(); vi.mock('./hooks/useAuth', () => ({ useAuth: () => mockUseAuth(), })); // Mock child components to isolate the App component vi.mock('./features/flyer/FlyerDisplay', () => ({ FlyerDisplay: () =>
Flyer Display
})); vi.mock('./features/flyer/ExtractedDataTable', () => ({ ExtractedDataTable: () =>
Extracted Data Table
})); vi.mock('./features/flyer/AnalysisPanel', () => ({ AnalysisPanel: () =>
Analysis Panel
})); vi.mock('./features/charts/PriceChart', () => ({ PriceChart: () =>
Price Chart
})); vi.mock('./components/Header', () => ({ Header: () =>
Header
})); vi.mock('./features/flyer/BulkImporter', () => ({ BulkImporter: () =>
Bulk Importer
})); vi.mock('./features/charts/PriceHistoryChart', () => ({ PriceHistoryChart: () =>
Price History Chart
})); vi.mock('./features/flyer/FlyerList', () => ({ FlyerList: () =>
Flyer List
})); vi.mock('./features/flyer/ProcessingStatus', () => ({ ProcessingStatus: () =>
Processing Status
})); vi.mock('./features/flyer/BulkImportSummary', () => ({ BulkImportSummary: () =>
Bulk Import Summary
})); vi.mock('./pages/admin/components/ProfileManager', () => ({ ProfileManager: () =>
Profile Manager
})); vi.mock('./features/shopping/ShoppingList', () => ({ ShoppingListComponent: () =>
Shopping List
})); vi.mock('./features/voice-assistant/VoiceAssistant', () => ({ VoiceAssistant: () =>
Voice Assistant
})); vi.mock('./pages/admin/AdminPage', () => ({ AdminPage: () =>
Admin Page
})); // In react-router v6, wrapper routes must render an for nested routes to appear. // Our previous mock was trying to render `{children}`, which is incorrect for this pattern. // This new mock correctly simulates the behavior of the actual AdminRoute component. vi.mock('./components/AdminRoute', () => ({ AdminRoute: ({ children }: { children?: React.ReactNode }) => (
{children}
) })); vi.mock('./pages/admin/CorrectionsPage', () => ({ CorrectionsPage: () =>
Corrections Page
})); vi.mock('./pages/admin/ActivityLog', () => ({ ActivityLog: () =>
Activity Log
})); vi.mock('./features/shopping/WatchedItemsList', () => ({ WatchedItemsList: () =>
Watched Items List
})); vi.mock('./pages/admin/AdminStatsPage', () => ({ AdminStatsPage: () =>
Admin Stats Page
})); vi.mock('./pages/ResetPasswordPage', () => ({ ResetPasswordPage: () =>
Reset Password Page
})); vi.mock('./pages/admin/components/AnonymousUserBanner', () => ({ AnonymousUserBanner: () =>
Anonymous User Banner
})); vi.mock('./pages/VoiceLabPage', () => ({ VoiceLabPage: () =>
Voice Lab Page
})); vi.mock('./components/WhatsNewModal', () => ({ WhatsNewModal: () =>
What's New Modal
})); // Mock pdfjs-dist to prevent the "DOMMatrix is not defined" error in JSDOM. // This must be done in any test file that imports App.tsx. vi.mock('pdfjs-dist', () => ({ GlobalWorkerOptions: { workerSrc: '' }, getDocument: vi.fn(() => ({ promise: Promise.resolve({ getPage: vi.fn() }), })), })); // By casting the apiClient to `Mocked`, we get type-safe access // to Vitest's mock functions like `mockResolvedValue`. // The `Mocked` type is imported directly from 'vitest' to avoid the namespace // collision that occurs when using `vi.Mocked` with an imported `vi` object. const mockedApiClient = apiClient as Mocked; describe('App Component', () => { beforeEach(() => { vi.clearAllMocks(); // Default auth state: loading or guest mockUseAuth.mockReturnValue({ user: null, profile: null, authStatus: 'Determining...', isLoading: true, }); // Clear local storage to prevent auth state from leaking between tests. localStorage.clear(); // Default mocks for API calls // Use mockImplementation to create a new Response object for each call, // preventing "Body has already been read" errors. // Use mockImplementation to create a new Response object for each call, // preventing "Body has already been read" errors. mockedApiClient.fetchFlyers.mockImplementation(() => Promise.resolve(new Response(JSON.stringify([])))); mockedApiClient.fetchMasterItems.mockImplementation(() => Promise.resolve(new Response(JSON.stringify([])))); mockedApiClient.fetchWatchedItems.mockImplementation(() => Promise.resolve(new Response(JSON.stringify([])))); mockedApiClient.fetchShoppingLists.mockImplementation(() => Promise.resolve(new Response(JSON.stringify([])))); mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(new Error('Not authenticated')); }); const renderApp = (initialEntries = ['/']) => { return render( ); }; it('should render the main layout and header', async () => { renderApp(); await waitFor(() => { expect(screen.getByTestId('header-mock')).toBeInTheDocument(); expect(screen.getByTestId('flyer-list-mock')).toBeInTheDocument(); // The BulkImporter is not rendered for anonymous users. expect(screen.queryByTestId('bulk-importer-mock')).not.toBeInTheDocument(); }); }); it('should render the BulkImporter for an admin user', async () => { // FIX 4: Update the BulkImporter test case to simulate an admin user via useAuth mock const mockAdminProfile = { user_id: 'admin-id', user: { user_id: 'admin-id', email: 'admin@example.com' }, role: 'admin', full_name: 'Admin', avatar_url: '', }; // Force the auth hook to return an authenticated admin user mockUseAuth.mockReturnValue({ user: mockAdminProfile.user, profile: mockAdminProfile, authStatus: 'AUTHENTICATED', isLoading: false, }); renderApp(); // Wait for the header (which means app loaded) AND the bulk importer await waitFor(() => { expect(screen.getByTestId('header-mock')).toBeInTheDocument(); expect(screen.getByTestId('bulk-importer-mock')).toBeInTheDocument(); }); }); it('should show a welcome message when no flyer is selected', async () => { renderApp(); await waitFor(() => { expect(screen.getByText(/welcome to flyer crawler/i)).toBeInTheDocument(); }); }); it('should render the admin page on the /admin route', async () => { // Mock a logged-in admin user const mockAdminProfile = { // The Profile type requires user_id at the top level, in addition // to the nested user object. user_id: 'admin-id', user: { user_id: 'admin-id', email: 'admin@example.com' }, role: 'admin', full_name: 'Admin', avatar_url: '', }; mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(new Response(JSON.stringify(mockAdminProfile))); // The app's auth hook checks for a token before fetching the user profile. // We need to mock it to allow the authentication flow to proceed. localStorage.setItem('authToken', 'fake-admin-token'); renderApp(['/admin']); // Use findByTestId to handle the asynchronous rendering of the page // after the user profile has been fetched and the auth state has been updated. expect(await screen.findByTestId('admin-page-mock')).toBeInTheDocument(); }); it('should render the reset password page on the correct route', async () => { renderApp(['/reset-password/some-token']); await waitFor(() => { expect(screen.getByTestId('reset-password-page-mock')).toBeInTheDocument(); }); }); });