Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions frontend/__tests__/e2e/pages/ChapterDetails.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ test.describe('Chapter Details Page', () => {
await expect(page.locator('#chapter-map')).toBeVisible()
await expect(page.locator('#chapter-map').locator('img').nth(1)).toBeVisible()

await page.getByRole('button', { name: 'Unlock map' }).click()

await expect(page.getByRole('button', { name: 'Zoom in' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Zoom out' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Marker' })).toBeVisible()
Expand Down
69 changes: 69 additions & 0 deletions frontend/__tests__/unit/components/ChapterMap.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ const mockLatLngBounds = {}

const mockIcon = {}

const mockZoomControl = {
addTo: jest.fn().mockReturnThis(),
remove: jest.fn(),
}

jest.mock('leaflet', () => ({
map: jest.fn(() => mockMap),
marker: jest.fn(() => mockMarker),
Expand All @@ -47,6 +52,9 @@ jest.mock('leaflet', () => ({
latLngBounds: jest.fn(() => mockLatLngBounds),
// eslint-disable-next-line @typescript-eslint/naming-convention
Icon: jest.fn(() => mockIcon),
control: {
zoom: jest.fn(() => mockZoomControl),
},
}))

jest.mock('leaflet/dist/leaflet.css', () => ({}))
Expand Down Expand Up @@ -135,6 +143,7 @@ describe('ChapterMap', () => {
],
maxBoundsViscosity: 1.0,
scrollWheelZoom: false,
zoomControl: false,
})
expect(mockMap.setView).toHaveBeenCalledWith([20, 0], 2)
})
Expand Down Expand Up @@ -399,4 +408,64 @@ describe('ChapterMap', () => {
expect(mapContainer).toHaveAttribute('id', 'chapter-map')
})
})

describe('Zoom Control Visibility', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('does not show zoom control initially', () => {
render(<ChapterMap {...defaultProps} />)
expect(L.map).toHaveBeenCalledWith(
'chapter-map',
expect.objectContaining({
zoomControl: false,
})
)
})

it('shows zoom control when unlock button is clicked', () => {
const { getByText } = render(<ChapterMap {...defaultProps} />)

const overlay = getByText('Unlock map').closest('button')
fireEvent.click(overlay)

expect(L.control.zoom).toHaveBeenCalledWith({ position: 'topleft' })
expect(mockZoomControl.addTo).toHaveBeenCalledWith(mockMap)
})
})

describe('Share Location Button Visibility', () => {
const mockOnShareLocation = jest.fn()

it('does not show share location button initially when map is not active', () => {
const { queryByLabelText } = render(
<ChapterMap {...defaultProps} onShareLocation={mockOnShareLocation} />
)

expect(queryByLabelText(/share location/i)).not.toBeInTheDocument()
})

it('shows share location button when map becomes active', () => {
const { getByText, getByLabelText } = render(
<ChapterMap {...defaultProps} onShareLocation={mockOnShareLocation} />
)

expect(getByText('Unlock map')).toBeInTheDocument()

const overlay = getByText('Unlock map').closest('button')
fireEvent.click(overlay)

expect(getByLabelText(/share location to find nearby chapters/i)).toBeInTheDocument()
})

it('does not render share location button when onShareLocation is not provided', () => {
const { getByText, queryByLabelText } = render(<ChapterMap {...defaultProps} />)

const overlay = getByText('Unlock map').closest('button')
fireEvent.click(overlay)

expect(queryByLabelText(/share location/i)).not.toBeInTheDocument()
})
})
})
68 changes: 48 additions & 20 deletions frontend/src/components/ChapterMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const ChapterMap = ({
const mapRef = useRef<L.Map | null>(null)
const markerClusterRef = useRef<MarkerClusterGroup | null>(null)
const userMarkerRef = useRef<L.Marker | null>(null)
const zoomControlRef = useRef<L.Control.Zoom | null>(null)
const initialViewRef = useRef<{ center: L.LatLngExpression; zoom: number } | null>(null)
const [isMapActive, setIsMapActive] = useState(false)

Expand All @@ -42,6 +43,7 @@ const ChapterMap = ({
],
maxBoundsViscosity: 1.0,
scrollWheelZoom: false,
zoomControl: false,
}).setView([20, 0], 2)

initialViewRef.current = {
Expand Down Expand Up @@ -192,6 +194,28 @@ const ChapterMap = ({
}
}, [geoLocData, showLocal, userLocation])

useEffect(() => {
const map = mapRef.current
if (!map) return

if (isMapActive) {
if (!zoomControlRef.current) {
zoomControlRef.current = L.control.zoom({ position: 'topleft' })
zoomControlRef.current.addTo(map)
}
} else if (zoomControlRef.current) {
zoomControlRef.current.remove()
zoomControlRef.current = null
}

return () => {
if (zoomControlRef.current) {
zoomControlRef.current.remove()
zoomControlRef.current = null
}
}
}, [isMapActive])

return (
<div className="relative" style={style}>
<div id="chapter-map" className="h-full w-full" />
Expand Down Expand Up @@ -219,28 +243,32 @@ const ChapterMap = ({
</p>
</button>
)}
<div className="absolute top-20 left-3 z-[999] w-fit">
{onShareLocation && (
<Tooltip
showArrow
content={
userLocation ? 'Reset location filter' : 'Share your location to find nearby chapters'
}
placement="bottom-start"
>
<Button
isIconOnly
className="h-[30px] w-[30px] min-w-[30px] rounded-xs bg-white text-gray-700 shadow-lg outline-2 outline-gray-400 hover:bg-gray-100 dark:outline-gray-700"
onPress={onShareLocation}
aria-label={
userLocation ? 'Reset location filter' : 'Share location to find nearby chapters'
{isMapActive && (
<div className="absolute top-20 left-3 z-[999] w-fit">
{onShareLocation && (
<Tooltip
showArrow
content={
userLocation
? 'Reset location filter'
: 'Share your location to find nearby chapters'
}
placement="bottom-start"
>
<FaLocationDot size={14} />
</Button>
</Tooltip>
)}
</div>
<Button
isIconOnly
className="h-[30px] w-[30px] min-w-[30px] rounded-xs bg-white text-gray-700 shadow-lg outline-2 outline-gray-400 hover:bg-gray-100 dark:outline-gray-700"
onPress={onShareLocation}
aria-label={
userLocation ? 'Reset location filter' : 'Share location to find nearby chapters'
}
>
<FaLocationDot size={14} />
</Button>
</Tooltip>
)}
</div>
)}
</div>
)
}
Expand Down