Skip to content

Commit 5a039b7

Browse files
committed
Fix tests
1 parent e99e646 commit 5a039b7

File tree

2 files changed

+183
-47
lines changed

2 files changed

+183
-47
lines changed

frontend/src/common/components/Router/__tests__/TabNavigation.test.tsx

Lines changed: 137 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,46 @@
11
import { describe, expect, it, vi, beforeAll, afterAll } from 'vitest';
22
import { render as defaultRender, screen } from '@testing-library/react';
33
import TabNavigation from '../TabNavigation';
4-
import WithMinimalProviders from 'test/wrappers/WithMinimalProviders';
54
import '@testing-library/jest-dom';
65

6+
// Create a mock i18n instance - MOVED UP before any uses
7+
const mockI18n = {
8+
t: (key: string, options?: Record<string, unknown>) => options?.defaultValue || key,
9+
language: 'en',
10+
languages: ['en'],
11+
use: () => mockI18n, // Return itself for chaining
12+
init: () => mockI18n, // Return itself for chaining
13+
changeLanguage: vi.fn(),
14+
exists: vi.fn(() => true),
15+
addResourceBundle: vi.fn(),
16+
};
17+
18+
// Mock i18next
19+
vi.mock('i18next', () => ({
20+
default: mockI18n,
21+
}));
22+
23+
// Mock i18next-browser-languagedetector
24+
vi.mock('i18next-browser-languagedetector', () => ({
25+
default: function () {
26+
return {};
27+
},
28+
}));
29+
30+
// Mock react-i18next
31+
vi.mock('react-i18next', () => ({
32+
useTranslation: () => ({
33+
t: (key: string, options?: Record<string, unknown>) => options?.defaultValue || key,
34+
}),
35+
initReactI18next: {},
36+
I18nextProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
37+
}));
38+
39+
// Mock common/utils/i18n
40+
vi.mock('common/utils/i18n', () => ({
41+
default: mockI18n,
42+
}));
43+
744
// Mock the window object for Ionic
845
beforeAll(() => {
946
// Mock window.matchMedia
@@ -97,6 +134,11 @@ vi.mock('@ionic/react', async () => {
97134
}
98135
return tabButton;
99136
},
137+
IonText: ({ children, className }: { children: React.ReactNode; className?: string }) => (
138+
<div data-testid="mock-ion-text" className={className}>
139+
{children}
140+
</div>
141+
),
100142
};
101143
});
102144

@@ -137,38 +179,73 @@ vi.mock('pages/Upload/UploadPage', () => ({
137179
default: () => <div data-testid="mock-upload-page">Upload Page</div>,
138180
}));
139181

182+
vi.mock('pages/Reports/ReportsListPage', () => ({
183+
default: () => <div data-testid="mock-reports-list-page">Reports List Page</div>,
184+
}));
185+
186+
vi.mock('pages/Reports/ReportDetailPage', () => ({
187+
default: () => <div data-testid="mock-report-detail-page">Report Detail Page</div>,
188+
}));
189+
190+
vi.mock('pages/Processing/ProcessingPage', () => ({
191+
default: () => <div data-testid="mock-processing-page">Processing Page</div>,
192+
}));
193+
140194
// Mock the AppMenu component
141195
vi.mock('../../Menu/AppMenu', () => ({
142196
default: () => <div data-testid="mock-app-menu">App Menu</div>,
143197
}));
144198

145-
// Mock the Icon component
146-
vi.mock('../../Icon/Icon', () => ({
199+
// Mock the SvgIcon component
200+
vi.mock('../../Icon/SvgIcon', () => ({
147201
default: ({
148-
icon,
149-
iconStyle,
202+
src,
203+
alt,
204+
active,
150205
className,
151-
size,
152-
fixedWidth,
206+
width,
207+
height,
208+
testid = 'svg-icon',
153209
}: {
154-
icon: string;
155-
iconStyle?: string;
210+
src: string;
211+
alt?: string;
212+
active?: boolean;
156213
className?: string;
157-
size?: string;
158-
fixedWidth?: boolean;
214+
width?: number | string;
215+
height?: number | string;
216+
testid?: string;
159217
}) => (
160218
<div
161-
data-testid={`mock-icon-${icon}`}
162-
data-icon-style={iconStyle}
219+
data-testid={testid}
220+
data-src={src}
221+
data-alt={alt}
222+
data-active={active ? 'true' : 'false'}
163223
className={className}
164-
data-size={size}
165-
data-fixed-width={fixedWidth ? 'true' : 'false'}
224+
data-width={width}
225+
data-height={height}
166226
>
167-
{icon}
227+
<img data-testid={`${testid}-img`} src={src} alt={alt} />
168228
</div>
169229
),
170230
}));
171231

232+
// Mock the SVG icons
233+
vi.mock('assets/icons/home.svg', () => ({
234+
default: 'mocked-home-icon.svg'
235+
}));
236+
vi.mock('assets/icons/reports.svg', () => ({
237+
default: 'mocked-reports-icon.svg'
238+
}));
239+
vi.mock('assets/icons/upload.svg', () => ({
240+
default: 'mocked-upload-icon.svg'
241+
}));
242+
vi.mock('assets/icons/chat.svg', () => ({
243+
default: 'mocked-chat-icon.svg'
244+
}));
245+
vi.mock('assets/icons/profile.svg', () => ({
246+
default: 'mocked-profile-icon.svg'
247+
}));
248+
172249
// Mock the UploadModal component
173250
vi.mock('../../Upload/UploadModal', () => ({
174251
default: ({
@@ -178,7 +255,7 @@ vi.mock('../../Upload/UploadModal', () => ({
178255
}: {
179256
isOpen: boolean;
180257
onClose: () => void;
181-
_onUploadComplete?: (report: Record<string, unknown>) => void;
258+
_onUploadComplete?: () => void;
182259
}) =>
183260
isOpen ? (
184261
<div data-testid="mock-upload-modal">
@@ -209,9 +286,15 @@ vi.mock('react-router-dom', async () => {
209286
useHistory: () => ({
210287
push: vi.fn(),
211288
}),
289+
useLocation: () => ({
290+
pathname: '/tabs/home',
291+
}),
212292
};
213293
});
214294

295+
// Import the WithMinimalProviders wrapper - THIS NEEDS TO COME AFTER ALL THE MOCKS
296+
import WithMinimalProviders from 'test/wrappers/WithMinimalProviders';
297+
215298
// Use a custom render that uses our minimal providers
216299
const render = (ui: React.ReactElement) => {
217300
return defaultRender(ui, { wrapper: WithMinimalProviders });
@@ -233,12 +316,31 @@ describe('TabNavigation', () => {
233316
render(<TabNavigation />);
234317

235318
// ASSERT
236-
// Check if all tab icons are rendered
237-
expect(screen.getByTestId('mock-icon-home')).toBeInTheDocument();
238-
expect(screen.getByTestId('mock-icon-fileLines')).toBeInTheDocument();
239-
expect(screen.getByTestId('mock-icon-arrowUpFromBracket')).toBeInTheDocument();
240-
expect(screen.getByTestId('mock-icon-comment')).toBeInTheDocument();
241-
expect(screen.getByTestId('mock-icon-userCircle')).toBeInTheDocument();
319+
// Check if all SvgIcon components are rendered
320+
const homeTab = screen.getByTestId('mock-ion-tab-button-home');
321+
const svgIconInHomeTab = homeTab.querySelector('[data-testid="svg-icon"]');
322+
expect(svgIconInHomeTab).toBeInTheDocument();
323+
expect(svgIconInHomeTab).toHaveAttribute('data-src', 'mocked-home-icon.svg');
324+
325+
const reportsTab = screen.getByTestId('mock-ion-tab-button-reports');
326+
const svgIconInReportsTab = reportsTab.querySelector('[data-testid="svg-icon"]');
327+
expect(svgIconInReportsTab).toBeInTheDocument();
328+
expect(svgIconInReportsTab).toHaveAttribute('data-src', 'mocked-reports-icon.svg');
329+
330+
const uploadTab = screen.getByTestId('mock-ion-tab-button-upload');
331+
const svgIconInUploadTab = uploadTab.querySelector('[data-testid="svg-icon"]');
332+
expect(svgIconInUploadTab).toBeInTheDocument();
333+
expect(svgIconInUploadTab).toHaveAttribute('data-src', 'mocked-upload-icon.svg');
334+
335+
const chatTab = screen.getByTestId('mock-ion-tab-button-chat');
336+
const svgIconInChatTab = chatTab.querySelector('[data-testid="svg-icon"]');
337+
expect(svgIconInChatTab).toBeInTheDocument();
338+
expect(svgIconInChatTab).toHaveAttribute('data-src', 'mocked-chat-icon.svg');
339+
340+
const accountTab = screen.getByTestId('mock-ion-tab-button-account');
341+
const svgIconInAccountTab = accountTab.querySelector('[data-testid="svg-icon"]');
342+
expect(svgIconInAccountTab).toBeInTheDocument();
343+
expect(svgIconInAccountTab).toHaveAttribute('data-src', 'mocked-profile-icon.svg');
242344
});
243345

244346
it('should have correct href attributes on tab buttons', () => {
@@ -252,7 +354,7 @@ describe('TabNavigation', () => {
252354
'/tabs/home',
253355
);
254356

255-
// Check for analytics tab button
357+
// Check for reports tab button
256358
expect(screen.getByTestId('mock-ion-tab-button-reports')).toHaveAttribute(
257359
'data-href',
258360
'/tabs/reports',
@@ -283,7 +385,7 @@ describe('TabNavigation', () => {
283385
// Check for home tab button
284386
expect(screen.getByTestId('mock-ion-tab-button-home')).toHaveAttribute('data-tab', 'home');
285387

286-
// Check for analytics tab button
388+
// Check for reports tab button
287389
expect(screen.getByTestId('mock-ion-tab-button-reports')).toHaveAttribute(
288390
'data-tab',
289391
'reports',
@@ -302,29 +404,19 @@ describe('TabNavigation', () => {
302404
);
303405
});
304406

305-
it('should have correct icon styles', () => {
407+
it('should have active state based on current location', () => {
306408
// ARRANGE
307409
render(<TabNavigation />);
308410

309411
// ASSERT
310-
// Home icon should not have a style (using default solid)
311-
const homeIcon = screen.getByTestId('mock-icon-home');
312-
expect(homeIcon).not.toHaveAttribute('data-icon-style');
313-
314-
// FileLines icon should have regular style
315-
const fileLinesIcon = screen.getByTestId('mock-icon-fileLines');
316-
expect(fileLinesIcon).toHaveAttribute('data-icon-style', 'regular');
317-
318-
// Upload icon should not have a style (using default solid)
319-
const uploadIcon = screen.getByTestId('mock-icon-arrowUpFromBracket');
320-
expect(uploadIcon).not.toHaveAttribute('data-icon-style');
321-
322-
// Comment icon should have regular style
323-
const commentIcon = screen.getByTestId('mock-icon-comment');
324-
expect(commentIcon).toHaveAttribute('data-icon-style', 'regular');
325-
326-
// User icon should not have a style (using default solid)
327-
const userIcon = screen.getByTestId('mock-icon-userCircle');
328-
expect(userIcon).not.toHaveAttribute('data-icon-style');
412+
// Home icon should be active since we mocked location.pathname to be '/tabs/home'
413+
const homeTab = screen.getByTestId('mock-ion-tab-button-home');
414+
const svgIconInHomeTab = homeTab.querySelector('[data-testid="svg-icon"]');
415+
expect(svgIconInHomeTab).toHaveAttribute('data-active', 'true');
416+
417+
// Other tabs should not be active
418+
const reportsTab = screen.getByTestId('mock-ion-tab-button-reports');
419+
const svgIconInReportsTab = reportsTab.querySelector('[data-testid="svg-icon"]');
420+
expect(svgIconInReportsTab).toHaveAttribute('data-active', 'false');
329421
});
330422
});

frontend/src/test/wrappers/WithMinimalProviders.tsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import { PropsWithChildren } from 'react';
22
import { MemoryRouter } from 'react-router';
33
import { I18nextProvider } from 'react-i18next';
44
import { QueryClientProvider } from '@tanstack/react-query';
5-
import i18n from 'common/utils/i18n';
5+
// Replace the import with a mock i18n
6+
// import i18n from 'common/utils/i18n';
67
import { queryClient } from '../query-client';
8+
import { vi } from 'vitest';
9+
import type { i18n } from 'i18next';
710

811
/* Core CSS required for Ionic components to work properly */
912
import '@ionic/react/css/core.css';
@@ -21,6 +24,47 @@ import '@ionic/react/css/text-transformation.css';
2124
import '@ionic/react/css/flex-utils.css';
2225
import '@ionic/react/css/display.css';
2326

27+
// Create a simple mock i18n object for testing
28+
const mockI18n = {
29+
t: (key: string, options?: Record<string, unknown>) => options?.defaultValue || key,
30+
language: 'en',
31+
languages: ['en'],
32+
use: () => mockI18n,
33+
init: () => mockI18n,
34+
changeLanguage: vi.fn(),
35+
exists: vi.fn(() => true),
36+
addResourceBundle: vi.fn(),
37+
// Add missing properties required by the i18n type
38+
loadResources: vi.fn(),
39+
modules: { external: [] },
40+
services: {},
41+
store: { resources: {} },
42+
isInitialized: true,
43+
options: {},
44+
isResourcesLoaded: true,
45+
dir: () => 'ltr',
46+
getFixedT: () => ((key: string) => key),
47+
format: vi.fn(),
48+
formatMessage: vi.fn(),
49+
hasLoadedNamespace: () => true,
50+
loadNamespaces: vi.fn(),
51+
reloadResources: vi.fn(),
52+
getResource: vi.fn(),
53+
addResource: vi.fn(),
54+
addResources: vi.fn(),
55+
getDataByLanguage: vi.fn(),
56+
hasResourceBundle: vi.fn(),
57+
removeResourceBundle: vi.fn(),
58+
on: vi.fn(),
59+
off: vi.fn(),
60+
emit: vi.fn(),
61+
setDefaultNamespace: vi.fn(),
62+
resolveNamespace: vi.fn(),
63+
createInstance: () => mockI18n,
64+
cloneInstance: () => mockI18n,
65+
toJSON: () => ({}),
66+
} as unknown as i18n;
67+
2468
// Mock Ionic components instead of using IonApp and IonReactRouter
2569
// to avoid "window is not defined" errors in the test environment
2670
const MockIonicApp = ({ children }: PropsWithChildren): JSX.Element => (
@@ -29,7 +73,7 @@ const MockIonicApp = ({ children }: PropsWithChildren): JSX.Element => (
2973

3074
const WithMinimalProviders = ({ children }: PropsWithChildren): JSX.Element => {
3175
return (
32-
<I18nextProvider i18n={i18n}>
76+
<I18nextProvider i18n={mockI18n}>
3377
<QueryClientProvider client={queryClient}>
3478
<MockIonicApp>
3579
<MemoryRouter>{children}</MemoryRouter>

0 commit comments

Comments
 (0)