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
165 changes: 160 additions & 5 deletions src/components/__tests__/map.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, {FunctionComponent, PropsWithChildren} from 'react';
import {cleanup, render, screen, waitFor} from '@testing-library/react';
import {act, cleanup, render, screen, waitFor} from '@testing-library/react';
import {initialize, mockInstances} from '@googlemaps/jest-mocks';
import '@testing-library/jest-dom';

Expand Down Expand Up @@ -309,13 +309,168 @@ describe('camera configuration', () => {
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"
);

test("invalid camera events don't overwrite the last known camera state", async () => {
const listeners: Record<string, Array<() => void>> = {};
google.maps.event.addListener = jest.fn((_, eventName, handler) => {
listeners[eventName] ??= [];
listeners[eventName].push(handler as () => void);
return {remove: jest.fn()};
});
jest.spyOn(console, 'warn').mockImplementation(() => {});

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

await waitFor(() => expect(screen.getByTestId('map')).toBeInTheDocument());

const mapInstance = jest.mocked(mockInstances.get(google.maps.Map).at(-1)!);
jest.mocked(mapInstance.getCenter).mockReturnValue({
toJSON: () => ({lat: 53.55, lng: 10.05})
} as google.maps.LatLng);
jest.mocked(mapInstance.getBounds).mockReturnValue({
toJSON: () => ({north: 54, east: 11, south: 53, west: 10})
} as google.maps.LatLngBounds);
jest.mocked(mapInstance.getZoom).mockReturnValue(8);

act(() => {
listeners.bounds_changed.forEach(listener => listener());
});

jest.mocked(mapInstance.moveCamera).mockClear();
jest.mocked(mapInstance.getZoom).mockReturnValue(NaN);

act(() => {
listeners.bounds_changed.forEach(listener => listener());
});

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

expect(mapInstance.moveCamera).not.toHaveBeenCalled();
});

test('tracked camera state accepts valid camera values while bounds are unavailable', async () => {
const listeners: Record<string, Array<() => void>> = {};
google.maps.event.addListener = jest.fn((_, eventName, handler) => {
listeners[eventName] ??= [];
listeners[eventName].push(handler as () => void);
return {remove: jest.fn()};
});

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

await waitFor(() => expect(screen.getByTestId('map')).toBeInTheDocument());

const mapInstance = jest.mocked(mockInstances.get(google.maps.Map).at(-1)!);
jest.mocked(mapInstance.getCenter).mockReturnValue({
toJSON: () => ({lat: 53.55, lng: 10.05})
} as google.maps.LatLng);
jest.mocked(mapInstance.getBounds).mockReturnValue(undefined);
jest.mocked(mapInstance.getZoom).mockReturnValue(8);

act(() => {
listeners.bounds_changed.forEach(listener => listener());
});

jest.mocked(mapInstance.moveCamera).mockClear();

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

expect(mapInstance.moveCamera).not.toHaveBeenCalled();
});
});

describe('map events and event-props', () => {
test.todo('events dispatched by the map are received via event-props');
test('events dispatched by the map are received via event-props', async () => {
const listeners: Record<string, Array<() => void>> = {};
google.maps.event.addListener = jest.fn((_, eventName, handler) => {
listeners[eventName] ??= [];
listeners[eventName].push(handler as () => void);
return {remove: jest.fn()};
});

const handleCameraChanged = jest.fn();

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

await waitFor(() => expect(screen.getByTestId('map')).toBeInTheDocument());

const mapInstance = jest.mocked(mockInstances.get(google.maps.Map).at(-1)!);
jest.mocked(mapInstance.getCenter).mockReturnValue({
toJSON: () => ({lat: 53.55, lng: 10.05})
} as google.maps.LatLng);
jest.mocked(mapInstance.getBounds).mockReturnValue({
toJSON: () => ({north: 54, east: 11, south: 53, west: 10})
} as google.maps.LatLngBounds);
jest.mocked(mapInstance.getZoom).mockReturnValue(8);

act(() => {
listeners.bounds_changed.forEach(listener => listener());
});

expect(handleCameraChanged).toHaveBeenCalledWith(
expect.objectContaining({
type: 'bounds_changed',
detail: {
center: {lat: 53.55, lng: 10.05},
zoom: 8,
heading: 0,
tilt: 0,
bounds: {north: 54, east: 11, south: 53, west: 10}
}
})
);
});

test('does not emit camera events with invalid map camera values', async () => {
const listeners: Record<string, Array<() => void>> = {};
google.maps.event.addListener = jest.fn((_, eventName, handler) => {
listeners[eventName] ??= [];
listeners[eventName].push(handler as () => void);
return {remove: jest.fn()};
});
jest.spyOn(console, 'warn').mockImplementation(() => {});

const handleCameraChanged = jest.fn();

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

await waitFor(() => expect(screen.getByTestId('map')).toBeInTheDocument());

const mapInstance = jest.mocked(mockInstances.get(google.maps.Map).at(-1)!);
jest.mocked(mapInstance.getCenter).mockReturnValue({
toJSON: () => ({lat: 53.55, lng: 10.05})
} as google.maps.LatLng);
jest.mocked(mapInstance.getBounds).mockReturnValue({
toJSON: () => ({north: 54, east: 11, south: 53, west: 10})
} as google.maps.LatLngBounds);
jest.mocked(mapInstance.getZoom).mockReturnValue(NaN);

act(() => {
listeners.bounds_changed.forEach(listener => listener());
});

expect(handleCameraChanged).not.toHaveBeenCalled();
});
});

describe('internalUsageAttributionIds', () => {
Expand Down
18 changes: 8 additions & 10 deletions src/components/map/use-map-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export function useMapEvents(
map,
eventType,
(ev?: google.maps.MapMouseEvent | google.maps.IconMouseEvent) => {
handler(createMapEvent(eventType, map, ev));
const mapEvent = createMapEvent(eventType, map, ev);
if (mapEvent) handler(mapEvent);
}
);

Expand All @@ -82,7 +83,7 @@ function createMapEvent(
type: string,
map: google.maps.Map,
srcEvent?: google.maps.MapMouseEvent | google.maps.IconMouseEvent
): MapEvent {
): MapEvent | null {
const ev: MapEvent = {
type,
map,
Expand All @@ -106,19 +107,16 @@ function createMapEvent(
'returned undefined. This is not expected to happen. Please ' +
'report an issue at https://github.com/visgl/react-google-maps/issues/new'
);

return null;
}

camEvent.detail = {
center: center?.toJSON() || {lat: 0, lng: 0},
zoom: (zoom as number) || 0,
center: center.toJSON(),
zoom: zoom as number,
heading: heading as number,
tilt: tilt as number,
bounds: bounds?.toJSON() || {
north: 90,
east: 180,
south: -90,
west: -180
}
bounds: bounds.toJSON()
};

return camEvent;
Expand Down
10 changes: 5 additions & 5 deletions src/components/map/use-tracked-camera-state-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@ function handleBoundsChange(map: google.maps.Map, ref: CameraStateRef) {
const zoom = map.getZoom();
const heading = map.getHeading() || 0;
const tilt = map.getTilt() || 0;
const bounds = map.getBounds();

if (!center || !bounds || !Number.isFinite(zoom)) {
if (!center || !Number.isFinite(zoom)) {
console.warn(
'[useTrackedCameraState] at least one of the values from the map ' +
'returned undefined. This is not expected to happen. Please ' +
'report an issue at https://github.com/visgl/react-google-maps/issues/new'
);

return;
Comment thread
usefulthink marked this conversation as resolved.
}

// fixme: do we need the `undefined` cases for the camera-params? When are they used in the maps API?
Object.assign(ref.current, {
center: center?.toJSON() || {lat: 0, lng: 0},
zoom: (zoom as number) || 0,
center: center.toJSON(),
zoom: zoom as number,
heading: heading as number,
tilt: tilt as number
});
Expand Down