From 06620da87fa5d19f820612ce681a60b371a81b5d Mon Sep 17 00:00:00 2001 From: David Chin Date: Mon, 26 Feb 2018 12:34:49 +1100 Subject: [PATCH] feat(core): CHECKOUT-2739 Add `ScriptLoader` responsible for loading JS files asynchronously --- src/create-script-loader.ts | 5 ++++ src/index.ts | 2 ++ src/script-loader.spec.ts | 46 +++++++++++++++++++++++++++++++++++++ src/script-loader.ts | 24 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 src/create-script-loader.ts create mode 100644 src/index.ts create mode 100644 src/script-loader.spec.ts create mode 100644 src/script-loader.ts diff --git a/src/create-script-loader.ts b/src/create-script-loader.ts new file mode 100644 index 0000000..6b7adca --- /dev/null +++ b/src/create-script-loader.ts @@ -0,0 +1,5 @@ +import ScriptLoader from './script-loader'; + +export default function createScriptLoader(): ScriptLoader { + return new ScriptLoader(document); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..353618f --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export { default as ScriptLoader } from './script-loader'; +export { default as createScriptLoader } from './create-script-loader'; diff --git a/src/script-loader.spec.ts b/src/script-loader.spec.ts new file mode 100644 index 0000000..7574e54 --- /dev/null +++ b/src/script-loader.spec.ts @@ -0,0 +1,46 @@ +import ScriptLoader from './script-loader'; + +describe('ScriptLoader', () => { + let loader: ScriptLoader; + let script: HTMLScriptElement; + + beforeEach(() => { + script = document.createElement('script'); + + jest.spyOn(document, 'createElement').mockImplementation(() => script); + jest.spyOn(document.body, 'appendChild').mockImplementation((element) => + element.onreadystatechange(new Event('readystatechange')) + ); + + loader = new ScriptLoader(document); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('attaches script tag to document', async () => { + await loader.loadScript('https://code.jquery.com/jquery-3.2.1.min.js'); + + expect(document.body.appendChild).toHaveBeenCalledWith(script); + expect(script.src).toEqual('https://code.jquery.com/jquery-3.2.1.min.js'); + }); + + it('resolves promise if script is loaded', async () => { + const output = await loader.loadScript('https://code.jquery.com/jquery-3.2.1.min.js'); + + expect(output).toBeInstanceOf(Event); + }); + + it('rejects promise if script is not loaded', async () => { + jest.spyOn(document.body, 'appendChild').mockImplementation( + (element) => element.onerror(new Event('error')) + ); + + try { + await loader.loadScript('https://code.jquery.com/jquery-3.2.1.min.js'); + } catch (output) { + expect(output).toBeInstanceOf(Event); + } + }); +}); diff --git a/src/script-loader.ts b/src/script-loader.ts new file mode 100644 index 0000000..c84fbce --- /dev/null +++ b/src/script-loader.ts @@ -0,0 +1,24 @@ +export default class ScriptLoader { + constructor( + private _document: Document + ) {} + + loadScript(src: string): Promise { + return new Promise((resolve, reject) => { + const script = this._document.createElement('script') as LegacyHTMLScriptElement; + + script.onload = (event) => resolve(event); + script.onreadystatechange = (event) => resolve(event); + script.onerror = (event) => reject(event); + script.async = true; + script.src = src; + + this._document.body.appendChild(script); + }); + } +} + +interface LegacyHTMLScriptElement extends HTMLScriptElement { + // `onreadystatechange` is needed to support legacy IE + onreadystatechange: (this: HTMLElement, event: Event) => any; +}