From 95333e7030c156bf309958c89cbad01694539157 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 1 Aug 2024 10:11:30 -0700 Subject: [PATCH] fix: Marker click bug (#208) --- .../src/components/DataLayersToggleGroup.tsx | 2 +- frontend/src/components/Footer.test.tsx | 1 - .../components/LocationIconButton.test.tsx | 1 - .../src/components/SearchByRadioGroup.tsx | 2 +- frontend/src/components/SearchInput.tsx | 5 +- frontend/src/features/omrr/omrr-slice.ts | 7 - frontend/src/hooks/useSwipe.test.tsx | 2 +- .../AuthorizationDetails.tsx | 2 +- .../authorizationDetails/DetailsSection.tsx | 2 +- .../authorizationList/ListSearchInput.tsx | 2 +- .../authorizationList/ListSearchSection.tsx | 6 +- .../src/pages/contactUs/ContactUs.test.tsx | 1 - .../src/pages/guidance/GuidancePage.test.tsx | 1 - frontend/src/pages/map/MapView.test.tsx | 17 +- .../pages/map/hooks/useSetSelectedItem.tsx | 3 +- .../pages/map/layers/AuthorizationMarker.tsx | 16 +- .../map/layers/AuthorizationMarkers.test.tsx | 153 ++++++++++++++++++ .../pages/map/layers/AuthorizationMarkers.tsx | 44 ++++- .../map/layers/CrosshairsTooltipMarker.tsx | 2 +- .../pages/map/layers/FindMeControl.test.tsx | 37 +++++ .../src/pages/map/layers/FindMeControl.tsx | 6 + .../src/pages/map/layers/TestMapContainer.tsx | 91 +++++++++++ .../pages/map/search/FindMeButton.test.tsx | 37 +++++ .../src/pages/map/search/FindMeButton.tsx | 6 + 24 files changed, 408 insertions(+), 38 deletions(-) create mode 100644 frontend/src/pages/map/layers/AuthorizationMarkers.test.tsx create mode 100644 frontend/src/pages/map/layers/FindMeControl.test.tsx create mode 100644 frontend/src/pages/map/layers/TestMapContainer.tsx create mode 100644 frontend/src/pages/map/search/FindMeButton.test.tsx diff --git a/frontend/src/components/DataLayersToggleGroup.tsx b/frontend/src/components/DataLayersToggleGroup.tsx index e8a955aa..86b27f6f 100644 --- a/frontend/src/components/DataLayersToggleGroup.tsx +++ b/frontend/src/components/DataLayersToggleGroup.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import { useState } from 'react' import { Button, Checkbox, diff --git a/frontend/src/components/Footer.test.tsx b/frontend/src/components/Footer.test.tsx index 91729f05..e4129a8b 100644 --- a/frontend/src/components/Footer.test.tsx +++ b/frontend/src/components/Footer.test.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { screen } from '@testing-library/react' import { render } from '@/test-utils' diff --git a/frontend/src/components/LocationIconButton.test.tsx b/frontend/src/components/LocationIconButton.test.tsx index f99722d7..c62a57b9 100644 --- a/frontend/src/components/LocationIconButton.test.tsx +++ b/frontend/src/components/LocationIconButton.test.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { screen } from '@testing-library/react' import { LatLngTuple } from 'leaflet' diff --git a/frontend/src/components/SearchByRadioGroup.tsx b/frontend/src/components/SearchByRadioGroup.tsx index 641aabf7..41d1c828 100644 --- a/frontend/src/components/SearchByRadioGroup.tsx +++ b/frontend/src/components/SearchByRadioGroup.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent } from 'react' +import { ChangeEvent } from 'react' import { useDispatch } from 'react-redux' import { FormControlLabel, Radio, RadioGroup } from '@mui/material' import { RadioGroupProps } from '@mui/material/RadioGroup/RadioGroup' diff --git a/frontend/src/components/SearchInput.tsx b/frontend/src/components/SearchInput.tsx index 78330d91..dd12b27c 100644 --- a/frontend/src/components/SearchInput.tsx +++ b/frontend/src/components/SearchInput.tsx @@ -1,13 +1,14 @@ -import React, { MouseEventHandler } from 'react' +import { MouseEventHandler } from 'react' import { IconButton, InputAdornment, TextField } from '@mui/material' import { TextFieldProps } from '@mui/material/TextField/TextField' import clsx from 'clsx' +import { LocationIconButton } from './LocationIconButton' + import SearchIcon from '@/assets/svgs/fa-search.svg?react' import CloseIcon from '@/assets/svgs/fa-close.svg?react' import './SearchInput.css' -import { LocationIconButton } from '@/components/LocationIconButton' interface Props extends Omit { showSearchIcon?: boolean diff --git a/frontend/src/features/omrr/omrr-slice.ts b/frontend/src/features/omrr/omrr-slice.ts index 2f78b8da..66db2b67 100644 --- a/frontend/src/features/omrr/omrr-slice.ts +++ b/frontend/src/features/omrr/omrr-slice.ts @@ -239,7 +239,6 @@ export const useHasSearchTextFilter = () => useSearchTextFilter().length >= MIN_SEARCH_LENGTH export const selectAllResults = (state: RootState) => state.omrr.allResults -const selectAllResultsCount = (state: RootState) => state.omrr.allResults.length export const selectFilteredResults = (state: RootState) => state.omrr.filteredResults @@ -267,12 +266,6 @@ export const useFindByAuthorizationNumber = ( return undefined } -export const useAllResultsShowing = () => { - const allResultsCount = useSelector(selectAllResultsCount) - const filteredResultsCount = useSelector(selectFilteredResultsCount) - return filteredResultsCount === allResultsCount -} - const selectLastSearchTime = (state: RootState) => state.omrr.lastSearchTime export const useLastSearchTime = () => useSelector(selectLastSearchTime) diff --git a/frontend/src/hooks/useSwipe.test.tsx b/frontend/src/hooks/useSwipe.test.tsx index 0a12ad65..364083f9 100644 --- a/frontend/src/hooks/useSwipe.test.tsx +++ b/frontend/src/hooks/useSwipe.test.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react' +import { useRef } from 'react' import { fireEvent, render, screen } from '@testing-library/react' import { SwipeCallback, useSwipe } from './useSwipe' diff --git a/frontend/src/pages/authorizationDetails/AuthorizationDetails.tsx b/frontend/src/pages/authorizationDetails/AuthorizationDetails.tsx index f1626f9a..457fb765 100644 --- a/frontend/src/pages/authorizationDetails/AuthorizationDetails.tsx +++ b/frontend/src/pages/authorizationDetails/AuthorizationDetails.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import { useEffect } from 'react' import { useNavigate } from 'react-router' import { useParams } from 'react-router-dom' import { Grid, Stack, Typography } from '@mui/material' diff --git a/frontend/src/pages/authorizationDetails/DetailsSection.tsx b/frontend/src/pages/authorizationDetails/DetailsSection.tsx index c6a51ecb..d85f082b 100644 --- a/frontend/src/pages/authorizationDetails/DetailsSection.tsx +++ b/frontend/src/pages/authorizationDetails/DetailsSection.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react' +import { ReactNode } from 'react' import { Grid, Stack, Typography } from '@mui/material' import OmrrData from '@/interfaces/omrr' diff --git a/frontend/src/pages/authorizationList/ListSearchInput.tsx b/frontend/src/pages/authorizationList/ListSearchInput.tsx index 8130c028..d3b95972 100644 --- a/frontend/src/pages/authorizationList/ListSearchInput.tsx +++ b/frontend/src/pages/authorizationList/ListSearchInput.tsx @@ -1,4 +1,4 @@ -import React, { ChangeEvent } from 'react' +import { ChangeEvent } from 'react' import { useDispatch } from 'react-redux' import { diff --git a/frontend/src/pages/authorizationList/ListSearchSection.tsx b/frontend/src/pages/authorizationList/ListSearchSection.tsx index d3c7306d..e2049885 100644 --- a/frontend/src/pages/authorizationList/ListSearchSection.tsx +++ b/frontend/src/pages/authorizationList/ListSearchSection.tsx @@ -1,11 +1,11 @@ -import React, { useState } from 'react' +import { useState } from 'react' import { Box, Button } from '@mui/material' import { ArrowDropDown, ArrowDropUp } from '@mui/icons-material' +import clsx from 'clsx' +import { FilterByCheckboxGroup } from '@/components/FilterByCheckboxGroup' import { ListSearchInput } from './ListSearchInput' import { ListSearchByGroup } from './ListSearchByGroup' -import { FilterByCheckboxGroup } from '@/components/FilterByCheckboxGroup' -import clsx from 'clsx' const styles = { searchByRow: { diff --git a/frontend/src/pages/contactUs/ContactUs.test.tsx b/frontend/src/pages/contactUs/ContactUs.test.tsx index 09e885ec..77102876 100644 --- a/frontend/src/pages/contactUs/ContactUs.test.tsx +++ b/frontend/src/pages/contactUs/ContactUs.test.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { screen } from '@testing-library/react' import { render } from '@/test-utils' diff --git a/frontend/src/pages/guidance/GuidancePage.test.tsx b/frontend/src/pages/guidance/GuidancePage.test.tsx index 4c19dc95..eb4eba96 100644 --- a/frontend/src/pages/guidance/GuidancePage.test.tsx +++ b/frontend/src/pages/guidance/GuidancePage.test.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { screen } from '@testing-library/react' import { DATA_LAYER_GROUPS } from '@/constants/data-layers' diff --git a/frontend/src/pages/map/MapView.test.tsx b/frontend/src/pages/map/MapView.test.tsx index 83d9ea60..ae23d0d9 100644 --- a/frontend/src/pages/map/MapView.test.tsx +++ b/frontend/src/pages/map/MapView.test.tsx @@ -34,7 +34,7 @@ describe('Test suite for MapView', () => { }) } it('should render the MapView with markers', async () => { - renderComponent(themeBreakpointValues.xxl) + const { user } = renderComponent(themeBreakpointValues.xxl) const mapView = screen.getByTestId('map-view') expect(mapView).not.toHaveClass('map-view--small') @@ -43,11 +43,20 @@ describe('Test suite for MapView', () => { expect(markers.length > 0).toBe(true) screen.getByPlaceholderText('Search') - screen.getByRole('button', { name: 'Find Me' }) + await screen.findByRole('button', { name: 'Find Me' }) expect(screen.queryByTitle('Show the data layers')).not.toBeInTheDocument() expect( screen.queryByTitle('Show my location on the map'), ).not.toBeInTheDocument() + + const showResults = screen.getByRole('button', { name: 'Show Results' }) + await user.click(showResults) + + const zoomToBtn = screen.getByRole('button', { name: 'Zoom To Results' }) + await user.click(zoomToBtn) + + const hideResults = screen.getByRole('button', { name: 'Hide Results' }) + await user.click(hideResults) }) it('should render the MapView with no markers on a small screen', async () => { @@ -140,10 +149,10 @@ describe('Test suite for MapView', () => { it('should render the MapView and test polygon search', async () => { const { user } = renderComponent(themeBreakpointValues.xxl, mockOmrrData) - const pointSearchBtn = screen.getByRole('button', { + const polygonSearchBtn = screen.getByRole('button', { name: 'Polygon Search', }) - await user.click(pointSearchBtn) + await user.click(polygonSearchBtn) const cancelBtn = screen.getByRole('button', { name: 'Cancel' }) const deleteBtn = screen.getByRole('button', { name: 'Delete Last Point' }) diff --git a/frontend/src/pages/map/hooks/useSetSelectedItem.tsx b/frontend/src/pages/map/hooks/useSetSelectedItem.tsx index 27991cd2..611d10ec 100644 --- a/frontend/src/pages/map/hooks/useSetSelectedItem.tsx +++ b/frontend/src/pages/map/hooks/useSetSelectedItem.tsx @@ -4,6 +4,7 @@ import { useDispatch } from 'react-redux' import OmrrData from '@/interfaces/omrr' import { setSelectedItem } from '@/features/map/map-slice' import { useZoomToAuthorization } from './useZoomTo' +import { isTest } from '@/constants/env' /** * Selects a single OmrrData item. @@ -20,7 +21,7 @@ export function useSetSelectedItem() { dispatch(setSelectedItem(item)) // Make sure this item is visible on the map // Delay the map zoom until the sidebar/bottom drawer finish expanding - zoomTo(item, 300) + zoomTo(item, isTest ? 0 : 300) }, [dispatch], ) diff --git a/frontend/src/pages/map/layers/AuthorizationMarker.tsx b/frontend/src/pages/map/layers/AuthorizationMarker.tsx index e3c8e275..1cb10bad 100644 --- a/frontend/src/pages/map/layers/AuthorizationMarker.tsx +++ b/frontend/src/pages/map/layers/AuthorizationMarker.tsx @@ -1,22 +1,20 @@ import { Tooltip } from 'react-leaflet' import OmrrData from '@/interfaces/omrr' -import { useSetSelectedItem } from '../hooks/useSetSelectedItem' -import { IconMarker } from './IconMarker' import { blueIcon1x, blueIcon2x } from '@/constants/marker-icons' +import { IconMarker } from './IconMarker' interface Props { item: OmrrData isSmall: boolean + onClick: () => void } -export function AuthorizationMarker({ item, isSmall }: Readonly) { - const selectItem = useSetSelectedItem() - - const onClick = () => { - selectItem(item) - } - +export function AuthorizationMarker({ + item, + isSmall, + onClick, +}: Readonly) { const title = item['Regulated Party'] return ( { + function renderComponent( + omrrState: Partial = {}, + mapState: Partial = {}, + screenWidth = 1500, + ) { + const state: State = {} + + const TestComponent = () => { + Object.assign(state, { + selectedItem: useSelectedItem(), + zoomPosition: useSelector(selectZoomPosition), + }) + return ( + + + + ) + } + + const { user } = render(, { + screenWidth, + withStateProvider: true, + initialState: { + omrr: { + ...initialOmrrState, + status: 'succeeded', + filteredResults: mockOmrrData, + ...omrrState, + }, + map: { + ...initialMapState, + ...mapState, + }, + }, + }) + return { user, state } + } + + it('should not render AuthorizationMarkers if status is not succeeded', () => { + renderComponent({ status: 'failed' }) + expect( + screen.queryByAltText('Authorization marker'), + ).not.toBeInTheDocument() + }) + + it('should not render AuthorizationMarkers if no items', () => { + renderComponent({ filteredResults: [] }) + expect( + screen.queryByAltText('Authorization marker'), + ).not.toBeInTheDocument() + }) + + it('should render AuthorizationMarkers and click on marker', async () => { + const { user, state } = renderComponent() + + const markers = screen.getAllByAltText('Authorization marker') + expect(markers).toHaveLength(mockOmrrData.length) + + const [marker] = markers + await user.click(marker) + + expect(state.selectedItem).toBe(mockOmrrData[0]) + expect(state.zoomPosition).toBeDefined() + }) + + it('should render AuthorizationMarkers in polygon search mode and click on marker', async () => { + const { user, state } = renderComponent( + {}, + { activeTool: ActiveToolEnum.polygonSearch }, + ) + + const markers = screen.getAllByAltText('Authorization marker') + const [marker] = markers + // Click is ignored when in polygon search mode + await user.click(marker) + + expect(state.selectedItem).toBeUndefined() + }) + + it('should render AuthorizationMarkers in polygon search mode with positions', async () => { + const { user, state } = renderComponent( + { + polygonFilter: { + positions: [[48.123, -123.123]], + finished: false, + }, + }, + { activeTool: ActiveToolEnum.polygonSearch }, + ) + + const marker = screen.getAllByAltText('Authorization marker')[0] + expect(marker).toBeDefined() + await user.click(marker) + + expect(state.selectedItem).toBeUndefined() + }) + + it('should render AuthorizationMarkers in point search mode and click on marker', async () => { + const { user, state } = renderComponent( + {}, + { activeTool: ActiveToolEnum.pointSearch }, + ) + + const markers = screen.getAllByAltText('Authorization marker') + const [marker] = markers + await user.click(marker) + + expect(state.selectedItem).toBeUndefined() + }) + + it('should render AuthorizationMarkers in point search mode with radius', async () => { + const { user, state } = renderComponent( + { + circleFilter: { radius: MIN_CIRCLE_RADIUS }, + }, + { activeTool: ActiveToolEnum.pointSearch }, + 500, + ) + + const marker = screen.getAllByAltText('Authorization marker')[0] + expect(marker).toBeDefined() + await user.click(marker) + + expect(state.selectedItem).toBeUndefined() + }) +}) diff --git a/frontend/src/pages/map/layers/AuthorizationMarkers.tsx b/frontend/src/pages/map/layers/AuthorizationMarkers.tsx index d6705646..732ed5cf 100644 --- a/frontend/src/pages/map/layers/AuthorizationMarkers.tsx +++ b/frontend/src/pages/map/layers/AuthorizationMarkers.tsx @@ -1,4 +1,4 @@ -import { useSelector } from 'react-redux' +import { useSelector, useStore } from 'react-redux' import MarkerClusterGroup from 'react-leaflet-cluster' import { useTheme } from '@mui/material/styles' import useMediaQuery from '@mui/material/useMediaQuery' @@ -6,16 +6,57 @@ import useMediaQuery from '@mui/material/useMediaQuery' import { selectStatus, useFilteredResults } from '@/features/omrr/omrr-slice' import OmrrData from '@/interfaces/omrr' import { AuthorizationMarker } from './AuthorizationMarker' +import { useSetSelectedItem } from '@/pages/map/hooks/useSetSelectedItem' +import { useCallback } from 'react' +import { RootState } from '@/app/store' +import { ActiveToolEnum } from '@/constants/constants' + +/** + * Looks at the activeTool value (map slice) and + * polygonFilter and circleFilter values (omrr slice) to + * determine if a polygon or point search is in progress. + * This is not reactive, and is intended for use inside an event handler. + * This is safe according to the Redux docs: + * @see https://react-redux.js.org/api/hooks#usestore + */ +function useShapeFilterInProgress() { + const store = useStore() + + return useCallback((): boolean => { + const { omrr, map } = store.getState() + const { activeTool } = map + const { polygonFilter, circleFilter } = omrr + if (activeTool === ActiveToolEnum.polygonSearch) { + // When the polygon search tool is active but not finished + return polygonFilter ? !polygonFilter.finished : true + } + // When the point search tool is active, but the center point isn't defined + if (activeTool === ActiveToolEnum.pointSearch) { + return circleFilter ? !circleFilter.center : true + } + return false + }, [store]) +} export function AuthorizationMarkers() { const values = useFilteredResults() const status = useSelector(selectStatus) const theme = useTheme() const isSmall = useMediaQuery(theme.breakpoints.down('sm')) + const selectItem = useSetSelectedItem() + const isShapeSearchInProgressFn = useShapeFilterInProgress() const hasMarkers = status === 'succeeded' && Array.isArray(values) && values.length > 0 + const onClick = (item: OmrrData) => { + // Disable clicks on markers when polygon/point search are in progress + const shapeSearchInProgress = isShapeSearchInProgressFn() + if (!shapeSearchInProgress) { + selectItem(item) + } + } + return hasMarkers ? ( onClick(item)} /> ))} diff --git a/frontend/src/pages/map/layers/CrosshairsTooltipMarker.tsx b/frontend/src/pages/map/layers/CrosshairsTooltipMarker.tsx index 31fea78e..a34e97df 100644 --- a/frontend/src/pages/map/layers/CrosshairsTooltipMarker.tsx +++ b/frontend/src/pages/map/layers/CrosshairsTooltipMarker.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useRef } from 'react' +import { ReactNode, useRef } from 'react' import { LatLngExpression, LeafletMouseEvent, diff --git a/frontend/src/pages/map/layers/FindMeControl.test.tsx b/frontend/src/pages/map/layers/FindMeControl.test.tsx new file mode 100644 index 00000000..ae530143 --- /dev/null +++ b/frontend/src/pages/map/layers/FindMeControl.test.tsx @@ -0,0 +1,37 @@ +import { screen, waitForElementToBeRemoved } from '@testing-library/react' +import { Mock } from 'vitest' + +import { render } from '@/test-utils' +import { FindMeControl } from './FindMeControl' + +describe('Test suite for FindMeControl', () => { + it('should render FindMeControl', async () => { + const { user } = render(, { + withStateProvider: true, + }) + + const btn = screen.getByTitle('Show my location on the map') + expect(btn).not.toHaveClass('map-control-button--active') + + await user.click(btn) + + expect(btn).toHaveClass('map-control-button--active') + }) + + it('should not render FindMeControl when no permission', async () => { + const queryMock = navigator.permissions.query as Mock + queryMock.mockResolvedValueOnce({ + name: 'geolocation', + state: 'denied', + }) + + render(, { + withStateProvider: true, + }) + + // Initially shows up until permissions query resolves + await waitForElementToBeRemoved(() => + screen.getByTitle('Show my location on the map'), + ) + }) +}) diff --git a/frontend/src/pages/map/layers/FindMeControl.tsx b/frontend/src/pages/map/layers/FindMeControl.tsx index 1cd7b0d1..a96da0b8 100644 --- a/frontend/src/pages/map/layers/FindMeControl.tsx +++ b/frontend/src/pages/map/layers/FindMeControl.tsx @@ -6,12 +6,18 @@ import { setMyLocationVisible, useMyLocationVisible, } from '@/features/map/map-slice' +import { useGeolocationPermission } from '@/hooks/useMyLocation' import GpsIcon from '@/assets/svgs/fa-gps.svg?react' export function FindMeControl() { const dispatch = useDispatch() const isMarkerVisible = useMyLocationVisible() + const state = useGeolocationPermission() + // No point in showing the button if the permission has been denied + if (state === 'denied') { + return null + } const onClick = () => { dispatch(setMyLocationVisible(!isMarkerVisible)) diff --git a/frontend/src/pages/map/layers/TestMapContainer.tsx b/frontend/src/pages/map/layers/TestMapContainer.tsx new file mode 100644 index 00000000..0cfe7d1c --- /dev/null +++ b/frontend/src/pages/map/layers/TestMapContainer.tsx @@ -0,0 +1,91 @@ +import { ReactNode, useEffect } from 'react' +import { LatLngExpression } from 'leaflet' +import { MapContainer, useMap } from 'react-leaflet' + +import { MapZoom } from './MapZoom' + +interface Props { + center?: LatLngExpression + zoom?: number + zoomControl?: boolean + children: ReactNode +} + +export function TestMapContainer({ + center = [48, -123], + zoom = 13, + zoomControl = false, + children, +}: Readonly) { + return ( + + + + {children} + + ) +} + +/** + * There is a bug/issue with the user-event v14 library + * and leaflet that causes click events to throw a NaN error. + * This is an ugly workaround. + * @see https://stackoverflow.com/questions/70791283/testing-react-leaflet-marker-click-event-triggers-error-invalid-latlng-object + */ +export function FixMap() { + const map = useMap() + + useEffect(() => { + const container = map.getContainer() + // First leaflet container to have a fixed size + if (container.clientWidth === 0) { + Object.defineProperty(container, 'clientWidth', { value: 800 }) + Object.defineProperty(container, 'clientHeight', { value: 600 }) + } + + // Fix bug where Util.extend doesn't clone getters properly + const mapAny: any = map + const realFireDOMEvent = mapAny._fireDOMEvent + mapAny._fireDOMEvent = (e: any, type: string, canvasTargets: any) => { + if (type === 'click') { + // the clientX and clientY getter properties are defined on the parent MouseEvent class + // when leaflet clones the event object to fire the 'preclick' event + // these properties are not copied and are therefore undefined + const { target, currentTarget, srcElement } = e + e = new MouseEvent(type, { + button: e.button, + buttons: e.buttons, + clientX: e.clientX, + clientY: e.clientY, + relatedTarget: e.target, + screenX: e.screenX, + screenY: e.screenY, + altKey: e.altKey, + ctrlKey: e.ctrlKey, + shiftKey: e.shiftKey, + metaKey: e.metaKey, + detail: e.detail, + view: e.view, + bubbles: e.bubbles, + cancelable: e.cancelable, + composed: e.composed, + }) + Object.defineProperty(e, 'target', { + value: target, + enumerable: true, + }) + Object.defineProperty(e, 'currentTarget', { + value: currentTarget, + enumerable: true, + }) + Object.defineProperty(e, 'srcElement', { + value: srcElement, + enumerable: true, + }) + } + return realFireDOMEvent.call(map, e, type, canvasTargets) + } + }, [map]) + + return null +} diff --git a/frontend/src/pages/map/search/FindMeButton.test.tsx b/frontend/src/pages/map/search/FindMeButton.test.tsx new file mode 100644 index 00000000..6404e2bf --- /dev/null +++ b/frontend/src/pages/map/search/FindMeButton.test.tsx @@ -0,0 +1,37 @@ +import { screen, waitForElementToBeRemoved } from '@testing-library/react' + +import { FindMeButton } from './FindMeButton' +import { render } from '@/test-utils' +import { Mock } from '~/vitest' + +describe('Test suite for FindMeButton', () => { + it('should render FindMeButton', async () => { + const { user } = render(, { + withStateProvider: true, + }) + + const btn = screen.getByRole('button', { name: 'Find Me' }) + expect(btn).not.toHaveClass('map-button--active') + + await user.click(btn) + + expect(btn).toHaveClass('map-button--active') + }) + + it('should not render FindMeButton when no permission', async () => { + const queryMock = navigator.permissions.query as Mock + queryMock.mockResolvedValueOnce({ + name: 'geolocation', + state: 'denied', + }) + + render(, { + withStateProvider: true, + }) + + // Initially shows up until permissions query resolves + await waitForElementToBeRemoved(() => + screen.getByRole('button', { name: 'Find Me' }), + ) + }) +}) diff --git a/frontend/src/pages/map/search/FindMeButton.tsx b/frontend/src/pages/map/search/FindMeButton.tsx index 2e4606b0..3ee74165 100644 --- a/frontend/src/pages/map/search/FindMeButton.tsx +++ b/frontend/src/pages/map/search/FindMeButton.tsx @@ -6,12 +6,18 @@ import { setMyLocationVisible, useMyLocationVisible, } from '@/features/map/map-slice' +import { useGeolocationPermission } from '@/hooks/useMyLocation' import FindMeIcon from '@/assets/svgs/fa-gps.svg?react' export function FindMeButton() { const dispatch = useDispatch() const isMarkerVisible = useMyLocationVisible() + const state = useGeolocationPermission() + // No point in showing the button if the permission has been denied + if (state === 'denied') { + return null + } const onClick = () => { dispatch(setMyLocationVisible(!isMarkerVisible))