Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 77 additions & 5 deletions frontend/__tests__/unit/components/ChapterMap.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { render } from '@testing-library/react'
import { render, fireEvent } from '@testing-library/react'
import { Chapter } from 'types/chapter'
import ChapterMap from 'components/ChapterMap'

const mockMap = {
setView: jest.fn().mockReturnThis(),
addLayer: jest.fn().mockReturnThis(),
fitBounds: jest.fn().mockReturnThis(),
on: jest.fn().mockReturnThis(),
scrollWheelZoom: {
enable: jest.fn(),
disable: jest.fn(),
},
}

const mockMarker = {
Expand Down Expand Up @@ -95,12 +100,16 @@ describe('ChapterMap', () => {

describe('rendering', () => {
it('renders the map container with correct id and style', () => {
render(<ChapterMap {...defaultProps} />)
const { container } = render(<ChapterMap {...defaultProps} />)

const mapContainer = document.getElementById('chapter-map')
expect(mapContainer).toBeInTheDocument()
expect(mapContainer).toHaveAttribute('id', 'chapter-map')
expect(mapContainer).toHaveStyle('width: 100%; height: 400px;')
expect(mapContainer).toHaveClass('h-full', 'w-full')

// Check that the parent container has the correct styles applied
const parentContainer = container.firstChild as HTMLElement
expect(parentContainer).toHaveClass('relative')
})

it('renders with empty data without crashing', () => {
Expand All @@ -123,6 +132,7 @@ describe('ChapterMap', () => {
[90, 180],
],
maxBoundsViscosity: 1.0,
scrollWheelZoom: false,
})
expect(mockMap.setView).toHaveBeenCalledWith([20, 0], 2)
})
Expand Down Expand Up @@ -150,6 +160,12 @@ describe('ChapterMap', () => {
expect(L.markerClusterGroup).toHaveBeenCalled()
expect(mockMap.addLayer).toHaveBeenCalledWith(mockMarkerClusterGroup)
})

it('sets up event listeners for map interaction', () => {
render(<ChapterMap {...defaultProps} />)
expect(mockMap.on).toHaveBeenCalledWith('click', expect.any(Function))
expect(mockMap.on).toHaveBeenCalledWith('mouseout', expect.any(Function))
})
})

describe('Markers', () => {
Expand Down Expand Up @@ -221,6 +237,57 @@ describe('ChapterMap', () => {
})
})

describe('Interactive Overlay', () => {
it('displays overlay with "Click to interact with map" message initially', () => {
const { getByText } = render(<ChapterMap {...defaultProps} />)
expect(getByText('Click to interact with map')).toBeInTheDocument()
})

it('removes overlay when clicked', () => {
const { getByText, queryByText } = render(<ChapterMap {...defaultProps} />)

const overlay = getByText('Click to interact with map').closest('button')
fireEvent.click(overlay!)

expect(queryByText('Click to interact with map')).not.toBeInTheDocument()
})

it('enables scroll wheel zoom when overlay is clicked', () => {
const { getByText } = render(<ChapterMap {...defaultProps} />)

const overlay = getByText('Click to interact with map').closest('button')
fireEvent.click(overlay!)

expect(mockMap.scrollWheelZoom.enable).toHaveBeenCalled()
})

it('handles keyboard interaction with Enter key', () => {
const { getByText } = render(<ChapterMap {...defaultProps} />)

const overlay = getByText('Click to interact with map').closest('button')
fireEvent.keyDown(overlay!, { key: 'Enter' })

expect(mockMap.scrollWheelZoom.enable).toHaveBeenCalled()
})

it('handles keyboard interaction with Space key', () => {
const { getByText } = render(<ChapterMap {...defaultProps} />)

const overlay = getByText('Click to interact with map').closest('button')
fireEvent.keyDown(overlay!, { key: ' ' })

expect(mockMap.scrollWheelZoom.enable).toHaveBeenCalled()
})

it('has proper accessibility attributes', () => {
const { getByText } = render(<ChapterMap {...defaultProps} />)

const overlay = getByText('Click to interact with map').closest('button')
expect(overlay).toHaveAttribute('tabIndex', '0')
expect(overlay).toHaveAttribute('aria-label', 'Click to interact with map')
})
})

describe('Local View', () => {
it('sets local view when showLocal is true', () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
Expand Down Expand Up @@ -281,10 +348,15 @@ describe('ChapterMap', () => {
it('applies custom styles correctly', () => {
const customStyle = { width: '800px', height: '600px', border: '1px solid red' }

render(<ChapterMap {...defaultProps} style={customStyle} />)
const { container } = render(<ChapterMap {...defaultProps} style={customStyle} />)

// Custom styles should be applied to the parent container
const parentContainer = container.firstChild as HTMLElement
expect(parentContainer).toHaveStyle('width: 800px; height: 600px; border: 1px solid red;')

// Map container should have Tailwind classes
const mapContainer = document.getElementById('chapter-map')
expect(mapContainer).toHaveStyle('width: 800px; height: 600px; border: 1px solid red;')
expect(mapContainer).toHaveClass('h-full', 'w-full')
})
})

Expand Down
47 changes: 45 additions & 2 deletions frontend/src/components/ChapterMap.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'
import L, { MarkerClusterGroup } from 'leaflet'
import React, { useEffect, useRef } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import type { Chapter } from 'types/chapter'
import 'leaflet.markercluster'
import 'leaflet/dist/leaflet.css'
Expand All @@ -19,6 +19,7 @@ const ChapterMap = ({
}) => {
const mapRef = useRef<L.Map | null>(null)
const markerClusterRef = useRef<MarkerClusterGroup | null>(null)
const [isMapActive, setIsMapActive] = useState(false)

useEffect(() => {
if (!mapRef.current) {
Expand All @@ -29,12 +30,27 @@ const ChapterMap = ({
[90, 180],
],
maxBoundsViscosity: 1.0,
scrollWheelZoom: false,
}).setView([20, 0], 2)

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
className: 'map-tiles',
}).addTo(mapRef.current)

mapRef.current.on('click', () => {
mapRef.current?.scrollWheelZoom.enable()
setIsMapActive(true)
})

mapRef.current.on('mouseout', (e: L.LeafletMouseEvent) => {
const relatedTarget = (e.originalEvent as MouseEvent).relatedTarget as Node | null
const popupPane = mapRef.current?.getPanes().popupPane
if (relatedTarget && popupPane?.contains(relatedTarget)) return

mapRef.current?.scrollWheelZoom.disable()
setIsMapActive(false)
})
}

const map = mapRef.current
Expand Down Expand Up @@ -102,7 +118,34 @@ const ChapterMap = ({
}
}, [geoLocData, showLocal])

return <div id="chapter-map" style={style} />
return (
<div className="relative" style={style}>
<div id="chapter-map" className="w-full h-full" />
{!isMapActive && (
<button
type="button"
tabIndex={0}
className="absolute inset-0 z-[1000] flex cursor-pointer items-center justify-center rounded-[inherit] bg-black/10"
onClick={() => {
mapRef.current?.scrollWheelZoom.enable()
setIsMapActive(true)
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
mapRef.current?.scrollWheelZoom.enable()
setIsMapActive(true)
}
}}
aria-label="Click to interact with map"
>
<p className="px-5 py-3 text-sm font-medium text-gray-700 rounded-md shadow-lg bg-white/90 dark:bg-slate-900 dark:text-white">
Click to interact with map
</p>
</button>
)}
</div>
)
}

export default ChapterMap