Files
flyer-crawler.projectium.com/src/App.test.tsx
Torben Sorensen ed857f588a
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Has been cancelled
more fixin tests
2025-12-22 08:47:18 -08:00

861 lines
30 KiB
TypeScript

// src/App.test.tsx
import React from 'react';
import { render, screen, waitFor, fireEvent, within, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MemoryRouter } from 'react-router-dom';
import App from './App';
import * as aiApiClient from './services/aiApiClient'; // Import aiApiClient
import * as apiClient from './services/apiClient';
import { AppProviders } from './providers/AppProviders';
import type { Flyer, UserProfile } from './types';
import {
createMockFlyer,
createMockUserProfile,
createMockUser,
} from './tests/utils/mockFactories';
import {
mockUseAuth,
mockUseFlyers,
mockUseMasterItems,
mockUseUserData,
mockUseFlyerItems,
} from './tests/setup/mockHooks';
// Mock top-level components rendered by App's routes
// Mock pdfjs-dist to prevent the "DOMMatrix is not defined" error in JSDOM.
// This must be done in any test file that imports App.tsx.
vi.mock('pdfjs-dist', () => ({
// pdfjsLib: { GlobalWorkerOptions: { workerSrc: '' } },
GlobalWorkerOptions: { workerSrc: '' },
getDocument: vi.fn(() => ({
promise: Promise.resolve({ getPage: vi.fn() }),
})),
}));
// Mock the new config module
vi.mock('./config', () => ({
default: {
app: { version: '1.0.0', commitMessage: 'Initial commit', commitUrl: '#' },
google: { mapsEmbedApiKey: 'mock-key' },
},
}));
// Explicitly mock the hooks to ensure the component uses our spies
vi.mock('./hooks/useFlyers', async () => {
const hooks = await import('./tests/setup/mockHooks');
return { useFlyers: hooks.mockUseFlyers };
});
vi.mock('./hooks/useFlyerItems', async () => {
const hooks = await import('./tests/setup/mockHooks');
return { useFlyerItems: hooks.mockUseFlyerItems };
});
vi.mock('./hooks/useAuth', async () => {
const hooks = await import('./tests/setup/mockHooks');
return { useAuth: hooks.mockUseAuth };
});
vi.mock('./components/Footer', async () => {
const { MockFooter } = await import('./tests/utils/componentMocks');
return { Footer: MockFooter };
});
vi.mock('./components/Header', async () => {
const { MockHeader } = await import('./tests/utils/componentMocks');
return { Header: MockHeader };
});
vi.mock('./pages/HomePage', async () => {
const { MockHomePage } = await import('./tests/utils/componentMocks');
return { HomePage: MockHomePage };
});
vi.mock('./pages/admin/AdminPage', async () => {
const { MockAdminPage } = await import('./tests/utils/componentMocks');
return { AdminPage: MockAdminPage };
});
vi.mock('./pages/admin/CorrectionsPage', async () => {
const { MockCorrectionsPage } = await import('./tests/utils/componentMocks');
return { CorrectionsPage: MockCorrectionsPage };
});
vi.mock('./pages/admin/AdminStatsPage', async () => {
const { MockAdminStatsPage } = await import('./tests/utils/componentMocks');
return { AdminStatsPage: MockAdminStatsPage };
});
vi.mock('./pages/VoiceLabPage', async () => {
const { MockVoiceLabPage } = await import('./tests/utils/componentMocks');
return { VoiceLabPage: MockVoiceLabPage };
});
vi.mock('./pages/ResetPasswordPage', async () => {
const { MockResetPasswordPage } = await import('./tests/utils/componentMocks');
return { ResetPasswordPage: MockResetPasswordPage };
});
vi.mock('./pages/admin/components/ProfileManager', async () => {
const { MockProfileManager } = await import('./tests/utils/componentMocks');
return { ProfileManager: MockProfileManager };
});
vi.mock('./features/voice-assistant/VoiceAssistant', async () => {
const { MockVoiceAssistant } = await import('./tests/utils/componentMocks');
return { VoiceAssistant: MockVoiceAssistant };
});
vi.mock('./components/FlyerCorrectionTool', async () => {
const { MockFlyerCorrectionTool } = await import('./tests/utils/componentMocks');
return { FlyerCorrectionTool: MockFlyerCorrectionTool };
});
vi.mock('./components/WhatsNewModal', async () => {
const { MockWhatsNewModal } = await import('./tests/utils/componentMocks');
return { WhatsNewModal: MockWhatsNewModal };
});
vi.mock('./layouts/MainLayout', async () => {
const { MockMainLayout } = await import('./tests/utils/componentMocks');
return { MainLayout: MockMainLayout };
});
const mockedAiApiClient = vi.mocked(aiApiClient); // Mock aiApiClient
const mockedApiClient = vi.mocked(apiClient);
const mockFlyers: Flyer[] = [
createMockFlyer({ flyer_id: 1, store: { store_id: 1, name: 'Old Store' } }),
createMockFlyer({ flyer_id: 2, store: { store_id: 2, name: 'Another Store' } }),
];
describe('App Component', () => {
// Mock localStorage
let storage: { [key: string]: string } = {};
const localStorageMock = {
getItem: vi.fn((key: string) => storage[key] || null),
setItem: vi.fn((key: string, value: string) => {
storage[key] = value;
}),
removeItem: vi.fn((key: string) => {
delete storage[key];
}),
clear: vi.fn(() => {
storage = {};
}),
};
// Mock matchMedia
const matchMediaMock = vi.fn().mockImplementation((query) => ({
matches: false, // Default to light mode
media: query,
onchange: null,
addListener: vi.fn(), // deprecated
removeListener: vi.fn(), // deprecated
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
}));
beforeEach(() => {
console.log('[TEST DEBUG] beforeEach: Clearing mocks and setting up defaults');
vi.clearAllMocks();
// Default auth state: loading or guest
// Mock the login function to simulate a successful login. Signature: (token, profile)
const mockLogin = vi.fn(async (_token: string, _profile?: UserProfile) => {
await act(async () => {
// Simulate fetching profile after login
const profileResponse = await mockedApiClient.getAuthenticatedUserProfile();
const userProfileData: UserProfile = await profileResponse.json();
mockUseAuth.mockReturnValue({
userProfile: userProfileData,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: mockLogin, // Self-reference the mock
logout: vi.fn(),
updateProfile: vi.fn(),
});
});
});
mockUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false, // Start with isLoading: false for most tests
login: mockLogin,
logout: vi.fn(),
updateProfile: vi.fn(),
});
// Default data states for the new hooks
mockUseFlyers.mockReturnValue({
flyers: mockFlyers,
isLoadingFlyers: false,
});
mockUseMasterItems.mockReturnValue({
masterItems: [],
isLoading: false,
});
mockUseUserData.mockReturnValue({
watchedItems: [],
shoppingLists: [],
setWatchedItems: vi.fn(),
setShoppingLists: vi.fn(),
});
mockUseFlyerItems.mockReturnValue({
flyerItems: [],
isLoading: false,
});
// Clear local storage to prevent state from leaking between tests.
localStorage.clear();
Object.defineProperty(window, 'localStorage', { value: localStorageMock, configurable: true });
Object.defineProperty(window, 'matchMedia', { value: matchMediaMock, configurable: true });
// Default mocks for API calls
// Use mockImplementation to create a new Response object for each call,
// preventing "Body has already been read" errors.
// Use mockImplementation to create a new Response object for each call,
// preventing "Body has already been read" errors.
mockedApiClient.fetchFlyers.mockImplementation(() =>
Promise.resolve(new Response(JSON.stringify([]))),
);
// Mock getAuthenticatedUserProfile as it's called by useAuth's checkAuthToken and login
mockedApiClient.getAuthenticatedUserProfile.mockImplementation(() =>
Promise.resolve(
new Response(
JSON.stringify(
createMockUserProfile({
user: { user_id: 'test-user-id', email: 'test@example.com' },
full_name: 'Test User',
role: 'user',
points: 0,
}),
),
),
),
);
mockedApiClient.fetchMasterItems.mockImplementation(() =>
Promise.resolve(new Response(JSON.stringify([]))),
);
mockedApiClient.fetchWatchedItems.mockImplementation(() =>
Promise.resolve(new Response(JSON.stringify([]))),
);
mockedApiClient.fetchShoppingLists.mockImplementation(() =>
Promise.resolve(new Response(JSON.stringify([]))),
);
mockedAiApiClient.rescanImageArea.mockResolvedValue(
new Response(JSON.stringify({ text: 'mocked text' })),
); // Mock for FlyerCorrectionTool
console.log('[TEST DEBUG] beforeEach: Setup complete');
});
const renderApp = (initialEntries = ['/']) => {
return render(
<MemoryRouter initialEntries={initialEntries}>
<AppProviders>
<App />
</AppProviders>
</MemoryRouter>,
);
};
it('should render the main layout and header', async () => {
// Simulate the auth hook finishing its initial check
mockUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
renderApp();
await waitFor(() => {
expect(screen.getByTestId('header-mock')).toBeInTheDocument();
// Check that the main layout and home page are rendered for the root path
expect(screen.getByTestId('main-layout-mock')).toBeInTheDocument();
expect(screen.getByTestId('home-page-mock')).toBeInTheDocument();
});
});
it('should render the footer', async () => {
renderApp();
await waitFor(() => {
// This test will pass because we added the mock for the Footer component
expect(screen.getByTestId('footer-mock')).toBeInTheDocument();
expect(screen.getByText('Mock Footer')).toBeInTheDocument();
});
});
it('should render the BulkImporter for an admin user', async () => {
console.log('[TEST DEBUG] Test Start: should render the BulkImporter for an admin user');
const mockAdminProfile: UserProfile = createMockUserProfile({
user: { user_id: 'admin-id', email: 'admin@example.com' },
role: 'admin',
});
// Force the auth hook to return an authenticated admin user
mockUseAuth.mockReturnValue({
userProfile: mockAdminProfile,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log('[TEST DEBUG] Rendering App with /admin route');
renderApp(['/admin']);
await waitFor(() => {
console.log('[TEST DEBUG] Waiting for admin-page-mock');
expect(screen.getByTestId('header-mock')).toBeInTheDocument();
expect(screen.getByTestId('admin-page-mock')).toBeInTheDocument();
});
});
it('should show a welcome message when no flyer is selected', async () => {
// Simulate the auth hook finishing its initial check
mockUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
renderApp();
await waitFor(() => {
// The welcome message is inside HomePage, which is now mocked. We check for the mock instead.
expect(screen.getByTestId('home-page-mock')).toBeInTheDocument();
});
});
it('should render the admin page on the /admin route', async () => {
const mockAdminProfile: UserProfile = createMockUserProfile({
user: createMockUser({ user_id: 'admin-id', email: 'admin@example.com' }),
role: 'admin',
});
// Directly set the auth state via the mocked hook
mockUseAuth.mockReturnValue({
userProfile: mockAdminProfile,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log('[TEST DEBUG] Rendering App with /admin route');
renderApp(['/admin']);
console.log('[TEST DEBUG] Waiting for admin-page-mock');
expect(await screen.findByTestId('admin-page-mock')).toBeInTheDocument();
});
it('should render the reset password page on the correct route', async () => {
renderApp(['/reset-password/some-token']);
await waitFor(() => {
expect(screen.getByTestId('reset-password-page-mock')).toBeInTheDocument();
});
});
describe('Theme and Unit System Synchronization', () => {
it('should set dark mode based on user profile preferences', async () => {
console.log(
'[TEST DEBUG] Test Start: should set dark mode based on user profile preferences',
);
const profileWithDarkMode: UserProfile = createMockUserProfile({
user: createMockUser({ user_id: 'user-1', email: 'dark@mode.com' }),
role: 'user',
points: 0,
preferences: { darkMode: true },
});
mockUseAuth.mockReturnValue({
userProfile: profileWithDarkMode,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log('[TEST DEBUG] Rendering App');
renderApp();
// The useEffect that sets the theme is asynchronous. We must wait for the update.
await waitFor(() => {
console.log(
'[TEST DEBUG] Checking for dark class. Current classes:',
document.documentElement.className,
);
expect(document.documentElement).toHaveClass('dark');
});
});
it('should set light mode based on user profile preferences', async () => {
const profileWithLightMode: UserProfile = createMockUserProfile({
user: createMockUser({ user_id: 'user-1', email: 'light@mode.com' }),
role: 'user',
points: 0,
preferences: { darkMode: false },
});
mockUseAuth.mockReturnValue({
userProfile: profileWithLightMode,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
renderApp();
await waitFor(() => {
expect(document.documentElement).not.toHaveClass('dark');
});
});
it('should set dark mode based on localStorage if profile has no preference', async () => {
localStorageMock.setItem('darkMode', 'true');
renderApp();
await waitFor(() => {
expect(document.documentElement).toHaveClass('dark');
});
});
it('should set dark mode based on system preference if no other setting exists', async () => {
matchMediaMock.mockImplementationOnce((query) => ({ matches: true, media: query }));
renderApp();
await waitFor(() => {
expect(document.documentElement).toHaveClass('dark');
});
});
it('should set unit system based on user profile preferences', async () => {
const profileWithMetric: UserProfile = createMockUserProfile({
user: createMockUser({ user_id: 'user-1', email: 'metric@user.com' }),
role: 'user',
points: 0,
preferences: { unitSystem: 'metric' },
});
mockUseAuth.mockReturnValue({
userProfile: profileWithMetric,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
renderApp();
// The unit system is passed as a prop to Header, which is mocked.
// We can't directly see the result in the DOM easily, so we trust the state is set.
// A more integrated test would be needed to verify the Header receives the prop.
// For now, this test ensures the useEffect logic runs without crashing.
await waitFor(() => {
expect(screen.getByTestId('header-mock')).toBeInTheDocument();
});
});
});
describe('OAuth Token Handling', () => {
it('should call login when a googleAuthToken is in the URL', async () => {
console.log(
'[TEST DEBUG] Test Start: should call login when a googleAuthToken is in the URL',
);
const mockLogin = vi.fn().mockResolvedValue(undefined);
mockUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: mockLogin,
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log('[TEST DEBUG] Rendering App with googleAuthToken');
renderApp(['/?googleAuthToken=test-google-token']);
await waitFor(() => {
console.log('[TEST DEBUG] Checking mockLogin calls:', mockLogin.mock.calls);
expect(mockLogin).toHaveBeenCalledWith('test-google-token');
});
});
it('should call login when a githubAuthToken is in the URL', async () => {
console.log(
'[TEST DEBUG] Test Start: should call login when a githubAuthToken is in the URL',
);
const mockLogin = vi.fn().mockResolvedValue(undefined);
mockUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: mockLogin,
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log('[TEST DEBUG] Rendering App with githubAuthToken');
renderApp(['/?githubAuthToken=test-github-token']);
await waitFor(() => {
console.log('[TEST DEBUG] Checking mockLogin calls:', mockLogin.mock.calls);
expect(mockLogin).toHaveBeenCalledWith('test-github-token');
});
});
it('should log an error if login with a GitHub token fails', async () => {
console.log(
'[TEST DEBUG] Test Start: should log an error if login with a GitHub token fails',
);
const mockLogin = vi.fn().mockRejectedValue(new Error('GitHub login failed'));
mockUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: mockLogin,
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log('[TEST DEBUG] Rendering App with githubAuthToken');
renderApp(['/?githubAuthToken=bad-token']);
await waitFor(() => {
console.log('[TEST DEBUG] Checking mockLogin calls:', mockLogin.mock.calls);
expect(mockLogin).toHaveBeenCalled();
});
});
it('should log an error if login with a token fails', async () => {
console.log('[TEST DEBUG] Test Start: should log an error if login with a token fails');
const mockLogin = vi.fn().mockRejectedValue(new Error('Token login failed'));
mockUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: mockLogin,
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log('[TEST DEBUG] Rendering App with googleAuthToken');
renderApp(['/?googleAuthToken=bad-token']);
await waitFor(() => {
console.log('[TEST DEBUG] Checking mockLogin calls:', mockLogin.mock.calls);
expect(mockLogin).toHaveBeenCalled();
});
});
});
describe('Flyer Selection from URL', () => {
it('should select a flyer when flyerId is present in the URL', async () => {
renderApp(['/flyers/2']);
// The HomePage mock will be rendered. The important part is that the selection logic
// in App.tsx runs and passes the correct `selectedFlyer` prop down.
// Since HomePage is mocked, we can't see the direct result, but we can
// infer that the logic ran without crashing and the correct route was matched.
await waitFor(() => {
expect(screen.getByTestId('home-page-mock')).toBeInTheDocument();
});
});
it('should not select a flyer if the flyerId from the URL does not exist', async () => {
// This test covers the `if (flyerToSelect)` branch in the useEffect.
renderApp(['/flyers/999']); // 999 does not exist in mockFlyers
await waitFor(() => {
expect(screen.getByTestId('home-page-mock')).toBeInTheDocument();
});
// The main assertion is that no error is thrown.
});
it('should select the first flyer if no flyer is selected and flyers are available', async () => {
renderApp(['/']);
await waitFor(() => {
expect(screen.getByTestId('home-page-mock')).toBeInTheDocument();
});
});
});
describe('Version and "What\'s New" Modal', () => {
it('should show the "What\'s New" modal if the app version is new', async () => {
// Mock the config module for this specific test
vi.mock('./config', () => ({
default: {
app: { version: '1.0.1', commitMessage: 'New feature!', commitUrl: '#' },
google: { mapsEmbedApiKey: 'mock-key' },
},
}));
localStorageMock.setItem('lastSeenVersion', '1.0.0');
renderApp();
await expect(screen.findByTestId('whats-new-modal-mock')).resolves.toBeInTheDocument();
});
});
describe('Modal Interactions', () => {
it('should open and close the ProfileManager modal', async () => {
renderApp();
expect(screen.queryByTestId('profile-manager-mock')).not.toBeInTheDocument();
// Open modal
fireEvent.click(screen.getByText('Open Profile'));
expect(await screen.findByTestId('profile-manager-mock')).toBeInTheDocument();
// Close modal
fireEvent.click(screen.getByText('Close Profile'));
await waitFor(() => {
expect(screen.queryByTestId('profile-manager-mock')).not.toBeInTheDocument();
});
});
it('should open and close the VoiceAssistant modal for authenticated users', async () => {
console.log('[TEST DEBUG] Test Start: should open and close the VoiceAssistant modal');
mockUseAuth.mockReturnValue({
userProfile: createMockUserProfile({
role: 'user',
user: { user_id: '1', email: 'test@test.com' },
}),
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log('[TEST DEBUG] Rendering App');
renderApp();
expect(screen.queryByTestId('voice-assistant-mock')).not.toBeInTheDocument();
// Open modal
console.log('[TEST DEBUG] Clicking Open Voice Assistant');
fireEvent.click(screen.getByText('Open Voice Assistant'));
console.log('[TEST DEBUG] Waiting for voice-assistant-mock');
expect(await screen.findByTestId('voice-assistant-mock')).toBeInTheDocument();
// Close modal
fireEvent.click(screen.getByText('Close Voice Assistant'));
await waitFor(() => {
expect(screen.queryByTestId('voice-assistant-mock')).not.toBeInTheDocument();
});
});
it('should not render the FlyerCorrectionTool if no flyer is selected', () => {
mockUseFlyers.mockReturnValue({ flyers: [], isLoadingFlyers: false });
renderApp();
// Try to open the modal via the mocked HomePage button
fireEvent.click(screen.getByText('Open Correction Tool'));
expect(screen.queryByTestId('flyer-correction-tool-mock')).not.toBeInTheDocument();
});
it('should open and close the FlyerCorrectionTool modal', async () => {
renderApp();
expect(screen.queryByTestId('flyer-correction-tool-mock')).not.toBeInTheDocument();
// Open modal
fireEvent.click(screen.getByText('Open Correction Tool'));
expect(await screen.findByTestId('flyer-correction-tool-mock')).toBeInTheDocument();
// Close modal
fireEvent.click(screen.getByText('Close Correction'));
await waitFor(() => {
expect(screen.queryByTestId('flyer-correction-tool-mock')).not.toBeInTheDocument();
});
});
it('should render admin sub-routes correctly', async () => {
console.log('[TEST DEBUG] Test Start: should render admin sub-routes correctly');
const mockAdminProfile: UserProfile = createMockUserProfile({
user: { user_id: 'admin-id', email: 'admin@example.com' },
role: 'admin',
});
mockUseAuth.mockReturnValue({
userProfile: mockAdminProfile,
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log(
'Testing admin sub-routes with renderApp wrapper to ensure ModalProvider context',
);
console.log('[TEST DEBUG] Rendering App with /admin/corrections');
renderApp(['/admin/corrections']);
await waitFor(() => {
console.log('[TEST DEBUG] Waiting for corrections-page-mock');
expect(screen.getByTestId('corrections-page-mock')).toBeInTheDocument();
});
});
});
describe('Flyer Correction Tool Data Handling', () => {
it('should handle store name extraction from the correction tool', async () => {
// Ensure flyers are present so a flyer is auto-selected
mockUseFlyers.mockReturnValue({
flyers: mockFlyers,
isLoadingFlyers: false,
});
renderApp();
fireEvent.click(screen.getByText('Open Correction Tool'));
const correctionTool = await screen.findByTestId(
'flyer-correction-tool-mock',
{},
{ timeout: 2000 },
);
// We trigger the callback from the mock and ensure it doesn't crash.
fireEvent.click(within(correctionTool).getByText('Extract Store'));
// The test passes if no errors are thrown here.
});
it('should handle date extraction from the correction tool', async () => {
// Ensure flyers are present so a flyer is auto-selected
mockUseFlyers.mockReturnValue({
flyers: mockFlyers,
isLoadingFlyers: false,
});
renderApp();
fireEvent.click(screen.getByText('Open Correction Tool'));
const correctionTool = await screen.findByTestId(
'flyer-correction-tool-mock',
{},
{ timeout: 2000 },
);
fireEvent.click(within(correctionTool).getByText('Extract Dates'));
// The test passes if no errors are thrown here, covering the 'dates' branch.
});
});
describe("Version Display and What's New", () => {
beforeEach(() => {
// Also mock the config module to reflect this change
vi.mock('./config', () => ({
default: {
app: {
version: '2.0.0',
commitMessage: 'A new version!',
commitUrl: 'http://example.com/commit/2.0.0',
},
google: { mapsEmbedApiKey: 'mock-key' },
},
}));
});
it('should display the version number and commit link', () => {
renderApp();
const versionLink = screen.getByText(`Version: 2.0.0`);
expect(versionLink).toBeInTheDocument();
expect(versionLink).toHaveAttribute('href', 'http://example.com/commit/2.0.0');
});
it('should open the "What\'s New" modal when the question mark icon is clicked', async () => {
// Pre-set the localStorage to prevent the modal from opening automatically
localStorageMock.setItem('lastSeenVersion', '2.0.0');
renderApp();
expect(screen.queryByTestId('whats-new-modal-mock')).not.toBeInTheDocument();
const openButton = await screen.findByTitle("Show what's new in this version");
fireEvent.click(openButton);
expect(await screen.findByTestId('whats-new-modal-mock')).toBeInTheDocument();
});
});
describe('Dynamic Toaster Styles', () => {
it('should render the correct CSS variables for toast styling in light mode', async () => {
renderApp();
await waitFor(() => {
const styleTag = document.querySelector('style');
expect(styleTag).not.toBeNull();
expect(styleTag!.innerHTML).toContain('--toast-bg: #FFFFFF');
expect(styleTag!.innerHTML).toContain('--toast-color: #1F2937');
});
});
it('should render the correct CSS variables for toast styling in dark mode', async () => {
localStorageMock.setItem('darkMode', 'true');
renderApp();
await waitFor(() => {
const styleTag = document.querySelector('style');
expect(styleTag).not.toBeNull();
expect(styleTag!.innerHTML).toContain('--toast-bg: #4B5563');
});
});
});
describe('Profile and Login Handlers', () => {
it('should call updateProfile when handleProfileUpdate is triggered', async () => {
console.log(
'[TEST DEBUG] Test Start: should call updateProfile when handleProfileUpdate is triggered',
);
const mockUpdateProfile = vi.fn();
// To test profile updates, the user must be authenticated to see the "Update Profile" button.
mockUseAuth.mockReturnValue({
userProfile: createMockUserProfile({
user: { user_id: 'test-user', email: 'test@example.com' },
role: 'user',
}),
authStatus: 'AUTHENTICATED',
isLoading: false,
login: vi.fn(),
logout: vi.fn(),
updateProfile: mockUpdateProfile,
});
console.log('[TEST DEBUG] Rendering App');
renderApp();
console.log('[TEST DEBUG] Opening Profile');
fireEvent.click(screen.getByText('Open Profile'));
const profileManager = await screen.findByTestId('profile-manager-mock');
console.log('[TEST DEBUG] Clicking Update Profile');
fireEvent.click(within(profileManager).getByText('Update Profile'));
await waitFor(() => {
console.log('[TEST DEBUG] Checking mockUpdateProfile calls:', mockUpdateProfile.mock.calls);
expect(mockUpdateProfile).toHaveBeenCalledWith(
expect.objectContaining({ full_name: 'Updated' }),
);
});
});
it('should set an error state if login fails inside handleLoginSuccess', async () => {
console.log(
'[TEST DEBUG] Test Start: should set an error state if login fails inside handleLoginSuccess',
);
const mockLogin = vi.fn().mockRejectedValue(new Error('Login failed'));
mockUseAuth.mockReturnValue({
userProfile: null,
authStatus: 'SIGNED_OUT',
isLoading: false,
login: mockLogin,
logout: vi.fn(),
updateProfile: vi.fn(),
});
console.log('[TEST DEBUG] Rendering App');
renderApp();
console.log('[TEST DEBUG] Opening Profile');
fireEvent.click(screen.getByText('Open Profile'));
const loginButton = await screen.findByText('Login');
console.log('[TEST DEBUG] Clicking Login');
fireEvent.click(loginButton);
// We need to wait for the async login function to be called and reject.
await waitFor(() => {
console.log('[TEST DEBUG] Checking mockLogin calls:', mockLogin.mock.calls);
expect(mockLogin).toHaveBeenCalled();
});
});
});
});