Skip to content

Commit 7972aaa

Browse files
committed
Merge remote-tracking branch 'origin/unit_testing' into develop
2 parents f0d57fd + ecd8181 commit 7972aaa

19 files changed

Lines changed: 1167 additions & 42 deletions

frontend/jest.config.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
1-
module.exports = {
1+
const nextJest = require('next/jest');
2+
3+
const createJestConfig = nextJest({
4+
dir: './', // Path to your Next.js app
5+
});
6+
7+
const customJestConfig = {
28
testEnvironment: 'jsdom',
39
setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
4-
moduleNameMapping: {
10+
moduleNameMapper: {
11+
// Mock CSS and assets
512
'\\.(css|less|scss|sass)$': '<rootDir>/src/__mocks__/styleMock.js',
6-
'\\.(gif|ttf|eot|svg|png)$': '<rootDir>/src/__mocks__/fileMock.js'
13+
'\\.(gif|ttf|eot|svg|png)$': '<rootDir>/src/__mocks__/fileMock.js',
714
},
15+
testMatch: ['**/src/**/*.test.{js,jsx}'],
816
collectCoverageFrom: [
917
'src/**/*.{js,jsx}',
1018
'!src/**/*.test.{js,jsx}',
1119
'!src/index.js',
12-
'!src/reportWebVitals.js'
20+
'!src/reportWebVitals.js',
1321
],
1422
coverageDirectory: 'coverage',
1523
coverageReporters: ['text', 'lcov', 'html'],
16-
testMatch: [
17-
'**/src/**/*.test.{js,jsx}'
18-
],
19-
transform: {
20-
'^.+\\.(js|jsx)$': ['@swc/jest'],
21-
},
22-
moduleFileExtensions: ['js', 'jsx', 'json'],
23-
transformIgnorePatterns: [
24-
'node_modules/(?!(react-leaflet|leaflet)/)'
25-
]
26-
};
24+
};
25+
26+
module.exports = createJestConfig(customJestConfig);

frontend/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@
2828
"@babel/preset-react": "^7.27.1",
2929
"@eslint/eslintrc": "^3",
3030
"@swc/jest": "^0.2.39",
31-
"@testing-library/jest-dom": "^5.16.5",
31+
"@testing-library/jest-dom": "^5.17.0",
3232
"@testing-library/react": "^13.4.0",
3333
"@testing-library/user-event": "^14.4.3",
3434
"autoprefixer": "^10.4.21",
3535
"babel-jest": "^29.5.0",
3636
"eslint": "^9",
3737
"eslint-config-next": "15.3.3",
38-
"jest": "^29.5.0",
38+
"jest": "^29.7.0",
3939
"jest-environment-jsdom": "^29.7.0",
4040
"postcss": "^8.5.6",
4141
"tailwindcss": "^4.1.11"

frontend/src/components/AddPoint.jsx

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -329,16 +329,19 @@ export default function AddPoint() {
329329
{/* Form */}
330330
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
331331
<div>
332-
<label style={{
333-
display: 'block',
334-
fontSize: '14px',
335-
fontWeight: '600',
336-
color: theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(40, 40, 40)',
337-
marginBottom: '6px'
338-
}}>
332+
<label htmlFor="latitude"
333+
style={{
334+
display: 'block',
335+
fontSize: '14px',
336+
fontWeight: '600',
337+
color: theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(40, 40, 40)',
338+
marginBottom: '6px'
339+
}}
340+
>
339341
Latitude
340342
</label>
341343
<input
344+
id="latitude"
342345
type="number"
343346
value={lat}
344347
onChange={e => setLat(e.target.value)}
@@ -368,16 +371,19 @@ export default function AddPoint() {
368371
</div>
369372

370373
<div>
371-
<label style={{
372-
display: 'block',
373-
fontSize: '14px',
374-
fontWeight: '600',
375-
color: theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(40, 40, 40)',
376-
marginBottom: '6px'
377-
}}>
374+
<label htmlFor="longitude"
375+
style={{
376+
display: 'block',
377+
fontSize: '14px',
378+
fontWeight: '600',
379+
color: theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(40, 40, 40)',
380+
marginBottom: '6px'
381+
}}
382+
>
378383
Longitude
379384
</label>
380385
<input
386+
id="longitude"
381387
type="number"
382388
value={lon}
383389
onChange={e => setLon(e.target.value)}
@@ -407,16 +413,19 @@ export default function AddPoint() {
407413
</div>
408414

409415
<div>
410-
<label style={{
411-
display: 'block',
412-
fontSize: '14px',
413-
fontWeight: '600',
414-
color: theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(40, 40, 40)',
415-
marginBottom: '6px'
416-
}}>
416+
<label htmlFor="temperature"
417+
style={{
418+
display: 'block',
419+
fontSize: '14px',
420+
fontWeight: '600',
421+
color: theme === 'dark' ? 'rgb(255, 255, 255)' : 'rgb(40, 40, 40)',
422+
marginBottom: '6px'
423+
}}
424+
>
417425
🌡️ Temperature (°C)
418426
</label>
419427
<input
428+
id="temperature"
420429
type="number"
421430
value={temp}
422431
onChange={e => setTemp(e.target.value)}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/**
2+
* @file AddPoint.test.jsx
3+
*/
4+
import React from 'react';
5+
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
6+
import AddPoint from './AddPoint';
7+
8+
// Mock APIs
9+
jest.mock('../lib/api', () => ({
10+
authAPI: {
11+
isAuthenticated: jest.fn(),
12+
getProfile: jest.fn(),
13+
},
14+
pointsAPI: {
15+
getUserPoints: jest.fn(),
16+
},
17+
}));
18+
19+
// Mock ThemeManager
20+
jest.mock('../utils/themeManager', () => ({
21+
ThemeManager: {
22+
getTheme: jest.fn(() => 'light'),
23+
addThemeChangeListener: jest.fn(() => jest.fn()), // returns cleanup fn
24+
},
25+
}));
26+
27+
const { authAPI, pointsAPI } = require('../lib/api');
28+
29+
beforeAll(() => {
30+
jest.useFakeTimers();
31+
});
32+
afterAll(() => {
33+
jest.useRealTimers();
34+
});
35+
36+
describe('AddPoint Component', () => {
37+
beforeEach(() => {
38+
jest.clearAllMocks();
39+
// Mock location
40+
delete window.location;
41+
window.location = { search: '', href: '' };
42+
// Mock geolocation
43+
global.navigator.geolocation = {
44+
getCurrentPosition: jest.fn((success) =>
45+
success({ coords: { latitude: 10, longitude: 20 } })
46+
),
47+
};
48+
});
49+
50+
function mockAuthAndRender() {
51+
authAPI.isAuthenticated.mockReturnValue(true);
52+
authAPI.getProfile.mockResolvedValue({
53+
success: true,
54+
data: { user: { firstName: 'Test', email: 'test@example.com' } },
55+
});
56+
return render(<AddPoint />);
57+
}
58+
59+
it('shows loading state initially and then renders form', async () => {
60+
mockAuthAndRender();
61+
62+
// Loading spinner
63+
expect(screen.getByText(/Loading/i)).toBeInTheDocument();
64+
65+
// Wait for form after profile resolves
66+
await waitFor(() =>
67+
expect(screen.getByText(/Add a temperature point/i)).toBeInTheDocument()
68+
);
69+
});
70+
71+
it('handles input changes and form submission success', async () => {
72+
mockAuthAndRender();
73+
await screen.findByText(/Add a temperature point/i);
74+
75+
localStorage.setItem('authToken', 'mock-token');
76+
global.fetch = jest.fn().mockResolvedValue({
77+
ok: true,
78+
json: async () => ({ success: true }),
79+
});
80+
pointsAPI.getUserPoints.mockResolvedValue({
81+
success: true,
82+
data: [
83+
{ lat: 10, lon: 20, temp: 30, timestamp: new Date().toISOString() },
84+
],
85+
});
86+
87+
fireEvent.change(screen.getByLabelText(/Latitude/i), { target: { value: '10' } });
88+
fireEvent.change(screen.getByLabelText(/Longitude/i), { target: { value: '20' } });
89+
fireEvent.change(screen.getByLabelText(/Temperature/i), { target: { value: '30' } });
90+
91+
fireEvent.click(screen.getByText(/Add Temperature Point/i));
92+
93+
// Fast-forward the 1.5s delay
94+
await act(() => jest.advanceTimersByTime(1500));
95+
// Wait for modal
96+
expect(await screen.findByText('Temperature point added successfully')).toBeInTheDocument();
97+
expect(screen.getByText(/Success!/i)).toBeInTheDocument();
98+
});
99+
100+
it('shows error if no token is present', async () => {
101+
mockAuthAndRender();
102+
await screen.findByText(/Add a temperature point/i);
103+
104+
localStorage.removeItem('authToken');
105+
fireEvent.change(screen.getByLabelText(/Latitude/i), { target: { value: '10' } });
106+
fireEvent.change(screen.getByLabelText(/Longitude/i), { target: { value: '20' } });
107+
fireEvent.change(screen.getByLabelText(/Temperature/i), { target: { value: '30' } });
108+
109+
fireEvent.click(screen.getByText(/Add Temperature Point/i));
110+
111+
await screen.findByText(/You must be logged in to add a point/i);
112+
expect(screen.getByText(/You must be logged in to add a point/i)).toBeInTheDocument();
113+
});
114+
115+
it('handles API failure gracefully', async () => {
116+
mockAuthAndRender();
117+
await screen.findByText(/Add a temperature point/i);
118+
119+
localStorage.setItem('authToken', 'mock-token');
120+
global.fetch = jest.fn().mockResolvedValue({
121+
ok: false,
122+
json: async () => ({ message: 'Server error' }),
123+
});
124+
125+
fireEvent.change(screen.getByLabelText(/Latitude/i), { target: { value: '10' } });
126+
fireEvent.change(screen.getByLabelText(/Longitude/i), { target: { value: '20' } });
127+
fireEvent.change(screen.getByLabelText(/Temperature/i), { target: { value: '30' } });
128+
129+
fireEvent.click(screen.getByText(/Add Temperature Point/i));
130+
131+
// Fast-forward the 1.5s delay
132+
await act(() => jest.advanceTimersByTime(1500));
133+
// Now the error banner should appear
134+
expect(await screen.findByText(/Server error/i)).toBeInTheDocument();
135+
});
136+
});

frontend/src/components/FullScreenMenu.jsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export function FullScreenMenu({ isOpen, onClose, theme, toggleTheme, loggedIn }
6969
return (
7070
<>
7171
<div
72+
data-testid="overlay"
7273
className={`map-mobile-sidebar-overlay ${isOpen ? 'active' : ''}`}
7374
onClick={onClose}
7475
></div>

0 commit comments

Comments
 (0)