Skip to content

Commit

Permalink
feat: better handling for missing map configuration (#308)
Browse files Browse the repository at this point in the history
Adds a warning to be logged to the console when the map is missing configuration for the viewport.

Closes #283.
  • Loading branch information
maciej-ka committed Apr 18, 2024
1 parent 65af089 commit b318d67
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 10 deletions.
30 changes: 30 additions & 0 deletions src/components/__tests__/__snapshots__/map.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,3 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`camera configuration logs a warning message when missing configuration 1`] = `
[
"<Map> 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`] = `
[
"<Map> 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`] = `
[
"<Map> 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`] = `
[
"<Map> 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`] = `
[
"<Map> 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`] = `"<Map> can only be used inside an <ApiProvider> component."`;
68 changes: 59 additions & 9 deletions src/components/__tests__/map.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -56,17 +56,21 @@ afterEach(() => {
test('map instance is created after api is loaded', async () => {
mockContextValue.status = APILoadingStatus.LOADING;

const {rerender} = render(<GoogleMap />, {wrapper});
const {rerender} = render(
<GoogleMap zoom={8} center={{lat: 53.55, lng: 10.05}} />,
{wrapper}
);

expect(createMapSpy).not.toHaveBeenCalled();

// rerender after loading completes
mockContextValue.status = APILoadingStatus.LOADED;
rerender(<GoogleMap />);
rerender(<GoogleMap zoom={8} center={{lat: 53.55, lng: 10.05}} />);
expect(createMapSpy).toHaveBeenCalled();
});

test("map is registered as 'default' when no id is specified", () => {
render(<GoogleMap />, {wrapper});
render(<GoogleMap zoom={8} center={{lat: 53.55, lng: 10.05}} />, {wrapper});

expect(mockContextValue.addMapInstance).toHaveBeenCalledWith(
mockInstances.get(google.maps.Map).at(-1),
Expand All @@ -79,7 +83,9 @@ test('throws an exception when rendering outside API provider', () => {
jest.spyOn(console, 'error').mockImplementation(() => {});

// render without wrapper
expect(() => render(<GoogleMap />)).toThrowErrorMatchingSnapshot();
expect(() =>
render(<GoogleMap zoom={8} center={{lat: 53.55, lng: 10.05}} />)
).toThrowErrorMatchingSnapshot();
});

describe('creating and updating map instance', () => {
Expand Down Expand Up @@ -152,15 +158,59 @@ 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(<GoogleMap {...props} />, {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(<GoogleMap />, {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");
test.todo(
"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');
});
19 changes: 19 additions & 0 deletions src/components/map/use-map-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
'<Map> 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))
Expand Down Expand Up @@ -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} =
Expand Down
4 changes: 3 additions & 1 deletion src/hooks/__tests__/use-map.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ test('returns the parent map instance when called without id', async () => {
// Create wrapper component
const wrapper = ({children}: React.PropsWithChildren) => (
<MockApiContextProvider>
<GoogleMap>{children}</GoogleMap>
<GoogleMap zoom={8} center={{lat: 53.55, lng: 10.05}}>
{children}
</GoogleMap>
</MockApiContextProvider>
);

Expand Down

0 comments on commit b318d67

Please sign in to comment.