Skip to content

Commit 4af51dd

Browse files
feat(api-loader): migrate to @googlemaps/js-api-loader (#885)
Migrates the loading mechanism for the Maps JavaScript API from a custom implementation to the official `@googlemaps/js-api-loader`. This involves several key steps: * Removing the existing custom loader and its associated tests. * Updating the `api-provider` to utilize `@googlemaps/js-api-loader`. * Refactoring existing tests. These refactorings aim to prevent integration testing and remove direct usage of the `APIProvider` component in unrelated tests Co-authored-by: Martin Schuhfuss <m.schuhfuss@gmail.com>
1 parent 96dc903 commit 4af51dd

18 files changed

Lines changed: 346 additions & 836 deletions

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"Maps"
5757
],
5858
"dependencies": {
59+
"@googlemaps/js-api-loader": "^2.0.2",
5960
"@types/google.maps": "^3.54.10",
6061
"fast-deep-equal": "^3.1.3"
6162
},

src/components/__tests__/api-provider.test.tsx

Lines changed: 79 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,60 @@
11
import React, {useContext} from 'react';
2-
import {act, render, screen} from '@testing-library/react';
3-
import {initialize} from '@googlemaps/jest-mocks';
2+
import {act, cleanup, render, screen, waitFor} from '@testing-library/react';
3+
import {
4+
importLibrary as importLibraryMock,
5+
initialize
6+
} from '@googlemaps/jest-mocks';
47
import '@testing-library/jest-dom';
58

6-
// FIXME: this should no longer be needed with the next version of @googlemaps/jest-mocks
7-
import {importLibraryMock} from '../../libraries/__mocks__/lib/import-library-mock';
8-
99
import {VERSION} from '../../version';
1010
import {
11+
__resetModuleState as resetAPIProviderState,
1112
APIProvider,
1213
APIProviderContext,
1314
APIProviderContextValue
1415
} from '../api-provider';
15-
import {ApiParams} from '../../libraries/google-maps-api-loader';
16+
1617
import {useApiIsLoaded} from '../../hooks/use-api-is-loaded';
1718
import {APILoadingStatus} from '../../libraries/api-loading-status';
1819

19-
const apiLoadSpy = jest.fn();
20-
const apiUnloadSpy = jest.fn();
20+
type ImportLibraryResult = Awaited<
21+
ReturnType<typeof google.maps.importLibrary>
22+
>;
23+
24+
let importLibraryPromise: Promise<ImportLibraryResult>;
25+
let resolveImportLibrary: (value: ImportLibraryResult) => void;
26+
let rejectImportLibrary: (reason?: unknown) => void;
27+
28+
const resetImportLibraryPromise = () => {
29+
({
30+
promise: importLibraryPromise,
31+
resolve: resolveImportLibrary,
32+
reject: rejectImportLibrary
33+
} = Promise.withResolvers<ImportLibraryResult>());
34+
};
35+
36+
const triggerMapsApiLoaded = () => {
37+
resolveImportLibrary({} as google.maps.CoreLibrary);
38+
};
39+
40+
const triggerLoadingFailed = () => {
41+
rejectImportLibrary(new Error('loading failed'));
42+
};
43+
44+
const setOptionsSpy = jest.fn();
45+
46+
jest.mock('@googlemaps/js-api-loader', () => {
47+
return {
48+
setOptions: jest.fn((options: Record<string, unknown>) => {
49+
setOptionsSpy(options);
50+
}),
51+
importLibrary: jest.fn(async (name: string) => {
52+
await importLibraryPromise;
53+
54+
return importLibraryMock(name);
55+
})
56+
};
57+
});
2158

2259
const ContextSpyComponent = () => {
2360
const context = useContext(APIProviderContext);
@@ -27,47 +64,20 @@ const ContextSpyComponent = () => {
2764
};
2865
ContextSpyComponent.spy = jest.fn();
2966

30-
let triggerMapsApiLoaded: () => void;
31-
let triggerLoadingFailed: () => void;
32-
33-
jest.mock('../../libraries/google-maps-api-loader', () => {
34-
class GoogleMapsApiLoader {
35-
static async load(
36-
params: ApiParams,
37-
onLoadingStatusChange: (s: APILoadingStatus) => void
38-
): Promise<void> {
39-
apiLoadSpy(params);
40-
onLoadingStatusChange(APILoadingStatus.LOADING);
41-
42-
google.maps.importLibrary = importLibraryMock;
43-
44-
return new Promise((resolve, reject) => {
45-
triggerLoadingFailed = () => {
46-
reject();
47-
onLoadingStatusChange(APILoadingStatus.FAILED);
48-
};
49-
50-
triggerMapsApiLoaded = () => {
51-
resolve();
52-
onLoadingStatusChange(APILoadingStatus.LOADED);
53-
};
54-
});
55-
}
56-
57-
static unload() {
58-
apiUnloadSpy();
59-
}
60-
}
61-
62-
return {__esModule: true, GoogleMapsApiLoader};
63-
});
64-
6567
beforeEach(() => {
6668
initialize();
6769
jest.clearAllMocks();
70+
// @ts-expect-error - accessing mock implementation
71+
window.google.maps.importLibrary = undefined;
72+
resetAPIProviderState();
73+
resetImportLibraryPromise();
6874
});
6975

70-
test('passes parameters to GoogleMapsAPILoader', () => {
76+
afterEach(() => {
77+
cleanup();
78+
});
79+
80+
test('passes parameters to GoogleMapsAPILoader', async () => {
7181
render(
7282
<APIProvider
7383
apiKey={'apikey'}
@@ -79,9 +89,11 @@ test('passes parameters to GoogleMapsAPILoader', () => {
7989
authReferrerPolicy={'origin'}></APIProvider>
8090
);
8191

82-
expect(apiLoadSpy.mock.lastCall[0]).toMatchObject({
92+
await waitFor(() => expect(setOptionsSpy).toHaveBeenCalled());
93+
94+
expect(setOptionsSpy.mock.lastCall[0]).toMatchObject({
8395
key: 'apikey',
84-
libraries: 'places,marker',
96+
libraries: ['places', 'marker'],
8597
v: 'beta',
8698
language: 'en',
8799
region: 'us',
@@ -90,24 +102,30 @@ test('passes parameters to GoogleMapsAPILoader', () => {
90102
});
91103
});
92104

93-
test('passes parameters to GoogleMapsAPILoader', () => {
105+
test('passes parameters to GoogleMapsAPILoader', async () => {
94106
render(<APIProvider apiKey={'apikey'} version={'version'}></APIProvider>);
95107

96-
const actual = apiLoadSpy.mock.lastCall[0];
108+
await waitFor(() => expect(setOptionsSpy).toHaveBeenCalled());
109+
110+
const actual = setOptionsSpy.mock.lastCall[0];
97111
expect(actual).toMatchObject({key: 'apikey', v: 'version'});
98112
});
99113

100-
test('uses default solutionChannel', () => {
114+
test('uses default solutionChannel', async () => {
101115
render(<APIProvider apiKey={'apikey'}></APIProvider>);
102116

103-
const actual = apiLoadSpy.mock.lastCall[0];
117+
await waitFor(() => expect(setOptionsSpy).toHaveBeenCalled());
118+
119+
const actual = setOptionsSpy.mock.lastCall[0];
104120
expect(actual.solutionChannel).toBe('GMP_visgl_rgmlibrary_v1_default');
105121
});
106122

107-
test("doesn't set solutionChannel when specified as empty string", () => {
123+
test("doesn't set solutionChannel when specified as empty string", async () => {
108124
render(<APIProvider apiKey={'apikey'} solutionChannel={''}></APIProvider>);
109125

110-
const actual = apiLoadSpy.mock.lastCall[0];
126+
await waitFor(() => expect(setOptionsSpy).toHaveBeenCalled());
127+
128+
const actual = setOptionsSpy.mock.lastCall[0];
111129
expect(actual).not.toHaveProperty('solutionChannel');
112130
});
113131

@@ -125,7 +143,9 @@ test('renders inner components', async () => {
125143

126144
expect(screen.getByText('not loaded')).toBeInTheDocument();
127145

128-
await act(() => triggerMapsApiLoaded());
146+
await act(async () => {
147+
triggerMapsApiLoaded();
148+
});
129149

130150
expect(screen.getByText('loaded')).toBeInTheDocument();
131151
});
@@ -145,7 +165,10 @@ test('provides context values', async () => {
145165
expect(actualContext.mapInstances).toEqual({});
146166

147167
contextSpy.mockReset();
148-
await act(() => triggerMapsApiLoaded());
168+
169+
await act(async () => {
170+
triggerMapsApiLoaded();
171+
});
149172

150173
expect(contextSpy).toHaveBeenCalled();
151174

@@ -192,9 +215,9 @@ test('calls onError when loading the Google Maps JavaScript API fails', async ()
192215

193216
render(<APIProvider apiKey={'apikey'} onError={onErrorMock}></APIProvider>);
194217

195-
await act(() => triggerLoadingFailed());
218+
triggerLoadingFailed();
196219

197-
expect(onErrorMock).toHaveBeenCalled();
220+
await waitFor(() => expect(onErrorMock).toHaveBeenCalled());
198221
});
199222

200223
describe('internalUsageAttributionIds', () => {

src/components/__tests__/info-window.test.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ afterEach(() => {
5050
});
5151

5252
describe('<InfoWindow> basic functionality', () => {
53-
test('Infowindow is created once mapsLibrary is ready', async () => {
53+
test('Infowindow is created once mapsLibrary is ready', () => {
5454
useMapsLibraryMock.mockReturnValue(null);
5555
useMapMock.mockReturnValue(null);
5656

@@ -75,7 +75,7 @@ describe('<InfoWindow> basic functionality', () => {
7575
expect(iw.open).toHaveBeenCalled();
7676
});
7777

78-
test('props get forwarded to constructor on initial creation', async () => {
78+
test('props get forwarded to constructor on initial creation', () => {
7979
render(
8080
<InfoWindow
8181
ariaLabel={'ariaLabel'}
@@ -105,7 +105,7 @@ describe('<InfoWindow> basic functionality', () => {
105105
expect(actualOptions.pixelOffset.height).toBe(6);
106106
});
107107

108-
test('changing options get passed to setOptions()', async () => {
108+
test('changing options get passed to setOptions()', () => {
109109
const position = {lat: 1, lng: 2};
110110
const {rerender} = render(<InfoWindow position={position}></InfoWindow>);
111111

@@ -131,7 +131,7 @@ describe('<InfoWindow> basic functionality', () => {
131131
});
132132
});
133133

134-
test('props get forwarded to openOptions', async () => {
134+
test('props get forwarded to openOptions', () => {
135135
const marker = new google.maps.marker.AdvancedMarkerElement();
136136

137137
render(<InfoWindow anchor={marker} shouldFocus={false}></InfoWindow>);
@@ -146,7 +146,7 @@ describe('<InfoWindow> basic functionality', () => {
146146
});
147147

148148
describe('<InfoWindow> content rendering', () => {
149-
test('InfoWindow should render content into portal node', async () => {
149+
test('InfoWindow should render content into portal node', () => {
150150
render(
151151
<InfoWindow
152152
className={'infowindow-content'}
@@ -178,15 +178,15 @@ describe('<InfoWindow> content rendering', () => {
178178
});
179179

180180
describe('<InfoWindow> headerContent rendering', () => {
181-
test('passes headerContent to options when its a string', async () => {
181+
test('passes headerContent to options when its a string', () => {
182182
render(<InfoWindow headerContent={'Infowindow Header'}></InfoWindow>);
183183

184184
expect(createInfowindowSpy).toHaveBeenCalled();
185185
const [options] = createInfowindowSpy.mock.lastCall;
186186
expect(options).toEqual({headerContent: 'Infowindow Header'});
187187
});
188188

189-
test('creates a dom-element when passing a ReactNode', async () => {
189+
test('creates a dom-element when passing a ReactNode', () => {
190190
render(<InfoWindow headerContent={<h3>Infowindow Header</h3>} />);
191191

192192
expect(createInfowindowSpy).toHaveBeenCalled();
@@ -195,7 +195,7 @@ describe('<InfoWindow> headerContent rendering', () => {
195195
expect(options.headerContent).toContainHTML('<h3>Infowindow Header</h3>');
196196
});
197197

198-
test('updates html-content when content props changes', async () => {
198+
test('updates html-content when content props changes', () => {
199199
const {rerender} = render(
200200
<InfoWindow headerContent={<h3>Infowindow Header</h3>}></InfoWindow>
201201
);
@@ -215,7 +215,7 @@ describe('<InfoWindow> headerContent rendering', () => {
215215
);
216216
});
217217

218-
test('changes from text- to html-content', async () => {
218+
test('changes from text- to html-content', () => {
219219
const {rerender} = render(<InfoWindow headerContent="abcd"></InfoWindow>);
220220

221221
rerender(
@@ -233,7 +233,7 @@ describe('<InfoWindow> headerContent rendering', () => {
233233
);
234234
});
235235

236-
test('changes from html-content to no content', async () => {
236+
test('changes from html-content to no content', () => {
237237
const {rerender} = render(
238238
<InfoWindow headerContent={<h3>New Infowindow Header</h3>}></InfoWindow>
239239
);
@@ -270,7 +270,7 @@ describe('<InfoWindow> cleanup', () => {
270270
});
271271

272272
describe('<InfoWindow> events', () => {
273-
test('triggers onClose and onCloseClick handlers on event', async () => {
273+
test('triggers onClose and onCloseClick handlers on event', () => {
274274
const onCloseSpy = jest.fn();
275275
const onCloseClickSpy = jest.fn();
276276

@@ -294,7 +294,7 @@ describe('<InfoWindow> events', () => {
294294
);
295295
});
296296

297-
test('removes handlers on unmount', async () => {
297+
test('removes handlers on unmount', () => {
298298
const listeners = {
299299
close: {remove: jest.fn()},
300300
closeclick: {remove: jest.fn()}

0 commit comments

Comments
 (0)