172 lines
9.1 KiB
TypeScript
172 lines
9.1 KiB
TypeScript
// 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: () => <div data-testid="flyer-display-mock">Flyer Display</div> }));
|
|
vi.mock('./features/flyer/ExtractedDataTable', () => ({ ExtractedDataTable: () => <div data-testid="extracted-data-table-mock">Extracted Data Table</div> }));
|
|
vi.mock('./features/flyer/AnalysisPanel', () => ({ AnalysisPanel: () => <div data-testid="analysis-panel-mock">Analysis Panel</div> }));
|
|
vi.mock('./features/charts/PriceChart', () => ({ PriceChart: () => <div data-testid="price-chart-mock">Price Chart</div> }));
|
|
vi.mock('./components/Header', () => ({ Header: () => <header data-testid="header-mock">Header</header> }));
|
|
vi.mock('./features/flyer/BulkImporter', () => ({ BulkImporter: () => <div data-testid="bulk-importer-mock">Bulk Importer</div> }));
|
|
vi.mock('./features/charts/PriceHistoryChart', () => ({ PriceHistoryChart: () => <div data-testid="price-history-chart-mock">Price History Chart</div> }));
|
|
vi.mock('./features/flyer/FlyerList', () => ({ FlyerList: () => <div data-testid="flyer-list-mock">Flyer List</div> }));
|
|
vi.mock('./features/flyer/ProcessingStatus', () => ({ ProcessingStatus: () => <div data-testid="processing-status-mock">Processing Status</div> }));
|
|
vi.mock('./features/flyer/BulkImportSummary', () => ({ BulkImportSummary: () => <div data-testid="bulk-import-summary-mock">Bulk Import Summary</div> }));
|
|
vi.mock('./pages/admin/components/ProfileManager', () => ({ ProfileManager: () => <div data-testid="profile-manager-mock">Profile Manager</div> }));
|
|
vi.mock('./features/shopping/ShoppingList', () => ({ ShoppingListComponent: () => <div data-testid="shopping-list-mock">Shopping List</div> }));
|
|
vi.mock('./features/voice-assistant/VoiceAssistant', () => ({ VoiceAssistant: () => <div data-testid="voice-assistant-mock">Voice Assistant</div> }));
|
|
vi.mock('./pages/admin/AdminPage', () => ({ AdminPage: () => <div data-testid="admin-page-mock">Admin Page</div> }));
|
|
// In react-router v6, wrapper routes must render an <Outlet /> 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 }) => (
|
|
<div data-testid="admin-route-mock">
|
|
{children}
|
|
<Outlet />
|
|
</div>
|
|
)
|
|
}));
|
|
vi.mock('./pages/admin/CorrectionsPage', () => ({ CorrectionsPage: () => <div data-testid="corrections-page-mock">Corrections Page</div> }));
|
|
vi.mock('./pages/admin/ActivityLog', () => ({ ActivityLog: () => <div data-testid="activity-log-mock">Activity Log</div> }));
|
|
vi.mock('./features/shopping/WatchedItemsList', () => ({ WatchedItemsList: () => <div data-testid="watched-items-list-mock">Watched Items List</div> }));
|
|
vi.mock('./pages/admin/AdminStatsPage', () => ({ AdminStatsPage: () => <div data-testid="admin-stats-page-mock">Admin Stats Page</div> }));
|
|
vi.mock('./pages/ResetPasswordPage', () => ({ ResetPasswordPage: () => <div data-testid="reset-password-page-mock">Reset Password Page</div> }));
|
|
vi.mock('./pages/admin/components/AnonymousUserBanner', () => ({ AnonymousUserBanner: () => <div data-testid="anonymous-user-banner-mock">Anonymous User Banner</div> }));
|
|
vi.mock('./pages/VoiceLabPage', () => ({ VoiceLabPage: () => <div data-testid="voice-lab-page-mock">Voice Lab Page</div> }));
|
|
vi.mock('./components/WhatsNewModal', () => ({ WhatsNewModal: () => <div data-testid="whats-new-modal-mock">What's New Modal</div> }));
|
|
// 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<typeof apiClient>`, 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<typeof apiClient>;
|
|
|
|
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(
|
|
<MemoryRouter initialEntries={initialEntries}>
|
|
<App />
|
|
</MemoryRouter>
|
|
);
|
|
};
|
|
|
|
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();
|
|
});
|
|
});
|
|
}); |