-
Notifications
You must be signed in to change notification settings - Fork 105
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: api-loader didn't call callback on repeat load calls
When `GoogleMapsApiLoader.load()` was called multiple times, only the loadingStateChanged callback specified with the first call was updated with changing loading-states. This commit also adds unit-tests for the api-loader.
- Loading branch information
1 parent
9459b74
commit 743878a
Showing
3 changed files
with
281 additions
and
49 deletions.
There are no files selected for viewing
9 changes: 9 additions & 0 deletions
9
src/libraries/__tests__/__snapshots__/google-maps-api-loader.test.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`GoogleMapsApiLoader logs a warning when called multiple times with different parameters 1`] = ` | ||
[ | ||
[ | ||
"[google-maps-api-loader] The maps API has already been loaded with different parameters and will not be loaded again. Refresh the page for new values to have effect.", | ||
], | ||
] | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,190 @@ | ||
import {APILoadingStatus} from '../api-loading-status'; | ||
|
||
let GoogleMapsApiLoader: typeof import('../google-maps-api-loader').GoogleMapsApiLoader; | ||
|
||
const timeout = (t: number = 0) => | ||
new Promise<void>(resolve => global.setTimeout(resolve, t)); | ||
|
||
describe('GoogleMapsApiLoader', () => { | ||
test.todo('creates script-tag for parameters'); | ||
test.todo('keeps script-tag for multiple calls with same parameters'); | ||
test.todo('unloads and reloads when called with different parameters'); | ||
beforeEach(async () => { | ||
// GoogleMapsApiLoader uses state stored in private static properties, so we have to | ||
// isolate it so the internal state doesn't crosstalk into other tests. | ||
await jest.isolateModulesAsync(async () => { | ||
GoogleMapsApiLoader = (await import('../google-maps-api-loader')) | ||
.GoogleMapsApiLoader; | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
// clean up JSDOM after tests | ||
document.body.innerHTML = ''; | ||
document.head.innerHTML = ''; | ||
|
||
delete window.__googleMapsCallback__; | ||
delete window.gm_authFailure; | ||
(window.google as unknown) = undefined; | ||
}); | ||
|
||
test.each([ | ||
{ | ||
params: {key: 'xyza'}, | ||
expected: {key: 'xyza', callback: '__googleMapsCallback__'} | ||
}, | ||
{ | ||
params: { | ||
key: 'abcd', | ||
v: 'version', | ||
language: 'language', | ||
region: 'region', | ||
solutionChannel: 'solutionChannel', | ||
authReferrerPolicy: 'origin' | ||
}, | ||
expected: { | ||
key: 'abcd', | ||
v: 'version', | ||
language: 'language', | ||
region: 'region', | ||
auth_referrer_policy: 'origin', | ||
solution_channel: 'solutionChannel', | ||
callback: '__googleMapsCallback__' | ||
} | ||
} | ||
])('creates script-tag with parameters', async ({params, expected}) => { | ||
void GoogleMapsApiLoader.load(params, jest.fn()); | ||
|
||
expect(window.__googleMapsCallback__).toBeDefined(); | ||
expect(window.gm_authFailure).toBeDefined(); | ||
|
||
const el = document.querySelector('script') as HTMLScriptElement; | ||
const url = new URL(el.src); | ||
|
||
expect(url.origin).toBe('https://maps.googleapis.com'); | ||
expect(url.pathname).toBe('/maps/api/js'); | ||
|
||
const actualParams = Object.fromEntries(url.searchParams.entries()); | ||
expect(actualParams).toMatchObject(expected); | ||
}); | ||
|
||
test('loads specified libraries', async () => { | ||
const statusCallback = jest.fn(); | ||
const promise = GoogleMapsApiLoader.load( | ||
{key: 'abc', libraries: 'a,b,c'}, | ||
statusCallback | ||
); | ||
|
||
expect(statusCallback).toHaveBeenCalledWith(APILoadingStatus.LOADING); | ||
|
||
// mock API being loaded | ||
const importLibraryMock = jest.fn(); | ||
google.maps.importLibrary = importLibraryMock; | ||
window.__googleMapsCallback__!(); | ||
|
||
// allow for internal promise .then() callbacks to run | ||
await timeout(); | ||
|
||
expect(statusCallback).toHaveBeenCalledTimes(2); | ||
expect(statusCallback).toHaveBeenLastCalledWith(APILoadingStatus.LOADED); | ||
expect(await promise.then(() => true)).toBeTruthy(); | ||
|
||
expect(importLibraryMock).toHaveBeenCalledTimes(4); | ||
const loadedLibraries = importLibraryMock.mock.calls.flat(); | ||
expect(loadedLibraries).toEqual(['maps', 'a', 'b', 'c']); | ||
}); | ||
|
||
test('handles multiple calls properly', async () => { | ||
const callback1 = jest.fn(); | ||
const callback2 = jest.fn(); | ||
const promise1 = GoogleMapsApiLoader.load({key: 'abc'}, callback1); | ||
const promise2 = GoogleMapsApiLoader.load({key: 'abc'}, callback2); | ||
|
||
// mock API being loaded | ||
google.maps.importLibrary = jest.fn(); | ||
window.__googleMapsCallback__!(); | ||
|
||
// allow for internal promise .then() callbacks to run | ||
await timeout(); | ||
|
||
expect(callback1).toHaveBeenLastCalledWith(APILoadingStatus.LOADED); | ||
expect(callback2).toHaveBeenLastCalledWith(APILoadingStatus.LOADED); | ||
|
||
expect( | ||
await Promise.all([promise1, promise2]).then(() => true) | ||
).toBeTruthy(); | ||
}); | ||
|
||
test('handle multiple calls when already loaded', async () => { | ||
const callback1 = jest.fn(); | ||
const promise1 = GoogleMapsApiLoader.load({key: 'abc'}, callback1); | ||
|
||
// mock API being loaded | ||
google.maps.importLibrary = jest.fn(); | ||
window.__googleMapsCallback__!(); | ||
|
||
// allow for internal promise .then() callbacks to run | ||
await timeout(); | ||
|
||
expect(await promise1.then(() => true)).toBeTruthy(); | ||
|
||
const callback2 = jest.fn(); | ||
const promise2 = GoogleMapsApiLoader.load({key: 'abc'}, callback2); | ||
|
||
expect(callback1).toHaveBeenLastCalledWith(APILoadingStatus.LOADED); | ||
expect(callback2).toHaveBeenLastCalledWith(APILoadingStatus.LOADED); | ||
expect(await promise2.then(() => true)).toBeTruthy(); | ||
}); | ||
|
||
test('logs a warning when called multiple times with different parameters', async () => { | ||
const consoleWarnSpy = jest | ||
.spyOn(console, 'warn') | ||
.mockImplementation(() => {}); | ||
|
||
const callback1 = jest.fn(); | ||
void GoogleMapsApiLoader.load({key: 'abc'}, callback1); | ||
|
||
expect(consoleWarnSpy).not.toHaveBeenCalled(); | ||
|
||
const callback2 = jest.fn(); | ||
void GoogleMapsApiLoader.load({key: 'def'}, callback2); | ||
|
||
// mock API being loaded | ||
google.maps.importLibrary = jest.fn(); | ||
window.__googleMapsCallback__!(); | ||
|
||
await timeout(); | ||
|
||
expect(consoleWarnSpy.mock.calls).toMatchSnapshot(); | ||
consoleWarnSpy.mockRestore(); | ||
}); | ||
|
||
test('treat externally loaded maps API as loaded', async () => { | ||
// mock API having already been loaded | ||
global.google = {maps: {importLibrary: jest.fn()}} as never; | ||
|
||
const callback = jest.fn(); | ||
const promise = GoogleMapsApiLoader.load({key: 'abc'}, callback); | ||
|
||
await timeout(); | ||
|
||
expect(callback).toHaveBeenLastCalledWith(APILoadingStatus.LOADED); | ||
expect(await promise.then(() => true)).toBeTruthy(); | ||
}); | ||
|
||
test('handle gm_authFailure', async () => { | ||
const callback = jest.fn(); | ||
void GoogleMapsApiLoader.load({key: 'abc'}, callback); | ||
|
||
// mock API being loaded | ||
google.maps.importLibrary = jest.fn(); | ||
window.__googleMapsCallback__!(); | ||
|
||
await timeout(); | ||
|
||
// mock auth failure | ||
window.gm_authFailure!(); | ||
|
||
expect(callback).toHaveBeenLastCalledWith(APILoadingStatus.AUTH_FAILURE); | ||
}); | ||
|
||
test.todo('handle loading error'); | ||
test.todo('handle CSP script nonce'); | ||
}); |
Oops, something went wrong.