Skip to content

Commit 32bdc12

Browse files
author
Thomas Wang
committed
fetch support
tl;dr - This commit add support for fetch. - pretender swap native fetch related API if exists - pretender.shutdown() restore native fetch related API - doesn't work with AbortController Pretender has been working well with xhr, but doesn't handle fetch. It's been 2 years since @rwjblue first open the [issue](pretenderjs#60) for fetch support. So people don't need to do extra work to polyfill for testing. Include a fetch ponyfill and swap the native fetch during pretender creation, then restore them when `shutdown`. Since fetch polyfill uses xhr behind the scene, pretender will "just work". Per Yetch homepage, the supplement set of yetch impl and spec include: - Inability to [set the redirect mode](JakeChampion/fetch#137) - Inability to [change the cache directive](JakeChampion/fetch#438 (comment)) - Inability to [disable same-origin cookies](JakeChampion/fetch#56 (comment)) - `xhr.abort()` first set state to done, finally response to a [network error](https://xhr.spec.whatwg.org/#the-abort()-method); - [fetch](https://dom.spec.whatwg.org/#aborting-ongoing-activities) will reject promise with a new "AbortError" DOMException. For `fake_xml_http_request` impl, the request is resolved once its state is changed to `DONE` so the `reject` is not cathed. So the senario happens in pretender is: 1. state chagne to `DONE`, trigger resolve request 2. abort, trigger reject 3. xhr.onerror, trigger reject The first resolve wins, error thus not rejected but an empty request is resolved. Though polyfilled by xhr, fetch returns a Promise and is asynchronous by nature.
1 parent 7101a31 commit 32bdc12

File tree

5 files changed

+259
-13
lines changed

5 files changed

+259
-13
lines changed

karma.conf.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ module.exports = function(config) {
2020
'bower_components/jquery-1/index.js',
2121
'bower_components/jquery/dist/jquery.js',
2222
'node_modules/es6-promise/dist/es6-promise.auto.js',
23+
'node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js',
24+
'node_modules/yetch/dist/yetch-ponyfill.js',
2325
'pretender.js',
2426
'test/**/*.js'
2527
],

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@
3737
"sinon": "^3.2.1"
3838
},
3939
"dependencies": {
40+
"abortcontroller-polyfill": "^1.1.9",
4041
"fake-xml-http-request": "^2.0.0",
41-
"route-recognizer": "^0.3.3"
42+
"route-recognizer": "^0.3.3",
43+
"yetch": "^0.0.1"
4244
},
4345
"jspm": {
4446
"shim": {

pretender.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ var RouteRecognizer = appearsBrowserified ? getModuleDefault(require('route-reco
1313
var FakeXMLHttpRequest = appearsBrowserified ? getModuleDefault(require('fake-xml-http-request')) :
1414
self.FakeXMLHttpRequest;
1515

16+
// fetch related ponyfills
17+
var Yetch = appearsBrowserified ? getModuleDefault(require('yetch/dist/yetch-polyfill')) : self.Yetch;
18+
1619
/**
1720
* parseURL - decompose a URL into its parts
1821
* @param {String} url a URL
@@ -139,6 +142,14 @@ function Pretender(/* routeMap1, routeMap2, ..., options*/) {
139142
// the route map.
140143
self.XMLHttpRequest = interceptor(ctx);
141144

145+
// polyfill fetch when xhr is ready
146+
// AbortController doesn't need restore
147+
this._fetchProps = ['fetch', 'Headers', 'Request', 'Response'];
148+
this._fetchProps.forEach(function(name) {
149+
this['_native' + name] = self[name];
150+
self[name] = Yetch[name];
151+
}, this);
152+
142153
// 'start' the server
143154
this.running = true;
144155

@@ -471,6 +482,9 @@ Pretender.prototype = {
471482
},
472483
shutdown: function shutdown() {
473484
self.XMLHttpRequest = this._nativeXMLHttpRequest;
485+
this._fetchProps.forEach(function(name) {
486+
self[name] = this['_native' + name];
487+
}, this);
474488
this.ctx.pretender = undefined;
475489
// 'stop' the server
476490
this.running = false;

test/fetch_test.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
var describe = QUnit.module;
2+
var it = QUnit.test;
3+
var clock;
4+
5+
describe('pretender invoking by fetch', function(config) {
6+
config.beforeEach(function() {
7+
this.pretender = new Pretender();
8+
});
9+
10+
config.afterEach(function() {
11+
if (clock) {
12+
clock.restore();
13+
}
14+
this.pretender.shutdown();
15+
});
16+
17+
it('fetch triggers pretender', function(assert) {
18+
var wasCalled;
19+
20+
this.pretender.get('/some/path', function() {
21+
wasCalled = true;
22+
});
23+
24+
fetch('/some/path');
25+
assert.ok(wasCalled);
26+
});
27+
28+
it('is resolved asynchronously', function(assert) {
29+
assert.expect(2);
30+
var done = assert.async();
31+
var val = 'unset';
32+
33+
this.pretender.get('/some/path', function(request) {
34+
return [200, {}, ''];
35+
});
36+
37+
fetch('/some/path').then(function() {
38+
assert.equal(val, 'set');
39+
done();
40+
});
41+
42+
assert.equal(val, 'unset');
43+
val = 'set';
44+
});
45+
46+
it('can NOT be resolved synchronously', function(assert) {
47+
assert.expect(1);
48+
var val = 0;
49+
50+
this.pretender.get(
51+
'/some/path',
52+
function(request) {
53+
return [200, {}, ''];
54+
},
55+
false
56+
);
57+
58+
fetch('/some/path').then(function() {
59+
// This won't be called
60+
assert.equal(val, 0);
61+
val++;
62+
});
63+
assert.equal(val, 0);
64+
});
65+
66+
it('has NO Abortable fetch', function(assert) {
67+
assert.expect(1);
68+
var done = assert.async();
69+
var wasCalled = false;
70+
this.pretender.get(
71+
'/downloads',
72+
function(request) {
73+
return [200, {}, 'FAIL'];
74+
},
75+
200
76+
);
77+
78+
var controller = new AbortController();
79+
var signal = controller.signal;
80+
setTimeout(function() {
81+
controller.abort();
82+
}, 10);
83+
fetch('/downloads', { signal: signal })
84+
.then(function(data) {
85+
assert.ok(data, 'AbortError was not rejected');
86+
done();
87+
})
88+
.catch(function(err) {
89+
// it should execute to here but won't due to FakeXmlHttpRequest limitation
90+
//
91+
// ### why it's not working for fetch
92+
// For `fake_xml_http_request` impl, the request is resolved once its state
93+
// is changed to `DONE` so the `reject` is not cathed.
94+
// So the senario happens in pretender is:
95+
// 1. state chagne to `DONE`, trigger resolve request
96+
// 2. abort, trigger reject
97+
// 3. xhr.onerror, trigger reject
98+
// The first resolve wins, error thus not rejected but an empty request is resolved.
99+
done();
100+
});
101+
});
102+
});

0 commit comments

Comments
 (0)