Skip to content

Commit

Permalink
fetch support
Browse files Browse the repository at this point in the history
tl;dr
- This commit adds support for fetch.
- pretender swap native fetch related API if exists
- pretender.shutdown() restore native fetch related API
- doesn't work with AbortController

Motivation
------
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.

Changes
------
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 should "just work".

Caveats
------
1. The supplement set of yetch impl and spec includes (not complete):
- 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))

2. Abort
- `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.

As implemented in `fake_xml_http_request`, the request is resolved once its
state is changed to `DONE`.
So the senario happens in pretender is:
  1). state changes 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.

3. Though polyfilled by xhr, fetch returns a Promise and is asynchronous by
nature.
  • Loading branch information
Thomas Wang committed May 23, 2018
1 parent 7101a31 commit 3369d4e
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 13 deletions.
2 changes: 2 additions & 0 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ module.exports = function(config) {
'bower_components/jquery-1/index.js',
'bower_components/jquery/dist/jquery.js',
'node_modules/es6-promise/dist/es6-promise.auto.js',
'node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js',
'node_modules/yetch/dist/yetch-ponyfill.js',
'pretender.js',
'test/**/*.js'
],
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"url": "https://github.com/pretenderjs/pretender.git"
},
"devDependencies": {
"abortcontroller-polyfill": "^1.1.9",
"bower": "^1.3.5",
"coveralls": "^2.11.3",
"es6-promise": "^4.0.5",
Expand All @@ -38,7 +39,8 @@
},
"dependencies": {
"fake-xml-http-request": "^2.0.0",
"route-recognizer": "^0.3.3"
"route-recognizer": "^0.3.3",
"yetch": "^0.0.1"
},
"jspm": {
"shim": {
Expand Down
14 changes: 14 additions & 0 deletions pretender.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ var RouteRecognizer = appearsBrowserified ? getModuleDefault(require('route-reco
var FakeXMLHttpRequest = appearsBrowserified ? getModuleDefault(require('fake-xml-http-request')) :
self.FakeXMLHttpRequest;

// fetch related ponyfills
var Yetch = appearsBrowserified ? getModuleDefault(require('yetch/dist/yetch-polyfill')) : self.Yetch;

/**
* parseURL - decompose a URL into its parts
* @param {String} url a URL
Expand Down Expand Up @@ -139,6 +142,14 @@ function Pretender(/* routeMap1, routeMap2, ..., options*/) {
// the route map.
self.XMLHttpRequest = interceptor(ctx);

// polyfill fetch when xhr is ready
// AbortController doesn't need restore
this._fetchProps = ['fetch', 'Headers', 'Request', 'Response'];
this._fetchProps.forEach(function(name) {
this['_native' + name] = self[name];
self[name] = Yetch[name];
}, this);

// 'start' the server
this.running = true;

Expand Down Expand Up @@ -471,6 +482,9 @@ Pretender.prototype = {
},
shutdown: function shutdown() {
self.XMLHttpRequest = this._nativeXMLHttpRequest;
this._fetchProps.forEach(function(name) {
self[name] = this['_native' + name];
}, this);
this.ctx.pretender = undefined;
// 'stop' the server
this.running = false;
Expand Down
102 changes: 102 additions & 0 deletions test/fetch_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
var describe = QUnit.module;
var it = QUnit.test;
var clock;

describe('pretender invoking by fetch', function(config) {
config.beforeEach(function() {
this.pretender = new Pretender();
});

config.afterEach(function() {
if (clock) {
clock.restore();
}
this.pretender.shutdown();
});

it('fetch triggers pretender', function(assert) {
var wasCalled;

this.pretender.get('/some/path', function() {
wasCalled = true;
});

fetch('/some/path');
assert.ok(wasCalled);
});

it('is resolved asynchronously', function(assert) {
assert.expect(2);
var done = assert.async();
var val = 'unset';

this.pretender.get('/some/path', function(request) {
return [200, {}, ''];
});

fetch('/some/path').then(function() {
assert.equal(val, 'set');
done();
});

assert.equal(val, 'unset');
val = 'set';
});

it('can NOT be resolved synchronously', function(assert) {
assert.expect(1);
var val = 0;

this.pretender.get(
'/some/path',
function(request) {
return [200, {}, ''];
},
false
);

fetch('/some/path').then(function() {
// This won't be called
assert.equal(val, 0);
val++;
});
assert.equal(val, 0);
});

it('has NO Abortable fetch', function(assert) {
assert.expect(1);
var done = assert.async();
var wasCalled = false;
this.pretender.get(
'/downloads',
function(request) {
return [200, {}, 'FAIL'];
},
200
);

var controller = new AbortController();
var signal = controller.signal;
setTimeout(function() {
controller.abort();
}, 10);
fetch('/downloads', { signal: signal })
.then(function(data) {
assert.ok(data, 'AbortError was not rejected');
done();
})
.catch(function(err) {
// it should execute to here but won't due to FakeXmlHttpRequest limitation
//
// ### why it's not working for fetch
// 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.
done();
});
});
});
Loading

0 comments on commit 3369d4e

Please sign in to comment.