Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: extract create-passthrough & interceptor #257

Merged
merged 2 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/create-passthrough.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
export function createPassthrough(fakeXHR, nativeXMLHttpRequest) {
// event types to handle on the xhr
var evts = ['error', 'timeout', 'abort', 'readystatechange'];

// event types to handle on the xhr.upload
var uploadEvents = [];

// properties to copy from the native xhr to fake xhr
var lifecycleProps = [
'readyState',
'responseText',
'response',
'responseXML',
'responseURL',
'status',
'statusText',
];

var xhr = (fakeXHR._passthroughRequest = new nativeXMLHttpRequest());
xhr.open(
fakeXHR.method,
fakeXHR.url,
fakeXHR.async,
fakeXHR.username,
fakeXHR.password
);

if (fakeXHR.responseType === 'arraybuffer') {
lifecycleProps = ['readyState', 'response', 'status', 'statusText'];
xhr.responseType = fakeXHR.responseType;
}

// use onload if the browser supports it
if ('onload' in xhr) {
evts.push('load');
}

// add progress event for async calls
// avoid using progress events for sync calls, they will hang https://bugs.webkit.org/show_bug.cgi?id=40996.
if (fakeXHR.async && fakeXHR.responseType !== 'arraybuffer') {
evts.push('progress');
uploadEvents.push('progress');
}

// update `propertyNames` properties from `fromXHR` to `toXHR`
function copyLifecycleProperties(propertyNames, fromXHR, toXHR) {
for (var i = 0; i < propertyNames.length; i++) {
var prop = propertyNames[i];
if (prop in fromXHR) {
toXHR[prop] = fromXHR[prop];
}
}
}

// fire fake event on `eventable`
function dispatchEvent(eventable, eventType, event) {
eventable.dispatchEvent(event);
if (eventable['on' + eventType]) {
eventable['on' + eventType](event);
}
}

// set the on- handler on the native xhr for the given eventType
function createHandler(eventType) {
xhr['on' + eventType] = function (event) {
copyLifecycleProperties(lifecycleProps, xhr, fakeXHR);
dispatchEvent(fakeXHR, eventType, event);
};
}

// set the on- handler on the native xhr's `upload` property for
// the given eventType
function createUploadHandler(eventType) {
if (xhr.upload) {
xhr.upload['on' + eventType] = function (event) {
dispatchEvent(fakeXHR.upload, eventType, event);
};
}
}

var i;
for (i = 0; i < evts.length; i++) {
createHandler(evts[i]);
}
for (i = 0; i < uploadEvents.length; i++) {
createUploadHandler(uploadEvents[i]);
}

if (fakeXHR.async) {
xhr.timeout = fakeXHR.timeout;
xhr.withCredentials = fakeXHR.withCredentials;
}
for (var h in fakeXHR.requestHeaders) {
xhr.setRequestHeader(h, fakeXHR.requestHeaders[h]);
}
return xhr;
}
152 changes: 1 addition & 151 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import FakeXMLHttpRequest from 'fake-xml-http-request';
import * as FakeFetch from 'whatwg-fetch';
import parseURL from './parse-url';
import Registry from './registry';
import Hosts from './hosts';
import { interceptor } from './interceptor';

function Pretender(/* routeMap1, routeMap2, ..., options*/) {
this.hosts = new Hosts();
Expand Down Expand Up @@ -48,156 +48,6 @@ function Pretender(/* routeMap1, routeMap2, ..., options*/) {
}
}

function interceptor(ctx) {
function FakeRequest() {
// super()
FakeXMLHttpRequest.call(this);
}
FakeRequest.prototype = Object.create(FakeXMLHttpRequest.prototype);
FakeRequest.prototype.constructor = FakeRequest;

// extend
FakeRequest.prototype.send = function send() {
this.sendArguments = arguments;
if (!ctx.pretender.running) {
throw new Error('You shut down a Pretender instance while there was a pending request. ' +
'That request just tried to complete. Check to see if you accidentally shut down ' +
'a pretender earlier than you intended to');
}

FakeXMLHttpRequest.prototype.send.apply(this, arguments);

if (ctx.pretender.checkPassthrough(this)) {
this.passthrough();
} else {
ctx.pretender.handleRequest(this);
}
};

FakeRequest.prototype.passthrough = function passthrough() {
if (!this.sendArguments) {
throw new Error('You attempted to passthrough a FakeRequest that was never sent. ' +
'Call `.send()` on the original request first');
}
let xhr = createPassthrough(this);
xhr.send.apply(xhr, this.sendArguments);
return xhr;
};


function createPassthrough(fakeXHR) {
// event types to handle on the xhr
let evts = ['error', 'timeout', 'abort', 'readystatechange'];

// event types to handle on the xhr.upload
let uploadEvents = [];

// properties to copy from the native xhr to fake xhr
let lifecycleProps = ['readyState', 'responseText', 'response', 'responseXML', 'responseURL', 'status', 'statusText'];

let xhr = fakeXHR._passthroughRequest = new ctx.pretender._nativeXMLHttpRequest();
xhr.open(fakeXHR.method, fakeXHR.url, fakeXHR.async, fakeXHR.username, fakeXHR.password);

if (fakeXHR.responseType === 'arraybuffer') {
lifecycleProps = ['readyState', 'response', 'status', 'statusText'];
xhr.responseType = fakeXHR.responseType;
}

// use onload if the browser supports it
if ('onload' in xhr) {
evts.push('load');
}

// add progress event for async calls
// avoid using progress events for sync calls, they will hang https://bugs.webkit.org/show_bug.cgi?id=40996.
if (fakeXHR.async && fakeXHR.responseType !== 'arraybuffer') {
evts.push('progress');
uploadEvents.push('progress');
}

// update `propertyNames` properties from `fromXHR` to `toXHR`
function copyLifecycleProperties(propertyNames, fromXHR, toXHR) {
for (let i = 0; i < propertyNames.length; i++) {
let prop = propertyNames[i];
if (prop in fromXHR) {
toXHR[prop] = fromXHR[prop];
}
}
}

// fire fake event on `eventable`
function dispatchEvent(eventable, eventType, event) {
eventable.dispatchEvent(event);
if (eventable['on' + eventType]) {
eventable['on' + eventType](event);
}
}

// set the on- handler on the native xhr for the given eventType
function createHandler(eventType) {
xhr['on' + eventType] = function(event) {
copyLifecycleProperties(lifecycleProps, xhr, fakeXHR);
dispatchEvent(fakeXHR, eventType, event);
};
}

// set the on- handler on the native xhr's `upload` property for
// the given eventType
function createUploadHandler(eventType) {
if (xhr.upload) {
xhr.upload['on' + eventType] = function(event) {
dispatchEvent(fakeXHR.upload, eventType, event);
};
}
}

let i;
for (i = 0; i < evts.length; i++) {
createHandler(evts[i]);
}
for (i = 0; i < uploadEvents.length; i++) {
createUploadHandler(uploadEvents[i]);
}

if (fakeXHR.async) {
xhr.timeout = fakeXHR.timeout;
xhr.withCredentials = fakeXHR.withCredentials;
}
for (let h in fakeXHR.requestHeaders) {
xhr.setRequestHeader(h, fakeXHR.requestHeaders[h]);
}
return xhr;
}

FakeRequest.prototype._passthroughCheck = function(method, args) {
if (this._passthroughRequest) {
return this._passthroughRequest[method].apply(this._passthroughRequest, args);
}
return FakeXMLHttpRequest.prototype[method].apply(this, args);
};

FakeRequest.prototype.abort = function abort() {
return this._passthroughCheck('abort', arguments);
};

FakeRequest.prototype.getResponseHeader = function getResponseHeader() {
return this._passthroughCheck('getResponseHeader', arguments);
};

FakeRequest.prototype.getAllResponseHeaders = function getAllResponseHeaders() {
return this._passthroughCheck('getAllResponseHeaders', arguments);
};

if (ctx.pretender._nativeXMLHttpRequest.prototype._passthroughCheck) {
// eslint-disable-next-line no-console
console.warn('You created a second Pretender instance while there was already one running. ' +
'Running two Pretender servers at once will lead to unexpected results and will ' +
'be removed entirely in a future major version.' +
'Please call .shutdown() on your instances when you no longer need them to respond.');
}
return FakeRequest;
}

function verbify(verb) {
return function(path, handler, async) {
return this.register(verb, path, handler, async);
Expand Down
67 changes: 67 additions & 0 deletions src/interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import FakeXMLHttpRequest from 'fake-xml-http-request';
import { createPassthrough } from './create-passthrough';

export function interceptor(ctx) {
function FakeRequest() {
// super()
FakeXMLHttpRequest.call(this);
}
FakeRequest.prototype = Object.create(FakeXMLHttpRequest.prototype);
FakeRequest.prototype.constructor = FakeRequest;

// extend
FakeRequest.prototype.send = function send() {
this.sendArguments = arguments;
if (!ctx.pretender.running) {
throw new Error('You shut down a Pretender instance while there was a pending request. ' +
'That request just tried to complete. Check to see if you accidentally shut down ' +
'a pretender earlier than you intended to');
}

FakeXMLHttpRequest.prototype.send.apply(this, arguments);

if (ctx.pretender.checkPassthrough(this)) {
this.passthrough();
} else {
ctx.pretender.handleRequest(this);
}
};

FakeRequest.prototype.passthrough = function passthrough() {
if (!this.sendArguments) {
throw new Error('You attempted to passthrough a FakeRequest that was never sent. ' +
'Call `.send()` on the original request first');
}
var xhr = createPassthrough(this, ctx.pretender._nativeXMLHttpRequest);
xhr.send.apply(xhr, this.sendArguments);
return xhr;
};

FakeRequest.prototype._passthroughCheck = function(method, args) {
if (this._passthroughRequest) {
return this._passthroughRequest[method].apply(this._passthroughRequest, args);
}
return FakeXMLHttpRequest.prototype[method].apply(this, args);
};

FakeRequest.prototype.abort = function abort() {
return this._passthroughCheck('abort', arguments);
};

FakeRequest.prototype.getResponseHeader = function getResponseHeader() {
return this._passthroughCheck('getResponseHeader', arguments);
};

FakeRequest.prototype.getAllResponseHeaders = function getAllResponseHeaders() {
return this._passthroughCheck('getAllResponseHeaders', arguments);
};

if (ctx.pretender._nativeXMLHttpRequest.prototype._passthroughCheck) {
// eslint-disable-next-line no-console
console.warn('You created a second Pretender instance while there was already one running. ' +
'Running two Pretender servers at once will lead to unexpected results and will ' +
'be removed entirely in a future major version.' +
'Please call .shutdown() on your instances when you no longer need them to respond.');
}
return FakeRequest;
}
2 changes: 1 addition & 1 deletion test/force_passthrough_test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
var describe = QUnit.module;
var it = QUnit.test;

describe('passthrough requests', function(config) {
describe('force passthrough requests', function(config) {
config.beforeEach(function() {
this.pretender = new Pretender({ forcePassthrough: true });
});
Expand Down