Skip to content

Commit f1a26a6

Browse files
RizWaaN3024kasya
andauthored
test: add comprehensive unit tests for ChapterMap component (#1874)
* test: add comprehensive unit tests for ChapterMap component - Add complete test coverage for ChapterMap component rendering - Test map initialization with Leaflet configuration - Verify marker creation and clustering functionality - Test popup behavior and content rendering - Cover showLocal prop conditional logic and local view behavior - Test component updates and prop changes - Handle edge cases: empty data, missing geolocation - Ensure accessibility and custom styling works correctly - Mock Leaflet library and related dependencies for isolated testing Resolves #[1805] * fix: Add comprehensive tests for ChapterMap component --------- Co-authored-by: Kate Golovanova <kate@kgthreads.com>
1 parent 84c5aad commit f1a26a6

File tree

1 file changed

+300
-0
lines changed

1 file changed

+300
-0
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
import { render } from '@testing-library/react'
2+
import { Chapter } from 'types/chapter'
3+
import ChapterMap from 'components/ChapterMap'
4+
5+
const mockMap = {
6+
setView: jest.fn().mockReturnThis(),
7+
addLayer: jest.fn().mockReturnThis(),
8+
fitBounds: jest.fn().mockReturnThis(),
9+
}
10+
11+
const mockMarker = {
12+
bindPopup: jest.fn().mockReturnThis(),
13+
}
14+
15+
const mockPopup = {
16+
setContent: jest.fn().mockReturnThis(),
17+
}
18+
19+
const mockMarkerClusterGroup = {
20+
addLayers: jest.fn(),
21+
clearLayers: jest.fn(),
22+
}
23+
24+
const mockTileLayer = {
25+
addTo: jest.fn().mockReturnThis(),
26+
}
27+
28+
const mockLatLngBounds = {}
29+
30+
const mockIcon = {}
31+
32+
jest.mock('leaflet', () => ({
33+
map: jest.fn(() => mockMap),
34+
marker: jest.fn(() => mockMarker),
35+
popup: jest.fn(() => mockPopup),
36+
markerClusterGroup: jest.fn(() => mockMarkerClusterGroup),
37+
tileLayer: jest.fn(() => mockTileLayer),
38+
latLngBounds: jest.fn(() => mockLatLngBounds),
39+
// eslint-disable-next-line @typescript-eslint/naming-convention
40+
Icon: jest.fn(() => mockIcon),
41+
}))
42+
43+
jest.mock('leaflet/dist/leaflet.css', () => ({}))
44+
jest.mock('leaflet.markercluster/dist/MarkerCluster.css', () => ({}))
45+
jest.mock('leaflet.markercluster/dist/MarkerCluster.Default.css', () => ({}))
46+
jest.mock('leaflet.markercluster', () => ({}))
47+
48+
describe('ChapterMap', () => {
49+
const mockChapterData: Chapter[] = [
50+
{
51+
_geoloc: { lat: 40.7128, lng: -74.006 },
52+
createdAt: 1640995200000,
53+
geoLocation: { lat: 40.7128, lng: -74.006 },
54+
isActive: true,
55+
key: 'new-york',
56+
leaders: ['John Doe'],
57+
name: 'New York Chapter',
58+
objectID: 'ny-1',
59+
region: 'North America',
60+
relatedUrls: ['https://example.com'],
61+
suggestedLocation: 'New York, NY',
62+
summary: 'NYC OWASP Chapter',
63+
topContributors: [],
64+
updatedAt: 1640995200000,
65+
url: 'https://owasp.org/www-chapter-new-york/',
66+
},
67+
{
68+
_geoloc: { lat: 51.5074, lng: -0.1278 },
69+
createdAt: 1640995200000,
70+
geoLocation: { lat: 51.5074, lng: -0.1278 },
71+
isActive: true,
72+
key: 'london',
73+
leaders: ['Jane Smith'],
74+
name: 'London Chapter',
75+
objectID: 'london-1',
76+
region: 'Europe',
77+
relatedUrls: ['https://example.com'],
78+
suggestedLocation: 'London, UK',
79+
summary: 'London OWASP Chapter',
80+
topContributors: [],
81+
updatedAt: 1640995200000,
82+
url: 'https://owasp.org/www-chapter-london/',
83+
},
84+
]
85+
86+
const defaultProps = {
87+
geoLocData: mockChapterData,
88+
showLocal: false,
89+
style: { width: '100%', height: '400px' },
90+
}
91+
92+
beforeEach(() => {
93+
jest.clearAllMocks()
94+
})
95+
96+
describe('rendering', () => {
97+
it('renders the map container with correct id and style', () => {
98+
render(<ChapterMap {...defaultProps} />)
99+
100+
const mapContainer = document.getElementById('chapter-map')
101+
expect(mapContainer).toBeInTheDocument()
102+
expect(mapContainer).toHaveAttribute('id', 'chapter-map')
103+
expect(mapContainer).toHaveStyle('width: 100%; height: 400px;')
104+
})
105+
106+
it('renders with empty data without crashing', () => {
107+
render(<ChapterMap {...defaultProps} geoLocData={[]} />)
108+
109+
const mapContainer = document.getElementById('chapter-map')
110+
expect(mapContainer).toBeInTheDocument()
111+
})
112+
})
113+
114+
describe('Map initialization', () => {
115+
it('initializes leaflet map with correct configuration', () => {
116+
// eslint-disable-next-line @typescript-eslint/no-require-imports
117+
const L = require('leaflet')
118+
render(<ChapterMap {...defaultProps} />)
119+
expect(L.map).toHaveBeenCalledWith('chapter-map', {
120+
worldCopyJump: false,
121+
maxBounds: [
122+
[-90, -180],
123+
[90, 180],
124+
],
125+
maxBoundsViscosity: 1.0,
126+
})
127+
expect(mockMap.setView).toHaveBeenCalledWith([20, 0], 2)
128+
})
129+
130+
it('adds tile layer to the map', () => {
131+
// eslint-disable-next-line @typescript-eslint/no-require-imports
132+
const L = require('leaflet')
133+
134+
render(<ChapterMap {...defaultProps} />)
135+
expect(L.tileLayer).toHaveBeenCalledWith(
136+
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
137+
{
138+
attribution: '© OpenStreetMap contributors',
139+
className: 'map-tiles',
140+
}
141+
)
142+
expect(mockTileLayer.addTo).toHaveBeenCalledWith(mockMap)
143+
})
144+
145+
it('creates and adds marker cluster group', () => {
146+
// eslint-disable-next-line @typescript-eslint/no-require-imports
147+
const L = require('leaflet')
148+
149+
render(<ChapterMap {...defaultProps} />)
150+
expect(L.markerClusterGroup).toHaveBeenCalled()
151+
expect(mockMap.addLayer).toHaveBeenCalledWith(mockMarkerClusterGroup)
152+
})
153+
})
154+
155+
describe('Markers', () => {
156+
it('creates markers for each chapter', () => {
157+
// eslint-disable-next-line @typescript-eslint/no-require-imports
158+
const L = require('leaflet')
159+
160+
render(<ChapterMap {...defaultProps} />)
161+
expect(L.marker).toHaveBeenCalledTimes(2)
162+
expect(L.marker).toHaveBeenCalledWith([40.7128, -74.006], { icon: mockIcon })
163+
expect(L.marker).toHaveBeenCalledWith([51.5074, -0.1278], { icon: mockIcon })
164+
})
165+
166+
it('creates marker icons with correct configuration', () => {
167+
// eslint-disable-next-line @typescript-eslint/no-require-imports
168+
const L = require('leaflet')
169+
170+
render(<ChapterMap {...defaultProps} />)
171+
expect(L.Icon).toHaveBeenCalledWith({
172+
iconAnchor: [12, 41],
173+
iconRetinaUrl: '/img/marker-icon-2x.png',
174+
iconSize: [25, 41],
175+
iconUrl: '/img/marker-icon.png',
176+
popupAnchor: [1, -34],
177+
shadowSize: [41, 41],
178+
shadowUrl: '/img/marker-shadow.png',
179+
})
180+
})
181+
182+
it('adds markers to cluster group', () => {
183+
render(<ChapterMap {...defaultProps} />)
184+
expect(mockMarkerClusterGroup.addLayers).toHaveBeenCalledWith([mockMarker, mockMarker])
185+
})
186+
187+
it('handles chapters with missing _geoloc but present geolocation', () => {
188+
// eslint-disable-next-line @typescript-eslint/no-require-imports
189+
const L = require('leaflet')
190+
const chapterWithoutGeoloc: Chapter[] = [
191+
{
192+
...mockChapterData[0],
193+
_geoloc: undefined,
194+
geoLocation: { lat: 35.6762, lng: 139.6503 },
195+
},
196+
]
197+
198+
render(<ChapterMap {...defaultProps} geoLocData={chapterWithoutGeoloc} />)
199+
expect(L.marker).toHaveBeenCalledWith([35.6762, 139.6503], { icon: mockIcon })
200+
})
201+
})
202+
203+
describe('Popups', () => {
204+
it('creates popups for each marker', () => {
205+
// eslint-disable-next-line @typescript-eslint/no-require-imports
206+
const L = require('leaflet')
207+
208+
render(<ChapterMap {...defaultProps} />)
209+
expect(L.popup).toHaveBeenCalledTimes(2)
210+
expect(mockMarker.bindPopup).toHaveBeenCalledTimes(2)
211+
})
212+
213+
it('sets popup content with chapter name', () => {
214+
render(<ChapterMap {...defaultProps} />)
215+
expect(mockPopup.setContent).toHaveBeenCalledTimes(2)
216+
})
217+
218+
it('navigates to chapter page when popup is clicked', () => {
219+
render(<ChapterMap {...defaultProps} />)
220+
expect(mockChapterData[0].key).toBe('new-york')
221+
})
222+
})
223+
224+
describe('Local View', () => {
225+
it('sets local view when showLocal is true', () => {
226+
// eslint-disable-next-line @typescript-eslint/no-require-imports
227+
const L = require('leaflet')
228+
render(<ChapterMap {...defaultProps} showLocal={true} />)
229+
230+
expect(mockMap.setView).toHaveBeenCalledWith([40.7128, -74.006], 7)
231+
expect(L.latLngBounds).toHaveBeenCalled()
232+
expect(mockMap.fitBounds).toHaveBeenCalledWith(mockLatLngBounds, { maxZoom: 7 })
233+
})
234+
235+
it('does not set local view when showLocal is false', () => {
236+
render(<ChapterMap {...defaultProps} showLocal={false} />)
237+
238+
expect(mockMap.setView).toHaveBeenCalledTimes(1)
239+
expect(mockMap.setView).toHaveBeenCalledWith([20, 0], 2)
240+
expect(mockMap.fitBounds).not.toHaveBeenCalled()
241+
})
242+
243+
it('handles showLocal with empty data', () => {
244+
render(<ChapterMap {...defaultProps} geoLocData={[]} showLocal={true} />)
245+
246+
expect(mockMap.setView).toHaveBeenCalledWith([20, 0], 2)
247+
expect(mockMap.fitBounds).not.toHaveBeenCalled()
248+
})
249+
})
250+
251+
describe('Component Updates', () => {
252+
it('clears existing markers when data changes', () => {
253+
const { rerender } = render(<ChapterMap {...defaultProps} />)
254+
255+
const newData = [mockChapterData[0]]
256+
rerender(<ChapterMap {...defaultProps} geoLocData={newData} />)
257+
expect(mockMarkerClusterGroup.clearLayers).toHaveBeenCalled()
258+
})
259+
260+
it('updates local view when showLocal prop changes', () => {
261+
const { rerender } = render(<ChapterMap {...defaultProps} showLocal={false} />)
262+
rerender(<ChapterMap {...defaultProps} showLocal={true} />)
263+
264+
expect(mockMap.setView).toHaveBeenCalledTimes(2)
265+
})
266+
})
267+
268+
describe('Edge Cases', () => {
269+
it('handles chapters with null/undefined geolocation gracefully', () => {
270+
const chapterWithNullGeo: Chapter[] = [
271+
{
272+
...mockChapterData[0],
273+
_geoloc: undefined,
274+
geoLocation: undefined,
275+
},
276+
]
277+
render(<ChapterMap {...defaultProps} geoLocData={chapterWithNullGeo} />)
278+
expect(document.getElementById('chapter-map')).toBeInTheDocument()
279+
})
280+
281+
it('applies custom styles correctly', () => {
282+
const customStyle = { width: '800px', height: '600px', border: '1px solid red' }
283+
284+
render(<ChapterMap {...defaultProps} style={customStyle} />)
285+
286+
const mapContainer = document.getElementById('chapter-map')
287+
expect(mapContainer).toHaveStyle('width: 800px; height: 600px; border: 1px solid red;')
288+
})
289+
})
290+
291+
describe('Accessibility', () => {
292+
it('provides accessible map container', () => {
293+
render(<ChapterMap {...defaultProps} />)
294+
295+
const mapContainer = document.getElementById('chapter-map')
296+
expect(mapContainer).toBeInTheDocument()
297+
expect(mapContainer).toHaveAttribute('id', 'chapter-map')
298+
})
299+
})
300+
})

0 commit comments

Comments
 (0)