Skip to content

Commit

Permalink
fix: api-loader didn't call callback on repeat load calls
Browse files Browse the repository at this point in the history
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
usefulthink committed Apr 4, 2024
1 parent 9459b74 commit 743878a
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 49 deletions.
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.",
],
]
`;
191 changes: 188 additions & 3 deletions src/libraries/__tests__/google-maps-api-loader.test.ts
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');
});
Loading

0 comments on commit 743878a

Please sign in to comment.