Skip to content

Commit

Permalink
fix(advanced-marker): apply marker class when rendering a Pin (#384)
Browse files Browse the repository at this point in the history
Previously, the AdvancedMarker would only apply the className prop to the content when custom html contents were specified, but there are some cases (applying css-animations to markers for example) where we also want the className to apply to markers that just contain a PinElement.

This also contains a rewrite and extension of the AdvancedMarker tests.
  • Loading branch information
usefulthink committed May 27, 2024
1 parent d608602 commit e8a4cc3
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 61 deletions.
7 changes: 4 additions & 3 deletions docs/api-reference/components/advanced-marker.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,10 @@ shown on the map.

#### `className`: string

A className to be added to the content-element. Since the content-element
isn't created when using the default-pin, this option is only available when
using custom HTML markers.
A className to be added to the markers content-element. The content-element is
either an element that contains the custom HTML content or the DOM
representation of the `google.maps.marker.PinElement` when a Pin or an
empty AdvancedMarker component is rendered.

#### `style`: [CSSProperties][react-dev-styling]

Expand Down
174 changes: 117 additions & 57 deletions src/components/__tests__/advanced-marker.test.tsx
Original file line number Diff line number Diff line change
@@ -1,100 +1,160 @@
import React, {JSX} from 'react';
import React from 'react';

import {initialize, mockInstances} from '@googlemaps/jest-mocks';
import {cleanup, render} from '@testing-library/react';
import {cleanup, queryByTestId, render} from '@testing-library/react';
import '@testing-library/jest-dom';

import {AdvancedMarker} from '../advanced-marker';
import {useMap} from '../../hooks/use-map';
import {useMapsLibrary} from '../../hooks/use-maps-library';

import {APIProvider} from '../api-provider';
import {Map as GoogleMap} from '../map';
import {AdvancedMarker as GoogleMapsMarker} from '../advanced-marker';
import {waitForMockInstance} from './__utils__/wait-for-mock-instance';

jest.mock('../../libraries/google-maps-api-loader');
jest.mock('../../hooks/use-map');
jest.mock('../../hooks/use-maps-library');

let wrapper: ({children}: {children: React.ReactNode}) => JSX.Element | null;
let createMarkerSpy: jest.Mock;
let useMapMock: jest.MockedFn<typeof useMap>;
let useMapsLibraryMock: jest.MockedFn<typeof useMapsLibrary>;

let markerLib: google.maps.MarkerLibrary;
let mapInstance: google.maps.Map;

beforeEach(() => {
beforeEach(async () => {
initialize();
jest.clearAllMocks();

google.maps.importLibrary = jest.fn(() => Promise.resolve()) as never;
// mocked versions of the useMap and useMapsLibrary functions
useMapMock = jest.mocked(useMap);
useMapsLibraryMock = jest.mocked(useMapsLibrary);

// Create wrapper component
wrapper = ({children}: {children: React.ReactNode}) => (
<APIProvider apiKey={'apikey'}>
<GoogleMap zoom={10} center={{lat: 0, lng: 0}}>
{children}
</GoogleMap>
</APIProvider>
);
// load the marker-lib that can be returned from the useMapsLibrary mock
markerLib = (await google.maps.importLibrary(
'marker'
)) as google.maps.MarkerLibrary;

// custom implementation of the AdvancedMarkerElement that has a properly
// initialized content and an observeable constructor.
createMarkerSpy = jest.fn();
google.maps.marker.AdvancedMarkerElement = class extends (
google.maps.marker.AdvancedMarkerElement
) {
const AdvancedMarkerElement = class extends google.maps.marker
.AdvancedMarkerElement {
constructor(o?: google.maps.marker.AdvancedMarkerElementOptions) {
createMarkerSpy(o);
super(o);

// @googlemaps/js-jest-mocks doesn't initialize the .content property
// as the real implementation does (this would normally be a pin-element,
// but for our purposes a div should suffice)
this.content = document.createElement('div');
}
};

// the element has to be registered for this to work, but since we can
// neither unregister nor re-register an element, a randomized name is
// the element has to be registered for this override to work, but since we
// can neither unregister nor re-register an element, a randomized name is
// used for the element.
customElements.define(
`gmp-advanced-marker-${Math.random().toString(36).slice(2)}`,
google.maps.marker.AdvancedMarkerElement
AdvancedMarkerElement
);

google.maps.marker.AdvancedMarkerElement = markerLib.AdvancedMarkerElement =
AdvancedMarkerElement;
});

afterEach(() => {
cleanup();
});

test('marker should be initialized', async () => {
render(<GoogleMapsMarker position={{lat: 0, lng: 0}} />, {
wrapper
test('creates marker instance once map is ready', async () => {
useMapsLibraryMock.mockReturnValue(null);
useMapMock.mockReturnValue(null);

const {rerender} = render(<AdvancedMarker position={{lat: 0, lng: 0}} />);

expect(useMapsLibraryMock).toHaveBeenCalledWith('marker');
expect(createMarkerSpy).not.toHaveBeenCalled();

useMapsLibraryMock.mockImplementation(name => {
expect(name).toEqual('marker');
return markerLib;
});
const marker = await waitForMockInstance(
google.maps.marker.AdvancedMarkerElement
);
const mockMap = new google.maps.Map(document.createElement('div'));
useMapMock.mockReturnValue(mockMap);

expect(createMarkerSpy).toHaveBeenCalledTimes(1);
expect(marker).toBeDefined();
rerender(<AdvancedMarker position={{lat: 1, lng: 2}} />);

expect(createMarkerSpy).toHaveBeenCalled();
});

test('multiple markers should be initialized', async () => {
render(
<>
<GoogleMapsMarker position={{lat: 0, lng: 0}} />
<GoogleMapsMarker position={{lat: 0, lng: 0}} />
</>,
{
wrapper
}
);
describe('map and marker-library loaded', () => {
beforeEach(() => {
useMapsLibraryMock.mockImplementation(name => {
expect(name).toEqual('marker');
return markerLib;
});

await waitForMockInstance(google.maps.marker.AdvancedMarkerElement);
mapInstance = new google.maps.Map(document.createElement('div'));
useMapMock.mockReturnValue(mapInstance);
});

const markers = mockInstances.get(google.maps.marker.AdvancedMarkerElement);
test('marker should be initialized', async () => {
render(<AdvancedMarker position={{lat: 0, lng: 0}} />);

expect(markers).toHaveLength(2);
});
expect(createMarkerSpy).toHaveBeenCalledTimes(1);
expect(
mockInstances.get(google.maps.marker.AdvancedMarkerElement)
).toHaveLength(1);
});

test('marker should have and update its position', async () => {
const {rerender} = render(<AdvancedMarker position={{lat: 1, lng: 0}} />);
const marker = await waitForMockInstance(
google.maps.marker.AdvancedMarkerElement
);

expect(marker.position).toEqual({lat: 1, lng: 0});

rerender(<AdvancedMarker position={{lat: 2, lng: 2}} />);

test('marker should have a position', async () => {
const {rerender} = render(<GoogleMapsMarker position={{lat: 1, lng: 0}} />, {
wrapper
expect(marker.position).toEqual({lat: 2, lng: 2});
});

const marker = await waitForMockInstance(
google.maps.marker.AdvancedMarkerElement
);
test('marker class is set correctly without html content', async () => {
render(
<AdvancedMarker
position={{lat: 1, lng: 2}}
className={'classname-test'}
/>
);

expect(marker.position).toEqual({lat: 1, lng: 0});
const marker = mockInstances
.get(google.maps.marker.AdvancedMarkerElement)
.at(0) as google.maps.marker.AdvancedMarkerElement;

rerender(<GoogleMapsMarker position={{lat: 2, lng: 2}} />);
expect(marker.content).toHaveClass('classname-test');
});

expect(marker.position).toEqual({lat: 2, lng: 2});
});
test('marker class and style are set correctly with html content', async () => {
render(
<AdvancedMarker
position={{lat: 1, lng: 2}}
className={'classname-test'}
style={{width: 200}}>
<div data-testid={'marker-content'}>Marker Content!</div>
</AdvancedMarker>
);

const marker = mockInstances
.get(google.maps.marker.AdvancedMarkerElement)
.at(0) as google.maps.marker.AdvancedMarkerElement;

expect(marker.content).toHaveClass('classname-test');
expect(marker.content).toHaveStyle('width: 200px');
expect(
queryByTestId(marker.content as HTMLElement, 'marker-content')
).toBeTruthy();
});

test.todo('marker should work with options');
test.todo('marker should have a click listener');
test.todo('marker should work with options');
test.todo('marker should have a click listener');
});
2 changes: 1 addition & 1 deletion src/components/__tests__/info-window.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ let useMapsLibraryMock: jest.MockedFn<typeof useMapsLibrary>;

beforeEach(async () => {
initialize();
jest.resetAllMocks();
jest.clearAllMocks();

useMapMock = jest.mocked(useMap);
useMapsLibraryMock = jest.mocked(useMapsLibrary);
Expand Down
6 changes: 6 additions & 0 deletions src/components/advanced-marker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,12 @@ function useAdvancedMarker(props: AdvancedMarkerProps) {
}, [map, markerLibrary, numChildren]);

// update className and styles of marker.content element
useEffect(() => {
if (!marker || !marker.content) return;

(marker.content as HTMLElement).className = className || '';
}, [marker, className]);

usePropBinding(contentContainer, 'className', className ?? '');
useEffect(() => {
if (!contentContainer) return;
Expand Down

0 comments on commit e8a4cc3

Please sign in to comment.