2a6b1234 by Gary Katsevman

Use sinon fakeXmlHttpRequest.

Change out the custom synchronous XHR to use sinon's fake XHR.
This allows to decide exactly when to respond to XHRs.
1 parent 9cd0cc95
/**
* Sinon.JS 1.9.0, 2014/03/05
*
* @author Christian Johansen (christian@cjohansen.no)
* @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
*
* (The BSD License)
*
* Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Christian Johansen nor the names of his contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/
/*global module, require, __dirname, document*/
/**
* Sinon core utilities. For internal use only.
*
* @author Christian Johansen (christian@cjohansen.no)
* @license BSD
*
* Copyright (c) 2010-2013 Christian Johansen
*/
var sinon = (function (formatio) {
var div = typeof document != "undefined" && document.createElement("div");
var hasOwn = Object.prototype.hasOwnProperty;
function isDOMNode(obj) {
var success = false;
try {
obj.appendChild(div);
success = div.parentNode == obj;
} catch (e) {
return false;
} finally {
try {
obj.removeChild(div);
} catch (e) {
// Remove failed, not much we can do about that
}
}
return success;
}
function isElement(obj) {
return div && obj && obj.nodeType === 1 && isDOMNode(obj);
}
function isFunction(obj) {
return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply);
}
function mirrorProperties(target, source) {
for (var prop in source) {
if (!hasOwn.call(target, prop)) {
target[prop] = source[prop];
}
}
}
function isRestorable (obj) {
return typeof obj === "function" && typeof obj.restore === "function" && obj.restore.sinon;
}
var sinon = {
wrapMethod: function wrapMethod(object, property, method) {
if (!object) {
throw new TypeError("Should wrap property of object");
}
if (typeof method != "function") {
throw new TypeError("Method wrapper should be function");
}
var wrappedMethod = object[property],
error;
if (!isFunction(wrappedMethod)) {
error = new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " +
property + " as function");
}
if (wrappedMethod.restore && wrappedMethod.restore.sinon) {
error = new TypeError("Attempted to wrap " + property + " which is already wrapped");
}
if (wrappedMethod.calledBefore) {
var verb = !!wrappedMethod.returns ? "stubbed" : "spied on";
error = new TypeError("Attempted to wrap " + property + " which is already " + verb);
}
if (error) {
if (wrappedMethod._stack) {
error.stack += '\n--------------\n' + wrappedMethod._stack;
}
throw error;
}
// IE 8 does not support hasOwnProperty on the window object and Firefox has a problem
// when using hasOwn.call on objects from other frames.
var owned = object.hasOwnProperty ? object.hasOwnProperty(property) : hasOwn.call(object, property);
object[property] = method;
method.displayName = property;
// Set up a stack trace which can be used later to find what line of
// code the original method was created on.
method._stack = (new Error('Stack Trace for original')).stack;
method.restore = function () {
// For prototype properties try to reset by delete first.
// If this fails (ex: localStorage on mobile safari) then force a reset
// via direct assignment.
if (!owned) {
delete object[property];
}
if (object[property] === method) {
object[property] = wrappedMethod;
}
};
method.restore.sinon = true;
mirrorProperties(method, wrappedMethod);
return method;
},
extend: function extend(target) {
for (var i = 1, l = arguments.length; i < l; i += 1) {
for (var prop in arguments[i]) {
if (arguments[i].hasOwnProperty(prop)) {
target[prop] = arguments[i][prop];
}
// DONT ENUM bug, only care about toString
if (arguments[i].hasOwnProperty("toString") &&
arguments[i].toString != target.toString) {
target.toString = arguments[i].toString;
}
}
}
return target;
},
create: function create(proto) {
var F = function () {};
F.prototype = proto;
return new F();
},
deepEqual: function deepEqual(a, b) {
if (sinon.match && sinon.match.isMatcher(a)) {
return a.test(b);
}
if (typeof a != "object" || typeof b != "object") {
return a === b;
}
if (isElement(a) || isElement(b)) {
return a === b;
}
if (a === b) {
return true;
}
if ((a === null && b !== null) || (a !== null && b === null)) {
return false;
}
if (a instanceof RegExp && b instanceof RegExp) {
return (a.source === b.source) && (a.global === b.global) &&
(a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline);
}
var aString = Object.prototype.toString.call(a);
if (aString != Object.prototype.toString.call(b)) {
return false;
}
if (aString == "[object Date]") {
return a.valueOf() === b.valueOf();
}
var prop, aLength = 0, bLength = 0;
if (aString == "[object Array]" && a.length !== b.length) {
return false;
}
for (prop in a) {
aLength += 1;
if (!deepEqual(a[prop], b[prop])) {
return false;
}
}
for (prop in b) {
bLength += 1;
}
return aLength == bLength;
},
functionName: function functionName(func) {
var name = func.displayName || func.name;
// Use function decomposition as a last resort to get function
// name. Does not rely on function decomposition to work - if it
// doesn't debugging will be slightly less informative
// (i.e. toString will say 'spy' rather than 'myFunc').
if (!name) {
var matches = func.toString().match(/function ([^\s\(]+)/);
name = matches && matches[1];
}
return name;
},
functionToString: function toString() {
if (this.getCall && this.callCount) {
var thisValue, prop, i = this.callCount;
while (i--) {
thisValue = this.getCall(i).thisValue;
for (prop in thisValue) {
if (thisValue[prop] === this) {
return prop;
}
}
}
}
return this.displayName || "sinon fake";
},
getConfig: function (custom) {
var config = {};
custom = custom || {};
var defaults = sinon.defaultConfig;
for (var prop in defaults) {
if (defaults.hasOwnProperty(prop)) {
config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop];
}
}
return config;
},
format: function (val) {
return "" + val;
},
defaultConfig: {
injectIntoThis: true,
injectInto: null,
properties: ["spy", "stub", "mock", "clock", "server", "requests"],
useFakeTimers: true,
useFakeServer: true
},
timesInWords: function timesInWords(count) {
return count == 1 && "once" ||
count == 2 && "twice" ||
count == 3 && "thrice" ||
(count || 0) + " times";
},
calledInOrder: function (spies) {
for (var i = 1, l = spies.length; i < l; i++) {
if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) {
return false;
}
}
return true;
},
orderByFirstCall: function (spies) {
return spies.sort(function (a, b) {
// uuid, won't ever be equal
var aCall = a.getCall(0);
var bCall = b.getCall(0);
var aId = aCall && aCall.callId || -1;
var bId = bCall && bCall.callId || -1;
return aId < bId ? -1 : 1;
});
},
log: function () {},
logError: function (label, err) {
var msg = label + " threw exception: ";
sinon.log(msg + "[" + err.name + "] " + err.message);
if (err.stack) { sinon.log(err.stack); }
setTimeout(function () {
err.message = msg + err.message;
throw err;
}, 0);
},
typeOf: function (value) {
if (value === null) {
return "null";
}
else if (value === undefined) {
return "undefined";
}
var string = Object.prototype.toString.call(value);
return string.substring(8, string.length - 1).toLowerCase();
},
createStubInstance: function (constructor) {
if (typeof constructor !== "function") {
throw new TypeError("The constructor should be a function.");
}
return sinon.stub(sinon.create(constructor.prototype));
},
restore: function (object) {
if (object !== null && typeof object === "object") {
for (var prop in object) {
if (isRestorable(object[prop])) {
object[prop].restore();
}
}
}
else if (isRestorable(object)) {
object.restore();
}
}
};
var isNode = typeof module !== "undefined" && module.exports;
var isAMD = typeof define === 'function' && typeof define.amd === 'object' && define.amd;
if (isAMD) {
define(function(){
return sinon;
});
} else if (isNode) {
try {
formatio = require("formatio");
} catch (e) {}
module.exports = sinon;
module.exports.spy = require("./sinon/spy");
module.exports.spyCall = require("./sinon/call");
module.exports.behavior = require("./sinon/behavior");
module.exports.stub = require("./sinon/stub");
module.exports.mock = require("./sinon/mock");
module.exports.collection = require("./sinon/collection");
module.exports.assert = require("./sinon/assert");
module.exports.sandbox = require("./sinon/sandbox");
module.exports.test = require("./sinon/test");
module.exports.testCase = require("./sinon/test_case");
module.exports.assert = require("./sinon/assert");
module.exports.match = require("./sinon/match");
}
if (formatio) {
var formatter = formatio.configure({ quoteStrings: false });
sinon.format = function () {
return formatter.ascii.apply(formatter, arguments);
};
} else if (isNode) {
try {
var util = require("util");
sinon.format = function (value) {
return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value;
};
} catch (e) {
/* Node, but no util module - would be very old, but better safe than
sorry */
}
}
return sinon;
}(typeof formatio == "object" && formatio));
/*jslint eqeqeq: false, onevar: false*/
/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
/**
* Minimal Event interface implementation
*
* Original implementation by Sven Fuchs: https://gist.github.com/995028
* Modifications and tests by Christian Johansen.
*
* @author Sven Fuchs (svenfuchs@artweb-design.de)
* @author Christian Johansen (christian@cjohansen.no)
* @license BSD
*
* Copyright (c) 2011 Sven Fuchs, Christian Johansen
*/
if (typeof sinon == "undefined") {
this.sinon = {};
}
(function () {
var push = [].push;
sinon.Event = function Event(type, bubbles, cancelable, target) {
this.initEvent(type, bubbles, cancelable, target);
};
sinon.Event.prototype = {
initEvent: function(type, bubbles, cancelable, target) {
this.type = type;
this.bubbles = bubbles;
this.cancelable = cancelable;
this.target = target;
},
stopPropagation: function () {},
preventDefault: function () {
this.defaultPrevented = true;
}
};
sinon.ProgressEvent = function ProgressEvent(type, progressEventRaw, target) {
this.initEvent(type, false, false, target);
this.loaded = progressEventRaw.loaded || null;
this.total = progressEventRaw.total || null;
};
sinon.ProgressEvent.prototype = new sinon.Event();
sinon.ProgressEvent.prototype.constructor = sinon.ProgressEvent;
sinon.CustomEvent = function CustomEvent(type, customData, target) {
this.initEvent(type, false, false, target);
this.detail = customData.detail || null;
};
sinon.CustomEvent.prototype = new sinon.Event();
sinon.CustomEvent.prototype.constructor = sinon.CustomEvent;
sinon.EventTarget = {
addEventListener: function addEventListener(event, listener) {
this.eventListeners = this.eventListeners || {};
this.eventListeners[event] = this.eventListeners[event] || [];
push.call(this.eventListeners[event], listener);
},
removeEventListener: function removeEventListener(event, listener) {
var listeners = this.eventListeners && this.eventListeners[event] || [];
for (var i = 0, l = listeners.length; i < l; ++i) {
if (listeners[i] == listener) {
return listeners.splice(i, 1);
}
}
},
dispatchEvent: function dispatchEvent(event) {
var type = event.type;
var listeners = this.eventListeners && this.eventListeners[type] || [];
for (var i = 0; i < listeners.length; i++) {
if (typeof listeners[i] == "function") {
listeners[i].call(this, event);
} else {
listeners[i].handleEvent(event);
}
}
return !!event.defaultPrevented;
}
};
}());
/**
* @depend ../../sinon.js
* @depend event.js
*/
/*jslint eqeqeq: false, onevar: false*/
/*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
/**
* Fake XMLHttpRequest object
*
* @author Christian Johansen (christian@cjohansen.no)
* @license BSD
*
* Copyright (c) 2010-2013 Christian Johansen
*/
// wrapper for global
(function(global) {
if (typeof sinon === "undefined") {
global.sinon = {};
}
var supportsProgress = typeof ProgressEvent !== "undefined";
var supportsCustomEvent = typeof CustomEvent !== "undefined";
sinon.xhr = { XMLHttpRequest: global.XMLHttpRequest };
var xhr = sinon.xhr;
xhr.GlobalXMLHttpRequest = global.XMLHttpRequest;
xhr.GlobalActiveXObject = global.ActiveXObject;
xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined";
xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined";
xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX
? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false;
xhr.supportsCORS = 'withCredentials' in (new sinon.xhr.GlobalXMLHttpRequest());
/*jsl:ignore*/
var unsafeHeaders = {
"Accept-Charset": true,
"Accept-Encoding": true,
"Connection": true,
"Content-Length": true,
"Cookie": true,
"Cookie2": true,
"Content-Transfer-Encoding": true,
"Date": true,
"Expect": true,
"Host": true,
"Keep-Alive": true,
"Referer": true,
"TE": true,
"Trailer": true,
"Transfer-Encoding": true,
"Upgrade": true,
"User-Agent": true,
"Via": true
};
/*jsl:end*/
function FakeXMLHttpRequest() {
this.readyState = FakeXMLHttpRequest.UNSENT;
this.requestHeaders = {};
this.requestBody = null;
this.status = 0;
this.statusText = "";
this.upload = new UploadProgress();
if (sinon.xhr.supportsCORS) {
this.withCredentials = false;
}
var xhr = this;
var events = ["loadstart", "load", "abort", "loadend"];
function addEventListener(eventName) {
xhr.addEventListener(eventName, function (event) {
var listener = xhr["on" + eventName];
if (listener && typeof listener == "function") {
listener.call(this, event);
}
});
}
for (var i = events.length - 1; i >= 0; i--) {
addEventListener(events[i]);
}
if (typeof FakeXMLHttpRequest.onCreate == "function") {
FakeXMLHttpRequest.onCreate(this);
}
}
// An upload object is created for each
// FakeXMLHttpRequest and allows upload
// events to be simulated using uploadProgress
// and uploadError.
function UploadProgress() {
this.eventListeners = {
"progress": [],
"load": [],
"abort": [],
"error": []
}
}
UploadProgress.prototype.addEventListener = function(event, listener) {
this.eventListeners[event].push(listener);
};
UploadProgress.prototype.removeEventListener = function(event, listener) {
var listeners = this.eventListeners[event] || [];
for (var i = 0, l = listeners.length; i < l; ++i) {
if (listeners[i] == listener) {
return listeners.splice(i, 1);
}
}
};
UploadProgress.prototype.dispatchEvent = function(event) {
var listeners = this.eventListeners[event.type] || [];
for (var i = 0, listener; (listener = listeners[i]) != null; i++) {
listener(event);
}
};
function verifyState(xhr) {
if (xhr.readyState !== FakeXMLHttpRequest.OPENED) {
throw new Error("INVALID_STATE_ERR");
}
if (xhr.sendFlag) {
throw new Error("INVALID_STATE_ERR");
}
}
// filtering to enable a white-list version of Sinon FakeXhr,
// where whitelisted requests are passed through to real XHR
function each(collection, callback) {
if (!collection) return;
for (var i = 0, l = collection.length; i < l; i += 1) {
callback(collection[i]);
}
}
function some(collection, callback) {
for (var index = 0; index < collection.length; index++) {
if(callback(collection[index]) === true) return true;
}
return false;
}
// largest arity in XHR is 5 - XHR#open
var apply = function(obj,method,args) {
switch(args.length) {
case 0: return obj[method]();
case 1: return obj[method](args[0]);
case 2: return obj[method](args[0],args[1]);
case 3: return obj[method](args[0],args[1],args[2]);
case 4: return obj[method](args[0],args[1],args[2],args[3]);
case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]);
}
};
FakeXMLHttpRequest.filters = [];
FakeXMLHttpRequest.addFilter = function(fn) {
this.filters.push(fn)
};
var IE6Re = /MSIE 6/;
FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) {
var xhr = new sinon.xhr.workingXHR();
each(["open","setRequestHeader","send","abort","getResponseHeader",
"getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"],
function(method) {
fakeXhr[method] = function() {
return apply(xhr,method,arguments);
};
});
var copyAttrs = function(args) {
each(args, function(attr) {
try {
fakeXhr[attr] = xhr[attr]
} catch(e) {
if(!IE6Re.test(navigator.userAgent)) throw e;
}
});
};
var stateChange = function() {
fakeXhr.readyState = xhr.readyState;
if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) {
copyAttrs(["status","statusText"]);
}
if(xhr.readyState >= FakeXMLHttpRequest.LOADING) {
copyAttrs(["responseText"]);
}
if(xhr.readyState === FakeXMLHttpRequest.DONE) {
copyAttrs(["responseXML"]);
}
if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr, { target: fakeXhr });
};
if(xhr.addEventListener) {
for(var event in fakeXhr.eventListeners) {
if(fakeXhr.eventListeners.hasOwnProperty(event)) {
each(fakeXhr.eventListeners[event],function(handler) {
xhr.addEventListener(event, handler);
});
}
}
xhr.addEventListener("readystatechange",stateChange);
} else {
xhr.onreadystatechange = stateChange;
}
apply(xhr,"open",xhrArgs);
};
FakeXMLHttpRequest.useFilters = false;
function verifyRequestOpened(xhr) {
if (xhr.readyState != FakeXMLHttpRequest.OPENED) {
throw new Error("INVALID_STATE_ERR - " + xhr.readyState);
}
}
function verifyRequestSent(xhr) {
if (xhr.readyState == FakeXMLHttpRequest.DONE) {
throw new Error("Request done");
}
}
function verifyHeadersReceived(xhr) {
if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) {
throw new Error("No headers received");
}
}
function verifyResponseBodyType(body) {
if (typeof body != "string") {
var error = new Error("Attempted to respond to fake XMLHttpRequest with " +
body + ", which is not a string.");
error.name = "InvalidBodyException";
throw error;
}
}
sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, {
async: true,
open: function open(method, url, async, username, password) {
this.method = method;
this.url = url;
this.async = typeof async == "boolean" ? async : true;
this.username = username;
this.password = password;
this.responseText = null;
this.responseXML = null;
this.requestHeaders = {};
this.sendFlag = false;
if(sinon.FakeXMLHttpRequest.useFilters === true) {
var xhrArgs = arguments;
var defake = some(FakeXMLHttpRequest.filters,function(filter) {
return filter.apply(this,xhrArgs)
});
if (defake) {
return sinon.FakeXMLHttpRequest.defake(this,arguments);
}
}
this.readyStateChange(FakeXMLHttpRequest.OPENED);
},
readyStateChange: function readyStateChange(state) {
this.readyState = state;
if (typeof this.onreadystatechange == "function") {
try {
this.onreadystatechange();
} catch (e) {
sinon.logError("Fake XHR onreadystatechange handler", e);
}
}
this.dispatchEvent(new sinon.Event("readystatechange"));
switch (this.readyState) {
case FakeXMLHttpRequest.DONE:
this.dispatchEvent(new sinon.Event("load", false, false, this));
this.dispatchEvent(new sinon.Event("loadend", false, false, this));
this.upload.dispatchEvent(new sinon.Event("load", false, false, this));
if (supportsProgress) {
this.upload.dispatchEvent(new sinon.ProgressEvent('progress', {loaded: 100, total: 100}));
}
break;
}
},
setRequestHeader: function setRequestHeader(header, value) {
verifyState(this);
if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) {
throw new Error("Refused to set unsafe header \"" + header + "\"");
}
if (this.requestHeaders[header]) {
this.requestHeaders[header] += "," + value;
} else {
this.requestHeaders[header] = value;
}
},
// Helps testing
setResponseHeaders: function setResponseHeaders(headers) {
verifyRequestOpened(this);
this.responseHeaders = {};
for (var header in headers) {
if (headers.hasOwnProperty(header)) {
this.responseHeaders[header] = headers[header];
}
}
if (this.async) {
this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED);
} else {
this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED;
}
},
// Currently treats ALL data as a DOMString (i.e. no Document)
send: function send(data) {
verifyState(this);
if (!/^(get|head)$/i.test(this.method)) {
if (this.requestHeaders["Content-Type"]) {
var value = this.requestHeaders["Content-Type"].split(";");
this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8";
} else {
this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8";
}
this.requestBody = data;
}
this.errorFlag = false;
this.sendFlag = this.async;
this.readyStateChange(FakeXMLHttpRequest.OPENED);
if (typeof this.onSend == "function") {
this.onSend(this);
}
this.dispatchEvent(new sinon.Event("loadstart", false, false, this));
},
abort: function abort() {
this.aborted = true;
this.responseText = null;
this.errorFlag = true;
this.requestHeaders = {};
if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) {
this.readyStateChange(sinon.FakeXMLHttpRequest.DONE);
this.sendFlag = false;
}
this.readyState = sinon.FakeXMLHttpRequest.UNSENT;
this.dispatchEvent(new sinon.Event("abort", false, false, this));
this.upload.dispatchEvent(new sinon.Event("abort", false, false, this));
if (typeof this.onerror === "function") {
this.onerror();
}
},
getResponseHeader: function getResponseHeader(header) {
if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
return null;
}
if (/^Set-Cookie2?$/i.test(header)) {
return null;
}
header = header.toLowerCase();
for (var h in this.responseHeaders) {
if (h.toLowerCase() == header) {
return this.responseHeaders[h];
}
}
return null;
},
getAllResponseHeaders: function getAllResponseHeaders() {
if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
return "";
}
var headers = "";
for (var header in this.responseHeaders) {
if (this.responseHeaders.hasOwnProperty(header) &&
!/^Set-Cookie2?$/i.test(header)) {
headers += header + ": " + this.responseHeaders[header] + "\r\n";
}
}
return headers;
},
setResponseBody: function setResponseBody(body) {
verifyRequestSent(this);
verifyHeadersReceived(this);
verifyResponseBodyType(body);
var chunkSize = this.chunkSize || 10;
var index = 0;
this.responseText = "";
do {
if (this.async) {
this.readyStateChange(FakeXMLHttpRequest.LOADING);
}
this.responseText += body.substring(index, index + chunkSize);
index += chunkSize;
} while (index < body.length);
var type = this.getResponseHeader("Content-Type");
if (this.responseText &&
(!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) {
try {
this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText);
} catch (e) {
// Unable to parse XML - no biggie
}
}
if (this.async) {
this.readyStateChange(FakeXMLHttpRequest.DONE);
} else {
this.readyState = FakeXMLHttpRequest.DONE;
}
},
respond: function respond(status, headers, body) {
this.status = typeof status == "number" ? status : 200;
this.statusText = FakeXMLHttpRequest.statusCodes[this.status];
this.setResponseHeaders(headers || {});
this.setResponseBody(body || "");
},
uploadProgress: function uploadProgress(progressEventRaw) {
if (supportsProgress) {
this.upload.dispatchEvent(new sinon.ProgressEvent("progress", progressEventRaw));
}
},
uploadError: function uploadError(error) {
if (supportsCustomEvent) {
this.upload.dispatchEvent(new sinon.CustomEvent("error", {"detail": error}));
}
}
});
sinon.extend(FakeXMLHttpRequest, {
UNSENT: 0,
OPENED: 1,
HEADERS_RECEIVED: 2,
LOADING: 3,
DONE: 4
});
// Borrowed from JSpec
FakeXMLHttpRequest.parseXML = function parseXML(text) {
var xmlDoc;
if (typeof DOMParser != "undefined") {
var parser = new DOMParser();
xmlDoc = parser.parseFromString(text, "text/xml");
} else {
xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
xmlDoc.async = "false";
xmlDoc.loadXML(text);
}
return xmlDoc;
};
FakeXMLHttpRequest.statusCodes = {
100: "Continue",
101: "Switching Protocols",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
300: "Multiple Choice",
301: "Moved Permanently",
302: "Found",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
307: "Temporary Redirect",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Requested Range Not Satisfiable",
417: "Expectation Failed",
422: "Unprocessable Entity",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Timeout",
505: "HTTP Version Not Supported"
};
sinon.useFakeXMLHttpRequest = function () {
sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) {
if (xhr.supportsXHR) {
global.XMLHttpRequest = xhr.GlobalXMLHttpRequest;
}
if (xhr.supportsActiveX) {
global.ActiveXObject = xhr.GlobalActiveXObject;
}
delete sinon.FakeXMLHttpRequest.restore;
if (keepOnCreate !== true) {
delete sinon.FakeXMLHttpRequest.onCreate;
}
};
if (xhr.supportsXHR) {
global.XMLHttpRequest = sinon.FakeXMLHttpRequest;
}
if (xhr.supportsActiveX) {
global.ActiveXObject = function ActiveXObject(objId) {
if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) {
return new sinon.FakeXMLHttpRequest();
}
return new xhr.GlobalActiveXObject(objId);
};
}
return sinon.FakeXMLHttpRequest;
};
sinon.FakeXMLHttpRequest = FakeXMLHttpRequest;
})(typeof global === "object" ? global : this);
if (typeof module !== 'undefined' && module.exports) {
module.exports = sinon;
}
/**
* @depend fake_xml_http_request.js
*/
/*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/
/*global module, require, window*/
/**
* The Sinon "server" mimics a web server that receives requests from
* sinon.FakeXMLHttpRequest and provides an API to respond to those requests,
* both synchronously and asynchronously. To respond synchronuously, canned
* answers have to be provided upfront.
*
* @author Christian Johansen (christian@cjohansen.no)
* @license BSD
*
* Copyright (c) 2010-2013 Christian Johansen
*/
if (typeof sinon == "undefined") {
var sinon = {};
}
sinon.fakeServer = (function () {
var push = [].push;
function F() {}
function create(proto) {
F.prototype = proto;
return new F();
}
function responseArray(handler) {
var response = handler;
if (Object.prototype.toString.call(handler) != "[object Array]") {
response = [200, {}, handler];
}
if (typeof response[2] != "string") {
throw new TypeError("Fake server response body should be string, but was " +
typeof response[2]);
}
return response;
}
var wloc = typeof window !== "undefined" ? window.location : {};
var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host);
function matchOne(response, reqMethod, reqUrl) {
var rmeth = response.method;
var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase();
var url = response.url;
var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl));
return matchMethod && matchUrl;
}
function match(response, request) {
var requestUrl = request.url;
if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) {
requestUrl = requestUrl.replace(rCurrLoc, "");
}
if (matchOne(response, this.getHTTPMethod(request), requestUrl)) {
if (typeof response.response == "function") {
var ru = response.url;
var args = [request].concat(ru && typeof ru.exec == "function" ? ru.exec(requestUrl).slice(1) : []);
return response.response.apply(response, args);
}
return true;
}
return false;
}
function log(response, request) {
var str;
str = "Request:\n" + sinon.format(request) + "\n\n";
str += "Response:\n" + sinon.format(response) + "\n\n";
sinon.log(str);
}
return {
create: function () {
var server = create(this);
this.xhr = sinon.useFakeXMLHttpRequest();
server.requests = [];
this.xhr.onCreate = function (xhrObj) {
server.addRequest(xhrObj);
};
return server;
},
addRequest: function addRequest(xhrObj) {
var server = this;
push.call(this.requests, xhrObj);
xhrObj.onSend = function () {
server.handleRequest(this);
if (server.autoRespond && !server.responding) {
setTimeout(function () {
server.responding = false;
server.respond();
}, server.autoRespondAfter || 10);
server.responding = true;
}
};
},
getHTTPMethod: function getHTTPMethod(request) {
if (this.fakeHTTPMethods && /post/i.test(request.method)) {
var matches = (request.requestBody || "").match(/_method=([^\b;]+)/);
return !!matches ? matches[1] : request.method;
}
return request.method;
},
handleRequest: function handleRequest(xhr) {
if (xhr.async) {
if (!this.queue) {
this.queue = [];
}
push.call(this.queue, xhr);
} else {
this.processRequest(xhr);
}
},
respondWith: function respondWith(method, url, body) {
if (arguments.length == 1 && typeof method != "function") {
this.response = responseArray(method);
return;
}
if (!this.responses) { this.responses = []; }
if (arguments.length == 1) {
body = method;
url = method = null;
}
if (arguments.length == 2) {
body = url;
url = method;
method = null;
}
push.call(this.responses, {
method: method,
url: url,
response: typeof body == "function" ? body : responseArray(body)
});
},
respond: function respond() {
if (arguments.length > 0) this.respondWith.apply(this, arguments);
var queue = this.queue || [];
var requests = queue.splice(0);
var request;
while(request = requests.shift()) {
this.processRequest(request);
}
},
processRequest: function processRequest(request) {
try {
if (request.aborted) {
return;
}
var response = this.response || [404, {}, ""];
if (this.responses) {
for (var l = this.responses.length, i = l - 1; i >= 0; i--) {
if (match.call(this, this.responses[i], request)) {
response = this.responses[i].response;
break;
}
}
}
if (request.readyState != 4) {
log(response, request);
request.respond(response[0], response[1], response[2]);
}
} catch (e) {
sinon.logError("Fake server request processing", e);
}
},
restore: function restore() {
return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments);
}
};
}());
if (typeof module !== 'undefined' && module.exports) {
module.exports = sinon;
}
/*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/
/*global module, require, window*/
/**
* Fake timer API
* setTimeout
* setInterval
* clearTimeout
* clearInterval
* tick
* reset
* Date
*
* Inspired by jsUnitMockTimeOut from JsUnit
*
* @author Christian Johansen (christian@cjohansen.no)
* @license BSD
*
* Copyright (c) 2010-2013 Christian Johansen
*/
if (typeof sinon == "undefined") {
var sinon = {};
}
(function (global) {
var id = 1;
function addTimer(args, recurring) {
if (args.length === 0) {
throw new Error("Function requires at least 1 parameter");
}
if (typeof args[0] === "undefined") {
throw new Error("Callback must be provided to timer calls");
}
var toId = id++;
var delay = args[1] || 0;
if (!this.timeouts) {
this.timeouts = {};
}
this.timeouts[toId] = {
id: toId,
func: args[0],
callAt: this.now + delay,
invokeArgs: Array.prototype.slice.call(args, 2)
};
if (recurring === true) {
this.timeouts[toId].interval = delay;
}
return toId;
}
function parseTime(str) {
if (!str) {
return 0;
}
var strings = str.split(":");
var l = strings.length, i = l;
var ms = 0, parsed;
if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
throw new Error("tick only understands numbers and 'h:m:s'");
}
while (i--) {
parsed = parseInt(strings[i], 10);
if (parsed >= 60) {
throw new Error("Invalid time " + str);
}
ms += parsed * Math.pow(60, (l - i - 1));
}
return ms * 1000;
}
function createObject(object) {
var newObject;
if (Object.create) {
newObject = Object.create(object);
} else {
var F = function () {};
F.prototype = object;
newObject = new F();
}
newObject.Date.clock = newObject;
return newObject;
}
sinon.clock = {
now: 0,
create: function create(now) {
var clock = createObject(this);
if (typeof now == "number") {
clock.now = now;
}
if (!!now && typeof now == "object") {
throw new TypeError("now should be milliseconds since UNIX epoch");
}
return clock;
},
setTimeout: function setTimeout(callback, timeout) {
return addTimer.call(this, arguments, false);
},
clearTimeout: function clearTimeout(timerId) {
if (!this.timeouts) {
this.timeouts = [];
}
if (timerId in this.timeouts) {
delete this.timeouts[timerId];
}
},
setInterval: function setInterval(callback, timeout) {
return addTimer.call(this, arguments, true);
},
clearInterval: function clearInterval(timerId) {
this.clearTimeout(timerId);
},
setImmediate: function setImmediate(callback) {
var passThruArgs = Array.prototype.slice.call(arguments, 1);
return addTimer.call(this, [callback, 0].concat(passThruArgs), false);
},
clearImmediate: function clearImmediate(timerId) {
this.clearTimeout(timerId);
},
tick: function tick(ms) {
ms = typeof ms == "number" ? ms : parseTime(ms);
var tickFrom = this.now, tickTo = this.now + ms, previous = this.now;
var timer = this.firstTimerInRange(tickFrom, tickTo);
var firstException;
while (timer && tickFrom <= tickTo) {
if (this.timeouts[timer.id]) {
tickFrom = this.now = timer.callAt;
try {
this.callTimer(timer);
} catch (e) {
firstException = firstException || e;
}
}
timer = this.firstTimerInRange(previous, tickTo);
previous = tickFrom;
}
this.now = tickTo;
if (firstException) {
throw firstException;
}
return this.now;
},
firstTimerInRange: function (from, to) {
var timer, smallest = null, originalTimer;
for (var id in this.timeouts) {
if (this.timeouts.hasOwnProperty(id)) {
if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) {
continue;
}
if (smallest === null || this.timeouts[id].callAt < smallest) {
originalTimer = this.timeouts[id];
smallest = this.timeouts[id].callAt;
timer = {
func: this.timeouts[id].func,
callAt: this.timeouts[id].callAt,
interval: this.timeouts[id].interval,
id: this.timeouts[id].id,
invokeArgs: this.timeouts[id].invokeArgs
};
}
}
}
return timer || null;
},
callTimer: function (timer) {
if (typeof timer.interval == "number") {
this.timeouts[timer.id].callAt += timer.interval;
} else {
delete this.timeouts[timer.id];
}
try {
if (typeof timer.func == "function") {
timer.func.apply(null, timer.invokeArgs);
} else {
eval(timer.func);
}
} catch (e) {
var exception = e;
}
if (!this.timeouts[timer.id]) {
if (exception) {
throw exception;
}
return;
}
if (exception) {
throw exception;
}
},
reset: function reset() {
this.timeouts = {};
},
Date: (function () {
var NativeDate = Date;
function ClockDate(year, month, date, hour, minute, second, ms) {
// Defensive and verbose to avoid potential harm in passing
// explicit undefined when user does not pass argument
switch (arguments.length) {
case 0:
return new NativeDate(ClockDate.clock.now);
case 1:
return new NativeDate(year);
case 2:
return new NativeDate(year, month);
case 3:
return new NativeDate(year, month, date);
case 4:
return new NativeDate(year, month, date, hour);
case 5:
return new NativeDate(year, month, date, hour, minute);
case 6:
return new NativeDate(year, month, date, hour, minute, second);
default:
return new NativeDate(year, month, date, hour, minute, second, ms);
}
}
return mirrorDateProperties(ClockDate, NativeDate);
}())
};
function mirrorDateProperties(target, source) {
if (source.now) {
target.now = function now() {
return target.clock.now;
};
} else {
delete target.now;
}
if (source.toSource) {
target.toSource = function toSource() {
return source.toSource();
};
} else {
delete target.toSource;
}
target.toString = function toString() {
return source.toString();
};
target.prototype = source.prototype;
target.parse = source.parse;
target.UTC = source.UTC;
target.prototype.toUTCString = source.prototype.toUTCString;
for (var prop in source) {
if (source.hasOwnProperty(prop)) {
target[prop] = source[prop];
}
}
return target;
}
var methods = ["Date", "setTimeout", "setInterval",
"clearTimeout", "clearInterval"];
if (typeof global.setImmediate !== "undefined") {
methods.push("setImmediate");
}
if (typeof global.clearImmediate !== "undefined") {
methods.push("clearImmediate");
}
function restore() {
var method;
for (var i = 0, l = this.methods.length; i < l; i++) {
method = this.methods[i];
if (global[method].hadOwnProperty) {
global[method] = this["_" + method];
} else {
try {
delete global[method];
} catch (e) {}
}
}
// Prevent multiple executions which will completely remove these props
this.methods = [];
}
function stubGlobal(method, clock) {
clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(global, method);
clock["_" + method] = global[method];
if (method == "Date") {
var date = mirrorDateProperties(clock[method], global[method]);
global[method] = date;
} else {
global[method] = function () {
return clock[method].apply(clock, arguments);
};
for (var prop in clock[method]) {
if (clock[method].hasOwnProperty(prop)) {
global[method][prop] = clock[method][prop];
}
}
}
global[method].clock = clock;
}
sinon.useFakeTimers = function useFakeTimers(now) {
var clock = sinon.clock.create(now);
clock.restore = restore;
clock.methods = Array.prototype.slice.call(arguments,
typeof now == "number" ? 1 : 0);
if (clock.methods.length === 0) {
clock.methods = methods;
}
for (var i = 0, l = clock.methods.length; i < l; i++) {
stubGlobal(clock.methods[i], clock);
}
return clock;
};
}(typeof global != "undefined" && typeof global !== "function" ? global : this));
sinon.timers = {
setTimeout: setTimeout,
clearTimeout: clearTimeout,
setImmediate: (typeof setImmediate !== "undefined" ? setImmediate : undefined),
clearImmediate: (typeof clearImmediate !== "undefined" ? clearImmediate: undefined),
setInterval: setInterval,
clearInterval: clearInterval,
Date: Date
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = sinon;
}
/**
* @depend fake_server.js
* @depend fake_timers.js
*/
/*jslint browser: true, eqeqeq: false, onevar: false*/
/*global sinon*/
/**
* Add-on for sinon.fakeServer that automatically handles a fake timer along with
* the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery
* 1.3.x, which does not use xhr object's onreadystatehandler at all - instead,
* it polls the object for completion with setInterval. Dispite the direct
* motivation, there is nothing jQuery-specific in this file, so it can be used
* in any environment where the ajax implementation depends on setInterval or
* setTimeout.
*
* @author Christian Johansen (christian@cjohansen.no)
* @license BSD
*
* Copyright (c) 2010-2013 Christian Johansen
*/
(function () {
function Server() {}
Server.prototype = sinon.fakeServer;
sinon.fakeServerWithClock = new Server();
sinon.fakeServerWithClock.addRequest = function addRequest(xhr) {
if (xhr.async) {
if (typeof setTimeout.clock == "object") {
this.clock = setTimeout.clock;
} else {
this.clock = sinon.useFakeTimers();
this.resetClock = true;
}
if (!this.longestTimeout) {
var clockSetTimeout = this.clock.setTimeout;
var clockSetInterval = this.clock.setInterval;
var server = this;
this.clock.setTimeout = function (fn, timeout) {
server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
return clockSetTimeout.apply(this, arguments);
};
this.clock.setInterval = function (fn, timeout) {
server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
return clockSetInterval.apply(this, arguments);
};
}
}
return sinon.fakeServer.addRequest.call(this, xhr);
};
sinon.fakeServerWithClock.respond = function respond() {
var returnVal = sinon.fakeServer.respond.apply(this, arguments);
if (this.clock) {
this.clock.tick(this.longestTimeout || 0);
this.longestTimeout = 0;
if (this.resetClock) {
this.clock.restore();
this.resetClock = false;
}
}
return returnVal;
};
sinon.fakeServerWithClock.restore = function restore() {
if (this.clock) {
this.clock.restore();
}
return sinon.fakeServer.restore.apply(this, arguments);
};
}());
......@@ -3,6 +3,9 @@
<head>
<meta charset="utf-8">
<title>video.js HLS Plugin Test Suite</title>
<!-- Load sinon server for fakeXHR -->
<script src="../libs/sinon/sinon-server-1.9.0.js"></script>
<!-- Load local QUnit. -->
<link rel="stylesheet" href="../libs/qunit/qunit.css" media="screen">
<script src="../libs/qunit/qunit.js"></script>
......
......@@ -29,6 +29,33 @@ var
oldSourceBuffer,
oldSupportsNativeHls,
xhrUrls,
requests,
xhr,
standardXHRResponse = function(request) {
if (!request.url) return;
var contentType = "application/json",
// contents off the global object
manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
if (manifestName) {
manifestName = manifestName[1];
} else {
request.url;
}
if (/\.m3u8?/.test(request.url)) {
contentType = 'application/vnd.apple.mpegurl';
} else if (/\.ts/.test(request.url)) {
contentType = 'video/MP2T';
}
request.response = new Uint8Array([1]).buffer;
request.respond(200,
{'Content-Type': contentType},
window.manifests[manifestName || request.url]);
},
mockSegmentParser = function(tags) {
if (tags === undefined) {
......@@ -87,25 +114,10 @@ module('HLS', {
oldSetTimeout = window.setTimeout;
// make XHRs synchronous
oldXhr = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
this.open = function(method, url) {
xhrUrls.push(url);
};
this.send = function() {
// if the request URL looks like one of the test manifests, grab the
// contents off the global object
var manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]);
if (manifestName) {
manifestName = manifestName[1];
}
this.responseText = window.manifests[manifestName || xhrUrls.slice(-1)[0]];
this.response = new Uint8Array([1]).buffer;
this.readyState = 4;
this.onreadystatechange();
};
this.abort = function() {};
xhr = sinon.useFakeXMLHttpRequest();
requests = [];
xhr.onCreate = function(xhr) {
requests.push(xhr);
};
xhrUrls = [];
},
......@@ -115,7 +127,8 @@ module('HLS', {
videojs.hls.SegmentParser = oldSegmentParser;
videojs.SourceBuffer = oldSourceBuffer;
window.setTimeout = oldSetTimeout;
window.XMLHttpRequest = oldXhr;
xhr.restore();
//window.XMLHttpRequest = oldXhr;
}
});
......@@ -130,6 +143,7 @@ test('starts playing if autoplay is specified', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
strictEqual(1, plays, 'play was called');
});
......@@ -147,6 +161,7 @@ test('loads the specified manifest URL on init', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
ok(loadedmanifest, 'loadedmanifest fires');
ok(loadedmetadata, 'loadedmetadata fires');
ok(player.hls.master, 'a master is inferred');
......@@ -171,6 +186,8 @@ test('sets the duration if one is available on the playlist', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(calls, 2, 'duration is set');
});
......@@ -187,6 +204,8 @@ test('calculates the duration if needed', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(durations.length, 2, 'duration is set');
strictEqual(durations[0],
player.hls.media.segments.length * 10,
......@@ -202,7 +221,9 @@ test('starts downloading a segment on loadedmetadata', function() {
type: 'sourceopen'
});
strictEqual(xhrUrls[1],
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(requests[1].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/00001.ts',
......@@ -215,7 +236,9 @@ test('recognizes absolute URIs and requests them unmodified', function() {
type: 'sourceopen'
});
strictEqual(xhrUrls[1],
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(requests[1].url,
'http://example.com/00001.ts',
'the first segment is requested');
});
......@@ -226,7 +249,9 @@ test('recognizes domain-relative URLs', function() {
type: 'sourceopen'
});
strictEqual(xhrUrls[1],
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(requests[1].url,
window.location.origin + '/00001.ts',
'the first segment is requested');
});
......@@ -272,13 +297,17 @@ test('downloads media playlists after loading the master', function() {
type: 'sourceopen'
});
strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested');
strictEqual(xhrUrls[1],
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
standardXHRResponse(requests[2]);
strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
strictEqual(requests[1].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/media.m3u8',
'media playlist requested');
strictEqual(xhrUrls[2],
strictEqual(requests[2].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/00001.ts',
......@@ -309,6 +338,9 @@ test('calculates the bandwidth after downloading a segment', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
ok(player.hls.bandwidth, 'bandwidth is calculated');
ok(player.hls.bandwidth > 0,
'bandwidth is positive: ' + player.hls.bandwidth);
......@@ -327,6 +359,10 @@ test('selects a playlist after segment downloads', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
standardXHRResponse(requests[2]);
strictEqual(calls, 1, 'selects after the initial segment');
player.currentTime = function() {
return 1;
......@@ -335,34 +371,27 @@ test('selects a playlist after segment downloads', function() {
return videojs.createTimeRange(0, 2);
};
player.trigger('timeupdate');
standardXHRResponse(requests[3]);
strictEqual(calls, 2, 'selects after additional segments');
});
asyncTest('moves to the next segment if there is a network error', function() {
test('moves to the next segment if there is a network error', function() {
var mediaIndex;
player.on('loadedmanifest', function() {
// fail the next segment request
window.XMLHttpRequest = function() {
this.open = function() {};
this.send = function() {
this.readyState = 4;
this.status = 400;
this.onreadystatechange();
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(mediaIndex + 1, player.hls.mediaIndex, 'media index is incremented');
start();
};
};
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
mediaIndex = player.hls.mediaIndex;
player.trigger('timeupdate');
});
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
requests[2].respond(400);
strictEqual(mediaIndex + 1, player.hls.mediaIndex, 'media index is incremented');
});
test('updates the duration after switching playlists', function() {
......@@ -387,6 +416,10 @@ test('updates the duration after switching playlists', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
standardXHRResponse(requests[2]);
standardXHRResponse(requests[3]);
ok(selectedPlaylist, 'selected playlist');
strictEqual(calls, 1, 'updates the duration');
});
......@@ -402,6 +435,8 @@ test('downloads additional playlists if required', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
// before an m3u8 is downloaded, no segments are available
player.hls.selectPlaylist = function() {
if (!called) {
......@@ -411,13 +446,15 @@ test('downloads additional playlists if required', function() {
playlist.segments = [1, 1, 1];
return playlist;
};
xhrUrls = [];
// the playlist selection is revisited after a new segment is downloaded
player.trigger('timeupdate');
strictEqual(2, xhrUrls.length, 'requests were made');
strictEqual(xhrUrls[1],
standardXHRResponse(requests[2]);
standardXHRResponse(requests[3]);
strictEqual(4, requests.length, 'requests were made');
strictEqual(requests[3].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/' +
......@@ -434,6 +471,8 @@ test('selects a playlist below the current bandwidth', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
// the default playlist has a really high bitrate
player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10;
// playlist 1 has a very low bitrate
......@@ -454,6 +493,8 @@ test('raises the minimum bitrate for a stream proportionially', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
// the default playlist's bandwidth + 10% is equal to the current bandwidth
player.hls.master.playlists[0].attributes.BANDWIDTH = 10;
player.hls.bandwidth = 11;
......@@ -474,6 +515,8 @@ test('uses the lowest bitrate if no other is suitable', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
// the lowest bitrate playlist is much greater than 1b/s
player.hls.bandwidth = 1;
playlist = player.hls.selectPlaylist();
......@@ -493,6 +536,8 @@ test('selects the correct rendition by player dimensions', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
player.width(640);
player.height(360);
player.hls.bandwidth = 3000000;
......@@ -525,9 +570,12 @@ test('does not download the next segment if the buffer is full', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
player.trigger('timeupdate');
strictEqual(xhrUrls.length, 1, 'no segment request was made');
strictEqual(requests.length, 1, 'no segment request was made');
});
test('downloads the next segment if the buffer is getting low', function() {
......@@ -535,7 +583,11 @@ test('downloads the next segment if the buffer is getting low', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(xhrUrls.length, 2, 'did not make a request');
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(requests.length, 2, 'did not make a request');
player.currentTime = function() {
return 15;
};
......@@ -544,8 +596,10 @@ test('downloads the next segment if the buffer is getting low', function() {
};
player.trigger('timeupdate');
strictEqual(xhrUrls.length, 3, 'made a request');
strictEqual(xhrUrls[2],
standardXHRResponse(requests[2]);
strictEqual(requests.length, 3, 'made a request');
strictEqual(requests[2].url,
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/00002.ts',
......@@ -557,7 +611,8 @@ test('stops downloading segments at the end of the playlist', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
xhrUrls = [];
standardXHRResponse(requests[0]);
requests = [];
player.hls.mediaIndex = 4;
player.trigger('timeupdate');
......@@ -570,6 +625,8 @@ test('only makes one segment request at a time', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
xhr.restore();
var oldXHR = window.XMLHttpRequest;
// mock out a long-running XHR
window.XMLHttpRequest = function() {
this.send = function() {};
......@@ -577,11 +634,14 @@ test('only makes one segment request at a time', function() {
openedXhrs++;
};
};
standardXHRResponse(requests[0]);
player.trigger('timeupdate');
strictEqual(1, openedXhrs, 'one XHR is made');
player.trigger('timeupdate');
strictEqual(1, openedXhrs, 'only one XHR is made');
window.XMLHttpRequest = oldXHR;
xhr = sinon.useFakeXMLHttpRequest();
});
test('uses the src attribute if no options are provided and it ends in ".m3u8"', function() {
......@@ -592,7 +652,7 @@ test('uses the src attribute if no options are provided and it ends in ".m3u8"',
type: 'sourceopen'
});
strictEqual(url, xhrUrls[0], 'currentSrc is used');
strictEqual(requests[0].url, url, 'currentSrc is used');
});
test('ignores src attribute if it doesn\'t have the "m3u8" extension', function() {
......@@ -600,27 +660,27 @@ test('ignores src attribute if it doesn\'t have the "m3u8" extension', function(
tech.src = 'basdfasdfasdfliel//.m3u9';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
strictEqual(requests.length, 0, 'no request is made');
tech.src = '';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
strictEqual(requests.length, 0, 'no request is made');
tech.src = 'http://example.com/movie.mp4?q=why.m3u8';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
strictEqual(requests.length, 0, 'no request is made');
tech.src = 'http://example.m3u8/movie.mp4';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
strictEqual(requests.length, 0, 'no request is made');
tech.src = '//example.com/movie.mp4#http://tricky.com/master.m3u8';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
strictEqual(requests.length, 0, 'no request is made');
});
test('activates if the first playable source is HLS', function() {
......@@ -651,6 +711,7 @@ test('cancels outstanding XHRs when seeking', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
player.hls.media = {
segments: [{
uri: '0.ts',
......@@ -661,27 +722,14 @@ test('cancels outstanding XHRs when seeking', function() {
}]
};
// XHR requests will never complete
window.XMLHttpRequest = function() {
this.open = function() {
opened++;
};
this.send = function() {};
this.abort = function() {
aborted = true;
this.readyState = 4;
this.status = 0;
this.onreadystatechange();
};
};
// trigger a segment download request
player.trigger('timeupdate');
opened = 0;
// attempt to seek while the download is in progress
player.trigger('seeking');
ok(aborted, 'XHR aborted');
strictEqual(1, opened, 'opened new XHR');
ok(requests[1].aborted, 'XHR aborted');
strictEqual(requests.length, 3, 'opened new XHR');
});
test('flushes the parser after each segment', function() {
......@@ -703,7 +751,9 @@ test('flushes the parser after each segment', function() {
type: 'sourceopen'
});
strictEqual(1, flushes, 'tags are flushed at the end of a segment');
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(flushes, 1, 'tags are flushed at the end of a segment');
});
test('drops tags before the target timestamp when seeking', function() {
......@@ -711,7 +761,8 @@ test('drops tags before the target timestamp when seeking', function() {
i = 10,
callbacks = [],
tags = [],
bytes = [];
bytes = [],
oldSetTimeout = window.setTimeout;
// mock out the parser and source buffer
videojs.hls.SegmentParser = mockSegmentParser(tags);
......@@ -732,6 +783,8 @@ test('drops tags before the target timestamp when seeking', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
while (callbacks.length) {
callbacks.shift()();
}
......@@ -748,19 +801,23 @@ test('drops tags before the target timestamp when seeking', function() {
return 7;
};
player.trigger('seeking');
standardXHRResponse(requests[2]);
while (callbacks.length) {
callbacks.shift()();
}
deepEqual(bytes, [7,8,9], 'three tags are appended');
window.setTimeout = oldSetTimeout;
});
test('clears pending buffer updates when seeking', function() {
var
bytes = [],
callbacks = [],
tags = [{ pts: 0, bytes: 0 }];
tags = [{ pts: 0, bytes: 0 }],
oldSetTimeout = window.setTimeout;
// mock out the parser and source buffer
videojs.hls.SegmentParser = mockSegmentParser(tags);
window.videojs.SourceBuffer = function() {
......@@ -779,18 +836,23 @@ test('clears pending buffer updates when seeking', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
// seek to 7s
tags.push({ pts: 7000, bytes: 7 });
player.currentTime = function() {
return 7;
};
player.trigger('seeking');
standardXHRResponse(requests[2]);
while (callbacks.length) {
callbacks.shift()();
}
deepEqual(bytes, ['flv', 7], 'tags queued to be appended should be cancelled');
window.setTimeout = oldSetTimeout;
});
test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
......@@ -825,23 +887,12 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
player.hls('manifest/media.m3u8');
player.on('loadedmanifest', function () {
window.XMLHttpRequest = function () {
this.open = function (method, url) {
xhrUrls.push(url);
};
this.send = function () {
this.readyState = 4;
this.status = 404;
this.onreadystatechange();
};
};
});
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
requests[1].respond(404);
ok(player.hls.error.message, 'an error message is available');
equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
});
......@@ -849,23 +900,12 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
player.hls('manifest/media.m3u8');
player.on('loadedmanifest', function () {
window.XMLHttpRequest = function () {
this.open = function (method, url) {
xhrUrls.push(url);
};
this.send = function () {
this.readyState = 4;
this.status = 500;
this.onreadystatechange();
};
};
});
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
requests[1].respond(500);
ok(player.hls.error.message, 'an error message is available');
equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED');
});
......@@ -879,7 +919,8 @@ test('has no effect if native HLS is available', function() {
});
test('reloads live playlists', function() {
var callbacks = [];
var callbacks = [],
oldSetTimeout = window.setTimeout;
// capture timeouts
window.setTimeout = function(callback, timeout) {
callbacks.push({ callback: callback, timeout: timeout });
......@@ -888,11 +929,13 @@ test('reloads live playlists', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
strictEqual(1, callbacks.length, 'refresh was scheduled');
strictEqual(player.hls.media.targetDuration * 1000,
callbacks[0].timeout,
'waited one target duration');
window.setTimeout = oldSetTimeout;
});
test('duration is Infinity for live playlists', function() {
......@@ -901,7 +944,9 @@ test('duration is Infinity for live playlists', function() {
type: 'sourceopen'
});
strictEqual(Infinity, player.duration(), 'duration is infinity');
standardXHRResponse(requests[0]);
strictEqual(player.duration(), Infinity, 'duration is infinity');
});
test('does not reload playlists with an endlist tag', function() {
......@@ -920,7 +965,8 @@ test('does not reload playlists with an endlist tag', function() {
test('reloads a live playlist after half a target duration if it has not ' +
'changed since the last request', function() {
var callbacks = [];
var callbacks = [],
oldSetTimeout = window.setTimeout;
// capture timeouts
window.setTimeout = function(callback, timeout) {
callbacks.push({ callback: callback, timeout: timeout });
......@@ -930,19 +976,25 @@ test('reloads a live playlist after half a target duration if it has not ' +
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(callbacks.length, 1, 'full-length refresh scheduled');
callbacks.pop().callback();
standardXHRResponse(requests[2]);
strictEqual(1, callbacks.length, 'half-length refresh was scheduled');
strictEqual(callbacks.length, 1, 'half-length refresh was scheduled');
strictEqual(callbacks[0].timeout,
player.hls.media.targetDuration / 2 * 1000,
'waited half a target duration');
window.setTimeout = oldSetTimeout;
});
test('merges playlist reloads', function() {
var
oldPlaylist,
callback;
callback,
oldSetTimeout = window.setTimeout;
// capture timeouts
window.setTimeout = function(cb) {
callback = cb;
......@@ -952,14 +1004,19 @@ test('merges playlist reloads', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
oldPlaylist = player.hls.media;
callback();
standardXHRResponse(requests[2]);
ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated');
window.setTimeout = oldSetTimeout;
});
test('updates the media index when a playlist reloads', function() {
var callback;
var callback,
oldSetTimeout = window.setTimeout;
window.setTimeout = function(cb) {
callback = cb;
};
......@@ -978,6 +1035,8 @@ test('updates the media index when a playlist reloads', function() {
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
// play the stream until 2.ts is playing
player.hls.mediaIndex = 3;
......@@ -991,8 +1050,10 @@ test('updates the media index when a playlist reloads', function() {
'#EXTINF:10,\n' +
'3.ts\n';
callback();
standardXHRResponse(requests[2]);
strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload');
window.setTimeout = oldSetTimeout;
});
test('mediaIndex is zero before the first segment loads', function() {
......@@ -1063,7 +1124,22 @@ test('does not reload master playlists', function() {
});
test('only reloads the active media playlist', function() {
var callbacks = [], urls = [], responses = [];
var callbacks = [],
i = 0,
filteredRequests = [],
oldSetTimeout = window.setTimeout,
customResponse;
customResponse = function(request) {
request.response = new Uint8Array([1]).buffer;
request.respond(200,
{'Content-Type': 'application/vnd.apple.mpegurl'},
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n');
};
window.setTimeout = function(callback) {
callbacks.push(callback);
};
......@@ -1072,24 +1148,9 @@ test('only reloads the active media playlist', function() {
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
window.XMLHttpRequest = function() {
this.open = function(method, url) {
urls.push(url);
};
this.send = function() {
var xhr = this;
responses.push(function() {
xhr.readyState = 4;
xhr.responseText = '#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1\n' +
'#EXTINF:10,\n' +
'1.ts\n';
xhr.response = new Uint8Array([1]).buffer;
xhr.onreadystatechange();
});
};
};
player.hls.selectPlaylist = function() {
return player.hls.master.playlists[1];
};
......@@ -1099,19 +1160,24 @@ test('only reloads the active media playlist', function() {
player.trigger('timeupdate');
strictEqual(callbacks.length, 1, 'a refresh is scheduled');
strictEqual(responses.length, 1, 'segment requested');
responses.shift()(); // segment response
responses.shift()(); // loaded switched.m3u8
standardXHRResponse(requests[2]); // segment response
customResponse(requests[3]); // loaded witched.m3u8
urls = [];
callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8
callbacks.shift()(); // refresh switched.m3u8
strictEqual(urls.length, 1, 'one refresh was made');
strictEqual(urls[0],
for (; i < requests.length; i++) {
if (/switched/.test(requests[i].url)) {
filteredRequests.push(requests[i]);
}
}
strictEqual(filteredRequests.length, 2, 'one refresh was made');
strictEqual(filteredRequests[1].url,
'http://example.com/switched.m3u8',
'refreshed the active playlist');
window.setTimeout = oldSetTimeout;
});
})(window, window.videojs);
......