From b318d676088e6f0ef787ffa911c552a12ecb4895 Mon Sep 17 00:00:00 2001 From: Maciej Kasprzyk Date: Thu, 18 Apr 2024 13:38:20 +0200 Subject: [PATCH] feat: better handling for missing map configuration (#308) Adds a warning to be logged to the console when the map is missing configuration for the viewport. Closes #283. --- .../__tests__/__snapshots__/map.test.tsx.snap | 30 ++++++++ src/components/__tests__/map.test.tsx | 68 ++++++++++++++++--- src/components/map/use-map-instance.ts | 19 ++++++ src/hooks/__tests__/use-map.test.tsx | 4 +- 4 files changed, 111 insertions(+), 10 deletions(-) diff --git a/src/components/__tests__/__snapshots__/map.test.tsx.snap b/src/components/__tests__/__snapshots__/map.test.tsx.snap index 4909361..dadc81b 100644 --- a/src/components/__tests__/__snapshots__/map.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/map.test.tsx.snap @@ -1,3 +1,33 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`camera configuration logs a warning message when missing configuration 1`] = ` +[ + " component is missing configuration. You have to provide zoom and center (via the \`zoom\`/\`defaultZoom\` and \`center\`/\`defaultCenter\` props) or specify the region to show using \`defaultBounds\`. See https://visgl.github.io/react-google-maps/docs/api-reference/components/map#required", +] +`; + +exports[`camera configuration logs a warning message when missing configuration 2`] = ` +[ + " component is missing configuration. You have to provide zoom and center (via the \`zoom\`/\`defaultZoom\` and \`center\`/\`defaultCenter\` props) or specify the region to show using \`defaultBounds\`. See https://visgl.github.io/react-google-maps/docs/api-reference/components/map#required", +] +`; + +exports[`camera configuration logs a warning message when missing configuration 3`] = ` +[ + " component is missing configuration. You have to provide zoom and center (via the \`zoom\`/\`defaultZoom\` and \`center\`/\`defaultCenter\` props) or specify the region to show using \`defaultBounds\`. See https://visgl.github.io/react-google-maps/docs/api-reference/components/map#required", +] +`; + +exports[`camera configuration logs a warning message when missing configuration 4`] = ` +[ + " component is missing configuration. You have to provide zoom and center (via the \`zoom\`/\`defaultZoom\` and \`center\`/\`defaultCenter\` props) or specify the region to show using \`defaultBounds\`. See https://visgl.github.io/react-google-maps/docs/api-reference/components/map#required", +] +`; + +exports[`camera configuration logs a warning message when missing configuration 5`] = ` +[ + " component is missing configuration. You have to provide zoom and center (via the \`zoom\`/\`defaultZoom\` and \`center\`/\`defaultCenter\` props) or specify the region to show using \`defaultBounds\`. See https://visgl.github.io/react-google-maps/docs/api-reference/components/map#required", +] +`; + exports[`throws an exception when rendering outside API provider 1`] = `" can only be used inside an component."`; diff --git a/src/components/__tests__/map.test.tsx b/src/components/__tests__/map.test.tsx index 2ec3169..c99602f 100644 --- a/src/components/__tests__/map.test.tsx +++ b/src/components/__tests__/map.test.tsx @@ -3,7 +3,7 @@ import {render, screen, waitFor} from '@testing-library/react'; import {initialize, mockInstances} from '@googlemaps/jest-mocks'; import '@testing-library/jest-dom'; -import {Map as GoogleMap} from '../map'; +import {Map as GoogleMap, MapProps} from '../map'; import {APIProviderContext, APIProviderContextValue} from '../api-provider'; import {APILoadingStatus} from '../../libraries/api-loading-status'; @@ -56,17 +56,21 @@ afterEach(() => { test('map instance is created after api is loaded', async () => { mockContextValue.status = APILoadingStatus.LOADING; - const {rerender} = render(, {wrapper}); + const {rerender} = render( + , + {wrapper} + ); + expect(createMapSpy).not.toHaveBeenCalled(); // rerender after loading completes mockContextValue.status = APILoadingStatus.LOADED; - rerender(); + rerender(); expect(createMapSpy).toHaveBeenCalled(); }); test("map is registered as 'default' when no id is specified", () => { - render(, {wrapper}); + render(, {wrapper}); expect(mockContextValue.addMapInstance).toHaveBeenCalledWith( mockInstances.get(google.maps.Map).at(-1), @@ -79,7 +83,9 @@ test('throws an exception when rendering outside API provider', () => { jest.spyOn(console, 'error').mockImplementation(() => {}); // render without wrapper - expect(() => render()).toThrowErrorMatchingSnapshot(); + expect(() => + render() + ).toThrowErrorMatchingSnapshot(); }); describe('creating and updating map instance', () => { @@ -152,11 +158,51 @@ describe('creating and updating map instance', () => { }); }); -describe('map events and event-props', () => { - test.todo('events dispatched by the map are received via event-props'); -}); +describe('camera configuration', () => { + test.each([ + [{}, true], + [{center: {lat: 0, lng: 0}}, true], + [{defaultCenter: {lat: 0, lng: 0}}, true], + [{zoom: 1}, true], + [{defaultZoom: 1}, true], + [{defaultBounds: {north: 1, east: 2, south: 3, west: 4}}, false], + [{defaultCenter: {lat: 0, lng: 0}, zoom: 0}, false], + [{center: {lat: 0, lng: 0}, zoom: 0}, false], + [{center: {lat: 0, lng: 0}, defaultZoom: 0}, false] + ])( + 'logs a warning message when missing configuration', + (props: MapProps, expectWarningMessage: boolean) => { + // mute warning in test output + const consoleWarn = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + + render(, {wrapper}); + + if (expectWarningMessage) + expect(consoleWarn.mock.lastCall).toMatchSnapshot(); + else expect(consoleWarn).not.toHaveBeenCalled(); + } + ); + + test('makes sure that map renders without viewport configuration', async () => { + // mute warning in test output + console.warn = jest.fn(); + + render(, {wrapper}); + await waitFor(() => expect(screen.getByTestId('map')).toBeInTheDocument()); + + expect(createMapSpy).toHaveBeenCalled(); + + const mapInstance = jest.mocked(mockInstances.get(google.maps.Map).at(0)!); + expect(mapInstance.fitBounds).toHaveBeenCalledWith({ + east: 180, + north: 90, + south: -90, + west: -180 + }); + }); -describe('camera updates', () => { test.todo('initial camera state is passed via mapOptions, not moveCamera'); test.todo('updated camera state is passed to moveCamera'); test.todo("re-renders with unchanged camera state don't trigger moveCamera"); @@ -164,3 +210,7 @@ describe('camera updates', () => { "re-renders with props received via events don't trigger moveCamera" ); }); + +describe('map events and event-props', () => { + test.todo('events dispatched by the map are received via event-props'); +}); diff --git a/src/components/map/use-map-instance.ts b/src/components/map/use-map-instance.ts index 9a1f10f..ccafd00 100644 --- a/src/components/map/use-map-instance.ts +++ b/src/components/map/use-map-instance.ts @@ -43,6 +43,20 @@ export function useMapInstance( ...mapOptions } = props; + const hasZoom = props.zoom !== undefined || props.defaultZoom !== undefined; + const hasCenter = + props.center !== undefined || props.defaultCenter !== undefined; + + if (!defaultBounds && (!hasZoom || !hasCenter)) { + console.warn( + ' component is missing configuration. ' + + 'You have to provide zoom and center (via the `zoom`/`defaultZoom` and ' + + '`center`/`defaultCenter` props) or specify the region to show using ' + + '`defaultBounds`. See ' + + 'https://visgl.github.io/react-google-maps/docs/api-reference/components/map#required' + ); + } + // apply default camera props if available and not overwritten by controlled props if (!mapOptions.center && defaultCenter) mapOptions.center = defaultCenter; if (!mapOptions.zoom && Number.isFinite(defaultZoom)) @@ -76,6 +90,11 @@ export function useMapInstance( newMap.fitBounds(defaultBounds); } + // prevent map not rendering due to missing configuration + else if (!hasZoom || !hasCenter) { + newMap.fitBounds({east: 180, west: -180, south: -90, north: 90}); + } + // the savedMapState is used to restore the camera parameters when the mapId is changed if (savedMapStateRef.current) { const {mapId: savedMapId, cameraState: savedCameraState} = diff --git a/src/hooks/__tests__/use-map.test.tsx b/src/hooks/__tests__/use-map.test.tsx index 63af14a..6cc7a76 100644 --- a/src/hooks/__tests__/use-map.test.tsx +++ b/src/hooks/__tests__/use-map.test.tsx @@ -56,7 +56,9 @@ test('returns the parent map instance when called without id', async () => { // Create wrapper component const wrapper = ({children}: React.PropsWithChildren) => ( - {children} + + {children} + );