unit test auto-provider refactor
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 13m30s
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 13m30s
This commit is contained in:
@@ -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 !
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>,
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
67
src/components/Dashboard.test.tsx
Normal file
67
src/components/Dashboard.test.tsx
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
161
src/components/RecipeSuggester.test.tsx
Normal file
161
src/components/RecipeSuggester.test.tsx
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 }));
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>} />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
72
src/providers/AppProviders.test.tsx
Normal file
72
src/providers/AppProviders.test.tsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
232
src/providers/AuthProvider.test.tsx
Normal file
232
src/providers/AuthProvider.test.tsx
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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
|
||||||
|
|||||||
31
src/tests/utils/renderWithProviders.tsx
Normal file
31
src/tests/utils/renderWithProviders.tsx
Normal 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 });
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user