FIX: Silence DOMException errors when running phantomjs

This commit is contained in:
Robin Ward 2017-02-17 16:15:36 -05:00
parent c6a0e74945
commit ba8202d199
1 changed files with 296 additions and 105 deletions

View File

@ -1,22 +1,113 @@
(function(window){
(function(self) {
'use strict';
var isNode = typeof process !== 'undefined' && process.toString() === '[object process]';
var RouteRecognizer = isNode ? require('route-recognizer')['default'] : window.RouteRecognizer;
var FakeXMLHttpRequest = isNode ? require('./bower_components/FakeXMLHttpRequest/fake_xml_http_request') : window.FakeXMLHttpRequest;
var slice = [].slice;
var appearsBrowserified = typeof self !== 'undefined' &&
typeof process !== 'undefined' &&
Object.prototype.toString.call(process) === '[object Object]';
function Pretender(/* routeMap1, routeMap2, ...*/){
maps = slice.call(arguments);
var RouteRecognizer = appearsBrowserified ? require('route-recognizer') : self.RouteRecognizer;
var FakeXMLHttpRequest = appearsBrowserified ? require('fake-xml-http-request') : self.FakeXMLHttpRequest;
/**
* parseURL - decompose a URL into its parts
* @param {String} url a URL
* @return {Object} parts of the URL, including the following
*
* 'https://www.yahoo.com:1234/mypage?test=yes#abc'
*
* {
* host: 'www.yahoo.com:1234',
* protocol: 'https:',
* search: '?test=yes',
* hash: '#abc',
* href: 'https://www.yahoo.com:1234/mypage?test=yes#abc',
* pathname: '/mypage',
* fullpath: '/mypage?test=yes'
* }
*/
function parseURL(url) {
// TODO: something for when document isn't present... #yolo
var anchor = document.createElement('a');
anchor.href = url;
if (!anchor.host) {
anchor.href = anchor.href; // IE: load the host and protocol
}
var pathname = anchor.pathname;
if (pathname.charAt(0) !== '/') {
pathname = '/' + pathname; // IE: prepend leading slash
}
var host = anchor.host;
if (anchor.port === '80' || anchor.port === '443') {
host = anchor.hostname; // IE: remove default port
}
return {
host: host,
protocol: anchor.protocol,
search: anchor.search,
hash: anchor.hash,
href: anchor.href,
pathname: pathname,
fullpath: pathname + (anchor.search || '') + (anchor.hash || '')
};
}
/**
* Registry
*
* A registry is a map of HTTP verbs to route recognizers.
*/
function Registry(/* host */) {
// Herein we keep track of RouteRecognizer instances
// keyed by HTTP method. Feel free to add more as needed.
this.registry = {
this.verbs = {
GET: new RouteRecognizer(),
PUT: new RouteRecognizer(),
POST: new RouteRecognizer(),
DELETE: new RouteRecognizer(),
PATCH: new RouteRecognizer(),
HEAD: new RouteRecognizer()
HEAD: new RouteRecognizer(),
OPTIONS: new RouteRecognizer()
};
}
/**
* Hosts
*
* a map of hosts to Registries, ultimately allowing
* a per-host-and-port, per HTTP verb lookup of RouteRecognizers
*/
function Hosts() {
this._registries = {};
}
/**
* Hosts#forURL - retrieve a map of HTTP verbs to RouteRecognizers
* for a given URL
*
* @param {String} url a URL
* @return {Registry} a map of HTTP verbs to RouteRecognizers
* corresponding to the provided URL's
* hostname and port
*/
Hosts.prototype.forURL = function(url) {
var host = parseURL(url).host;
var registry = this._registries[host];
if (registry === undefined) {
registry = (this._registries[host] = new Registry(host));
}
return registry.verbs;
};
function Pretender(/* routeMap1, routeMap2, ...*/) {
this.hosts = new Hosts();
this.handlers = [];
this.handledRequests = [];
@ -26,22 +117,22 @@ function Pretender(/* routeMap1, routeMap2, ...*/){
// reference the native XMLHttpRequest object so
// it can be restored later
this._nativeXMLHttpRequest = window.XMLHttpRequest;
this._nativeXMLHttpRequest = self.XMLHttpRequest;
// capture xhr requests, channeling them into
// the route map.
window.XMLHttpRequest = interceptor(this);
self.XMLHttpRequest = interceptor(this, this._nativeXMLHttpRequest);
// "start" the server
// 'start' the server
this.running = true;
// trigger the route map DSL.
for(i=0; i < arguments.length; i++){
for (var i = 0; i < arguments.length; i++) {
this.map(arguments[i]);
}
}
function interceptor(pretender) {
function interceptor(pretender, nativeRequest) {
function FakeRequest() {
// super()
FakeXMLHttpRequest.call(this);
@ -58,81 +149,147 @@ function interceptor(pretender) {
FakeXMLHttpRequest.prototype.send.apply(this, arguments);
if (!pretender.checkPassthrough(this)) {
pretender.handleRequest(this);
}
else {
} else {
var xhr = createPassthrough(this);
xhr.send.apply(xhr, arguments);
}
};
// passthrough handling
var evts = ['load', 'error', 'timeout', 'progress', 'abort', 'readystatechange'];
var lifecycleProps = ['readyState', 'responseText', 'responseXML', 'status', 'statusText'];
function createPassthrough(fakeXHR) {
// 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', 'responseXML', 'status', 'statusText'];
var xhr = fakeXHR._passthroughRequest = new pretender._nativeXMLHttpRequest();
// listen to all events to update lifecycle properties
for (var i = 0; i < evts.length; i++) (function(evt) {
xhr['on' + evt] = function(e) {
// update lifecycle props on each event
for (var i = 0; i < lifecycleProps.length; i++) {
var prop = lifecycleProps[i];
if (xhr[prop]) {
fakeXHR[prop] = xhr[prop];
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 (fromXHR[prop]) {
toXHR[prop] = fromXHR[prop];
}
}
// fire fake events where applicable
fakeXHR.dispatchEvent(evt, e);
if (fakeXHR['on' + evt]) {
fakeXHR['on' + evt](e);
}
// 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);
};
})(evts[i]);
}
// 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);
};
}
}
xhr.open(fakeXHR.method, fakeXHR.url, fakeXHR.async, fakeXHR.username, fakeXHR.password);
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;
}
proto._passthroughCheck = function(method, arguments) {
proto._passthroughCheck = function(method, args) {
if (this._passthroughRequest) {
return this._passthroughRequest[method].apply(this._passthroughRequest, arguments);
}
return FakeXMLHttpRequest.prototype[method].apply(this, arguments);
return this._passthroughRequest[method].apply(this._passthroughRequest, args);
}
return FakeXMLHttpRequest.prototype[method].apply(this, args);
};
proto.abort = function abort() {
return this._passthroughCheck('abort', arguments);
}
};
proto.getResponseHeader = function getResponseHeader() {
return this._passthroughCheck('getResponseHeader', arguments);
}
};
proto.getAllResponseHeaders = function getAllResponseHeaders() {
return this._passthroughCheck('getAllResponseHeaders', arguments);
}
};
FakeRequest.prototype = proto;
if (nativeRequest.prototype._passthroughCheck) {
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) {
this.register(verb, path, handler, async);
return this.register(verb, path, handler, async);
};
}
function throwIfURLDetected(url){
var HTTP_REGEXP = /^https?/;
var message;
if(HTTP_REGEXP.test(url)) {
var parser = window.document.createElement('a');
parser.href = url;
message = "Pretender will not respond to requests for URLs. It is not possible to accurately simluate the browser's CSP. "+
"Remove the " + parser.protocol +"//"+ parser.hostname +" from " + url + " and try again";
throw new Error(message)
function scheduleProgressEvent(request, startTime, totalTime) {
setTimeout(function() {
if (!request.aborted && !request.status) {
var ellapsedTime = new Date().getTime() - startTime.getTime();
request.upload._progress(true, ellapsedTime, totalTime);
request._progress(true, ellapsedTime, totalTime);
scheduleProgressEvent(request, startTime, totalTime);
}
}, 50);
}
function isArray(array) {
return Object.prototype.toString.call(array) === '[object Array]';
}
var PASSTHROUGH = {};
@ -144,33 +301,40 @@ Pretender.prototype = {
'delete': verbify('DELETE'),
patch: verbify('PATCH'),
head: verbify('HEAD'),
options: verbify('OPTIONS'),
map: function(maps) {
maps.call(this);
},
register: function register(verb, path, handler, async){
register: function register(verb, url, handler, async) {
if (!handler) {
throw new Error("The function you tried passing to Pretender to handle " + verb + " " + path + " is undefined or missing.");
throw new Error('The function you tried passing to Pretender to handle ' +
verb + ' ' + url + ' is undefined or missing.');
}
handler.numberOfCalls = 0;
handler.async = async;
this.handlers.push(handler);
var registry = this.registry[verb];
registry.add([{path: path, handler: handler}]);
var registry = this.hosts.forURL(url)[verb];
registry.add([{
path: parseURL(url).fullpath,
handler: handler
}]);
return handler;
},
passthrough: PASSTHROUGH,
checkPassthrough: function(request) {
checkPassthrough: function checkPassthrough(request) {
var verb = request.method.toUpperCase();
var path = request.url;
throwIfURLDetected(path);
var path = parseURL(request.url).fullpath;
verb = verb.toUpperCase();
var recognized = this.registry[verb].recognize(path);
var recognized = this.hosts.forURL(request.url)[verb].recognize(path);
var match = recognized && recognized[0];
if (match && match.handler == PASSTHROUGH) {
if (match && match.handler === PASSTHROUGH) {
this.passthroughRequests.push(request);
this.passthroughRequest(verb, path, request);
return true;
@ -189,17 +353,34 @@ Pretender.prototype = {
var async = handler.handler.async;
this.handledRequests.push(request);
try {
var statusHeadersAndBody = handler.handler(request),
status = statusHeadersAndBody[0],
headers = this.prepareHeaders(statusHeadersAndBody[1]),
body = this.prepareBody(statusHeadersAndBody[2]),
pretender = this;
var pretender = this;
this.handleResponse(request, async, function() {
var _handleRequest = function(statusHeadersAndBody) {
if (!isArray(statusHeadersAndBody)) {
var note = 'Remember to `return [status, headers, body];` in your route handler.';
throw new Error('Nothing returned by handler for ' + path + '. ' + note);
}
var status = statusHeadersAndBody[0],
headers = pretender.prepareHeaders(statusHeadersAndBody[1]),
body = pretender.prepareBody(statusHeadersAndBody[2], headers);
pretender.handleResponse(request, async, function() {
request.respond(status, headers, body);
pretender.handledRequest(verb, path, request);
});
};
try {
var result = handler.handler(request);
if (result && typeof result.then === 'function') {
// `result` is a promise, resolve it
result.then(function(resolvedResult) {
_handleRequest(resolvedResult);
});
} else {
_handleRequest(result);
}
} catch (error) {
this.erroredRequest(verb, path, request, error);
this.resolve(request);
@ -210,9 +391,10 @@ Pretender.prototype = {
}
},
handleResponse: function handleResponse(request, strategy, callback) {
strategy = typeof strategy === 'function' ? strategy() : strategy;
var delay = typeof strategy === 'function' ? strategy() : strategy;
delay = typeof delay === 'boolean' || typeof delay === 'number' ? delay : 0;
if (strategy === false) {
if (delay === false) {
callback();
} else {
var pretender = this;
@ -221,10 +403,11 @@ Pretender.prototype = {
callback: callback
});
if (strategy !== true) {
if (delay !== true) {
scheduleProgressEvent(request, new Date(), delay);
setTimeout(function() {
pretender.resolve(request);
}, typeof strategy === 'number' ? strategy : 0);
}, delay);
}
}
},
@ -247,18 +430,20 @@ Pretender.prototype = {
},
prepareBody: function(body) { return body; },
prepareHeaders: function(headers) { return headers; },
handledRequest: function(verb, path, request) { /* no-op */},
passthroughRequest: function(verb, path, request) { /* no-op */},
unhandledRequest: function(verb, path, request) {
throw new Error("Pretender intercepted "+verb+" "+path+" but no handler was defined for this type of request");
handledRequest: function(/* verb, path, request */) { /* no-op */},
passthroughRequest: function(/* verb, path, request */) { /* no-op */},
unhandledRequest: function(verb, path/*, request */) {
throw new Error('Pretender intercepted ' + verb + ' ' +
path + ' but no handler was defined for this type of request');
},
erroredRequest: function(verb, path, request, error) {
error.message = "Pretender intercepted "+verb+" "+path+" but encountered an error: " + error.message;
error.message = 'Pretender intercepted ' + verb + ' ' +
path + ' but encountered an error: ' + error.message;
throw error;
},
_handlerFor: function(verb, path, request){
var registry = this.registry[verb];
var matches = registry.recognize(path);
_handlerFor: function(verb, url, request) {
var registry = this.hosts.forURL(url)[verb];
var matches = registry.recognize(parseURL(url).fullpath);
var match = matches ? matches[0] : null;
if (match) {
@ -269,17 +454,23 @@ Pretender.prototype = {
return match;
},
shutdown: function shutdown() {
window.XMLHttpRequest = this._nativeXMLHttpRequest;
self.XMLHttpRequest = this._nativeXMLHttpRequest;
// "stop" the server
// 'stop' the server
this.running = false;
}
};
if (isNode) {
module.exports = Pretender;
} else {
window.Pretender = Pretender;
}
Pretender.parseURL = parseURL;
Pretender.Hosts = Hosts;
Pretender.Registry = Registry;
})(window);
if (typeof module === 'object') {
module.exports = Pretender;
} else if (typeof define !== 'undefined') {
define('pretender', [], function() {
return Pretender;
});
}
self.Pretender = Pretender;
}(self));