genesys-spark 4.0.0-beta.51 → 4.0.0-beta.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,26 +2,42 @@
2
2
 
3
3
  This package is the default way to use Spark. It provides access to Spark's lazy-loading custom elements via a single shared CDN, as well as utilities for non-rendering UI tasks.
4
4
 
5
- ## Installation
5
+ ## Getting Started
6
+
7
+ ### Installation
6
8
 
7
9
  Use your package manager of choice to install the package in your project.
8
10
 
9
11
  `npm install genesys-spark`
10
12
 
11
- ## Setup
13
+ ### Localization
14
+
15
+ To ensure components are localized correctly, use the [`lang` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) to set the language of
16
+ your page. Components will respect the language of their closest ancestor with
17
+ a recognized `lang` attribute.
18
+
19
+ ### Loading the components
12
20
 
13
- When initializing your app/page, call `registerSparkCompnents`, which will inject
21
+ When initializing your app/page, call `registerSparkComponents`, which will inject
14
22
  the script and style tags into your page that define the main Spark custom elements:
15
23
 
16
24
  ```js
17
25
  import { registerSparkComponents } from 'genesys-spark';
18
26
 
19
27
  // It's not _required_ to await component loading, but it can help prevent a flash
20
- // of unstyled content
28
+ // of unstyled content.
21
29
  await registerSparkComponents();
22
30
  ```
23
31
 
24
- ## Loading Mechanism
32
+ ### Font Loading
33
+
34
+ While `registerSparkComponents()` will add the required webfonts to your page if
35
+ they are not already present, it is recommended that you link stylesheets to load
36
+ the Urbanist and Noto Sans fonts in your document prior to starting your app. This
37
+ will improve loading performance and prevent a jarring font transition when the
38
+ required fonts load.
39
+
40
+ ## Asset loading details
25
41
 
26
42
  By adding a script tag to the document, we ensure that the components are always
27
43
  loaded from the same location. This keeps the bundle size of the consuming application
package/dist/hosts.d.ts CHANGED
@@ -6,3 +6,4 @@ declare global {
6
6
  * Will use the domain of the current window if it matches a Genesys domain.
7
7
  */
8
8
  export declare function getAssetsOrigin(): string;
9
+ export declare function getFontOrigin(): string;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  /**
2
- * Loads the spark webcomponents from a shared CDN
2
+ * Loads the spark web components, as well as required CSS and fonts from a
3
+ * shared CDN. Performance can be optimized by pre-loading fonts in static HTML.
4
+ *
3
5
  * @returns a promise that succeeds if the component script and styles
4
- * load successfully.
6
+ * load successfully. It is not recommended to wait on this promise or to stop
7
+ * application bootstrap if it rejects. Its primary use should be for logging
8
+ * unexpected failures.
5
9
  */
6
10
  export declare function registerSparkComponents(): Promise<void>;
package/dist/index.js CHANGED
@@ -23,29 +23,57 @@ var DOMAIN_LIST = [
23
23
  * Will use the domain of the current window if it matches a Genesys domain.
24
24
  */
25
25
  function getAssetsOrigin() {
26
+ var matchedDomain = getRegionDomain();
27
+ return "https://app.".concat(matchedDomain || DEFAULT_DOMAIN);
28
+ }
29
+ function getFontOrigin() {
30
+ return getAssetsOrigin();
31
+ }
32
+ function getRegionDomain() {
26
33
  var pageHost = window.location.hostname;
27
- var matchedDomain = DOMAIN_LIST.find(function (regionDomain) {
34
+ return DOMAIN_LIST.find(function (regionDomain) {
28
35
  return pageHost.endsWith(regionDomain);
29
36
  });
30
- return "https://app.".concat(matchedDomain || DEFAULT_DOMAIN);
31
37
  }
32
38
 
33
- var ASSET_PREFIX = "/spark-components/build-assets/4.0.0-beta.51-74/genesys-webcomponents/";
34
- var SCRIPT_PATH = "genesys-webcomponents.esm.js";
35
- var STYLE_PATH = "genesys-webcomponents.css";
36
- var SCRIPT_SRC = "".concat(getAssetsOrigin()).concat(ASSET_PREFIX).concat(SCRIPT_PATH);
37
- var STYLE_HREF = "".concat(getAssetsOrigin()).concat(ASSET_PREFIX).concat(STYLE_PATH);
39
+ /******************************************************************************
40
+ Copyright (c) Microsoft Corporation.
41
+
42
+ Permission to use, copy, modify, and/or distribute this software for any
43
+ purpose with or without fee is hereby granted.
44
+
45
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
46
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
47
+ AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
48
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
49
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
50
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
51
+ PERFORMANCE OF THIS SOFTWARE.
52
+ ***************************************************************************** */
53
+ /* global Reflect, Promise, SuppressedError, Symbol */
54
+
55
+
56
+ var __assign = function() {
57
+ __assign = Object.assign || function __assign(t) {
58
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
59
+ s = arguments[i];
60
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
61
+ }
62
+ return t;
63
+ };
64
+ return __assign.apply(this, arguments);
65
+ };
66
+
67
+ typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
68
+ var e = new Error(message);
69
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
70
+ };
71
+
38
72
  /**
39
- * Loads the spark webcomponents from a shared CDN
40
- * @returns a promise that succeeds if the component script and styles
41
- * load successfully.
73
+ * Add a script tag to the document if it is not already present.
74
+ * @param scriptSrc The src attribute of the script
75
+ * @returns a promise that resolves if the script loads or is already present
42
76
  */
43
- function registerSparkComponents() {
44
- return Promise.all([
45
- checkAndLoadScript(SCRIPT_SRC),
46
- checkAndLoadStyle(STYLE_HREF)
47
- ]).then();
48
- }
49
77
  function checkAndLoadScript(scriptSrc) {
50
78
  var existingTag = document.querySelector("script[src=\"".concat(scriptSrc, "\"]"));
51
79
  if (existingTag) {
@@ -67,6 +95,11 @@ function checkAndLoadScript(scriptSrc) {
67
95
  return result;
68
96
  }
69
97
  }
98
+ /**
99
+ * Add a stylesheet link tag to the document if it is not already present.
100
+ * @param styleHref The href attribute of the stylesheet
101
+ * @returns a promise that resolves if the style loads or is already present
102
+ */
70
103
  function checkAndLoadStyle(styleHref) {
71
104
  var existingTag = document.querySelector("link[href=\"".concat(styleHref, "\"]"));
72
105
  if (existingTag) {
@@ -88,6 +121,66 @@ function checkAndLoadStyle(styleHref) {
88
121
  return result;
89
122
  }
90
123
  }
124
+ /**
125
+ * Given an object that maps font-family identifiers to CSS urls e.g:
126
+ * {
127
+ * "Urbanist": "/urbanist.css",
128
+ * "Noto Sans": "/noto-sans.css"
129
+ * }
130
+ * This function checks for loaded fonts with those identifiers. If a font is
131
+ * not found, the corresponding stylesheet is added to the document.
132
+ * @param fonts An object mapping font-family identifiers to CSS file urls
133
+ * @returns A promise that resolves once the script tags has finished loading.
134
+ * It does not fail if the script tags fail to load because we don't want to fail
135
+ * the whole component loading process in that situation.
136
+ */
137
+ function checkAndLoadFonts(fonts) {
138
+ var fontsToLoad = __assign({}, fonts); //clone our input so we can safely mutate it.
139
+ document.fonts.forEach(function (fontFace) {
140
+ // If the family is defined with quotes in CSS (e.g. `font-family: "Noto Sans"),
141
+ // those quotes may be preserved JS, depending on the browser.
142
+ var normalizedFamily = fontFace.family.replace(/"/g, "");
143
+ if (fontsToLoad[normalizedFamily]) {
144
+ // remove the font from the set to load
145
+ delete fontsToLoad[normalizedFamily];
146
+ }
147
+ });
148
+ return Promise.all(Object.values(fontsToLoad).map(function (href) {
149
+ return checkAndLoadStyle(href)["catch"](function () {
150
+ // Don't fail loading process for fonts, since the components
151
+ // should still be reasonably usable.
152
+ console.info("genesys-spark: couldn't load font style ".concat(href));
153
+ });
154
+ })).then(function () { }); // flatten the promise array
155
+ }
156
+
157
+ var ASSET_PREFIX = "/spark-components/build-assets/4.0.0-beta.52-77/genesys-webcomponents/";
158
+ var SCRIPT_PATH = "genesys-webcomponents.esm.js";
159
+ var STYLE_PATH = "genesys-webcomponents.css";
160
+ var assetsOrigin = getAssetsOrigin();
161
+ var SCRIPT_SRC = "".concat(assetsOrigin).concat(ASSET_PREFIX).concat(SCRIPT_PATH);
162
+ var STYLE_HREF = "".concat(assetsOrigin).concat(ASSET_PREFIX).concat(STYLE_PATH);
163
+ var fontOrigin = getFontOrigin();
164
+ var FONTS = {
165
+ "Urbanist": "".concat(fontOrigin, "/webfonts/urbanist.css"),
166
+ "Noto Sans": "".concat(fontOrigin, "/webfonts/noto-sans.css")
167
+ };
168
+ /**
169
+ * Loads the spark web components, as well as required CSS and fonts from a
170
+ * shared CDN. Performance can be optimized by pre-loading fonts in static HTML.
171
+ *
172
+ * @returns a promise that succeeds if the component script and styles
173
+ * load successfully. It is not recommended to wait on this promise or to stop
174
+ * application bootstrap if it rejects. Its primary use should be for logging
175
+ * unexpected failures.
176
+ */
177
+ function registerSparkComponents() {
178
+ return Promise.all([
179
+ checkAndLoadScript(SCRIPT_SRC),
180
+ checkAndLoadStyle(STYLE_HREF),
181
+ checkAndLoadFonts(FONTS)
182
+ ]).then();
183
+ }
91
184
  // TODO: Build out utility functions where components aren't the right solution
92
185
  // export function formatDate(...)
93
186
 
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Add a script tag to the document if it is not already present.
3
+ * @param scriptSrc The src attribute of the script
4
+ * @returns a promise that resolves if the script loads or is already present
5
+ */
6
+ export declare function checkAndLoadScript(scriptSrc: string): Promise<void>;
7
+ /**
8
+ * Add a stylesheet link tag to the document if it is not already present.
9
+ * @param styleHref The href attribute of the stylesheet
10
+ * @returns a promise that resolves if the style loads or is already present
11
+ */
12
+ export declare function checkAndLoadStyle(styleHref: string): Promise<void>;
13
+ /**
14
+ * Given an object that maps font-family identifiers to CSS urls e.g:
15
+ * {
16
+ * "Urbanist": "/urbanist.css",
17
+ * "Noto Sans": "/noto-sans.css"
18
+ * }
19
+ * This function checks for loaded fonts with those identifiers. If a font is
20
+ * not found, the corresponding stylesheet is added to the document.
21
+ * @param fonts An object mapping font-family identifiers to CSS file urls
22
+ * @returns A promise that resolves once the script tags has finished loading.
23
+ * It does not fail if the script tags fail to load because we don't want to fail
24
+ * the whole component loading process in that situation.
25
+ */
26
+ export declare function checkAndLoadFonts(fonts: {
27
+ [key: string]: string;
28
+ }): Promise<void>;
package/jest.config.js ADDED
@@ -0,0 +1,5 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ export default {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'jsdom'
5
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genesys-spark",
3
- "version": "4.0.0-beta.51",
3
+ "version": "4.0.0-beta.52",
4
4
  "description": "",
5
5
  "license": "ISC",
6
6
  "author": "",
@@ -9,7 +9,8 @@
9
9
  "scripts": {
10
10
  "build": "rollup -c",
11
11
  "dev": "rollup -c --watch",
12
- "test": "echo \"No test specified\"",
12
+ "test": "jest",
13
+ "test.watch": "jest --watch",
13
14
  "version-sync": "npm version --no-git-tag-version --allow-same-version"
14
15
  },
15
16
  "dependencies": {
@@ -18,6 +19,9 @@
18
19
  "rollup": "^3.29.4"
19
20
  },
20
21
  "devDependencies": {
21
- "@tsconfig/strictest": "^2.0.2"
22
+ "@tsconfig/strictest": "^2.0.2",
23
+ "jest": "^29.7.0",
24
+ "jest-environment-jsdom": "^29.7.0",
25
+ "ts-jest": "^29.1.1"
22
26
  }
23
27
  }
package/rollup.config.js CHANGED
@@ -16,6 +16,6 @@ export default {
16
16
  },
17
17
  preventAssignment: true
18
18
  }),
19
- typescript()
19
+ typescript({noEmitOnError: false})
20
20
  ]
21
21
  };
package/src/hosts.ts CHANGED
@@ -37,10 +37,21 @@ export function getAssetsOrigin(): string {
37
37
  return "http://localhost:3333";
38
38
  }
39
39
 
40
+ const matchedDomain = getRegionDomain();
41
+ return `https://app.${matchedDomain || DEFAULT_DOMAIN}`;
42
+ }
43
+
44
+ export function getFontOrigin(): string {
45
+ if (IS_DEV_MODE == true) {
46
+ // Fonts aren't locally hosted during dev mode
47
+ return "http://app.inindca.com"
48
+ }
49
+ return getAssetsOrigin();
50
+ }
51
+
52
+ function getRegionDomain() {
40
53
  const pageHost = window.location.hostname;
41
- const matchedDomain = DOMAIN_LIST.find(regionDomain =>
54
+ return DOMAIN_LIST.find(regionDomain =>
42
55
  pageHost.endsWith(regionDomain)
43
56
  );
44
-
45
- return `https://app.${matchedDomain || DEFAULT_DOMAIN}`;
46
57
  }
package/src/index.ts CHANGED
@@ -1,65 +1,38 @@
1
- import { getAssetsOrigin } from "./hosts";
1
+ import { getAssetsOrigin, getFontOrigin } from "./hosts";
2
+ import { checkAndLoadScript, checkAndLoadStyle, checkAndLoadFonts } from "./loading";
2
3
 
3
4
  const ASSET_PREFIX = "__ASSET_PREFIX__";
4
5
  const SCRIPT_PATH = "genesys-webcomponents.esm.js";
5
6
  const STYLE_PATH = "genesys-webcomponents.css";
6
7
 
7
- const SCRIPT_SRC = `${getAssetsOrigin()}${ASSET_PREFIX}${SCRIPT_PATH}`;
8
- const STYLE_HREF = `${getAssetsOrigin()}${ASSET_PREFIX}${STYLE_PATH}`;
8
+ const assetsOrigin = getAssetsOrigin();
9
+ const SCRIPT_SRC = `${assetsOrigin}${ASSET_PREFIX}${SCRIPT_PATH}`;
10
+ const STYLE_HREF = `${assetsOrigin}${ASSET_PREFIX}${STYLE_PATH}`;
11
+ const fontOrigin = getFontOrigin();
12
+ const FONTS = {
13
+ "Urbanist": `${fontOrigin}/webfonts/urbanist.css`,
14
+ "Noto Sans": `${fontOrigin}/webfonts/noto-sans.css`
15
+ }
9
16
 
10
17
 
11
18
  /**
12
- * Loads the spark webcomponents from a shared CDN
19
+ * Loads the spark web components, as well as required CSS and fonts from a
20
+ * shared CDN. Performance can be optimized by pre-loading fonts in static HTML.
21
+ *
13
22
  * @returns a promise that succeeds if the component script and styles
14
- * load successfully.
23
+ * load successfully. It is not recommended to wait on this promise or to stop
24
+ * application bootstrap if it rejects. Its primary use should be for logging
25
+ * unexpected failures.
15
26
  */
16
27
  export function registerSparkComponents() : Promise<void> {
17
28
  return Promise.all([
18
29
  checkAndLoadScript(SCRIPT_SRC),
19
- checkAndLoadStyle(STYLE_HREF)
30
+ checkAndLoadStyle(STYLE_HREF),
31
+ checkAndLoadFonts(FONTS)
20
32
  ]).then()
21
33
  }
22
34
 
23
- function checkAndLoadScript(scriptSrc: string): Promise<void> {
24
- const existingTag = document.querySelector(`script[src="${scriptSrc}"]`);
25
- if (existingTag) {
26
- return Promise.resolve();
27
- } else {
28
- const scriptTag = document.createElement('script');
29
- scriptTag.setAttribute("type", "module");
30
- scriptTag.setAttribute("src", scriptSrc);
31
- const result = new Promise<void>((resolve, reject) => {
32
- scriptTag.addEventListener('load', () => {
33
- resolve();
34
- })
35
- scriptTag.addEventListener('error', () => {
36
- reject(`Spark script failed to load: ${scriptSrc}`);
37
- })
38
- });
39
- document.head.appendChild(scriptTag);
40
- return result;
41
- }
42
- }
43
35
 
44
- function checkAndLoadStyle(styleHref: string): Promise<void> {
45
- const existingTag = document.querySelector(`link[href="${styleHref}"]`);
46
- if (existingTag) {
47
- return Promise.resolve();
48
- } else {
49
- const styleTag = document.createElement('link');
50
- styleTag.setAttribute("href", styleHref);
51
- styleTag.setAttribute("rel", "stylesheet");
52
- const result = new Promise<void>((resolve, reject) => {
53
- styleTag.addEventListener('load', () => {
54
- resolve();
55
- })
56
- styleTag.addEventListener('error', () => {
57
- reject(`Spark styles failed to load: ${styleHref}`);
58
- })
59
- });
60
- document.head.appendChild(styleTag);
61
- return result;
62
- }
63
- }
64
36
  // TODO: Build out utility functions where components aren't the right solution
65
- // export function formatDate(...)
37
+ // export function formatDate(...)
38
+
package/src/loading.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Add a script tag to the document if it is not already present.
3
+ * @param scriptSrc The src attribute of the script
4
+ * @returns a promise that resolves if the script loads or is already present
5
+ */
6
+ export function checkAndLoadScript(scriptSrc: string): Promise<void> {
7
+ const existingTag = document.querySelector(`script[src="${scriptSrc}"]`);
8
+ if (existingTag) {
9
+ return Promise.resolve();
10
+ } else {
11
+ const scriptTag = document.createElement('script');
12
+ scriptTag.setAttribute("type", "module");
13
+ scriptTag.setAttribute("src", scriptSrc);
14
+ const result = new Promise<void>((resolve, reject) => {
15
+ scriptTag.addEventListener('load', () => {
16
+ resolve();
17
+ })
18
+ scriptTag.addEventListener('error', () => {
19
+ reject(`Spark script failed to load: ${scriptSrc}`);
20
+ })
21
+ });
22
+ document.head.appendChild(scriptTag);
23
+ return result;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Add a stylesheet link tag to the document if it is not already present.
29
+ * @param styleHref The href attribute of the stylesheet
30
+ * @returns a promise that resolves if the style loads or is already present
31
+ */
32
+ export function checkAndLoadStyle(styleHref: string): Promise<void> {
33
+ const existingTag = document.querySelector(`link[href="${styleHref}"]`);
34
+ if (existingTag) {
35
+ return Promise.resolve();
36
+ } else {
37
+ const styleTag = document.createElement('link');
38
+ styleTag.setAttribute("href", styleHref);
39
+ styleTag.setAttribute("rel", "stylesheet");
40
+ const result = new Promise<void>((resolve, reject) => {
41
+ styleTag.addEventListener('load', () => {
42
+ resolve();
43
+ })
44
+ styleTag.addEventListener('error', () => {
45
+ reject(`Spark styles failed to load: ${styleHref}`);
46
+ })
47
+ });
48
+ document.head.appendChild(styleTag);
49
+ return result;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Given an object that maps font-family identifiers to CSS urls e.g:
55
+ * {
56
+ * "Urbanist": "/urbanist.css",
57
+ * "Noto Sans": "/noto-sans.css"
58
+ * }
59
+ * This function checks for loaded fonts with those identifiers. If a font is
60
+ * not found, the corresponding stylesheet is added to the document.
61
+ * @param fonts An object mapping font-family identifiers to CSS file urls
62
+ * @returns A promise that resolves once the script tags has finished loading.
63
+ * It does not fail if the script tags fail to load because we don't want to fail
64
+ * the whole component loading process in that situation.
65
+ */
66
+ export function checkAndLoadFonts(fonts: {[key: string]: string}): Promise<void> {
67
+ const fontsToLoad = {...fonts}; //clone our input so we can safely mutate it.
68
+
69
+ document.fonts.forEach((fontFace) => {
70
+ // If the family is defined with quotes in CSS (e.g. `font-family: "Noto Sans"),
71
+ // those quotes may be preserved JS, depending on the browser.
72
+ const normalizedFamily = fontFace.family.replace(/"/g, "");
73
+ if(fontsToLoad[normalizedFamily]) {
74
+ // remove the font from the set to load
75
+ delete fontsToLoad[normalizedFamily];
76
+ }
77
+ });
78
+
79
+ return Promise.all(
80
+ Object.values(fontsToLoad).map((href) => {
81
+ return checkAndLoadStyle(href).catch(() => {
82
+ // Don't fail loading process for fonts, since the components
83
+ // should still be reasonably usable.
84
+ console.info(`genesys-spark: couldn't load font style ${href}`)
85
+ });
86
+ })
87
+ ).then(() => {}) // flatten the promise array
88
+ }
@@ -0,0 +1,146 @@
1
+ import { checkAndLoadFonts, checkAndLoadScript, checkAndLoadStyle } from '../src/loading';
2
+
3
+ const SCRIPT_URL = 'https://localhost/script.js';
4
+ const STYLE_URL = 'https://localhost/style.css';
5
+ const FONTS = {
6
+ "Font1": `font1.css`,
7
+ "Font2": `font2.css`
8
+ }
9
+
10
+ describe('The loading module', () => {
11
+ afterEach(() => {
12
+ document.head.innerHTML = '';
13
+ document.body.innerHTML = '';
14
+ });
15
+
16
+ describe('checkAndLoadScript', () => {
17
+ test('will create a new script tag and wait for it to load', async () => {
18
+ const promise = checkAndLoadScript(SCRIPT_URL);
19
+ const tag = document.querySelector(`script[src="${SCRIPT_URL}"]`);
20
+
21
+ expect(tag).not.toBeNull(); // The tag was created
22
+ tag?.dispatchEvent(new Event('load')); // The promise will resolve
23
+
24
+ expect(promise).resolves.toBe(undefined);
25
+ });
26
+ test('will fail if the script fails to load', async () => {
27
+ const promise = checkAndLoadScript(SCRIPT_URL);
28
+ const tag = document.querySelector(`script[src="${SCRIPT_URL}"]`);
29
+
30
+ expect(tag).not.toBeNull(); // The tag was created
31
+ tag?.dispatchEvent(new Event('error')); // The promise will reject
32
+
33
+ expect(promise).rejects.toContain(SCRIPT_URL);
34
+ });
35
+ test('will not create a tag if one already exists', async () => {
36
+ const script = document.createElement('script');
37
+ script.setAttribute('src', SCRIPT_URL);
38
+ document.body.appendChild(script);
39
+
40
+ await checkAndLoadScript(SCRIPT_URL); // Should resolve immediately
41
+ const tags = document.querySelectorAll(`script[src="${SCRIPT_URL}"]`);
42
+ expect(tags.length).toBe(1); // A second tag was not created
43
+ });
44
+ });
45
+ describe('checkAndLoadStyle', () => {
46
+ test('will create a new link tag and wait for it to load', async () => {
47
+ const promise = checkAndLoadStyle(STYLE_URL);
48
+ const tag = document.querySelector(
49
+ `link[href="${STYLE_URL}"][rel="stylesheet"]`
50
+ );
51
+
52
+ expect(tag).not.toBeNull(); // The tag was created
53
+ tag?.dispatchEvent(new Event('load')); // The promise will resolve
54
+
55
+ expect(promise).resolves.toBe(undefined);
56
+ });
57
+ test('will fail if the style fails to load', async () => {
58
+ const promise = checkAndLoadStyle(STYLE_URL);
59
+ const tag = document.querySelector(
60
+ `link[href="${STYLE_URL}"][rel="stylesheet"]`
61
+ );
62
+
63
+ expect(tag).not.toBeNull(); // The tag was created
64
+ tag?.dispatchEvent(new Event('error')); // The promise will reject
65
+
66
+ expect(promise).rejects.toContain(STYLE_URL);
67
+ });
68
+ test('will not create a tag if one already exists', async () => {
69
+ const link = document.createElement('link');
70
+ link.setAttribute('href', STYLE_URL);
71
+ link.setAttribute('rel', 'stylesheet');
72
+ document.body.appendChild(link);
73
+
74
+ await checkAndLoadStyle(STYLE_URL); // Should resolve immediately
75
+ const tags = document.querySelectorAll(`link[href="${STYLE_URL}"]`);
76
+ expect(tags.length).toBe(1); // A second tag was not created
77
+ });
78
+ });
79
+ describe("checkAndLoadFonts", () => {
80
+ let documentFonts : Array<any> = [];
81
+
82
+ beforeEach(() => {
83
+ documentFonts = [];
84
+ //@ts-ignore - needed to be able to stub out font API
85
+ document.fonts = documentFonts;
86
+ })
87
+
88
+ test('Will load missing fonts', async () => {
89
+ const promise = checkAndLoadFonts(FONTS);
90
+ const tag1 = document.querySelector(`link[href="font1.css"]`);
91
+ const tag2 = document.querySelector(`link[href="font2.css"]`);
92
+
93
+ expect(tag1).not.toBeNull();
94
+ expect(tag2).not.toBeNull();
95
+
96
+ tag1?.dispatchEvent(new Event("load"));
97
+ tag2?.dispatchEvent(new Event("load"));
98
+
99
+ expect(promise).resolves.toBe(undefined);
100
+ });
101
+
102
+ test('Will not load fonts that are already loaded', async () => {
103
+ documentFonts.push({family: "Font2"});
104
+
105
+ const promise = checkAndLoadFonts(FONTS);
106
+ const tag1 = document.querySelector(`link[href="font1.css"]`);
107
+ const tag2 = document.querySelector(`link[href="font2.css"]`);
108
+
109
+ expect(tag1).not.toBeNull();
110
+ expect(tag2).toBeNull();
111
+
112
+ tag1?.dispatchEvent(new Event("load"));
113
+
114
+ expect(promise).resolves.toBe(undefined);
115
+ })
116
+
117
+ test('Ignores quotes on the family when checking for loaded fonts', async () => {
118
+ documentFonts.push({family: '"Font2"'});
119
+
120
+ const promise = checkAndLoadFonts(FONTS);
121
+ const tag1 = document.querySelector(`link[href="font1.css"]`);
122
+ const tag2 = document.querySelector(`link[href="font2.css"]`);
123
+
124
+ expect(tag1).not.toBeNull();
125
+ expect(tag2).toBeNull();
126
+
127
+ tag1?.dispatchEvent(new Event("load"));
128
+
129
+ expect(promise).resolves.toBe(undefined);
130
+ })
131
+
132
+ test('Still resolves even if fonts fail to load', async () => {
133
+ const promise = checkAndLoadFonts(FONTS);
134
+ const tag1 = document.querySelector(`link[href="font1.css"]`);
135
+ const tag2 = document.querySelector(`link[href="font2.css"]`);
136
+
137
+ expect(tag1).not.toBeNull();
138
+ expect(tag2).not.toBeNull();
139
+
140
+ tag1?.dispatchEvent(new Event("error"));
141
+ tag2?.dispatchEvent(new Event("error"));
142
+
143
+ expect(promise).resolves.toBe(undefined);
144
+ });
145
+ });
146
+ });