FIX: Silence DOMException errors when running phantomjs
This commit is contained in:
parent
c6a0e74945
commit
ba8202d199
|
@ -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,113 +117,179 @@ 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 FakeRequest(){
|
||||
function interceptor(pretender, nativeRequest) {
|
||||
function FakeRequest() {
|
||||
// super()
|
||||
FakeXMLHttpRequest.call(this);
|
||||
}
|
||||
// extend
|
||||
var proto = new FakeXMLHttpRequest();
|
||||
proto.send = function send(){
|
||||
proto.send = function send() {
|
||||
if (!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 '+
|
||||
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 (!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];
|
||||
}
|
||||
}
|
||||
// fire fake events where applicable
|
||||
fakeXHR.dispatchEvent(evt, e);
|
||||
if (fakeXHR['on' + evt]) {
|
||||
fakeXHR['on' + evt](e);
|
||||
|
||||
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 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);
|
||||
xhr.timeout = fakeXHR.timeout;
|
||||
xhr.withCredentials = fakeXHR.withCredentials;
|
||||
|
||||
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 this._passthroughRequest[method].apply(this._passthroughRequest, args);
|
||||
}
|
||||
return FakeXMLHttpRequest.prototype[method].apply(this, arguments);
|
||||
}
|
||||
proto.abort = function abort(){
|
||||
return FakeXMLHttpRequest.prototype[method].apply(this, args);
|
||||
};
|
||||
|
||||
proto.abort = function abort() {
|
||||
return this._passthroughCheck('abort', arguments);
|
||||
}
|
||||
proto.getResponseHeader = function getResponseHeader(){
|
||||
};
|
||||
|
||||
proto.getResponseHeader = function getResponseHeader() {
|
||||
return this._passthroughCheck('getResponseHeader', arguments);
|
||||
}
|
||||
proto.getAllResponseHeaders = function getAllResponseHeaders(){
|
||||
};
|
||||
|
||||
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);
|
||||
function verbify(verb) {
|
||||
return function(path, handler, async) {
|
||||
return this.register(verb, path, handler, async);
|
||||
};
|
||||
}
|
||||
|
||||
function throwIfURLDetected(url){
|
||||
var HTTP_REGEXP = /^https?/;
|
||||
var 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);
|
||||
}
|
||||
|
||||
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 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'),
|
||||
map: function(maps){
|
||||
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;
|
||||
|
@ -178,7 +342,7 @@ Pretender.prototype = {
|
|||
|
||||
return false;
|
||||
},
|
||||
handleRequest: function handleRequest(request){
|
||||
handleRequest: function handleRequest(request) {
|
||||
var verb = request.method.toUpperCase();
|
||||
var path = request.url;
|
||||
|
||||
|
@ -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,15 +403,16 @@ 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);
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: function resolve(request) {
|
||||
for(var i = 0, len = this.requestReferences.length; i < len; i++) {
|
||||
for (var i = 0, len = this.requestReferences.length; i < len; i++) {
|
||||
var res = this.requestReferences[i];
|
||||
if (res.request === request) {
|
||||
res.callback();
|
||||
|
@ -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;
|
||||
erroredRequest: function(verb, path, request, error) {
|
||||
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) {
|
||||
|
@ -268,18 +453,24 @@ Pretender.prototype = {
|
|||
|
||||
return match;
|
||||
},
|
||||
shutdown: function shutdown(){
|
||||
window.XMLHttpRequest = this._nativeXMLHttpRequest;
|
||||
shutdown: function shutdown() {
|
||||
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));
|
||||
|
|
Loading…
Reference in New Issue