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

This commit is contained in:
2026-01-01 23:12:27 -08:00
parent ce82034b9d
commit 19885a50f7
33 changed files with 922 additions and 211 deletions

View File

@@ -13,6 +13,12 @@ RULES:
latest refacter
Refactor `RecipeSuggester.test.tsx` to use `renderWithProviders`.
Create a new test file for `StatCard.tsx` to verify its props and rendering.
UPC SCANNING ! UPC SCANNING !

View File

@@ -1,9 +1,10 @@
// src/components/AchievementsList.test.tsx // src/components/AchievementsList.test.tsx
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { AchievementsList } from './AchievementsList'; import { AchievementsList } from './AchievementsList';
import { createMockUserAchievement } from '../tests/utils/mockFactories'; import { createMockUserAchievement } from '../tests/utils/mockFactories';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
describe('AchievementsList', () => { describe('AchievementsList', () => {
it('should render the list of achievements with correct details', () => { it('should render the list of achievements with correct details', () => {
@@ -24,7 +25,7 @@ describe('AchievementsList', () => {
createMockUserAchievement({ achievement_id: 3, name: 'Unknown Achievement', icon: 'star' }), // This icon is not in the component's map createMockUserAchievement({ achievement_id: 3, name: 'Unknown Achievement', icon: 'star' }), // This icon is not in the component's map
]; ];
render(<AchievementsList achievements={mockAchievements} />); renderWithProviders(<AchievementsList achievements={mockAchievements} />);
expect(screen.getByRole('heading', { name: /achievements/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /achievements/i })).toBeInTheDocument();
@@ -44,7 +45,7 @@ describe('AchievementsList', () => {
}); });
it('should render a message when there are no achievements', () => { it('should render a message when there are no achievements', () => {
render(<AchievementsList achievements={[]} />); renderWithProviders(<AchievementsList achievements={[]} />);
expect( expect(
screen.getByText('No achievements earned yet. Keep exploring to unlock them!'), screen.getByText('No achievements earned yet. Keep exploring to unlock them!'),
).toBeInTheDocument(); ).toBeInTheDocument();

View File

@@ -1,11 +1,12 @@
// src/components/AdminRoute.test.tsx // src/components/AdminRoute.test.tsx
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { MemoryRouter, Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import { AdminRoute } from './AdminRoute'; import { AdminRoute } from './AdminRoute';
import type { Profile } from '../types'; import type { Profile } from '../types';
import { createMockProfile } from '../tests/utils/mockFactories'; import { createMockProfile } from '../tests/utils/mockFactories';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Unmock the component to test the real implementation // Unmock the component to test the real implementation
vi.unmock('./AdminRoute'); vi.unmock('./AdminRoute');
@@ -14,15 +15,14 @@ const AdminContent = () => <div>Admin Page Content</div>;
const HomePage = () => <div>Home Page</div>; const HomePage = () => <div>Home Page</div>;
const renderWithRouter = (profile: Profile | null, initialPath: string) => { const renderWithRouter = (profile: Profile | null, initialPath: string) => {
render( renderWithProviders(
<MemoryRouter initialEntries={[initialPath]}> <Routes>
<Routes> <Route path="/" element={<HomePage />} />
<Route path="/" element={<HomePage />} /> <Route path="/admin" element={<AdminRoute profile={profile} />}>
<Route path="/admin" element={<AdminRoute profile={profile} />}> <Route index element={<AdminContent />} />
<Route index element={<AdminContent />} /> </Route>
</Route> </Routes>,
</Routes> { initialEntries: [initialPath] },
</MemoryRouter>,
); );
}; };

View File

@@ -1,8 +1,9 @@
// src/components/AnonymousUserBanner.test.tsx // src/components/AnonymousUserBanner.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { AnonymousUserBanner } from './AnonymousUserBanner'; import { AnonymousUserBanner } from './AnonymousUserBanner';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock the icon to ensure it is rendered correctly // Mock the icon to ensure it is rendered correctly
vi.mock('./icons/InformationCircleIcon', () => ({ vi.mock('./icons/InformationCircleIcon', () => ({
@@ -14,7 +15,7 @@ vi.mock('./icons/InformationCircleIcon', () => ({
describe('AnonymousUserBanner', () => { describe('AnonymousUserBanner', () => {
it('should render the banner with the correct text content and accessibility role', () => { it('should render the banner with the correct text content and accessibility role', () => {
const mockOnOpenProfile = vi.fn(); const mockOnOpenProfile = vi.fn();
render(<AnonymousUserBanner onOpenProfile={mockOnOpenProfile} />); renderWithProviders(<AnonymousUserBanner onOpenProfile={mockOnOpenProfile} />);
// Check for accessibility role // Check for accessibility role
expect(screen.getByRole('alert')).toBeInTheDocument(); expect(screen.getByRole('alert')).toBeInTheDocument();
@@ -30,7 +31,7 @@ describe('AnonymousUserBanner', () => {
it('should call onOpenProfile when the "sign up or log in" button is clicked', () => { it('should call onOpenProfile when the "sign up or log in" button is clicked', () => {
const mockOnOpenProfile = vi.fn(); const mockOnOpenProfile = vi.fn();
render(<AnonymousUserBanner onOpenProfile={mockOnOpenProfile} />); renderWithProviders(<AnonymousUserBanner onOpenProfile={mockOnOpenProfile} />);
const loginButton = screen.getByRole('button', { name: /sign up or log in/i }); const loginButton = screen.getByRole('button', { name: /sign up or log in/i });
fireEvent.click(loginButton); fireEvent.click(loginButton);

View File

@@ -1,14 +1,16 @@
// src/components/AppGuard.test.tsx // src/components/AppGuard.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, waitFor } from '@testing-library/react'; 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 { useModal } from '../hooks/useModal'; import { useModal } from '../hooks/useModal';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock dependencies // Mock dependencies
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,
@@ -38,7 +40,7 @@ describe('AppGuard', () => {
}); });
it('should render children', () => { it('should render children', () => {
render( renderWithProviders(
<AppGuard> <AppGuard>
<div>Child Content</div> <div>Child Content</div>
</AppGuard>, </AppGuard>,
@@ -51,7 +53,7 @@ describe('AppGuard', () => {
...mockedUseModal(), ...mockedUseModal(),
isModalOpen: (modalId) => modalId === 'whatsNew', isModalOpen: (modalId) => modalId === 'whatsNew',
}); });
render( renderWithProviders(
<AppGuard> <AppGuard>
<div>Child</div> <div>Child</div>
</AppGuard>, </AppGuard>,
@@ -64,7 +66,7 @@ describe('AppGuard', () => {
isDarkMode: true, isDarkMode: true,
unitSystem: 'imperial', unitSystem: 'imperial',
}); });
render( renderWithProviders(
<AppGuard> <AppGuard>
<div>Child</div> <div>Child</div>
</AppGuard>, </AppGuard>,
@@ -78,7 +80,7 @@ describe('AppGuard', () => {
}); });
it('should set light mode styles for toaster', async () => { it('should set light mode styles for toaster', async () => {
render( renderWithProviders(
<AppGuard> <AppGuard>
<div>Child</div> <div>Child</div>
</AppGuard>, </AppGuard>,

View File

@@ -1,8 +1,9 @@
// src/components/ConfirmationModal.test.tsx // src/components/ConfirmationModal.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ConfirmationModal } from './ConfirmationModal'; import { ConfirmationModal } from './ConfirmationModal';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
describe('ConfirmationModal (in components)', () => { describe('ConfirmationModal (in components)', () => {
const mockOnClose = vi.fn(); const mockOnClose = vi.fn();
@@ -21,12 +22,12 @@ describe('ConfirmationModal (in components)', () => {
}); });
it('should not render when isOpen is false', () => { it('should not render when isOpen is false', () => {
const { container } = render(<ConfirmationModal {...defaultProps} isOpen={false} />); const { container } = renderWithProviders(<ConfirmationModal {...defaultProps} isOpen={false} />);
expect(container.firstChild).toBeNull(); expect(container.firstChild).toBeNull();
}); });
it('should render correctly when isOpen is true', () => { it('should render correctly when isOpen is true', () => {
render(<ConfirmationModal {...defaultProps} />); renderWithProviders(<ConfirmationModal {...defaultProps} />);
expect(screen.getByRole('heading', { name: 'Confirm Action' })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Confirm Action' })).toBeInTheDocument();
expect(screen.getByText('Are you sure you want to do this?')).toBeInTheDocument(); expect(screen.getByText('Are you sure you want to do this?')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Confirm' })).toBeInTheDocument();
@@ -34,38 +35,38 @@ describe('ConfirmationModal (in components)', () => {
}); });
it('should call onConfirm when the confirm button is clicked', () => { it('should call onConfirm when the confirm button is clicked', () => {
render(<ConfirmationModal {...defaultProps} />); renderWithProviders(<ConfirmationModal {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'Confirm' })); fireEvent.click(screen.getByRole('button', { name: 'Confirm' }));
expect(mockOnConfirm).toHaveBeenCalledTimes(1); expect(mockOnConfirm).toHaveBeenCalledTimes(1);
}); });
it('should call onClose when the cancel button is clicked', () => { it('should call onClose when the cancel button is clicked', () => {
render(<ConfirmationModal {...defaultProps} />); renderWithProviders(<ConfirmationModal {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(mockOnClose).toHaveBeenCalledTimes(1); expect(mockOnClose).toHaveBeenCalledTimes(1);
}); });
it('should call onClose when the close icon is clicked', () => { it('should call onClose when the close icon is clicked', () => {
render(<ConfirmationModal {...defaultProps} />); renderWithProviders(<ConfirmationModal {...defaultProps} />);
fireEvent.click(screen.getByLabelText('Close confirmation modal')); fireEvent.click(screen.getByLabelText('Close confirmation modal'));
expect(mockOnClose).toHaveBeenCalledTimes(1); expect(mockOnClose).toHaveBeenCalledTimes(1);
}); });
it('should call onClose when the overlay is clicked', () => { it('should call onClose when the overlay is clicked', () => {
render(<ConfirmationModal {...defaultProps} />); renderWithProviders(<ConfirmationModal {...defaultProps} />);
// The overlay is the parent of the modal content div // The overlay is the parent of the modal content div
fireEvent.click(screen.getByRole('dialog')); fireEvent.click(screen.getByRole('dialog'));
expect(mockOnClose).toHaveBeenCalledTimes(1); expect(mockOnClose).toHaveBeenCalledTimes(1);
}); });
it('should not call onClose when clicking inside the modal content', () => { it('should not call onClose when clicking inside the modal content', () => {
render(<ConfirmationModal {...defaultProps} />); renderWithProviders(<ConfirmationModal {...defaultProps} />);
fireEvent.click(screen.getByText('Are you sure you want to do this?')); fireEvent.click(screen.getByText('Are you sure you want to do this?'));
expect(mockOnClose).not.toHaveBeenCalled(); expect(mockOnClose).not.toHaveBeenCalled();
}); });
it('should render custom button text and classes', () => { it('should render custom button text and classes', () => {
render( renderWithProviders(
<ConfirmationModal <ConfirmationModal
{...defaultProps} {...defaultProps}
confirmButtonText="Yes, Delete" confirmButtonText="Yes, Delete"

View File

@@ -1,8 +1,9 @@
// src/components/DarkModeToggle.test.tsx // src/components/DarkModeToggle.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { DarkModeToggle } from './DarkModeToggle'; import { DarkModeToggle } from './DarkModeToggle';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock the icon components to isolate the toggle's logic // Mock the icon components to isolate the toggle's logic
vi.mock('./icons/SunIcon', () => ({ vi.mock('./icons/SunIcon', () => ({
@@ -20,7 +21,7 @@ describe('DarkModeToggle', () => {
}); });
it('should render in light mode state', () => { it('should render in light mode state', () => {
render(<DarkModeToggle isDarkMode={false} onToggle={mockOnToggle} />); renderWithProviders(<DarkModeToggle isDarkMode={false} onToggle={mockOnToggle} />);
const checkbox = screen.getByRole('checkbox'); const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked(); expect(checkbox).not.toBeChecked();
@@ -29,7 +30,7 @@ describe('DarkModeToggle', () => {
}); });
it('should render in dark mode state', () => { it('should render in dark mode state', () => {
render(<DarkModeToggle isDarkMode={true} onToggle={mockOnToggle} />); renderWithProviders(<DarkModeToggle isDarkMode={true} onToggle={mockOnToggle} />);
const checkbox = screen.getByRole('checkbox'); const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeChecked(); expect(checkbox).toBeChecked();
@@ -38,7 +39,7 @@ describe('DarkModeToggle', () => {
}); });
it('should call onToggle when the label is clicked', () => { it('should call onToggle when the label is clicked', () => {
render(<DarkModeToggle isDarkMode={false} onToggle={mockOnToggle} />); renderWithProviders(<DarkModeToggle isDarkMode={false} onToggle={mockOnToggle} />);
// Clicking the label triggers the checkbox change // Clicking the label triggers the checkbox change
const label = screen.getByTitle('Switch to Dark Mode'); const label = screen.getByTitle('Switch to Dark Mode');

View File

@@ -0,0 +1,67 @@
// src/components/Dashboard.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { screen } from '@testing-library/react';
import { Dashboard } from './Dashboard';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock child components to isolate Dashboard logic
// Note: The Dashboard component imports these using '../components/RecipeSuggester'
// which resolves to the same file as './RecipeSuggester' when inside src/components.
vi.mock('./RecipeSuggester', () => ({
RecipeSuggester: () => <div data-testid="recipe-suggester-mock">Recipe Suggester</div>,
}));
vi.mock('./FlyerCountDisplay', () => ({
FlyerCountDisplay: () => <div data-testid="flyer-count-display-mock">Flyer Count Display</div>,
}));
vi.mock('./Leaderboard', () => ({
Leaderboard: () => <div data-testid="leaderboard-mock">Leaderboard</div>,
}));
describe('Dashboard Component', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders the dashboard title', () => {
console.log('TEST: Verifying dashboard title render');
renderWithProviders(<Dashboard />);
expect(screen.getByRole('heading', { name: /dashboard/i, level: 1 })).toBeInTheDocument();
});
it('renders the RecipeSuggester widget', () => {
console.log('TEST: Verifying RecipeSuggester presence');
renderWithProviders(<Dashboard />);
expect(screen.getByTestId('recipe-suggester-mock')).toBeInTheDocument();
});
it('renders the FlyerCountDisplay widget within the "Your Flyers" section', () => {
console.log('TEST: Verifying FlyerCountDisplay presence and section title');
renderWithProviders(<Dashboard />);
// Check for the section heading
expect(screen.getByRole('heading', { name: /your flyers/i, level: 2 })).toBeInTheDocument();
// Check for the component
expect(screen.getByTestId('flyer-count-display-mock')).toBeInTheDocument();
});
it('renders the Leaderboard widget in the sidebar area', () => {
console.log('TEST: Verifying Leaderboard presence');
renderWithProviders(<Dashboard />);
expect(screen.getByTestId('leaderboard-mock')).toBeInTheDocument();
});
it('renders with the correct grid layout classes', () => {
console.log('TEST: Verifying layout classes');
const { container } = renderWithProviders(<Dashboard />);
// The main grid container
const gridContainer = container.querySelector('.grid');
expect(gridContainer).toBeInTheDocument();
expect(gridContainer).toHaveClass('grid-cols-1');
expect(gridContainer).toHaveClass('lg:grid-cols-3');
expect(gridContainer).toHaveClass('gap-6');
});
});

View File

@@ -1,24 +1,25 @@
// src/components/ErrorDisplay.test.tsx // src/components/ErrorDisplay.test.tsx
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { ErrorDisplay } from './ErrorDisplay'; import { ErrorDisplay } from './ErrorDisplay';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
describe('ErrorDisplay (in components)', () => { describe('ErrorDisplay (in components)', () => {
it('should not render when the message is empty', () => { it('should not render when the message is empty', () => {
const { container } = render(<ErrorDisplay message="" />); const { container } = renderWithProviders(<ErrorDisplay message="" />);
expect(container.firstChild).toBeNull(); expect(container.firstChild).toBeNull();
}); });
it('should not render when the message is null', () => { it('should not render when the message is null', () => {
// The component expects a string, but we test for nullish values as a safeguard. // The component expects a string, but we test for nullish values as a safeguard.
const { container } = render(<ErrorDisplay message={null as unknown as string} />); const { container } = renderWithProviders(<ErrorDisplay message={null as unknown as string} />);
expect(container.firstChild).toBeNull(); expect(container.firstChild).toBeNull();
}); });
it('should render the error message when provided', () => { it('should render the error message when provided', () => {
const errorMessage = 'Something went terribly wrong.'; const errorMessage = 'Something went terribly wrong.';
render(<ErrorDisplay message={errorMessage} />); renderWithProviders(<ErrorDisplay message={errorMessage} />);
const alert = screen.getByRole('alert'); const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument(); expect(alert).toBeInTheDocument();

View File

@@ -1,10 +1,11 @@
// src/components/FlyerCorrectionTool.test.tsx // src/components/FlyerCorrectionTool.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import { screen, fireEvent, waitFor, 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 { FlyerCorrectionTool } from './FlyerCorrectionTool'; import { FlyerCorrectionTool } from './FlyerCorrectionTool';
import * as aiApiClient from '../services/aiApiClient'; import * as aiApiClient from '../services/aiApiClient';
import { notifyError, notifySuccess } from '../services/notificationService'; import { notifyError, notifySuccess } from '../services/notificationService';
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');
@@ -54,12 +55,12 @@ describe('FlyerCorrectionTool', () => {
}); });
it('should not render when isOpen is false', () => { it('should not render when isOpen is false', () => {
const { container } = render(<FlyerCorrectionTool {...defaultProps} isOpen={false} />); const { container } = renderWithProviders(<FlyerCorrectionTool {...defaultProps} isOpen={false} />);
expect(container.firstChild).toBeNull(); expect(container.firstChild).toBeNull();
}); });
it('should render correctly when isOpen is true', () => { it('should render correctly when isOpen is true', () => {
render(<FlyerCorrectionTool {...defaultProps} />); renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
expect(screen.getByRole('heading', { name: /flyer correction tool/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /flyer correction tool/i })).toBeInTheDocument();
expect(screen.getByAltText('Flyer for correction')).toBeInTheDocument(); expect(screen.getByAltText('Flyer for correction')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /extract store name/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /extract store name/i })).toBeInTheDocument();
@@ -67,7 +68,7 @@ describe('FlyerCorrectionTool', () => {
}); });
it('should call onClose when the close button is clicked', () => { it('should call onClose when the close button is clicked', () => {
render(<FlyerCorrectionTool {...defaultProps} />); renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
// Use the specific aria-label defined in the component to find the close button // Use the specific aria-label defined in the component to find the close button
const closeButton = screen.getByLabelText(/close correction tool/i); const closeButton = screen.getByLabelText(/close correction tool/i);
fireEvent.click(closeButton); fireEvent.click(closeButton);
@@ -75,13 +76,13 @@ describe('FlyerCorrectionTool', () => {
}); });
it('should have disabled extraction buttons initially', () => { it('should have disabled extraction buttons initially', () => {
render(<FlyerCorrectionTool {...defaultProps} />); renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
expect(screen.getByRole('button', { name: /extract store name/i })).toBeDisabled(); expect(screen.getByRole('button', { name: /extract store name/i })).toBeDisabled();
expect(screen.getByRole('button', { name: /extract sale dates/i })).toBeDisabled(); expect(screen.getByRole('button', { name: /extract sale dates/i })).toBeDisabled();
}); });
it('should enable extraction buttons after a selection is made', () => { it('should enable extraction buttons after a selection is made', () => {
render(<FlyerCorrectionTool {...defaultProps} />); renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
const canvas = screen.getByRole('dialog').querySelector('canvas')!; const canvas = screen.getByRole('dialog').querySelector('canvas')!;
// Simulate drawing a rectangle // Simulate drawing a rectangle
@@ -94,7 +95,7 @@ describe('FlyerCorrectionTool', () => {
}); });
it('should stop drawing when the mouse leaves the canvas', () => { it('should stop drawing when the mouse leaves the canvas', () => {
render(<FlyerCorrectionTool {...defaultProps} />); renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
const canvas = screen.getByRole('dialog').querySelector('canvas')!; const canvas = screen.getByRole('dialog').querySelector('canvas')!;
fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 }); fireEvent.mouseDown(canvas, { clientX: 10, clientY: 10 });
@@ -114,7 +115,7 @@ describe('FlyerCorrectionTool', () => {
}); });
mockedAiApiClient.rescanImageArea.mockReturnValue(rescanPromise); mockedAiApiClient.rescanImageArea.mockReturnValue(rescanPromise);
render(<FlyerCorrectionTool {...defaultProps} />); renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
// Wait for the image fetch to complete to ensure 'imageFile' state is populated // Wait for the image fetch to complete to ensure 'imageFile' state is populated
console.log('--- [TEST LOG] ---: Awaiting image fetch inside component...'); console.log('--- [TEST LOG] ---: Awaiting image fetch inside component...');
@@ -192,7 +193,7 @@ describe('FlyerCorrectionTool', () => {
// Mock fetch to reject // Mock fetch to reject
global.fetch = vi.fn(() => Promise.reject(new Error('Network error'))) as Mocked<typeof fetch>; global.fetch = vi.fn(() => Promise.reject(new Error('Network error'))) as Mocked<typeof fetch>;
render(<FlyerCorrectionTool {...defaultProps} />); renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
await waitFor(() => { await waitFor(() => {
expect(mockedNotifyError).toHaveBeenCalledWith('Could not load the image for correction.'); expect(mockedNotifyError).toHaveBeenCalledWith('Could not load the image for correction.');
@@ -211,7 +212,7 @@ describe('FlyerCorrectionTool', () => {
return new Promise(() => {}); return new Promise(() => {});
}) as Mocked<typeof fetch>; }) as Mocked<typeof fetch>;
render(<FlyerCorrectionTool {...defaultProps} />); renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
const canvas = screen.getByRole('dialog').querySelector('canvas')!; const canvas = screen.getByRole('dialog').querySelector('canvas')!;
@@ -238,7 +239,7 @@ describe('FlyerCorrectionTool', () => {
it('should handle non-standard API errors during rescan', async () => { it('should handle non-standard API errors during rescan', async () => {
console.log('TEST: Starting "should handle non-standard API errors during rescan"'); console.log('TEST: Starting "should handle non-standard API errors during rescan"');
mockedAiApiClient.rescanImageArea.mockRejectedValue('A plain string error'); mockedAiApiClient.rescanImageArea.mockRejectedValue('A plain string error');
render(<FlyerCorrectionTool {...defaultProps} />); renderWithProviders(<FlyerCorrectionTool {...defaultProps} />);
// Wait for image fetch to ensure imageFile is set before we interact // Wait for image fetch to ensure imageFile is set before we interact
await waitFor(() => expect(global.fetch).toHaveBeenCalled()); await waitFor(() => expect(global.fetch).toHaveBeenCalled());

View File

@@ -1,11 +1,12 @@
// src/components/FlyerCountDisplay.test.tsx // src/components/FlyerCountDisplay.test.tsx
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { FlyerCountDisplay } from './FlyerCountDisplay'; import { FlyerCountDisplay } from './FlyerCountDisplay';
import { useFlyers } from '../hooks/useFlyers'; import { useFlyers } from '../hooks/useFlyers';
import type { Flyer } from '../types'; import type { Flyer } from '../types';
import { createMockFlyer } from '../tests/utils/mockFactories'; import { createMockFlyer } from '../tests/utils/mockFactories';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock the dependencies // Mock the dependencies
vi.mock('../hooks/useFlyers'); vi.mock('../hooks/useFlyers');
@@ -32,7 +33,7 @@ describe('FlyerCountDisplay', () => {
}); });
// Act: Render the component. // Act: Render the component.
render(<FlyerCountDisplay />); renderWithProviders(<FlyerCountDisplay />);
// Assert: Check that the loading spinner is visible. // Assert: Check that the loading spinner is visible.
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument(); expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
@@ -53,7 +54,7 @@ describe('FlyerCountDisplay', () => {
}); });
// Act // Act
render(<FlyerCountDisplay />); renderWithProviders(<FlyerCountDisplay />);
// Assert: Check that the error message is displayed. // Assert: Check that the error message is displayed.
expect(screen.getByRole('alert')).toHaveTextContent(errorMessage); expect(screen.getByRole('alert')).toHaveTextContent(errorMessage);
@@ -73,7 +74,7 @@ describe('FlyerCountDisplay', () => {
}); });
// Act // Act
render(<FlyerCountDisplay />); renderWithProviders(<FlyerCountDisplay />);
// Assert: Check that the correct count is displayed. // Assert: Check that the correct count is displayed.
const countDisplay = screen.getByTestId('flyer-count'); const countDisplay = screen.getByTestId('flyer-count');

View File

@@ -1,8 +1,9 @@
// src/components/Footer.test.tsx // src/components/Footer.test.tsx
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Footer } from './Footer'; import { Footer } from './Footer';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
describe('Footer', () => { describe('Footer', () => {
beforeEach(() => { beforeEach(() => {
@@ -21,7 +22,7 @@ describe('Footer', () => {
vi.setSystemTime(mockDate); vi.setSystemTime(mockDate);
// Act: Render the component // Act: Render the component
render(<Footer />); renderWithProviders(<Footer />);
// Assert: Check that the rendered text includes the mocked year // Assert: Check that the rendered text includes the mocked year
expect(screen.getByText('Copyright 2025-2025')).toBeInTheDocument(); expect(screen.getByText('Copyright 2025-2025')).toBeInTheDocument();
@@ -29,7 +30,7 @@ describe('Footer', () => {
it('should display the correct year when it changes', () => { it('should display the correct year when it changes', () => {
vi.setSystemTime(new Date('2030-01-01T00:00:00Z')); vi.setSystemTime(new Date('2030-01-01T00:00:00Z'));
render(<Footer />); renderWithProviders(<Footer />);
expect(screen.getByText('Copyright 2025-2030')).toBeInTheDocument(); expect(screen.getByText('Copyright 2025-2030')).toBeInTheDocument();
}); });
}); });

View File

@@ -1,11 +1,11 @@
// src/components/Header.test.tsx // src/components/Header.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import { Header } from './Header'; import { Header } from './Header';
import type { UserProfile } from '../types'; import type { UserProfile } from '../types';
import { createMockUserProfile } from '../tests/utils/mockFactories'; import { createMockUserProfile } from '../tests/utils/mockFactories';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Unmock the component to test the real implementation // Unmock the component to test the real implementation
vi.unmock('./Header'); vi.unmock('./Header');
@@ -34,12 +34,8 @@ const defaultProps = {
}; };
// Helper to render with router context // Helper to render with router context
const renderWithRouter = (props: Partial<React.ComponentProps<typeof Header>>) => { const renderHeader = (props: Partial<React.ComponentProps<typeof Header>>) => {
return render( return renderWithProviders(<Header {...defaultProps} {...props} />);
<MemoryRouter>
<Header {...defaultProps} {...props} />
</MemoryRouter>,
);
}; };
describe('Header', () => { describe('Header', () => {
@@ -48,30 +44,30 @@ describe('Header', () => {
}); });
it('should render the application title', () => { it('should render the application title', () => {
renderWithRouter({}); renderHeader({});
expect(screen.getByRole('heading', { name: /flyer crawler/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /flyer crawler/i })).toBeInTheDocument();
}); });
it('should display unit system and theme mode', () => { it('should display unit system and theme mode', () => {
renderWithRouter({ isDarkMode: true, unitSystem: 'metric' }); renderHeader({ isDarkMode: true, unitSystem: 'metric' });
expect(screen.getByText(/metric/i)).toBeInTheDocument(); expect(screen.getByText(/metric/i)).toBeInTheDocument();
expect(screen.getByText(/dark mode/i)).toBeInTheDocument(); expect(screen.getByText(/dark mode/i)).toBeInTheDocument();
}); });
describe('When user is logged out', () => { describe('When user is logged out', () => {
it('should show a Login button', () => { it('should show a Login button', () => {
renderWithRouter({ userProfile: null, authStatus: 'SIGNED_OUT' }); renderHeader({ userProfile: null, authStatus: 'SIGNED_OUT' });
expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /login/i })).toBeInTheDocument();
}); });
it('should call onOpenProfile when Login button is clicked', () => { it('should call onOpenProfile when Login button is clicked', () => {
renderWithRouter({ userProfile: null, authStatus: 'SIGNED_OUT' }); renderHeader({ userProfile: null, authStatus: 'SIGNED_OUT' });
fireEvent.click(screen.getByRole('button', { name: /login/i })); fireEvent.click(screen.getByRole('button', { name: /login/i }));
expect(mockOnOpenProfile).toHaveBeenCalledTimes(1); expect(mockOnOpenProfile).toHaveBeenCalledTimes(1);
}); });
it('should not show user-specific buttons', () => { it('should not show user-specific buttons', () => {
renderWithRouter({ userProfile: null, authStatus: 'SIGNED_OUT' }); renderHeader({ userProfile: null, authStatus: 'SIGNED_OUT' });
expect(screen.queryByLabelText(/open voice assistant/i)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/open voice assistant/i)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/open my account settings/i)).not.toBeInTheDocument(); expect(screen.queryByLabelText(/open my account settings/i)).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /logout/i })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /logout/i })).not.toBeInTheDocument();
@@ -80,29 +76,29 @@ describe('Header', () => {
describe('When user is authenticated', () => { describe('When user is authenticated', () => {
it('should display the user email', () => { it('should display the user email', () => {
renderWithRouter({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' }); renderHeader({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' });
expect(screen.getByText(mockUserProfile.user.email)).toBeInTheDocument(); expect(screen.getByText(mockUserProfile.user.email)).toBeInTheDocument();
}); });
it('should display "Guest" for anonymous users', () => { it('should display "Guest" for anonymous users', () => {
renderWithRouter({ userProfile: mockUserProfile, authStatus: 'SIGNED_OUT' }); renderHeader({ userProfile: mockUserProfile, authStatus: 'SIGNED_OUT' });
expect(screen.getByText(/guest/i)).toBeInTheDocument(); expect(screen.getByText(/guest/i)).toBeInTheDocument();
}); });
it('should call onOpenVoiceAssistant when microphone icon is clicked', () => { it('should call onOpenVoiceAssistant when microphone icon is clicked', () => {
renderWithRouter({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' }); renderHeader({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' });
fireEvent.click(screen.getByLabelText(/open voice assistant/i)); fireEvent.click(screen.getByLabelText(/open voice assistant/i));
expect(mockOnOpenVoiceAssistant).toHaveBeenCalledTimes(1); expect(mockOnOpenVoiceAssistant).toHaveBeenCalledTimes(1);
}); });
it('should call onOpenProfile when cog icon is clicked', () => { it('should call onOpenProfile when cog icon is clicked', () => {
renderWithRouter({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' }); renderHeader({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' });
fireEvent.click(screen.getByLabelText(/open my account settings/i)); fireEvent.click(screen.getByLabelText(/open my account settings/i));
expect(mockOnOpenProfile).toHaveBeenCalledTimes(1); expect(mockOnOpenProfile).toHaveBeenCalledTimes(1);
}); });
it('should call onSignOut when Logout button is clicked', () => { it('should call onSignOut when Logout button is clicked', () => {
renderWithRouter({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' }); renderHeader({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' });
fireEvent.click(screen.getByRole('button', { name: /logout/i })); fireEvent.click(screen.getByRole('button', { name: /logout/i }));
expect(mockOnSignOut).toHaveBeenCalledTimes(1); expect(mockOnSignOut).toHaveBeenCalledTimes(1);
}); });
@@ -110,14 +106,14 @@ describe('Header', () => {
describe('Admin user', () => { describe('Admin user', () => {
it('should show the Admin Area link for admin users', () => { it('should show the Admin Area link for admin users', () => {
renderWithRouter({ userProfile: mockAdminProfile, authStatus: 'AUTHENTICATED' }); renderHeader({ userProfile: mockAdminProfile, authStatus: 'AUTHENTICATED' });
const adminLink = screen.getByTitle(/admin area/i); const adminLink = screen.getByTitle(/admin area/i);
expect(adminLink).toBeInTheDocument(); expect(adminLink).toBeInTheDocument();
expect(adminLink.closest('a')).toHaveAttribute('href', '/admin'); expect(adminLink.closest('a')).toHaveAttribute('href', '/admin');
}); });
it('should not show the Admin Area link for non-admin users', () => { it('should not show the Admin Area link for non-admin users', () => {
renderWithRouter({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' }); renderHeader({ userProfile: mockUserProfile, authStatus: 'AUTHENTICATED' });
expect(screen.queryByTitle(/admin area/i)).not.toBeInTheDocument(); expect(screen.queryByTitle(/admin area/i)).not.toBeInTheDocument();
}); });
}); });

View File

@@ -1,12 +1,13 @@
// src/components/Leaderboard.test.tsx // src/components/Leaderboard.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, waitFor } from '@testing-library/react'; import { 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 Leaderboard from './Leaderboard'; import Leaderboard from './Leaderboard';
import * as apiClient from '../services/apiClient'; import * as apiClient from '../services/apiClient';
import { LeaderboardUser } from '../types'; import { LeaderboardUser } from '../types';
import { createMockLeaderboardUser } from '../tests/utils/mockFactories'; import { createMockLeaderboardUser } from '../tests/utils/mockFactories';
import { createMockLogger } from '../tests/utils/mockLogger'; import { createMockLogger } from '../tests/utils/mockLogger';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock the apiClient // Mock the apiClient
vi.mock('../services/apiClient'); // This was correct vi.mock('../services/apiClient'); // This was correct
@@ -45,13 +46,13 @@ describe('Leaderboard', () => {
it('should display a loading message initially', () => { it('should display a loading message initially', () => {
// Mock a pending promise that never resolves to keep it in the loading state // Mock a pending promise that never resolves to keep it in the loading state
mockedApiClient.fetchLeaderboard.mockReturnValue(new Promise(() => {})); mockedApiClient.fetchLeaderboard.mockReturnValue(new Promise(() => {}));
render(<Leaderboard />); renderWithProviders(<Leaderboard />);
expect(screen.getByText('Loading Leaderboard...')).toBeInTheDocument(); expect(screen.getByText('Loading Leaderboard...')).toBeInTheDocument();
}); });
it('should display an error message if the API call fails', async () => { it('should display an error message if the API call fails', async () => {
mockedApiClient.fetchLeaderboard.mockResolvedValue(new Response(null, { status: 500 })); mockedApiClient.fetchLeaderboard.mockResolvedValue(new Response(null, { status: 500 }));
render(<Leaderboard />); renderWithProviders(<Leaderboard />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument(); expect(screen.getByRole('alert')).toBeInTheDocument();
@@ -62,7 +63,7 @@ describe('Leaderboard', () => {
it('should display a generic error for unknown error types', async () => { it('should display a generic error for unknown error types', async () => {
const unknownError = 'A string error'; const unknownError = 'A string error';
mockedApiClient.fetchLeaderboard.mockRejectedValue(unknownError); mockedApiClient.fetchLeaderboard.mockRejectedValue(unknownError);
render(<Leaderboard />); renderWithProviders(<Leaderboard />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument(); expect(screen.getByRole('alert')).toBeInTheDocument();
@@ -72,7 +73,7 @@ describe('Leaderboard', () => {
it('should display a message when the leaderboard is empty', async () => { it('should display a message when the leaderboard is empty', async () => {
mockedApiClient.fetchLeaderboard.mockResolvedValue(new Response(JSON.stringify([]))); mockedApiClient.fetchLeaderboard.mockResolvedValue(new Response(JSON.stringify([])));
render(<Leaderboard />); renderWithProviders(<Leaderboard />);
await waitFor(() => { await waitFor(() => {
expect( expect(
@@ -85,7 +86,7 @@ describe('Leaderboard', () => {
mockedApiClient.fetchLeaderboard.mockResolvedValue( mockedApiClient.fetchLeaderboard.mockResolvedValue(
new Response(JSON.stringify(mockLeaderboardData)), new Response(JSON.stringify(mockLeaderboardData)),
); );
render(<Leaderboard />); renderWithProviders(<Leaderboard />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByRole('heading', { name: 'Top Users' })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: 'Top Users' })).toBeInTheDocument();
@@ -110,7 +111,7 @@ describe('Leaderboard', () => {
mockedApiClient.fetchLeaderboard.mockResolvedValue( mockedApiClient.fetchLeaderboard.mockResolvedValue(
new Response(JSON.stringify(mockLeaderboardData)), new Response(JSON.stringify(mockLeaderboardData)),
); );
render(<Leaderboard />); renderWithProviders(<Leaderboard />);
await waitFor(() => { await waitFor(() => {
// Rank 1, 2, and 3 should have a crown icon // Rank 1, 2, and 3 should have a crown icon
@@ -129,7 +130,7 @@ describe('Leaderboard', () => {
mockedApiClient.fetchLeaderboard.mockResolvedValue( mockedApiClient.fetchLeaderboard.mockResolvedValue(
new Response(JSON.stringify(dataWithMissingNames)), new Response(JSON.stringify(dataWithMissingNames)),
); );
render(<Leaderboard />); renderWithProviders(<Leaderboard />);
await waitFor(() => { await waitFor(() => {
// Check for fallback name // Check for fallback name

View File

@@ -1,19 +1,19 @@
// src/components/LoadingSpinner.test.tsx // src/components/LoadingSpinner.test.tsx
import React from 'react'; import React from 'react';
import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { LoadingSpinner } from './LoadingSpinner'; import { LoadingSpinner } from './LoadingSpinner';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
describe('LoadingSpinner (in components)', () => { describe('LoadingSpinner (in components)', () => {
it('should render the SVG with animation classes', () => { it('should render the SVG with animation classes', () => {
const { container } = render(<LoadingSpinner />); const { container } = renderWithProviders(<LoadingSpinner />);
const svgElement = container.querySelector('svg'); const svgElement = container.querySelector('svg');
expect(svgElement).toBeInTheDocument(); expect(svgElement).toBeInTheDocument();
expect(svgElement).toHaveClass('animate-spin'); expect(svgElement).toHaveClass('animate-spin');
}); });
it('should contain the correct SVG paths for the spinner graphic', () => { it('should contain the correct SVG paths for the spinner graphic', () => {
const { container } = render(<LoadingSpinner />); const { container } = renderWithProviders(<LoadingSpinner />);
const circle = container.querySelector('circle'); const circle = container.querySelector('circle');
const path = container.querySelector('path'); const path = container.querySelector('path');
expect(circle).toBeInTheDocument(); expect(circle).toBeInTheDocument();

View File

@@ -1,9 +1,10 @@
// src/components/MapView.test.tsx // src/components/MapView.test.tsx
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MapView } from './MapView'; import { MapView } from './MapView';
import config from '../config'; import config from '../config';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Create a type-safe mocked version of the config for easier manipulation // Create a type-safe mocked version of the config for easier manipulation
const mockedConfig = vi.mocked(config); const mockedConfig = vi.mocked(config);
@@ -40,14 +41,14 @@ describe('MapView', () => {
describe('when API key is not configured', () => { describe('when API key is not configured', () => {
it('should render a disabled message', () => { it('should render a disabled message', () => {
render(<MapView {...defaultProps} />); renderWithProviders(<MapView {...defaultProps} />);
expect( expect(
screen.getByText('Map view is disabled: API key is not configured.'), screen.getByText('Map view is disabled: API key is not configured.'),
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
it('should not render the iframe', () => { it('should not render the iframe', () => {
render(<MapView {...defaultProps} />); renderWithProviders(<MapView {...defaultProps} />);
// Use queryByTitle because iframes don't have a default "iframe" role // Use queryByTitle because iframes don't have a default "iframe" role
expect(screen.queryByTitle('Map view')).not.toBeInTheDocument(); expect(screen.queryByTitle('Map view')).not.toBeInTheDocument();
}); });
@@ -62,7 +63,7 @@ describe('MapView', () => {
}); });
it('should render the iframe with the correct src URL', () => { it('should render the iframe with the correct src URL', () => {
render(<MapView {...defaultProps} />); renderWithProviders(<MapView {...defaultProps} />);
// Use getByTitle to access the iframe // Use getByTitle to access the iframe
const iframe = screen.getByTitle('Map view'); const iframe = screen.getByTitle('Map view');

View File

@@ -1,8 +1,9 @@
// src/components/PasswordInput.test.tsx // src/components/PasswordInput.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import { PasswordInput } from './PasswordInput'; import { PasswordInput } from './PasswordInput';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Mock the child PasswordStrengthIndicator component to isolate the test (relative to new location) // Mock the child PasswordStrengthIndicator component to isolate the test (relative to new location)
vi.mock('./PasswordStrengthIndicator', () => ({ vi.mock('./PasswordStrengthIndicator', () => ({
PasswordStrengthIndicator: ({ password }: { password?: string }) => ( PasswordStrengthIndicator: ({ password }: { password?: string }) => (
@@ -12,13 +13,13 @@ vi.mock('./PasswordStrengthIndicator', () => ({
describe('PasswordInput (in auth feature)', () => { describe('PasswordInput (in auth feature)', () => {
it('should render as a password input by default', () => { it('should render as a password input by default', () => {
render(<PasswordInput placeholder="Enter password" />); renderWithProviders(<PasswordInput placeholder="Enter password" />);
const input = screen.getByPlaceholderText('Enter password'); const input = screen.getByPlaceholderText('Enter password');
expect(input).toHaveAttribute('type', 'password'); expect(input).toHaveAttribute('type', 'password');
}); });
it('should toggle input type between password and text when the eye icon is clicked', () => { it('should toggle input type between password and text when the eye icon is clicked', () => {
render(<PasswordInput placeholder="Enter password" />); renderWithProviders(<PasswordInput placeholder="Enter password" />);
const input = screen.getByPlaceholderText('Enter password'); const input = screen.getByPlaceholderText('Enter password');
const toggleButton = screen.getByRole('button', { name: /show password/i }); const toggleButton = screen.getByRole('button', { name: /show password/i });
@@ -38,7 +39,7 @@ describe('PasswordInput (in auth feature)', () => {
it('should pass through standard input attributes', () => { it('should pass through standard input attributes', () => {
const handleChange = vi.fn(); const handleChange = vi.fn();
render( renderWithProviders(
<PasswordInput <PasswordInput
value="test" value="test"
onChange={handleChange} onChange={handleChange}
@@ -56,38 +57,38 @@ describe('PasswordInput (in auth feature)', () => {
}); });
it('should not show strength indicator by default', () => { it('should not show strength indicator by default', () => {
render(<PasswordInput value="some-password" onChange={() => {}} />); renderWithProviders(<PasswordInput value="some-password" onChange={() => {}} />);
expect(screen.queryByTestId('strength-indicator')).not.toBeInTheDocument(); expect(screen.queryByTestId('strength-indicator')).not.toBeInTheDocument();
}); });
it('should show strength indicator when showStrength is true and there is a value', () => { it('should show strength indicator when showStrength is true and there is a value', () => {
render(<PasswordInput value="some-password" showStrength onChange={() => {}} />); renderWithProviders(<PasswordInput value="some-password" showStrength onChange={() => {}} />);
const indicator = screen.getByTestId('strength-indicator'); const indicator = screen.getByTestId('strength-indicator');
expect(indicator).toBeInTheDocument(); expect(indicator).toBeInTheDocument();
expect(indicator).toHaveTextContent('Strength for: some-password'); expect(indicator).toHaveTextContent('Strength for: some-password');
}); });
it('should not show strength indicator when showStrength is true but value is empty', () => { it('should not show strength indicator when showStrength is true but value is empty', () => {
render(<PasswordInput value="" showStrength onChange={() => {}} />); renderWithProviders(<PasswordInput value="" showStrength onChange={() => {}} />);
expect(screen.queryByTestId('strength-indicator')).not.toBeInTheDocument(); expect(screen.queryByTestId('strength-indicator')).not.toBeInTheDocument();
}); });
it('should handle undefined className gracefully', () => { it('should handle undefined className gracefully', () => {
render(<PasswordInput placeholder="No class" />); renderWithProviders(<PasswordInput placeholder="No class" />);
const input = screen.getByPlaceholderText('No class'); const input = screen.getByPlaceholderText('No class');
expect(input.className).not.toContain('undefined'); expect(input.className).not.toContain('undefined');
expect(input.className).toContain('block w-full'); expect(input.className).toContain('block w-full');
}); });
it('should not show strength indicator if value is undefined', () => { it('should not show strength indicator if value is undefined', () => {
render(<PasswordInput showStrength onChange={() => {}} />); renderWithProviders(<PasswordInput showStrength onChange={() => {}} />);
expect(screen.queryByTestId('strength-indicator')).not.toBeInTheDocument(); expect(screen.queryByTestId('strength-indicator')).not.toBeInTheDocument();
}); });
it('should not show strength indicator if value is not a string', () => { it('should not show strength indicator if value is not a string', () => {
// Force a non-string value to test the typeof check // Force a non-string value to test the typeof check
const props = { value: 12345, showStrength: true, onChange: () => {} } as any; const props = { value: 12345, showStrength: true, onChange: () => {} } as any;
render(<PasswordInput {...props} />); renderWithProviders(<PasswordInput {...props} />);
expect(screen.queryByTestId('strength-indicator')).not.toBeInTheDocument(); expect(screen.queryByTestId('strength-indicator')).not.toBeInTheDocument();
}); });
}); });

View File

@@ -1,8 +1,9 @@
// src/pages/admin/components/PasswordStrengthIndicator.test.tsx // src/pages/admin/components/PasswordStrengthIndicator.test.tsx
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { describe, it, expect, vi, type Mock } from 'vitest'; import { describe, it, expect, vi, type Mock } from 'vitest';
import { PasswordStrengthIndicator } from './PasswordStrengthIndicator'; import { PasswordStrengthIndicator } from './PasswordStrengthIndicator';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
import zxcvbn from 'zxcvbn'; import zxcvbn from 'zxcvbn';
// Mock the zxcvbn library to control its output for testing // Mock the zxcvbn library to control its output for testing
@@ -11,7 +12,7 @@ vi.mock('zxcvbn');
describe('PasswordStrengthIndicator', () => { describe('PasswordStrengthIndicator', () => {
it('should render 5 gray bars when no password is provided', () => { it('should render 5 gray bars when no password is provided', () => {
(zxcvbn as Mock).mockReturnValue({ score: -1, feedback: { warning: '', suggestions: [] } }); (zxcvbn as Mock).mockReturnValue({ score: -1, feedback: { warning: '', suggestions: [] } });
const { container } = render(<PasswordStrengthIndicator password="" />); const { container } = renderWithProviders(<PasswordStrengthIndicator password="" />);
const bars = container.querySelectorAll('.h-1\\.5'); const bars = container.querySelectorAll('.h-1\\.5');
expect(bars).toHaveLength(5); expect(bars).toHaveLength(5);
bars.forEach((bar) => { bars.forEach((bar) => {
@@ -28,7 +29,7 @@ describe('PasswordStrengthIndicator', () => {
{ score: 4, label: 'Strong', color: 'bg-green-500', bars: 5 }, { score: 4, label: 'Strong', color: 'bg-green-500', bars: 5 },
])('should render correctly for score $score ($label)', ({ score, label, color, bars }) => { ])('should render correctly for score $score ($label)', ({ score, label, color, bars }) => {
(zxcvbn as Mock).mockReturnValue({ score, feedback: { warning: '', suggestions: [] } }); (zxcvbn as Mock).mockReturnValue({ score, feedback: { warning: '', suggestions: [] } });
const { container } = render(<PasswordStrengthIndicator password="some-password" />); const { container } = renderWithProviders(<PasswordStrengthIndicator password="some-password" />);
// Check the label // Check the label
expect(screen.getByText(label)).toBeInTheDocument(); expect(screen.getByText(label)).toBeInTheDocument();
@@ -54,7 +55,7 @@ describe('PasswordStrengthIndicator', () => {
suggestions: [], suggestions: [],
}, },
}); });
render(<PasswordStrengthIndicator password="password" />); renderWithProviders(<PasswordStrengthIndicator password="password" />);
expect(screen.getByText(/this is a very common password/i)).toBeInTheDocument(); expect(screen.getByText(/this is a very common password/i)).toBeInTheDocument();
}); });
@@ -66,7 +67,7 @@ describe('PasswordStrengthIndicator', () => {
suggestions: ['Add another word or two'], suggestions: ['Add another word or two'],
}, },
}); });
render(<PasswordStrengthIndicator password="pass" />); renderWithProviders(<PasswordStrengthIndicator password="pass" />);
expect(screen.getByText(/add another word or two/i)).toBeInTheDocument(); expect(screen.getByText(/add another word or two/i)).toBeInTheDocument();
}); });
@@ -75,14 +76,14 @@ describe('PasswordStrengthIndicator', () => {
score: 1, score: 1,
feedback: { warning: 'A warning here', suggestions: ['A suggestion here'] }, feedback: { warning: 'A warning here', suggestions: ['A suggestion here'] },
}); });
render(<PasswordStrengthIndicator password="password" />); renderWithProviders(<PasswordStrengthIndicator password="password" />);
expect(screen.getByText(/a warning here/i)).toBeInTheDocument(); expect(screen.getByText(/a warning here/i)).toBeInTheDocument();
expect(screen.queryByText(/a suggestion here/i)).not.toBeInTheDocument(); expect(screen.queryByText(/a suggestion here/i)).not.toBeInTheDocument();
}); });
it('should use default empty string if password prop is undefined', () => { it('should use default empty string if password prop is undefined', () => {
(zxcvbn as Mock).mockReturnValue({ score: 0, feedback: { warning: '', suggestions: [] } }); (zxcvbn as Mock).mockReturnValue({ score: 0, feedback: { warning: '', suggestions: [] } });
const { container } = render(<PasswordStrengthIndicator />); const { container } = renderWithProviders(<PasswordStrengthIndicator />);
const bars = container.querySelectorAll('.h-1\\.5'); const bars = container.querySelectorAll('.h-1\\.5');
expect(bars).toHaveLength(5); expect(bars).toHaveLength(5);
bars.forEach((bar) => { bars.forEach((bar) => {
@@ -94,7 +95,7 @@ describe('PasswordStrengthIndicator', () => {
it('should handle out-of-range scores gracefully (defensive)', () => { it('should handle out-of-range scores gracefully (defensive)', () => {
// Mock a score that isn't 0-4 to hit default switch cases // Mock a score that isn't 0-4 to hit default switch cases
(zxcvbn as Mock).mockReturnValue({ score: 99, feedback: { warning: '', suggestions: [] } }); (zxcvbn as Mock).mockReturnValue({ score: 99, feedback: { warning: '', suggestions: [] } });
const { container } = render(<PasswordStrengthIndicator password="test" />); const { container } = renderWithProviders(<PasswordStrengthIndicator password="test" />);
// Check bars - should hit default case in getBarColor which returns gray // Check bars - should hit default case in getBarColor which returns gray
const bars = container.querySelectorAll('.h-1\\.5'); const bars = container.querySelectorAll('.h-1\\.5');

View File

@@ -0,0 +1,161 @@
// src/components/RecipeSuggester.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { RecipeSuggester } from './RecipeSuggester';
import { suggestRecipe } from '../services/apiClient';
import { logger } from '../services/logger.client';
// Mock the API client
vi.mock('../services/apiClient', () => ({
suggestRecipe: vi.fn(),
}));
// Mock the logger
vi.mock('../services/logger.client', () => ({
logger: {
error: vi.fn(),
},
}));
describe('RecipeSuggester Component', () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset console logs if needed, or just keep them for debug visibility
});
it('renders correctly with initial state', () => {
console.log('TEST: Verifying initial render state');
render(<RecipeSuggester />);
expect(screen.getByText('Get a Recipe Suggestion')).toBeInTheDocument();
expect(screen.getByLabelText(/Ingredients:/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Suggest a Recipe/i })).toBeInTheDocument();
expect(screen.queryByText('Getting suggestion...')).not.toBeInTheDocument();
});
it('shows validation error if no ingredients are entered', async () => {
console.log('TEST: Verifying validation for empty input');
const user = userEvent.setup();
render(<RecipeSuggester />);
const button = screen.getByRole('button', { name: /Suggest a Recipe/i });
await user.click(button);
expect(await screen.findByText('Please enter at least one ingredient.')).toBeInTheDocument();
expect(suggestRecipe).not.toHaveBeenCalled();
console.log('TEST: Validation error displayed correctly');
});
it('calls suggestRecipe and displays suggestion on success', async () => {
console.log('TEST: Verifying successful recipe suggestion flow');
const user = userEvent.setup();
render(<RecipeSuggester />);
const input = screen.getByLabelText(/Ingredients:/i);
await user.type(input, 'chicken, rice');
// Mock successful API response
const mockSuggestion = 'Here is a nice Chicken and Rice recipe...';
vi.mocked(suggestRecipe).mockResolvedValue({
ok: true,
json: async () => ({ suggestion: mockSuggestion }),
} as Response);
const button = screen.getByRole('button', { name: /Suggest a Recipe/i });
await user.click(button);
// Check loading state
expect(screen.getByRole('button')).toBeDisabled();
expect(screen.getByText('Getting suggestion...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(mockSuggestion)).toBeInTheDocument();
});
expect(suggestRecipe).toHaveBeenCalledWith(['chicken', 'rice']);
console.log('TEST: Suggestion displayed and API called with correct args');
});
it('handles API errors (non-200 response) gracefully', async () => {
console.log('TEST: Verifying API error handling (400/500 responses)');
const user = userEvent.setup();
render(<RecipeSuggester />);
const input = screen.getByLabelText(/Ingredients:/i);
await user.type(input, 'rocks');
// Mock API failure response
const errorMessage = 'Invalid ingredients provided.';
vi.mocked(suggestRecipe).mockResolvedValue({
ok: false,
json: async () => ({ message: errorMessage }),
} as Response);
const button = screen.getByRole('button', { name: /Suggest a Recipe/i });
await user.click(button);
await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
// Ensure loading state is reset
expect(screen.getByRole('button', { name: /Suggest a Recipe/i })).toBeEnabled();
console.log('TEST: API error message displayed to user');
});
it('handles network exceptions and logs them', async () => {
console.log('TEST: Verifying network exception handling');
const user = userEvent.setup();
render(<RecipeSuggester />);
const input = screen.getByLabelText(/Ingredients:/i);
await user.type(input, 'beef');
// Mock network error
const networkError = new Error('Network Error');
vi.mocked(suggestRecipe).mockRejectedValue(networkError);
const button = screen.getByRole('button', { name: /Suggest a Recipe/i });
await user.click(button);
await waitFor(() => {
expect(screen.getByText('Network Error')).toBeInTheDocument();
});
expect(logger.error).toHaveBeenCalledWith(
{ error: networkError },
'Failed to fetch recipe suggestion.'
);
console.log('TEST: Network error caught and logged');
});
it('clears previous errors when submitting again', async () => {
console.log('TEST: Verifying error clearing on re-submit');
const user = userEvent.setup();
render(<RecipeSuggester />);
// Trigger validation error first
const button = screen.getByRole('button', { name: /Suggest a Recipe/i });
await user.click(button);
expect(screen.getByText('Please enter at least one ingredient.')).toBeInTheDocument();
// Now type something to clear it (state change doesn't clear it, submit does)
const input = screen.getByLabelText(/Ingredients:/i);
await user.type(input, 'tofu');
// Mock success for the second click
vi.mocked(suggestRecipe).mockResolvedValue({
ok: true,
json: async () => ({ suggestion: 'Tofu Stir Fry' }),
} as Response);
await user.click(button);
await waitFor(() => {
expect(screen.queryByText('Please enter at least one ingredient.')).not.toBeInTheDocument();
expect(screen.getByText('Tofu Stir Fry')).toBeInTheDocument();
});
console.log('TEST: Previous error cleared successfully');
});
});

View File

@@ -1,8 +1,9 @@
// src/components/UnitSystemToggle.test.tsx // src/components/UnitSystemToggle.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UnitSystemToggle } from './UnitSystemToggle'; import { UnitSystemToggle } from './UnitSystemToggle';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
describe('UnitSystemToggle', () => { describe('UnitSystemToggle', () => {
const mockOnToggle = vi.fn(); const mockOnToggle = vi.fn();
@@ -12,7 +13,7 @@ describe('UnitSystemToggle', () => {
}); });
it('should render correctly for imperial system', () => { it('should render correctly for imperial system', () => {
render(<UnitSystemToggle currentSystem="imperial" onToggle={mockOnToggle} />); renderWithProviders(<UnitSystemToggle currentSystem="imperial" onToggle={mockOnToggle} />);
const checkbox = screen.getByRole('checkbox'); const checkbox = screen.getByRole('checkbox');
expect(checkbox).toBeChecked(); expect(checkbox).toBeChecked();
@@ -23,7 +24,7 @@ describe('UnitSystemToggle', () => {
}); });
it('should render correctly for metric system', () => { it('should render correctly for metric system', () => {
render(<UnitSystemToggle currentSystem="metric" onToggle={mockOnToggle} />); renderWithProviders(<UnitSystemToggle currentSystem="metric" onToggle={mockOnToggle} />);
const checkbox = screen.getByRole('checkbox'); const checkbox = screen.getByRole('checkbox');
expect(checkbox).not.toBeChecked(); expect(checkbox).not.toBeChecked();
@@ -34,7 +35,7 @@ describe('UnitSystemToggle', () => {
}); });
it('should call onToggle when the toggle is clicked', () => { it('should call onToggle when the toggle is clicked', () => {
render(<UnitSystemToggle currentSystem="metric" onToggle={mockOnToggle} />); renderWithProviders(<UnitSystemToggle currentSystem="metric" onToggle={mockOnToggle} />);
fireEvent.click(screen.getByRole('checkbox')); fireEvent.click(screen.getByRole('checkbox'));
expect(mockOnToggle).toHaveBeenCalledTimes(1); expect(mockOnToggle).toHaveBeenCalledTimes(1);
}); });

View File

@@ -1,34 +1,34 @@
// src/components/UserMenuSkeleton.test.tsx // src/components/UserMenuSkeleton.test.tsx
import React from 'react'; import React from 'react';
import { render } from '@testing-library/react';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { UserMenuSkeleton } from './UserMenuSkeleton'; import { UserMenuSkeleton } from './UserMenuSkeleton';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
describe('UserMenuSkeleton', () => { describe('UserMenuSkeleton', () => {
it('should render without crashing', () => { it('should render without crashing', () => {
const { container } = render(<UserMenuSkeleton />); const { container } = renderWithProviders(<UserMenuSkeleton />);
expect(container.firstChild).toBeInTheDocument(); expect(container.firstChild).toBeInTheDocument();
}); });
it('should have the main container with pulse animation', () => { it('should have the main container with pulse animation', () => {
const { container } = render(<UserMenuSkeleton />); const { container } = renderWithProviders(<UserMenuSkeleton />);
expect(container.firstChild).toHaveClass('animate-pulse'); expect(container.firstChild).toHaveClass('animate-pulse');
}); });
it('should render two child placeholder elements', () => { it('should render two child placeholder elements', () => {
const { container } = render(<UserMenuSkeleton />); const { container } = renderWithProviders(<UserMenuSkeleton />);
expect(container.firstChild?.childNodes.length).toBe(2); expect(container.firstChild?.childNodes.length).toBe(2);
}); });
it('should render a rectangular placeholder with correct styles', () => { it('should render a rectangular placeholder with correct styles', () => {
const { container } = render(<UserMenuSkeleton />); const { container } = renderWithProviders(<UserMenuSkeleton />);
expect(container.querySelector('.rounded-md')).toHaveClass( expect(container.querySelector('.rounded-md')).toHaveClass(
'h-8 w-24 bg-gray-200 dark:bg-gray-700', 'h-8 w-24 bg-gray-200 dark:bg-gray-700',
); );
}); });
it('should render a circular placeholder with correct styles', () => { it('should render a circular placeholder with correct styles', () => {
const { container } = render(<UserMenuSkeleton />); const { container } = renderWithProviders(<UserMenuSkeleton />);
expect(container.querySelector('.rounded-full')).toHaveClass( expect(container.querySelector('.rounded-full')).toHaveClass(
'h-10 w-10 bg-gray-200 dark:bg-gray-700', 'h-10 w-10 bg-gray-200 dark:bg-gray-700',
); );

View File

@@ -1,8 +1,9 @@
// src/components/WhatsNewModal.test.tsx // src/components/WhatsNewModal.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { WhatsNewModal } from './WhatsNewModal'; import { WhatsNewModal } from './WhatsNewModal';
import { renderWithProviders } from '../tests/utils/renderWithProviders';
// Unmock the component to test the real implementation // Unmock the component to test the real implementation
vi.unmock('./WhatsNewModal'); vi.unmock('./WhatsNewModal');
@@ -21,13 +22,13 @@ describe('WhatsNewModal', () => {
}); });
it('should not render when isOpen is false', () => { it('should not render when isOpen is false', () => {
const { container } = render(<WhatsNewModal {...defaultProps} isOpen={false} />); const { container } = renderWithProviders(<WhatsNewModal {...defaultProps} isOpen={false} />);
// The component returns null, so the container should be empty. // The component returns null, so the container should be empty.
expect(container.firstChild).toBeNull(); expect(container.firstChild).toBeNull();
}); });
it('should render correctly when isOpen is true', () => { it('should render correctly when isOpen is true', () => {
render(<WhatsNewModal {...defaultProps} />); renderWithProviders(<WhatsNewModal {...defaultProps} />);
expect(screen.getByRole('heading', { name: /what's new/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /what's new/i })).toBeInTheDocument();
expect(screen.getByText(`Version: ${defaultProps.version}`)).toBeInTheDocument(); expect(screen.getByText(`Version: ${defaultProps.version}`)).toBeInTheDocument();
@@ -36,13 +37,13 @@ describe('WhatsNewModal', () => {
}); });
it('should call onClose when the "Got it!" button is clicked', () => { it('should call onClose when the "Got it!" button is clicked', () => {
render(<WhatsNewModal {...defaultProps} />); renderWithProviders(<WhatsNewModal {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /got it/i })); fireEvent.click(screen.getByRole('button', { name: /got it/i }));
expect(mockOnClose).toHaveBeenCalledTimes(1); expect(mockOnClose).toHaveBeenCalledTimes(1);
}); });
it('should call onClose when the close icon button is clicked', () => { it('should call onClose when the close icon button is clicked', () => {
render(<WhatsNewModal {...defaultProps} />); renderWithProviders(<WhatsNewModal {...defaultProps} />);
// The close button is an SVG icon inside a button, best queried by its aria-label. // The close button is an SVG icon inside a button, best queried by its aria-label.
const closeButton = screen.getByRole('button', { name: /close/i }); const closeButton = screen.getByRole('button', { name: /close/i });
fireEvent.click(closeButton); fireEvent.click(closeButton);
@@ -50,7 +51,7 @@ describe('WhatsNewModal', () => {
}); });
it('should call onClose when clicking on the overlay', () => { it('should call onClose when clicking on the overlay', () => {
render(<WhatsNewModal {...defaultProps} />); renderWithProviders(<WhatsNewModal {...defaultProps} />);
// The overlay is the root div with the background color. // The overlay is the root div with the background color.
const overlay = screen.getByRole('dialog').parentElement; const overlay = screen.getByRole('dialog').parentElement;
fireEvent.click(overlay!); fireEvent.click(overlay!);
@@ -58,7 +59,7 @@ describe('WhatsNewModal', () => {
}); });
it('should not call onClose when clicking inside the modal content', () => { it('should not call onClose when clicking inside the modal content', () => {
render(<WhatsNewModal {...defaultProps} />); renderWithProviders(<WhatsNewModal {...defaultProps} />);
fireEvent.click(screen.getByText(defaultProps.commitMessage)); fireEvent.click(screen.getByText(defaultProps.commitMessage));
expect(mockOnClose).not.toHaveBeenCalled(); expect(mockOnClose).not.toHaveBeenCalled();
}); });

View File

@@ -1,9 +1,10 @@
// src/pages/admin/components/AddressForm.test.tsx // src/pages/admin/components/AddressForm.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react'; import { screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AddressForm } from './AddressForm'; import { AddressForm } from './AddressForm';
import { createMockAddress } from '../../../tests/utils/mockFactories'; import { createMockAddress } from '../../../tests/utils/mockFactories';
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
// Mock child components and icons to isolate the form's logic // Mock child components and icons to isolate the form's logic
vi.mock('lucide-react', () => ({ vi.mock('lucide-react', () => ({
@@ -30,7 +31,7 @@ describe('AddressForm', () => {
}); });
it('should render all address fields correctly', () => { it('should render all address fields correctly', () => {
render(<AddressForm {...defaultProps} />); renderWithProviders(<AddressForm {...defaultProps} />);
expect(screen.getByRole('heading', { name: /home address/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /home address/i })).toBeInTheDocument();
expect(screen.getByLabelText(/address line 1/i)).toBeInTheDocument(); expect(screen.getByLabelText(/address line 1/i)).toBeInTheDocument();
@@ -48,7 +49,7 @@ describe('AddressForm', () => {
city: 'Anytown', city: 'Anytown',
country: 'Canada', country: 'Canada',
}); });
render(<AddressForm {...defaultProps} address={fullAddress} />); renderWithProviders(<AddressForm {...defaultProps} address={fullAddress} />);
expect(screen.getByLabelText(/address line 1/i)).toHaveValue('123 Main St'); expect(screen.getByLabelText(/address line 1/i)).toHaveValue('123 Main St');
expect(screen.getByLabelText(/city/i)).toHaveValue('Anytown'); expect(screen.getByLabelText(/city/i)).toHaveValue('Anytown');
@@ -56,7 +57,7 @@ describe('AddressForm', () => {
}); });
it('should call onAddressChange with the correct field and value for all inputs', () => { it('should call onAddressChange with the correct field and value for all inputs', () => {
render(<AddressForm {...defaultProps} />); renderWithProviders(<AddressForm {...defaultProps} />);
const inputs = [ const inputs = [
{ label: /address line 1/i, name: 'address_line_1', value: '123 St' }, { label: /address line 1/i, name: 'address_line_1', value: '123 St' },
@@ -75,7 +76,7 @@ describe('AddressForm', () => {
}); });
it('should call onGeocode when the "Re-Geocode" button is clicked', () => { it('should call onGeocode when the "Re-Geocode" button is clicked', () => {
render(<AddressForm {...defaultProps} />); renderWithProviders(<AddressForm {...defaultProps} />);
const geocodeButton = screen.getByRole('button', { name: /re-geocode/i }); const geocodeButton = screen.getByRole('button', { name: /re-geocode/i });
fireEvent.click(geocodeButton); fireEvent.click(geocodeButton);
@@ -84,14 +85,14 @@ describe('AddressForm', () => {
}); });
it('should show MapPinIcon when not geocoding', () => { it('should show MapPinIcon when not geocoding', () => {
render(<AddressForm {...defaultProps} isGeocoding={false} />); renderWithProviders(<AddressForm {...defaultProps} isGeocoding={false} />);
expect(screen.getByTestId('map-pin-icon')).toBeInTheDocument(); expect(screen.getByTestId('map-pin-icon')).toBeInTheDocument();
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument(); expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
}); });
describe('when isGeocoding is true', () => { describe('when isGeocoding is true', () => {
it('should disable the button and show a loading spinner', () => { it('should disable the button and show a loading spinner', () => {
render(<AddressForm {...defaultProps} isGeocoding={true} />); renderWithProviders(<AddressForm {...defaultProps} isGeocoding={true} />);
const geocodeButton = screen.getByRole('button', { name: /re-geocode/i }); const geocodeButton = screen.getByRole('button', { name: /re-geocode/i });
expect(geocodeButton).toBeDisabled(); expect(geocodeButton).toBeDisabled();

View File

@@ -1,11 +1,12 @@
// src/pages/admin/components/AdminBrandManager.test.tsx // src/pages/admin/components/AdminBrandManager.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { AdminBrandManager } from './AdminBrandManager'; import { AdminBrandManager } from './AdminBrandManager';
import * as apiClient from '../../../services/apiClient'; import * as apiClient from '../../../services/apiClient';
import { createMockBrand } from '../../../tests/utils/mockFactories'; import { createMockBrand } from '../../../tests/utils/mockFactories';
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
// After mocking, we can get a type-safe mocked version of the module. // After mocking, we can get a type-safe mocked version of the module.
// This allows us to use .mockResolvedValue, .mockRejectedValue, etc. on the functions. // This allows us to use .mockResolvedValue, .mockRejectedValue, etc. on the functions.
@@ -34,7 +35,7 @@ describe('AdminBrandManager', () => {
mockedApiClient.fetchAllBrands.mockReturnValue(new Promise(() => {})); mockedApiClient.fetchAllBrands.mockReturnValue(new Promise(() => {}));
console.log('TEST ACTION: Rendering AdminBrandManager component.'); console.log('TEST ACTION: Rendering AdminBrandManager component.');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
console.log('TEST ASSERTION: Checking for the loading text.'); console.log('TEST ASSERTION: Checking for the loading text.');
expect(screen.getByText('Loading brands...')).toBeInTheDocument(); expect(screen.getByText('Loading brands...')).toBeInTheDocument();
@@ -49,7 +50,7 @@ describe('AdminBrandManager', () => {
mockedApiClient.fetchAllBrands.mockRejectedValue(new Error('Network Error')); mockedApiClient.fetchAllBrands.mockRejectedValue(new Error('Network Error'));
console.log('TEST ACTION: Rendering AdminBrandManager component.'); console.log('TEST ACTION: Rendering AdminBrandManager component.');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
console.log('TEST ASSERTION: Waiting for error message to be displayed.'); console.log('TEST ASSERTION: Waiting for error message to be displayed.');
await waitFor(() => { await waitFor(() => {
@@ -69,7 +70,7 @@ describe('AdminBrandManager', () => {
); );
console.log('TEST ACTION: Rendering AdminBrandManager component.'); console.log('TEST ACTION: Rendering AdminBrandManager component.');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
console.log('TEST ASSERTION: Waiting for brand list to render.'); console.log('TEST ASSERTION: Waiting for brand list to render.');
await waitFor(() => { await waitFor(() => {
@@ -98,7 +99,7 @@ describe('AdminBrandManager', () => {
mockedToast.loading.mockReturnValue('toast-1'); mockedToast.loading.mockReturnValue('toast-1');
console.log('TEST ACTION: Rendering AdminBrandManager component.'); console.log('TEST ACTION: Rendering AdminBrandManager component.');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
console.log('TEST ACTION: Waiting for initial brands to render.'); console.log('TEST ACTION: Waiting for initial brands to render.');
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
@@ -135,7 +136,7 @@ describe('AdminBrandManager', () => {
mockedApiClient.uploadBrandLogo.mockRejectedValue('A string error'); mockedApiClient.uploadBrandLogo.mockRejectedValue('A string error');
mockedToast.loading.mockReturnValue('toast-non-error'); mockedToast.loading.mockReturnValue('toast-non-error');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
const file = new File(['logo'], 'logo.png', { type: 'image/png' }); const file = new File(['logo'], 'logo.png', { type: 'image/png' });
@@ -162,7 +163,7 @@ describe('AdminBrandManager', () => {
mockedToast.loading.mockReturnValue('toast-2'); mockedToast.loading.mockReturnValue('toast-2');
console.log('TEST ACTION: Rendering AdminBrandManager component.'); console.log('TEST ACTION: Rendering AdminBrandManager component.');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
console.log('TEST ACTION: Waiting for initial brands to render.'); console.log('TEST ACTION: Waiting for initial brands to render.');
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
@@ -189,7 +190,7 @@ describe('AdminBrandManager', () => {
async () => new Response(JSON.stringify(mockBrands), { status: 200 }), async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
); );
console.log('TEST ACTION: Rendering AdminBrandManager component.'); console.log('TEST ACTION: Rendering AdminBrandManager component.');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
console.log('TEST ACTION: Waiting for initial brands to render.'); console.log('TEST ACTION: Waiting for initial brands to render.');
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
@@ -217,7 +218,7 @@ describe('AdminBrandManager', () => {
async () => new Response(JSON.stringify(mockBrands), { status: 200 }), async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
); );
console.log('TEST ACTION: Rendering AdminBrandManager component.'); console.log('TEST ACTION: Rendering AdminBrandManager component.');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
console.log('TEST ACTION: Waiting for initial brands to render.'); console.log('TEST ACTION: Waiting for initial brands to render.');
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
@@ -247,7 +248,7 @@ describe('AdminBrandManager', () => {
); );
mockedToast.loading.mockReturnValue('toast-3'); mockedToast.loading.mockReturnValue('toast-3');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
const file = new File(['logo'], 'logo.png', { type: 'image/png' }); const file = new File(['logo'], 'logo.png', { type: 'image/png' });
@@ -270,7 +271,7 @@ describe('AdminBrandManager', () => {
mockedApiClient.fetchAllBrands.mockImplementation( mockedApiClient.fetchAllBrands.mockImplementation(
async () => new Response(JSON.stringify(mockBrands), { status: 200 }), async () => new Response(JSON.stringify(mockBrands), { status: 200 }),
); );
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
console.log('TEST ACTION: Waiting for initial brands to render.'); console.log('TEST ACTION: Waiting for initial brands to render.');
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
@@ -291,7 +292,7 @@ describe('AdminBrandManager', () => {
mockedApiClient.fetchAllBrands.mockImplementation( mockedApiClient.fetchAllBrands.mockImplementation(
async () => new Response(JSON.stringify([]), { status: 200 }), async () => new Response(JSON.stringify([]), { status: 200 }),
); );
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /brand management/i })).toBeInTheDocument();
@@ -309,7 +310,7 @@ describe('AdminBrandManager', () => {
); );
mockedToast.loading.mockReturnValue('toast-fallback'); mockedToast.loading.mockReturnValue('toast-fallback');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
const file = new File(['logo'], 'logo.png', { type: 'image/png' }); const file = new File(['logo'], 'logo.png', { type: 'image/png' });
@@ -333,7 +334,7 @@ describe('AdminBrandManager', () => {
); );
mockedToast.loading.mockReturnValue('toast-opt'); mockedToast.loading.mockReturnValue('toast-opt');
render(<AdminBrandManager />); renderWithProviders(<AdminBrandManager />);
await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('No Frills')).toBeInTheDocument());
// Brand 1: No Frills (initially null logo) // Brand 1: No Frills (initially null logo)

View File

@@ -1,11 +1,12 @@
// src/pages/admin/components/AuthView.test.tsx // src/pages/admin/components/AuthView.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import { screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { AuthView } from './AuthView'; import { AuthView } from './AuthView';
import * as apiClient from '../../../services/apiClient'; import * as apiClient from '../../../services/apiClient';
import { notifySuccess, notifyError } from '../../../services/notificationService'; import { notifySuccess, notifyError } from '../../../services/notificationService';
import { createMockUserProfile } from '../../../tests/utils/mockFactories'; import { createMockUserProfile } from '../../../tests/utils/mockFactories';
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
const mockedApiClient = vi.mocked(apiClient, true); const mockedApiClient = vi.mocked(apiClient, true);
@@ -46,7 +47,7 @@ describe('AuthView', () => {
describe('Initial Render and Login', () => { describe('Initial Render and Login', () => {
it('should render the Sign In form by default', () => { it('should render the Sign In form by default', () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
expect(screen.getByRole('heading', { name: /sign in/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /sign in/i })).toBeInTheDocument();
expect(screen.getByLabelText(/email address/i)).toBeInTheDocument(); expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument(); expect(screen.getByLabelText(/^password$/i)).toBeInTheDocument();
@@ -54,7 +55,7 @@ describe('AuthView', () => {
}); });
it('should allow typing in email and password fields', () => { it('should allow typing in email and password fields', () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
const emailInput = screen.getByLabelText(/email address/i); const emailInput = screen.getByLabelText(/email address/i);
const passwordInput = screen.getByLabelText(/^password$/i); const passwordInput = screen.getByLabelText(/^password$/i);
@@ -66,7 +67,7 @@ describe('AuthView', () => {
}); });
it('should call loginUser and onLoginSuccess on successful login', async () => { it('should call loginUser and onLoginSuccess on successful login', async () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.change(screen.getByLabelText(/email address/i), { fireEvent.change(screen.getByLabelText(/email address/i), {
target: { value: 'test@example.com' }, target: { value: 'test@example.com' },
}); });
@@ -94,7 +95,7 @@ describe('AuthView', () => {
it('should display an error on failed login', async () => { it('should display an error on failed login', async () => {
(mockedApiClient.loginUser as Mock).mockRejectedValueOnce(new Error('Invalid credentials')); (mockedApiClient.loginUser as Mock).mockRejectedValueOnce(new Error('Invalid credentials'));
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.submit(screen.getByTestId('auth-form')); fireEvent.submit(screen.getByTestId('auth-form'));
await waitFor(() => { await waitFor(() => {
@@ -107,7 +108,7 @@ describe('AuthView', () => {
(mockedApiClient.loginUser as Mock).mockResolvedValueOnce( (mockedApiClient.loginUser as Mock).mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 401 }), new Response(JSON.stringify({ message: 'Unauthorized' }), { status: 401 }),
); );
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.submit(screen.getByTestId('auth-form')); fireEvent.submit(screen.getByTestId('auth-form'));
await waitFor(() => { await waitFor(() => {
@@ -120,7 +121,7 @@ describe('AuthView', () => {
describe('Registration', () => { describe('Registration', () => {
it('should switch to the registration form', () => { it('should switch to the registration form', () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i }));
expect(screen.getByRole('heading', { name: /create an account/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /create an account/i })).toBeInTheDocument();
@@ -129,7 +130,7 @@ describe('AuthView', () => {
}); });
it('should call registerUser on successful registration', async () => { it('should call registerUser on successful registration', async () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i }));
fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'Test User' } }); fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: 'Test User' } });
@@ -157,7 +158,7 @@ describe('AuthView', () => {
}); });
it('should allow registration without providing a full name', async () => { it('should allow registration without providing a full name', async () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i }));
// Do not fill in the full name, which is marked as optional // Do not fill in the full name, which is marked as optional
@@ -184,7 +185,7 @@ describe('AuthView', () => {
(mockedApiClient.registerUser as Mock).mockRejectedValueOnce( (mockedApiClient.registerUser as Mock).mockRejectedValueOnce(
new Error('Email already exists'), new Error('Email already exists'),
); );
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i }));
fireEvent.submit(screen.getByTestId('auth-form')); fireEvent.submit(screen.getByTestId('auth-form'));
@@ -197,7 +198,7 @@ describe('AuthView', () => {
(mockedApiClient.registerUser as Mock).mockResolvedValueOnce( (mockedApiClient.registerUser as Mock).mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'User exists' }), { status: 409 }), new Response(JSON.stringify({ message: 'User exists' }), { status: 409 }),
); );
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i }));
fireEvent.submit(screen.getByTestId('auth-form')); fireEvent.submit(screen.getByTestId('auth-form'));
@@ -209,7 +210,7 @@ describe('AuthView', () => {
describe('Forgot Password', () => { describe('Forgot Password', () => {
it('should switch to the reset password form', () => { it('should switch to the reset password form', () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i }));
expect(screen.getByRole('heading', { name: /reset password/i })).toBeInTheDocument(); expect(screen.getByRole('heading', { name: /reset password/i })).toBeInTheDocument();
@@ -217,7 +218,7 @@ describe('AuthView', () => {
}); });
it('should call requestPasswordReset and show success message', async () => { it('should call requestPasswordReset and show success message', async () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i }));
fireEvent.change(screen.getByLabelText(/email address/i), { fireEvent.change(screen.getByLabelText(/email address/i), {
@@ -238,7 +239,7 @@ describe('AuthView', () => {
(mockedApiClient.requestPasswordReset as Mock).mockRejectedValueOnce( (mockedApiClient.requestPasswordReset as Mock).mockRejectedValueOnce(
new Error('User not found'), new Error('User not found'),
); );
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i }));
fireEvent.submit(screen.getByTestId('reset-password-form')); fireEvent.submit(screen.getByTestId('reset-password-form'));
@@ -251,7 +252,7 @@ describe('AuthView', () => {
(mockedApiClient.requestPasswordReset as Mock).mockResolvedValueOnce( (mockedApiClient.requestPasswordReset as Mock).mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'Rate limit exceeded' }), { status: 429 }), new Response(JSON.stringify({ message: 'Rate limit exceeded' }), { status: 429 }),
); );
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i }));
fireEvent.submit(screen.getByTestId('reset-password-form')); fireEvent.submit(screen.getByTestId('reset-password-form'));
@@ -261,7 +262,7 @@ describe('AuthView', () => {
}); });
it('should switch back to sign in from forgot password', () => { it('should switch back to sign in from forgot password', () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i }));
fireEvent.click(screen.getByRole('button', { name: /back to sign in/i })); fireEvent.click(screen.getByRole('button', { name: /back to sign in/i }));
@@ -287,13 +288,13 @@ describe('AuthView', () => {
}); });
it('should set window.location.href for Google OAuth', () => { it('should set window.location.href for Google OAuth', () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /sign in with google/i })); fireEvent.click(screen.getByRole('button', { name: /sign in with google/i }));
expect(window.location.href).toBe('/api/auth/google'); expect(window.location.href).toBe('/api/auth/google');
}); });
it('should set window.location.href for GitHub OAuth', () => { it('should set window.location.href for GitHub OAuth', () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /sign in with github/i })); fireEvent.click(screen.getByRole('button', { name: /sign in with github/i }));
expect(window.location.href).toBe('/api/auth/github'); expect(window.location.href).toBe('/api/auth/github');
}); });
@@ -301,7 +302,7 @@ describe('AuthView', () => {
describe('UI Logic and Loading States', () => { describe('UI Logic and Loading States', () => {
it('should toggle "Remember me" checkbox', () => { it('should toggle "Remember me" checkbox', () => {
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
const rememberMeCheckbox = screen.getByRole('checkbox', { name: /remember me/i }); const rememberMeCheckbox = screen.getByRole('checkbox', { name: /remember me/i });
expect(rememberMeCheckbox).not.toBeChecked(); expect(rememberMeCheckbox).not.toBeChecked();
@@ -316,7 +317,7 @@ describe('AuthView', () => {
it('should show loading state during login submission', async () => { it('should show loading state during login submission', async () => {
// Mock a promise that doesn't resolve immediately // Mock a promise that doesn't resolve immediately
(mockedApiClient.loginUser as Mock).mockReturnValue(new Promise(() => {})); (mockedApiClient.loginUser as Mock).mockReturnValue(new Promise(() => {}));
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.change(screen.getByLabelText(/email address/i), { fireEvent.change(screen.getByLabelText(/email address/i), {
target: { value: 'test@example.com' }, target: { value: 'test@example.com' },
@@ -341,7 +342,7 @@ describe('AuthView', () => {
it('should show loading state during password reset submission', async () => { it('should show loading state during password reset submission', async () => {
(mockedApiClient.requestPasswordReset as Mock).mockReturnValue(new Promise(() => {})); (mockedApiClient.requestPasswordReset as Mock).mockReturnValue(new Promise(() => {}));
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i })); fireEvent.click(screen.getByRole('button', { name: /forgot password\?/i }));
@@ -362,7 +363,7 @@ describe('AuthView', () => {
it('should show loading state during registration submission', async () => { it('should show loading state during registration submission', async () => {
// Mock a promise that doesn't resolve immediately // Mock a promise that doesn't resolve immediately
(mockedApiClient.registerUser as Mock).mockReturnValue(new Promise(() => {})); (mockedApiClient.registerUser as Mock).mockReturnValue(new Promise(() => {}));
render(<AuthView {...defaultProps} />); renderWithProviders(<AuthView {...defaultProps} />);
// Switch to registration view // Switch to registration view
fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i })); fireEvent.click(screen.getByRole('button', { name: /don't have an account\? register/i }));

View File

@@ -1,7 +1,7 @@
// src/pages/admin/components/CorrectionRow.test.tsx // src/pages/admin/components/CorrectionRow.test.tsx
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { screen, fireEvent, 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 { CorrectionRow } from './CorrectionRow'; import { CorrectionRow } from './CorrectionRow';
import * as apiClient from '../../../services/apiClient'; import * as apiClient from '../../../services/apiClient';
@@ -10,6 +10,7 @@ import {
createMockMasterGroceryItem, createMockMasterGroceryItem,
createMockCategory, createMockCategory,
} from '../../../tests/utils/mockFactories'; } from '../../../tests/utils/mockFactories';
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
// Cast the mocked module to its mocked type to retain type safety and autocompletion. // 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. // The apiClient is now mocked globally via src/tests/setup/tests-setup-unit.ts.
@@ -80,7 +81,7 @@ const defaultProps = {
// Helper to render the component inside a table structure // Helper to render the component inside a table structure
const renderInTable = (props = defaultProps) => { const renderInTable = (props = defaultProps) => {
return render( return renderWithProviders(
<table> <table>
<tbody> <tbody>
<CorrectionRow {...props} /> <CorrectionRow {...props} />

View File

@@ -881,6 +881,26 @@ describe('ProfileManager', () => {
// Should not attempt to fetch address // Should not attempt to fetch address
expect(mockedApiClient.getUserAddress).not.toHaveBeenCalled(); expect(mockedApiClient.getUserAddress).not.toHaveBeenCalled();
}); });
it('should call onSignOut when clicking the sign out button', async () => {
render(<ProfileManager {...defaultAuthenticatedProps} />);
// Get the sign out button via its text
const signOutButton = screen.getByRole('button', { name: /sign out/i });
fireEvent.click(signOutButton);
await waitFor(() => {
expect(mockOnSignOut).toHaveBeenCalled();
});
});
it('should not render auth views when the user is already authenticated', () => {
render(<ProfileManager {...defaultAuthenticatedProps} />);
expect(screen.queryByText('Sign In')).not.toBeInTheDocument();
expect(screen.queryByText('Create an Account')).not.toBeInTheDocument();
});
}); });
it('should log warning if address fetch returns null', async () => { it('should log warning if address fetch returns null', async () => {
@@ -905,5 +925,106 @@ describe('ProfileManager', () => {
); );
}); });
}); });
it('should handle updating the user profile and address with empty strings', async () => {
mockedApiClient.updateUserProfile.mockResolvedValue(
new Response(JSON.stringify(authenticatedProfile), { status: 200 }),
);
mockedApiClient.updateUserAddress.mockResolvedValue(
new Response(JSON.stringify(mockAddress), { status: 200 }),
);
render(<ProfileManager {...defaultAuthenticatedProps} />);
await waitFor(() => {
expect(screen.getByLabelText(/full name/i)).toHaveValue(authenticatedProfile.full_name);
});
await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));
fireEvent.change(screen.getByLabelText(/full name/i), { target: { value: '' } });
fireEvent.change(screen.getByLabelText(/city/i), { target: { value: '' } });
const saveButton = screen.getByRole('button', { name: /save profile/i });
fireEvent.click(saveButton);
await waitFor(() => {
expect(mockedApiClient.updateUserProfile).toHaveBeenCalledWith(
{ full_name: '', avatar_url: authenticatedProfile.avatar_url },
expect.objectContaining({ signal: expect.anything() }),
);
expect(mockedApiClient.updateUserAddress).toHaveBeenCalledWith(
expect.objectContaining({ city: '' }),
expect.objectContaining({ signal: expect.anything() }),
);
expect(mockOnProfileUpdate).toHaveBeenCalledWith(
expect.objectContaining({ full_name: '' }),
);
expect(notifySuccess).toHaveBeenCalledWith('Profile updated successfully!');
});
});
it('should correctly clear the form when userProfile.address_id is null', async () => {
const profileNoAddress = { ...authenticatedProfile, address_id: null };
render(
<ProfileManager
{...defaultAuthenticatedProps}
userProfile={profileNoAddress as any} // Forcefully override the type to simulate address_id: null
/>,
);
await waitFor(() => {
expect(screen.getByLabelText(/address line 1/i)).toHaveValue('');
expect(screen.getByLabelText(/city/i)).toHaveValue('');
expect(screen.getByLabelText(/province \/ state/i)).toHaveValue('');
expect(screen.getByLabelText(/postal \/ zip code/i)).toHaveValue('');
expect(screen.getByLabelText(/country/i)).toHaveValue('');
});
});
it('should show error notification when manual geocoding fails', async () => {
render(<ProfileManager {...defaultAuthenticatedProps} />);
await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));
(mockedApiClient.geocodeAddress as Mock).mockRejectedValue(new Error('Geocoding failed'));
fireEvent.click(screen.getByRole('button', { name: /re-geocode/i }));
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Geocoding failed');
});
});
it('should show error notification when auto-geocoding fails', async () => {
vi.useFakeTimers();
render(<ProfileManager {...defaultAuthenticatedProps} />);
// Wait for initial load
await act(async () => {
await vi.runAllTimersAsync();
});
(mockedApiClient.geocodeAddress as Mock).mockRejectedValue(new Error('Auto-geocode error'));
fireEvent.change(screen.getByLabelText(/city/i), { target: { value: 'ErrorCity' } });
await act(async () => {
await vi.runAllTimersAsync();
});
expect(toast.error).toHaveBeenCalledWith('Auto-geocode error');
});
it('should handle permission denied error during geocoding', async () => {
render(<ProfileManager {...defaultAuthenticatedProps} />);
await waitFor(() => expect(screen.getByLabelText(/city/i)).toHaveValue(mockAddress.city));
(mockedApiClient.geocodeAddress as Mock).mockRejectedValue(new Error('Permission denied'));
fireEvent.click(screen.getByRole('button', { name: /re-geocode/i }));
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith('Permission denied');
});
});
}); });
}); });

View File

@@ -1,18 +1,19 @@
import React from 'react'; import React from 'react';
import { render, screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest'; import { describe, it, expect } from 'vitest';
import { StatCard } from './StatCard'; import { StatCard } from './StatCard';
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
describe('StatCard', () => { describe('StatCard', () => {
it('should render the title and value correctly', () => { it('should render the title and value correctly', () => {
render(<StatCard title="Test Stat" value="1,234" icon={<div data-testid="icon" />} />); renderWithProviders(<StatCard title="Test Stat" value="1,234" icon={<div data-testid="icon" />} />);
expect(screen.getByText('Test Stat')).toBeInTheDocument(); expect(screen.getByText('Test Stat')).toBeInTheDocument();
expect(screen.getByText('1,234')).toBeInTheDocument(); expect(screen.getByText('1,234')).toBeInTheDocument();
}); });
it('should render the icon', () => { it('should render the icon', () => {
render( renderWithProviders(
<StatCard title="Test Stat" value={100} icon={<div data-testid="test-icon">Icon</div>} />, <StatCard title="Test Stat" value={100} icon={<div data-testid="test-icon">Icon</div>} />,
); );

View File

@@ -1,11 +1,12 @@
// src/pages/admin/components/SystemCheck.test.tsx // src/pages/admin/components/SystemCheck.test.tsx
import React from 'react'; import React from 'react';
import { render, screen, waitFor, cleanup, fireEvent, act } from '@testing-library/react'; import { screen, waitFor, cleanup, fireEvent, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'; import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest';
import { SystemCheck } from './SystemCheck'; import { SystemCheck } from './SystemCheck';
import * as apiClient from '../../../services/apiClient'; import * as apiClient from '../../../services/apiClient';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { createMockUser } from '../../../tests/utils/mockFactories'; import { createMockUser } from '../../../tests/utils/mockFactories';
import { renderWithProviders } from '../../../tests/utils/renderWithProviders';
// Mock the entire apiClient module to ensure all exports are defined. // 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. // This is the primary fix for the error: [vitest] No "..." export is defined on the mock.
@@ -100,7 +101,7 @@ describe('SystemCheck', () => {
it('should render initial idle state and then run checks automatically on mount', async () => { it('should render initial idle state and then run checks automatically on mount', async () => {
setGeminiApiKey('mock-api-key'); setGeminiApiKey('mock-api-key');
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
// Initially, all checks should be in 'running' state due to auto-run // Initially, all checks should be in 'running' state due to auto-run
// However, the API key check is synchronous and resolves immediately. // However, the API key check is synchronous and resolves immediately.
@@ -126,7 +127,7 @@ describe('SystemCheck', () => {
it('should show API key as failed if GEMINI_API_KEY is not set', async () => { it('should show API key as failed if GEMINI_API_KEY is not set', async () => {
setGeminiApiKey(undefined); setGeminiApiKey(undefined);
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
// Wait for the specific error message to appear. // Wait for the specific error message to appear.
expect( expect(
@@ -139,7 +140,7 @@ describe('SystemCheck', () => {
it('should show backend connection as failed if pingBackend fails', async () => { it('should show backend connection as failed if pingBackend fails', async () => {
setGeminiApiKey('mock-api-key'); setGeminiApiKey('mock-api-key');
(mockedApiClient.pingBackend as Mock).mockRejectedValueOnce(new Error('Network error')); (mockedApiClient.pingBackend as Mock).mockRejectedValueOnce(new Error('Network error'));
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Network error')).toBeInTheDocument(); expect(screen.getByText('Network error')).toBeInTheDocument();
@@ -164,7 +165,7 @@ describe('SystemCheck', () => {
new Response(JSON.stringify({ success: false, message: 'PM2 process not found' })), new Response(JSON.stringify({ success: false, message: 'PM2 process not found' })),
), ),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('PM2 process not found')).toBeInTheDocument(); expect(screen.getByText('PM2 process not found')).toBeInTheDocument();
@@ -174,7 +175,7 @@ describe('SystemCheck', () => {
it('should show database pool check as failed if checkDbPoolHealth fails', async () => { it('should show database pool check as failed if checkDbPoolHealth fails', async () => {
setGeminiApiKey('mock-api-key'); // This was missing setGeminiApiKey('mock-api-key'); // This was missing
mockedApiClient.checkDbPoolHealth.mockRejectedValueOnce(new Error('DB connection refused')); mockedApiClient.checkDbPoolHealth.mockRejectedValueOnce(new Error('DB connection refused'));
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('DB connection refused')).toBeInTheDocument(); expect(screen.getByText('DB connection refused')).toBeInTheDocument();
@@ -184,7 +185,7 @@ describe('SystemCheck', () => {
it('should show Redis check as failed if checkRedisHealth fails', async () => { it('should show Redis check as failed if checkRedisHealth fails', async () => {
setGeminiApiKey('mock-api-key'); setGeminiApiKey('mock-api-key');
mockedApiClient.checkRedisHealth.mockRejectedValueOnce(new Error('Redis connection refused')); mockedApiClient.checkRedisHealth.mockRejectedValueOnce(new Error('Redis connection refused'));
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Redis connection refused')).toBeInTheDocument(); expect(screen.getByText('Redis connection refused')).toBeInTheDocument();
@@ -197,7 +198,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkDbPoolHealth.mockImplementationOnce(() => mockedApiClient.checkDbPoolHealth.mockImplementationOnce(() =>
Promise.reject(new Error('DB connection refused')), Promise.reject(new Error('DB connection refused')),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
// Verify the specific "skipped" messages for DB-dependent checks // Verify the specific "skipped" messages for DB-dependent checks
@@ -214,7 +215,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkDbSchema.mockImplementationOnce(() => mockedApiClient.checkDbSchema.mockImplementationOnce(() =>
Promise.resolve(new Response(JSON.stringify({ success: false, message: 'Schema mismatch' }))), Promise.resolve(new Response(JSON.stringify({ success: false, message: 'Schema mismatch' }))),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Schema mismatch')).toBeInTheDocument(); expect(screen.getByText('Schema mismatch')).toBeInTheDocument();
@@ -224,7 +225,7 @@ describe('SystemCheck', () => {
it('should show seeded user check as failed if loginUser fails', async () => { it('should show seeded user check as failed if loginUser fails', async () => {
setGeminiApiKey('mock-api-key'); setGeminiApiKey('mock-api-key');
mockedApiClient.loginUser.mockRejectedValueOnce(new Error('Incorrect email or password')); mockedApiClient.loginUser.mockRejectedValueOnce(new Error('Incorrect email or password'));
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect( expect(
@@ -236,7 +237,7 @@ describe('SystemCheck', () => {
it('should show a generic failure message for other login errors', async () => { it('should show a generic failure message for other login errors', async () => {
setGeminiApiKey('mock-api-key'); setGeminiApiKey('mock-api-key');
mockedApiClient.loginUser.mockRejectedValueOnce(new Error('Server is on fire')); mockedApiClient.loginUser.mockRejectedValueOnce(new Error('Server is on fire'));
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Failed: Server is on fire')).toBeInTheDocument(); expect(screen.getByText('Failed: Server is on fire')).toBeInTheDocument();
@@ -246,7 +247,7 @@ describe('SystemCheck', () => {
it('should show storage directory check as failed if checkStorage fails', async () => { it('should show storage directory check as failed if checkStorage fails', async () => {
setGeminiApiKey('mock-api-key'); setGeminiApiKey('mock-api-key');
mockedApiClient.checkStorage.mockRejectedValueOnce(new Error('Storage not writable')); mockedApiClient.checkStorage.mockRejectedValueOnce(new Error('Storage not writable'));
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Storage not writable')).toBeInTheDocument(); expect(screen.getByText('Storage not writable')).toBeInTheDocument();
@@ -262,7 +263,7 @@ describe('SystemCheck', () => {
}); });
mockedApiClient.pingBackend.mockImplementation(() => mockPromise); mockedApiClient.pingBackend.mockImplementation(() => mockPromise);
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
// The button text changes to "Running Checks..." // The button text changes to "Running Checks..."
const runningButton = screen.getByRole('button', { name: /running checks/i }); const runningButton = screen.getByRole('button', { name: /running checks/i });
@@ -283,7 +284,7 @@ describe('SystemCheck', () => {
it('should re-run checks when the "Re-run Checks" button is clicked', async () => { it('should re-run checks when the "Re-run Checks" button is clicked', async () => {
setGeminiApiKey('mock-api-key'); setGeminiApiKey('mock-api-key');
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
// Wait for initial auto-run to complete // Wait for initial auto-run to complete
await waitFor(() => expect(screen.getByText(/finished in/i)).toBeInTheDocument()); await waitFor(() => expect(screen.getByText(/finished in/i)).toBeInTheDocument());
@@ -328,7 +329,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkDbSchema.mockImplementationOnce(() => mockedApiClient.checkDbSchema.mockImplementationOnce(() =>
Promise.resolve(new Response(JSON.stringify({ success: false, message: 'Schema mismatch' }))), Promise.resolve(new Response(JSON.stringify({ success: false, message: 'Schema mismatch' }))),
); );
const { container } = render(<SystemCheck />); const { container } = renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
// Instead of test-ids, we check for the result: the icon's color class. // Instead of test-ids, we check for the result: the icon's color class.
@@ -344,7 +345,7 @@ describe('SystemCheck', () => {
it('should display elapsed time after checks complete', async () => { it('should display elapsed time after checks complete', async () => {
setGeminiApiKey('mock-api-key'); setGeminiApiKey('mock-api-key');
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
const elapsedTimeText = screen.getByText(/finished in \d+\.\d{2} seconds\./i); const elapsedTimeText = screen.getByText(/finished in \d+\.\d{2} seconds\./i);
@@ -357,7 +358,7 @@ describe('SystemCheck', () => {
describe('Integration: Job Queue Retries', () => { describe('Integration: Job Queue Retries', () => {
it('should call triggerFailingJob and show a success toast', async () => { it('should call triggerFailingJob and show a success toast', async () => {
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
const triggerButton = screen.getByRole('button', { name: /trigger failing job/i }); const triggerButton = screen.getByRole('button', { name: /trigger failing job/i });
fireEvent.click(triggerButton); fireEvent.click(triggerButton);
@@ -374,7 +375,7 @@ describe('SystemCheck', () => {
}); });
mockedApiClient.triggerFailingJob.mockImplementation(() => mockPromise); mockedApiClient.triggerFailingJob.mockImplementation(() => mockPromise);
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
const triggerButton = screen.getByRole('button', { name: /trigger failing job/i }); const triggerButton = screen.getByRole('button', { name: /trigger failing job/i });
fireEvent.click(triggerButton); fireEvent.click(triggerButton);
@@ -390,7 +391,7 @@ describe('SystemCheck', () => {
it('should show an error toast if triggering the job fails', async () => { it('should show an error toast if triggering the job fails', async () => {
mockedApiClient.triggerFailingJob.mockRejectedValueOnce(new Error('Queue is down')); mockedApiClient.triggerFailingJob.mockRejectedValueOnce(new Error('Queue is down'));
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
const triggerButton = screen.getByRole('button', { name: /trigger failing job/i }); const triggerButton = screen.getByRole('button', { name: /trigger failing job/i });
fireEvent.click(triggerButton); fireEvent.click(triggerButton);
@@ -403,7 +404,7 @@ describe('SystemCheck', () => {
mockedApiClient.triggerFailingJob.mockResolvedValueOnce( mockedApiClient.triggerFailingJob.mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'Server error' }), { status: 500 }), new Response(JSON.stringify({ message: 'Server error' }), { status: 500 }),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
const triggerButton = screen.getByRole('button', { name: /trigger failing job/i }); const triggerButton = screen.getByRole('button', { name: /trigger failing job/i });
fireEvent.click(triggerButton); fireEvent.click(triggerButton);
@@ -420,7 +421,7 @@ describe('SystemCheck', () => {
}); });
it('should call clearGeocodeCache and show a success toast', async () => { it('should call clearGeocodeCache and show a success toast', async () => {
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
// Wait for checks to run and Redis to be OK // Wait for checks to run and Redis to be OK
await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument());
@@ -435,7 +436,7 @@ describe('SystemCheck', () => {
it('should show an error toast if clearing the cache fails', async () => { it('should show an error toast if clearing the cache fails', async () => {
mockedApiClient.clearGeocodeCache.mockRejectedValueOnce(new Error('Redis is busy')); mockedApiClient.clearGeocodeCache.mockRejectedValueOnce(new Error('Redis is busy'));
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument());
fireEvent.click(screen.getByRole('button', { name: /clear geocode cache/i })); fireEvent.click(screen.getByRole('button', { name: /clear geocode cache/i }));
await waitFor(() => expect(vi.mocked(toast).error).toHaveBeenCalledWith('Redis is busy')); await waitFor(() => expect(vi.mocked(toast).error).toHaveBeenCalledWith('Redis is busy'));
@@ -443,7 +444,7 @@ describe('SystemCheck', () => {
it('should not call clearGeocodeCache if user cancels confirmation', async () => { it('should not call clearGeocodeCache if user cancels confirmation', async () => {
vi.spyOn(window, 'confirm').mockReturnValue(false); vi.spyOn(window, 'confirm').mockReturnValue(false);
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument());
const clearButton = screen.getByRole('button', { name: /clear geocode cache/i }); const clearButton = screen.getByRole('button', { name: /clear geocode cache/i });
@@ -456,7 +457,7 @@ describe('SystemCheck', () => {
mockedApiClient.clearGeocodeCache.mockResolvedValueOnce( mockedApiClient.clearGeocodeCache.mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'Cache clear failed' }), { status: 500 }), new Response(JSON.stringify({ message: 'Cache clear failed' }), { status: 500 }),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Redis OK')).toBeInTheDocument());
fireEvent.click(screen.getByRole('button', { name: /clear geocode cache/i })); fireEvent.click(screen.getByRole('button', { name: /clear geocode cache/i }));
@@ -470,7 +471,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkRedisHealth.mockResolvedValueOnce( mockedApiClient.checkRedisHealth.mockResolvedValueOnce(
new Response(JSON.stringify({ success: false, message: 'Redis down' })), new Response(JSON.stringify({ success: false, message: 'Redis down' })),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => expect(screen.getByText('Redis down')).toBeInTheDocument()); await waitFor(() => expect(screen.getByText('Redis down')).toBeInTheDocument());
@@ -486,7 +487,7 @@ describe('SystemCheck', () => {
mockedApiClient.pingBackend.mockResolvedValueOnce( mockedApiClient.pingBackend.mockResolvedValueOnce(
new Response('unexpected response', { status: 200 }), new Response('unexpected response', { status: 200 }),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect( expect(
@@ -499,7 +500,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkStorage.mockResolvedValueOnce( mockedApiClient.checkStorage.mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'Permission denied' }), { status: 403 }), new Response(JSON.stringify({ message: 'Permission denied' }), { status: 403 }),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Permission denied')).toBeInTheDocument(); expect(screen.getByText('Permission denied')).toBeInTheDocument();
@@ -511,7 +512,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkDbSchema.mockResolvedValueOnce( mockedApiClient.checkDbSchema.mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'Schema check failed 500' }), { status: 500 }), new Response(JSON.stringify({ message: 'Schema check failed 500' }), { status: 500 }),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Schema check failed 500')).toBeInTheDocument(); expect(screen.getByText('Schema check failed 500')).toBeInTheDocument();
@@ -523,7 +524,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkDbPoolHealth.mockResolvedValueOnce( mockedApiClient.checkDbPoolHealth.mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'DB Pool check failed 500' }), { status: 500 }), new Response(JSON.stringify({ message: 'DB Pool check failed 500' }), { status: 500 }),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('DB Pool check failed 500')).toBeInTheDocument(); expect(screen.getByText('DB Pool check failed 500')).toBeInTheDocument();
@@ -535,7 +536,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkPm2Status.mockResolvedValueOnce( mockedApiClient.checkPm2Status.mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'PM2 check failed 500' }), { status: 500 }), new Response(JSON.stringify({ message: 'PM2 check failed 500' }), { status: 500 }),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('PM2 check failed 500')).toBeInTheDocument(); expect(screen.getByText('PM2 check failed 500')).toBeInTheDocument();
@@ -547,7 +548,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkRedisHealth.mockResolvedValueOnce( mockedApiClient.checkRedisHealth.mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'Redis check failed 500' }), { status: 500 }), new Response(JSON.stringify({ message: 'Redis check failed 500' }), { status: 500 }),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Redis check failed 500')).toBeInTheDocument(); expect(screen.getByText('Redis check failed 500')).toBeInTheDocument();
@@ -559,7 +560,7 @@ describe('SystemCheck', () => {
mockedApiClient.checkRedisHealth.mockResolvedValueOnce( mockedApiClient.checkRedisHealth.mockResolvedValueOnce(
new Response(JSON.stringify({ success: false, message: 'Redis is down' })), new Response(JSON.stringify({ success: false, message: 'Redis is down' })),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Redis is down')).toBeInTheDocument(); expect(screen.getByText('Redis is down')).toBeInTheDocument();
@@ -571,7 +572,7 @@ describe('SystemCheck', () => {
mockedApiClient.loginUser.mockResolvedValueOnce( mockedApiClient.loginUser.mockResolvedValueOnce(
new Response(JSON.stringify({ message: 'Invalid credentials' }), { status: 401 }), new Response(JSON.stringify({ message: 'Invalid credentials' }), { status: 401 }),
); );
render(<SystemCheck />); renderWithProviders(<SystemCheck />);
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('Failed: Invalid credentials')).toBeInTheDocument(); expect(screen.getByText('Failed: Invalid credentials')).toBeInTheDocument();

View File

@@ -0,0 +1,72 @@
// src/providers/AppProviders.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { AppProviders } from './AppProviders';
// Mock all the providers to avoid their side effects and isolate AppProviders logic.
// We render a simple div with a data-testid for each to verify nesting.
vi.mock('./ModalProvider', () => ({
ModalProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="modal-provider">{children}</div>
),
}));
vi.mock('./AuthProvider', () => ({
AuthProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="auth-provider">{children}</div>
),
}));
vi.mock('./FlyersProvider', () => ({
FlyersProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="flyers-provider">{children}</div>
),
}));
vi.mock('./MasterItemsProvider', () => ({
MasterItemsProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="master-items-provider">{children}</div>
),
}));
vi.mock('./UserDataProvider', () => ({
UserDataProvider: ({ children }: { children: React.ReactNode }) => (
<div data-testid="user-data-provider">{children}</div>
),
}));
describe('AppProviders', () => {
it('renders children correctly', () => {
render(
<AppProviders>
<div data-testid="test-child">Test Child</div>
</AppProviders>,
);
expect(screen.getByTestId('test-child')).toBeInTheDocument();
expect(screen.getByText('Test Child')).toBeInTheDocument();
});
it('renders providers in the correct nesting order', () => {
render(
<AppProviders>
<div data-testid="test-child">Test Child</div>
</AppProviders>,
);
const modalProvider = screen.getByTestId('modal-provider');
const authProvider = screen.getByTestId('auth-provider');
const flyersProvider = screen.getByTestId('flyers-provider');
const masterItemsProvider = screen.getByTestId('master-items-provider');
const userDataProvider = screen.getByTestId('user-data-provider');
const child = screen.getByTestId('test-child');
// Verify nesting structure: Modal -> Auth -> Flyers -> MasterItems -> UserData -> Child
expect(modalProvider).toContainElement(authProvider);
expect(authProvider).toContainElement(flyersProvider);
expect(flyersProvider).toContainElement(masterItemsProvider);
expect(masterItemsProvider).toContainElement(userDataProvider);
expect(userDataProvider).toContainElement(child);
});
});

View File

@@ -0,0 +1,232 @@
// src/providers/AuthProvider.test.tsx
import React, { useContext } from 'react';
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';
// Mocks
vi.mock('../services/apiClient');
vi.mock('../services/tokenStorage');
vi.mock('../services/logger.client', () => ({
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
const mockedApiClient = apiClient as Mocked<typeof apiClient>;
const mockedTokenStorage = tokenStorage as Mocked<typeof tokenStorage>;
const mockProfile = createMockUserProfile({
user: { user_id: 'user-123', email: 'test@example.com' },
});
// A simple consumer component to access and display context values
const TestConsumer = () => {
const context = useContext(AuthContext);
if (!context) {
return <div>No Context</div>;
}
return (
<div>
<div data-testid="auth-status">{context.authStatus}</div>
<div data-testid="user-email">{context.userProfile?.user.email ?? 'No User'}</div>
<div data-testid="is-loading">{context.isLoading.toString()}</div>
<button onClick={() => context.login('test-token', mockProfile)}>Login with Profile</button>
<button onClick={() => context.login('test-token-no-profile')}>Login without Profile</button>
<button onClick={context.logout}>Logout</button>
<button onClick={() => context.updateProfile({ full_name: 'Updated Name' })}>
Update Profile
</button>
</div>
);
};
const renderWithProvider = () => {
return render(
<AuthProvider>
<TestConsumer />
</AuthProvider>,
);
};
describe('AuthProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should start in "Determining..." state and transition to "SIGNED_OUT" if no token exists', async () => {
mockedTokenStorage.getToken.mockReturnValue(null);
renderWithProvider();
expect(screen.getByTestId('auth-status')).toHaveTextContent('Determining...');
expect(screen.getByTestId('is-loading')).toHaveTextContent('true');
await waitFor(() => {
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
});
expect(mockedApiClient.getAuthenticatedUserProfile).not.toHaveBeenCalled();
});
it('should transition to "AUTHENTICATED" if a valid token exists', async () => {
mockedTokenStorage.getToken.mockReturnValue('valid-token');
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
new Response(JSON.stringify(mockProfile)),
);
renderWithProvider();
await waitFor(() => {
expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED');
expect(screen.getByTestId('user-email')).toHaveTextContent('test@example.com');
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
});
expect(mockedApiClient.getAuthenticatedUserProfile).toHaveBeenCalledTimes(1);
});
it('should handle token validation failure by signing out', async () => {
mockedTokenStorage.getToken.mockReturnValue('invalid-token');
mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(new Error('Invalid Token'));
renderWithProvider();
await waitFor(() => {
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
});
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
});
it('should handle a valid token that returns no profile by signing out', async () => {
// This test covers lines 51-55
mockedTokenStorage.getToken.mockReturnValue('valid-token-no-profile');
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
new Response(JSON.stringify(null)),
);
renderWithProvider();
expect(screen.getByTestId('auth-status')).toHaveTextContent('Determining...');
await waitFor(() => {
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
});
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
expect(screen.getByTestId('user-email')).toHaveTextContent('No User');
expect(screen.getByTestId('is-loading')).toHaveTextContent('false');
});
it('should log in a user with provided profile data', async () => {
mockedTokenStorage.getToken.mockReturnValue(null);
renderWithProvider();
await waitFor(() => expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT'));
const loginButton = screen.getByRole('button', { name: 'Login with Profile' });
await act(async () => {
fireEvent.click(loginButton);
});
expect(mockedTokenStorage.setToken).toHaveBeenCalledWith('test-token');
expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED');
expect(screen.getByTestId('user-email')).toHaveTextContent('test@example.com');
// API should not be called if profile is provided
expect(mockedApiClient.getAuthenticatedUserProfile).not.toHaveBeenCalled();
});
it('should log in a user and fetch profile if not provided', async () => {
mockedTokenStorage.getToken.mockReturnValue(null);
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
new Response(JSON.stringify(mockProfile)),
);
renderWithProvider();
await waitFor(() => expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT'));
const loginButton = screen.getByRole('button', { name: 'Login without Profile' });
await act(async () => {
fireEvent.click(loginButton);
});
await waitFor(() => {
expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED');
expect(screen.getByTestId('user-email')).toHaveTextContent('test@example.com');
});
expect(mockedTokenStorage.setToken).toHaveBeenCalledWith('test-token-no-profile');
expect(mockedApiClient.getAuthenticatedUserProfile).toHaveBeenCalledTimes(1);
});
it('should throw an error and log out if profile fetch fails after login', async () => {
// This test covers lines 109-111
mockedTokenStorage.getToken.mockReturnValue(null);
const fetchError = new Error('API is down');
mockedApiClient.getAuthenticatedUserProfile.mockRejectedValue(fetchError);
renderWithProvider();
await waitFor(() => {
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
});
const loginButton = screen.getByRole('button', { name: 'Login without Profile' });
// The login function throws an error, so we wrap it to assert the throw
await expect(
act(async () => {
fireEvent.click(loginButton);
}),
).rejects.toThrow('Login succeeded, but failed to fetch your data: API is down');
// After the error is thrown, the state should be rolled back
await waitFor(() => {
expect(mockedTokenStorage.setToken).toHaveBeenCalledWith('test-token-no-profile');
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
});
});
it('should log out the user', async () => {
mockedTokenStorage.getToken.mockReturnValue('valid-token');
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
new Response(JSON.stringify(mockProfile)),
);
renderWithProvider();
await waitFor(() => expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED'));
const logoutButton = screen.getByRole('button', { name: 'Logout' });
fireEvent.click(logoutButton);
expect(screen.getByTestId('auth-status')).toHaveTextContent('SIGNED_OUT');
expect(screen.getByTestId('user-email')).toHaveTextContent('No User');
expect(mockedTokenStorage.removeToken).toHaveBeenCalled();
});
it('should update the user profile', async () => {
mockedTokenStorage.getToken.mockReturnValue('valid-token');
mockedApiClient.getAuthenticatedUserProfile.mockResolvedValue(
new Response(JSON.stringify(mockProfile)),
);
renderWithProvider();
await waitFor(() => expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED'));
const updateButton = screen.getByRole('button', { name: 'Update Profile' });
fireEvent.click(updateButton);
await waitFor(() => {
// The profile object is internal, so we can't directly check it.
// A good proxy is to see if a component that uses it would re-render.
// Since our consumer doesn't display the name, we just confirm the function was called.
// In a real app, we'd check the updated UI element.
expect(screen.getByTestId('auth-status')).toHaveTextContent('AUTHENTICATED');
});
});
});

View File

@@ -114,6 +114,7 @@ describe('AI Service (Server)', () => {
// Restore all environment variables and clear all mocks before each test // Restore all environment variables and clear all mocks before each test
vi.restoreAllMocks(); vi.restoreAllMocks();
vi.clearAllMocks(); vi.clearAllMocks();
mockGenerateContent.mockReset();
// Reset modules to ensure the service re-initializes with the mocks // Reset modules to ensure the service re-initializes with the mocks
mockAiClient.generateContent.mockResolvedValue({ mockAiClient.generateContent.mockResolvedValue({
@@ -322,7 +323,8 @@ describe('AI Service (Server)', () => {
// Access private property for testing purposes to ensure test stays in sync with implementation // Access private property for testing purposes to ensure test stays in sync with implementation
const models = (serviceWithFallback as any).models as string[]; const models = (serviceWithFallback as any).models as string[];
const errors = models.map((model, i) => new Error(`Error for model ${model} (${i})`)); // Use a quota error to trigger the fallback logic for each model
const errors = models.map((model, i) => new Error(`Quota error for model ${model} (${i})`));
const lastError = errors[errors.length - 1]; const lastError = errors[errors.length - 1];
// Dynamically setup mocks // Dynamically setup mocks
@@ -361,7 +363,8 @@ describe('AI Service (Server)', () => {
// Access private property for testing purposes // Access private property for testing purposes
const modelsLite = (serviceWithFallback as any).models_lite as string[]; const modelsLite = (serviceWithFallback as any).models_lite as string[];
const errors = modelsLite.map((model, i) => new Error(`Error for lite model ${model} (${i})`)); // Use a quota error to trigger the fallback logic for each model
const errors = modelsLite.map((model, i) => new Error(`Quota error for lite model ${model} (${i})`));
const lastError = errors[errors.length - 1]; const lastError = errors[errors.length - 1];
// Dynamically setup mocks // Dynamically setup mocks

View File

@@ -0,0 +1,31 @@
// src/tests/utils/renderWithProviders.tsx
import React, { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { AppProviders } from '../../providers/AppProviders';
import { MemoryRouter } from 'react-router-dom';
interface ExtendedRenderOptions extends Omit<RenderOptions, 'wrapper'> {
initialEntries?: string[];
}
/**
* A custom render function that wraps the component with all application providers.
* This is useful for testing components that rely on context values (Auth, Modal, etc.).
*
* @param ui The component to render
* @param options Additional render options
* @returns The result of the render function
*/
export const renderWithProviders = (
ui: ReactElement,
options?: ExtendedRenderOptions,
) => {
const { initialEntries, ...renderOptions } = options || {};
// console.log('[renderWithProviders] Wrapping component with AppProviders context.');
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<MemoryRouter initialEntries={initialEntries}>
<AppProviders>{children}</AppProviders>
</MemoryRouter>
);
return render(ui, { wrapper: Wrapper, ...renderOptions });
};