Skip to content

Commit 6fe1903

Browse files
authored
feat: Adopt the inline bootstrap loader (#797)
* Adopt the inline bootstrap loader: https://developers.google.com/maps/documentation/javascript/load-maps-js-api#dynamic-library-import and provide an importLibrary() alias
1 parent e8d1ecd commit 6fe1903

File tree

2 files changed

+175
-91
lines changed

2 files changed

+175
-91
lines changed

src/index.test.ts

Lines changed: 65 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ jest.useFakeTimers();
2121
afterEach(() => {
2222
document.getElementsByTagName("html")[0].innerHTML = "";
2323
delete Loader["instance"];
24+
if (window.google) delete window.google;
2425
});
2526

2627
test.each([
@@ -65,59 +66,59 @@ test("uses default id if empty string", () => {
6566
expect(new Loader({ apiKey: "foo", id: "" }).id).toBe(DEFAULT_ID);
6667
});
6768

68-
test("setScript adds a script to head with correct attributes", () => {
69+
test("setScript adds a script to head with correct attributes", async () => {
6970
const loader = new Loader({ apiKey: "foo" });
7071

7172
loader["setScript"]();
73+
await 0;
7274

7375
const script = document.head.childNodes[0] as HTMLScriptElement;
7476

7577
expect(script.id).toEqual(loader.id);
76-
expect(script.src).toEqual(loader.createUrl());
77-
expect(script.defer).toBeTruthy();
78-
expect(script.async).toBeTruthy();
79-
expect(script.onerror).toBeTruthy();
80-
expect(script.type).toEqual("text/javascript");
8178
});
8279

83-
test("setScript does not add second script with same id", () => {
84-
new Loader({ apiKey: "foo", id: "bar" })["setScript"]();
85-
new Loader({ apiKey: "foo", id: "bar" })["setScript"]();
86-
87-
expect(document.head.childNodes.length).toBe(1);
88-
});
89-
90-
test("setScript adds a script with id", () => {
80+
test("setScript adds a script with id", async () => {
9181
const loader = new Loader({ apiKey: "foo", id: "bar" });
9282
loader["setScript"]();
83+
await 0;
9384

9485
const script = document.head.childNodes[0] as HTMLScriptElement;
95-
expect(script.id).toEqual(loader.id);
86+
expect(script.localName).toEqual("script");
87+
expect(loader.id).toEqual("bar");
88+
expect(script.id).toEqual("bar");
89+
});
90+
91+
test("setScript does not add second script with same id", async () => {
92+
new Loader({ apiKey: "foo", id: "bar" })["setScript"]();
93+
new Loader({ apiKey: "foo", id: "bar" })["setScript"]();
94+
await 0;
95+
new Loader({ apiKey: "foo", id: "bar" })["setScript"]();
96+
await 0;
97+
98+
expect(document.head.childNodes.length).toBe(1);
9699
});
97100

98101
test("load should return a promise that resolves even if called twice", () => {
99102
const loader = new Loader({ apiKey: "foo" });
103+
loader.importLibrary = (() => Promise.resolve()) as any;
100104

101105
expect.assertions(1);
102106
const promise = Promise.all([loader.load(), loader.load()]).then(() => {
103107
expect(loader["done"]).toBeTruthy();
104108
});
105109

106-
window.__googleMapsCallback(null);
107-
108110
return promise;
109111
});
110112

111113
test("loadCallback callback should fire", () => {
112114
const loader = new Loader({ apiKey: "foo" });
115+
loader.importLibrary = (() => Promise.resolve()) as any;
113116

114117
expect.assertions(2);
115118
loader.loadCallback((e: Event) => {
116119
expect(loader["done"]).toBeTruthy();
117120
expect(e).toBeUndefined();
118121
});
119-
120-
window.__googleMapsCallback(null);
121122
});
122123

123124
test("script onerror should reject promise", async () => {
@@ -163,82 +164,78 @@ test("script onerror should reject promise with multiple loaders", async () => {
163164
test("script onerror should retry", async () => {
164165
const loader = new Loader({ apiKey: "foo", retries: 1 });
165166
const deleteScript = jest.spyOn(loader, "deleteScript");
167+
loader.importLibrary = (() => Promise.reject(new Error("fake error"))) as any;
166168
const rejection = expect(loader.load()).rejects.toBeInstanceOf(Error);
167169
// eslint-disable-next-line @typescript-eslint/no-empty-function
168-
console.log = jest.fn();
170+
console.error = jest.fn();
169171

170-
loader["loadErrorCallback"](
171-
new ErrorEvent("ErrorEvent(", { error: new Error("") })
172-
);
173-
loader["loadErrorCallback"](
174-
new ErrorEvent("ErrorEvent(", { error: new Error("") })
175-
);
172+
// wait for the first failure
173+
await 0;
174+
expect(loader["errors"].length).toBe(1);
175+
// trigger the retry delay:
176176
jest.runAllTimers();
177177

178178
await rejection;
179+
expect(loader["errors"].length).toBe(2);
179180
expect(loader["done"]).toBeTruthy();
181+
expect(loader["failed"]).toBeTruthy();
180182
expect(loader["loading"]).toBeFalsy();
181-
expect(loader["errors"].length).toBe(2);
182183
expect(deleteScript).toHaveBeenCalledTimes(1);
183-
expect(console.log).toHaveBeenCalledTimes(loader.retries);
184+
expect(console.error).toHaveBeenCalledTimes(loader.retries);
184185
});
185186

186187
test("script onerror should reset retry mechanism with next loader", async () => {
187188
const loader = new Loader({ apiKey: "foo", retries: 1 });
188189
const deleteScript = jest.spyOn(loader, "deleteScript");
190+
loader.importLibrary = (() => Promise.reject(new Error("fake error"))) as any;
189191
// eslint-disable-next-line @typescript-eslint/no-empty-function
190-
console.log = jest.fn();
192+
console.error = jest.fn();
191193

192194
let rejection = expect(loader.load()).rejects.toBeInstanceOf(Error);
193-
loader["loadErrorCallback"](
194-
new ErrorEvent("ErrorEvent(", { error: new Error("") })
195-
);
196-
loader["loadErrorCallback"](
197-
new ErrorEvent("ErrorEvent(", { error: new Error("") })
198-
);
195+
// wait for the first first failure
196+
await 0;
197+
expect(loader["errors"].length).toBe(1);
198+
// trigger the retry delay:
199199
jest.runAllTimers();
200200
await rejection;
201201

202+
// try again...
202203
rejection = expect(loader.load()).rejects.toBeInstanceOf(Error);
203204
expect(loader["done"]).toBeFalsy();
205+
expect(loader["failed"]).toBeFalsy();
204206
expect(loader["loading"]).toBeTruthy();
205207
expect(loader["errors"].length).toBe(0);
206208

207-
loader["loadErrorCallback"](
208-
new ErrorEvent("ErrorEvent(", { error: new Error("") })
209-
);
210-
loader["loadErrorCallback"](
211-
new ErrorEvent("ErrorEvent(", { error: new Error("") })
212-
);
209+
// wait for the second first failure
210+
await 0;
211+
expect(loader["errors"].length).toBe(1);
212+
// trigger the retry delay:
213213
jest.runAllTimers();
214214

215215
await rejection;
216216
expect(deleteScript).toHaveBeenCalledTimes(3);
217-
expect(console.log).toHaveBeenCalledTimes(loader.retries * 2);
217+
expect(console.error).toHaveBeenCalledTimes(loader.retries * 2);
218218
});
219219

220220
test("script onerror should not reset retry mechanism with parallel loaders", async () => {
221221
const loader = new Loader({ apiKey: "foo", retries: 1 });
222222
const deleteScript = jest.spyOn(loader, "deleteScript");
223+
loader.importLibrary = (() => Promise.reject(new Error("fake error"))) as any;
223224
// eslint-disable-next-line @typescript-eslint/no-empty-function
224-
console.log = jest.fn();
225+
console.error = jest.fn();
225226

226227
const rejection1 = expect(loader.load()).rejects.toBeInstanceOf(Error);
227228
const rejection2 = expect(loader.load()).rejects.toBeInstanceOf(Error);
228-
loader["loadErrorCallback"](
229-
new ErrorEvent("ErrorEvent(", { error: new Error("") })
230-
);
231-
loader["loadErrorCallback"](
232-
new ErrorEvent("ErrorEvent(", { error: new Error("") })
233-
);
229+
// wait for the first first failure
230+
await 0;
234231
jest.runAllTimers();
235232

236233
await Promise.all([rejection1, rejection2]);
237234
expect(loader["done"]).toBeTruthy();
238235
expect(loader["loading"]).toBeFalsy();
239236
expect(loader["errors"].length).toBe(2);
240237
expect(deleteScript).toHaveBeenCalledTimes(1);
241-
expect(console.log).toHaveBeenCalledTimes(loader.retries);
238+
expect(console.error).toHaveBeenCalledTimes(loader.retries);
242239
});
243240

244241
test("reset should clear state", () => {
@@ -288,12 +285,12 @@ test("failed getter should be correct", () => {
288285
expect(loader["failed"]).toBeTruthy();
289286
});
290287

291-
test("loader should not reset retry mechanism if successfully loaded", () => {
288+
test("loader should not reset retry mechanism if successfully loaded", async () => {
292289
const loader = new Loader({ apiKey: "foo", retries: 0 });
293290
const deleteScript = jest.spyOn(loader, "deleteScript");
291+
loader.importLibrary = (() => Promise.resolve()) as any;
294292

295-
loader["done"] = true;
296-
expect(loader.load()).resolves.toBeUndefined();
293+
await expect(loader.load()).resolves.not.toBeUndefined();
297294

298295
expect(loader["done"]).toBeTruthy();
299296
expect(loader["loading"]).toBeFalsy();
@@ -344,10 +341,11 @@ test("loader should wait if already loading", () => {
344341
loader.load();
345342
});
346343

347-
test("setScript adds a nonce", () => {
344+
test("setScript adds a nonce", async () => {
348345
const nonce = "bar";
349346
const loader = new Loader({ apiKey: "foo", nonce });
350347
loader["setScript"]();
348+
await 0;
351349
const script = document.head.childNodes[0] as HTMLScriptElement;
352350
expect(script.nonce).toBe(nonce);
353351
});
@@ -371,13 +369,25 @@ test("loader should not warn if done and google.maps is defined", async () => {
371369
expect(console.warn).toHaveBeenCalledTimes(0);
372370
});
373371

374-
test("deleteScript removes script tag from head", () => {
372+
test("deleteScript removes script tag from head", async () => {
375373
const loader = new Loader({ apiKey: "foo" });
376374
loader["setScript"]();
375+
await 0;
377376
expect(document.head.childNodes.length).toBe(1);
378377
loader.deleteScript();
379378
expect(document.head.childNodes.length).toBe(0);
380379
// should work without script existing
381380
loader.deleteScript();
382381
expect(document.head.childNodes.length).toBe(0);
383382
});
383+
384+
test("importLibrary resolves correctly", async () => {
385+
window.google = { maps: {} } as any;
386+
google.maps.importLibrary = async (name) => ({ [name]: "fake" } as any);
387+
388+
const loader = new Loader({ apiKey: "foo" });
389+
const corePromise = loader.importLibrary("core");
390+
391+
const core = await corePromise;
392+
expect(core).toEqual({ core: "fake" });
393+
});

0 commit comments

Comments
 (0)