Skip to content

Commit b44f590

Browse files
committed
fix: add Promise rejecting and tests
1 parent 5b96731 commit b44f590

File tree

8 files changed

+2547
-67
lines changed

8 files changed

+2547
-67
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
.jest-cache
2+
coverage
3+
dist
14
es
25
lib
3-
dist
46
node_modules
57
yarn-error.log

jest.config.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
module.exports = {
2+
cacheDirectory: '.jest-cache',
3+
coverageThreshold: {
4+
global: {
5+
branches: 100,
6+
functions: 100,
7+
lines: 100,
8+
statements: 100,
9+
},
10+
},
11+
coverageReporters: [
12+
'html',
13+
],
14+
moduleFileExtensions: ['js'],
15+
roots: [
16+
'<rootDir>/test',
17+
'<rootDir>/src',
18+
],
19+
setupFiles: [
20+
'<rootDir>/test/test-setup.js',
21+
],
22+
};

package.json

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "fetch-script",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"description": "Async loading of scripts using the Fetch API",
55
"main": "lib/fetch-script.js",
66
"unpkg": "dist/fetch-script.js",
@@ -11,11 +11,15 @@
1111
"build:umd:min": "cross-env BABEL_ENV=es NODE_ENV=production rollup -c -o dist/fetch-script.min.js",
1212
"build:umd": "cross-env BABEL_ENV=es NODE_ENV=development rollup -c -o dist/fetch-script.js",
1313
"build": "yarn run clean && yarn run build:es && yarn run build:commonjs && yarn run build:umd && yarn run build:umd:min",
14-
"check": "yarn run lint",
14+
"check": "yarn run lint && yarn run test",
1515
"clean-install": "yarn run clean && rimraf node_modules && yarn install --pure-lockfile",
1616
"clean": "rimraf dist lib es",
1717
"lint": "eslint src/**",
18-
"prepare": "yarn run lint && yarn run build"
18+
"prepare": "yarn run lint && yarn run test:cov && yarn run build",
19+
"test:clean": "rimraf .jest-cache",
20+
"test:cov": "rimraf coverage && yarn run test --coverage",
21+
"test:watch": "yarn run test --watch",
22+
"test": "cross-env BABEL_ENV=test NODE_ENV=production jest"
1923
},
2024
"author": "Stephane Rufer <stephane.rufer@gmail.com>",
2125
"license": "MIT",
@@ -28,16 +32,23 @@
2832
"devDependencies": {
2933
"@babel/core": "^7.1.0",
3034
"@babel/preset-env": "^7.1.0",
35+
"babel-core": "^7.0.0-bridge.0",
36+
"babel-jest": "^23.6.0",
3137
"babel-preset-env": "^1.7.0",
3238
"babel-preset-es2015-rollup": "^3.0.0",
3339
"cross-env": "^5.2.0",
3440
"eslint": "^5.6.0",
3541
"eslint-config-airbnb-base": "^13.1.0",
3642
"eslint-plugin-import": "^2.14.0",
43+
"jest": "^23.6.0",
44+
"jest-fetch-mock": "^1.6.6",
45+
"jsdom": "^12.0.0",
46+
"regenerator-runtime": "^0.12.1",
3747
"rollup": "^0.66.2",
3848
"rollup-plugin-babel": "^4.0.3",
3949
"rollup-plugin-commonjs": "^9.1.8",
4050
"rollup-plugin-node-resolve": "^3.4.0",
41-
"rollup-plugin-uglify": "^6.0.0"
51+
"rollup-plugin-uglify": "^6.0.0",
52+
"whatwg-fetch": "^3.0.0"
4253
}
4354
}

src/index.js

+33-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import { head as injectHead } from './injectors';
1+
import { head, headCors } from './injectors';
22

3+
const networkError = 'Network response was not ok.';
4+
5+
function contentLoadedEvent() {
6+
const DOMContentLoadedEvent = document.createEvent('Event');
7+
DOMContentLoadedEvent.initEvent('DOMContentLoaded', true, true);
8+
document.dispatchEvent(DOMContentLoadedEvent);
9+
}
310
/**
411
* Fetch Script module.
512
*/
@@ -24,22 +31,39 @@ function fetchInject(scripts, promise) {
2431
if (typeof input === 'object') {
2532
({ options, url } = input);
2633
}
27-
deferreds.push(
28-
window.fetch(url, options)
29-
.then((res) => [res.clone().text(), res.blob()])
30-
.then((promises) => Promise.all(promises)
31-
.then((resolved) => resources.push({ text: resolved[0], blob: resolved[1] }))),
32-
);
34+
if (options && options.mode === 'no-cors' && options.method === 'GET') {
35+
// can not use fetch, inject the script into the head
36+
deferreds.push(new Promise((resolve, reject) => {
37+
headCors(window, document, 'script', url, () => {
38+
contentLoadedEvent();
39+
resolve();
40+
}, () => {
41+
reject(new Error(networkError));
42+
});
43+
}));
44+
} else {
45+
deferreds.push(
46+
window.fetch(url, options)
47+
.then((res) => {
48+
if (!res.ok) {
49+
throw Error(networkError);
50+
}
51+
return [res.clone().text(), res.blob()];
52+
})
53+
.then((promises) => Promise.all(promises)
54+
.then((resolved) => resources.push({ text: resolved[0], blob: resolved[1] }))),
55+
);
56+
}
3357
});
3458

3559
return Promise.all(deferreds).then(() => {
3660
resources.forEach((resource) => {
3761
thenables.push({
3862
then: (resolve) => {
3963
if (resource.blob.type.includes('text/css')) {
40-
injectHead(window, document, 'style', resource, resolve);
64+
head(window, document, 'style', resource, resolve);
4165
} else {
42-
injectHead(window, document, 'script', resource, resolve);
66+
head(window, document, 'script', resource, resolve);
4367
}
4468
},
4569
});

src/injectors.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
export const head = (function(i,n,j,e,c,t,s){t=n.createElement(j),s=n.getElementsByTagName(j)[0];t.appendChild(n.createTextNode(e.text));t.onload=c(e);s?s.parentNode.insertBefore(t,s):n.head.appendChild(t)}); // eslint-disable-line
1+
export const head = (function(w,d,t,o,r,c,s){c=d.createElement(t),s=d.getElementsByTagName(t)[0];c.appendChild(d.createTextNode(o.text));c.onload=r(o);s?s.parentNode.insertBefore(c,s):d.appendChild(c)}); // eslint-disable-line
2+
3+
export const headCors = (function(w,d,t,u,r,e,c,s){c=d.createElement(t),s=d.head.getElementsByTagName(t)[0];c.src=u;c.onload=r;c.onerror=e;s?s.parentNode.insertBefore(c,s):d.head.appendChild(c) // eslint-disable-line
4+
});

test/index.spec.js

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/* eslint-env jest */
2+
3+
import fetchInject from '../src';
4+
5+
describe('fetchInject', () => {
6+
const scriptName = 'test.script.js';
7+
const cssName = 'test.css';
8+
const scriptText = 'function theBestTeam() { console.log("Dodgers."); }';
9+
const cssText = '.baseball{ color: blue; }';
10+
const networkError = 'Network response was not ok.';
11+
12+
beforeEach(() => {
13+
fetch.resetMocks();
14+
});
15+
16+
test('should fetch a script in non cors mode', async (done) => {
17+
const DOMContentLoadedEvent = document.createEvent('Event');
18+
DOMContentLoadedEvent.initEvent('DOMContentLoaded', true, true);
19+
const loadEvent = document.createEvent('Event');
20+
loadEvent.initEvent('load', true, true);
21+
const contentloadedListener = jest.fn();
22+
document.addEventListener('DOMContentLoaded', contentloadedListener);
23+
24+
setTimeout(() => {
25+
const injectedScriptElement = document.getElementsByTagName('script')[0];
26+
injectedScriptElement.dispatchEvent(loadEvent);
27+
}, 10);
28+
29+
fetchInject([{
30+
url: `//${scriptName}`,
31+
options: { method: 'GET', mode: 'no-cors' },
32+
}]).then(() => {
33+
expect(document.getElementsByTagName('script')[0].src)
34+
.toEqual(`http://${scriptName}/`);
35+
expect(contentloadedListener).toHaveBeenCalledWith(DOMContentLoadedEvent);
36+
done();
37+
});
38+
});
39+
40+
test('should reject the promise of a script fails to load in non cors mode', async (done) => {
41+
const loadErrorEvent = document.createEvent('Event');
42+
loadErrorEvent.initEvent('error', true, true);
43+
const contentloadedListener = jest.fn();
44+
document.addEventListener('DOMContentLoaded', contentloadedListener);
45+
46+
setTimeout(() => {
47+
const injectedScriptElement = document.getElementsByTagName('script')[0];
48+
injectedScriptElement.dispatchEvent(loadErrorEvent);
49+
}, 10);
50+
51+
fetchInject([{
52+
url: `//${scriptName}`,
53+
options: { method: 'GET', mode: 'no-cors' },
54+
}]).catch((err) => {
55+
expect(document.getElementsByTagName('script')[0].src)
56+
.toEqual(`http://${scriptName}/`);
57+
expect(contentloadedListener).not.toHaveBeenCalled();
58+
expect(err.message).toEqual(networkError);
59+
done();
60+
});
61+
});
62+
63+
test('should fetch a script in cors mode', async (done) => {
64+
fetch.mockResponseOnce(scriptText);
65+
66+
fetchInject([{
67+
url: `//${scriptName}`,
68+
options: { method: 'GET', mode: 'cors' },
69+
}]).then((res) => {
70+
expect(res[0].text).toEqual(scriptText);
71+
expect(fetch.mock.calls.length).toEqual(1);
72+
done();
73+
});
74+
});
75+
76+
test('should fetch without any options', async (done) => {
77+
fetch.mockResponseOnce(scriptText);
78+
79+
fetchInject([{
80+
url: `//${scriptName}`,
81+
}]).then((res) => {
82+
expect(res[0].text).toEqual(scriptText);
83+
expect(fetch.mock.calls.length).toEqual(1);
84+
done();
85+
});
86+
});
87+
88+
test('should fetch when only given a url', async (done) => {
89+
fetch.mockResponseOnce(scriptText);
90+
91+
fetchInject([`//${scriptName}`]).then((res) => {
92+
expect(res[0].text).toEqual(scriptText);
93+
expect(fetch.mock.calls.length).toEqual(1);
94+
done();
95+
});
96+
});
97+
98+
test('should fetch after passed promise resolves', async (done) => {
99+
const timeBeforeResolve = Date.now();
100+
const timeoutTime = 500;
101+
fetch.mockResponseOnce(scriptText);
102+
const myPromise = new Promise((resolve) => {
103+
setTimeout(resolve, timeoutTime);
104+
});
105+
106+
fetchInject([`//${scriptName}`], myPromise).then((res) => {
107+
const timeAfterResolve = Date.now();
108+
expect(timeAfterResolve - timeBeforeResolve).toBeGreaterThanOrEqual(timeoutTime);
109+
expect(res[0].text).toEqual(scriptText);
110+
expect(fetch.mock.calls.length).toEqual(1);
111+
done();
112+
});
113+
});
114+
115+
test('should fetch css in cors mode', async (done) => {
116+
fetch.mockResponseOnce(new Blob([cssText], { type: 'text/css' }));
117+
118+
fetchInject([{
119+
url: `//${cssName}`,
120+
options: { method: 'GET', mode: 'cors' },
121+
}]).then((res) => {
122+
expect(res[0].text).toEqual(cssText);
123+
expect(fetch.mock.calls.length).toEqual(1);
124+
done();
125+
});
126+
});
127+
128+
test('should return an error for a failed fetch in cors mode', async (done) => {
129+
fetch.mockResponses([
130+
scriptText,
131+
{ status: 500 },
132+
]);
133+
134+
fetchInject([{
135+
url: `//${scriptName}`,
136+
options: { method: 'GET', mode: 'cors' },
137+
}]).catch((err) => {
138+
expect(err.message).toEqual(networkError);
139+
done();
140+
});
141+
});
142+
143+
test('should reject promise, if no scripts are defined', () => {
144+
expect(fetchInject()).rejects.toThrow("Failed to execute 'fetchInject': 1 argument required but only 0 present.");
145+
});
146+
147+
test('should reject promise, if scripts is not an array', () => {
148+
const errorMessage = "Failed to execute 'fetchInject': argument 1 must be of type 'Array'.";
149+
150+
expect(fetchInject({ an: 'object' })).rejects.toThrow(errorMessage);
151+
expect(fetchInject('string')).rejects.toThrow(errorMessage);
152+
expect(fetchInject(true)).rejects.toThrow(errorMessage);
153+
expect(fetchInject(42)).rejects.toThrow(errorMessage);
154+
});
155+
156+
test('should reject promise, the second argument is not a Promise', () => {
157+
const errorMessage = "Failed to execute 'fetchInject': argument 2 must be of type 'Promise'.";
158+
159+
expect(fetchInject([], { an: 'object' })).rejects.toThrow(errorMessage);
160+
expect(fetchInject([], 'string')).rejects.toThrow(errorMessage);
161+
expect(fetchInject([], true)).rejects.toThrow(errorMessage);
162+
expect(fetchInject([], 42)).rejects.toThrow(errorMessage);
163+
expect(fetchInject([], [])).rejects.toThrow(errorMessage);
164+
});
165+
});

test/test-setup.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const { JSDOM } = require('jsdom');
2+
3+
require('whatwg-fetch');
4+
global.fetch = require('jest-fetch-mock');
5+
6+
const jsdom = new JSDOM('<!doctype html><html><head></head><body></body></html>');
7+
const { window } = jsdom;
8+
9+
global.window = window;
10+
global.document = window.document;
11+
global.self = window.self;
12+
global.navigator = {
13+
userAgent: 'node.js',
14+
};
15+
global.HTMLElement = window.HTMLElement;

0 commit comments

Comments
 (0)