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
1 /**
2 * Sinon.JS 1.9.0, 2014/03/05
3 *
4 * @author Christian Johansen (christian@cjohansen.no)
5 * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
6 *
7 * (The BSD License)
8 *
9 * Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without modification,
13 * are permitted provided that the following conditions are met:
14 *
15 * * Redistributions of source code must retain the above copyright notice,
16 * this list of conditions and the following disclaimer.
17 * * Redistributions in binary form must reproduce the above copyright notice,
18 * this list of conditions and the following disclaimer in the documentation
19 * and/or other materials provided with the distribution.
20 * * Neither the name of Christian Johansen nor the names of his contributors
21 * may be used to endorse or promote products derived from this software
22 * without specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
33 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34 */
35
36 /*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/
37 /*global module, require, __dirname, document*/
38 /**
39 * Sinon core utilities. For internal use only.
40 *
41 * @author Christian Johansen (christian@cjohansen.no)
42 * @license BSD
43 *
44 * Copyright (c) 2010-2013 Christian Johansen
45 */
46
47 var sinon = (function (formatio) {
48 var div = typeof document != "undefined" && document.createElement("div");
49 var hasOwn = Object.prototype.hasOwnProperty;
50
51 function isDOMNode(obj) {
52 var success = false;
53
54 try {
55 obj.appendChild(div);
56 success = div.parentNode == obj;
57 } catch (e) {
58 return false;
59 } finally {
60 try {
61 obj.removeChild(div);
62 } catch (e) {
63 // Remove failed, not much we can do about that
64 }
65 }
66
67 return success;
68 }
69
70 function isElement(obj) {
71 return div && obj && obj.nodeType === 1 && isDOMNode(obj);
72 }
73
74 function isFunction(obj) {
75 return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply);
76 }
77
78 function mirrorProperties(target, source) {
79 for (var prop in source) {
80 if (!hasOwn.call(target, prop)) {
81 target[prop] = source[prop];
82 }
83 }
84 }
85
86 function isRestorable (obj) {
87 return typeof obj === "function" && typeof obj.restore === "function" && obj.restore.sinon;
88 }
89
90 var sinon = {
91 wrapMethod: function wrapMethod(object, property, method) {
92 if (!object) {
93 throw new TypeError("Should wrap property of object");
94 }
95
96 if (typeof method != "function") {
97 throw new TypeError("Method wrapper should be function");
98 }
99
100 var wrappedMethod = object[property],
101 error;
102
103 if (!isFunction(wrappedMethod)) {
104 error = new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " +
105 property + " as function");
106 }
107
108 if (wrappedMethod.restore && wrappedMethod.restore.sinon) {
109 error = new TypeError("Attempted to wrap " + property + " which is already wrapped");
110 }
111
112 if (wrappedMethod.calledBefore) {
113 var verb = !!wrappedMethod.returns ? "stubbed" : "spied on";
114 error = new TypeError("Attempted to wrap " + property + " which is already " + verb);
115 }
116
117 if (error) {
118 if (wrappedMethod._stack) {
119 error.stack += '\n--------------\n' + wrappedMethod._stack;
120 }
121 throw error;
122 }
123
124 // IE 8 does not support hasOwnProperty on the window object and Firefox has a problem
125 // when using hasOwn.call on objects from other frames.
126 var owned = object.hasOwnProperty ? object.hasOwnProperty(property) : hasOwn.call(object, property);
127 object[property] = method;
128 method.displayName = property;
129 // Set up a stack trace which can be used later to find what line of
130 // code the original method was created on.
131 method._stack = (new Error('Stack Trace for original')).stack;
132
133 method.restore = function () {
134 // For prototype properties try to reset by delete first.
135 // If this fails (ex: localStorage on mobile safari) then force a reset
136 // via direct assignment.
137 if (!owned) {
138 delete object[property];
139 }
140 if (object[property] === method) {
141 object[property] = wrappedMethod;
142 }
143 };
144
145 method.restore.sinon = true;
146 mirrorProperties(method, wrappedMethod);
147
148 return method;
149 },
150
151 extend: function extend(target) {
152 for (var i = 1, l = arguments.length; i < l; i += 1) {
153 for (var prop in arguments[i]) {
154 if (arguments[i].hasOwnProperty(prop)) {
155 target[prop] = arguments[i][prop];
156 }
157
158 // DONT ENUM bug, only care about toString
159 if (arguments[i].hasOwnProperty("toString") &&
160 arguments[i].toString != target.toString) {
161 target.toString = arguments[i].toString;
162 }
163 }
164 }
165
166 return target;
167 },
168
169 create: function create(proto) {
170 var F = function () {};
171 F.prototype = proto;
172 return new F();
173 },
174
175 deepEqual: function deepEqual(a, b) {
176 if (sinon.match && sinon.match.isMatcher(a)) {
177 return a.test(b);
178 }
179 if (typeof a != "object" || typeof b != "object") {
180 return a === b;
181 }
182
183 if (isElement(a) || isElement(b)) {
184 return a === b;
185 }
186
187 if (a === b) {
188 return true;
189 }
190
191 if ((a === null && b !== null) || (a !== null && b === null)) {
192 return false;
193 }
194
195 if (a instanceof RegExp && b instanceof RegExp) {
196 return (a.source === b.source) && (a.global === b.global) &&
197 (a.ignoreCase === b.ignoreCase) && (a.multiline === b.multiline);
198 }
199
200 var aString = Object.prototype.toString.call(a);
201 if (aString != Object.prototype.toString.call(b)) {
202 return false;
203 }
204
205 if (aString == "[object Date]") {
206 return a.valueOf() === b.valueOf();
207 }
208
209 var prop, aLength = 0, bLength = 0;
210
211 if (aString == "[object Array]" && a.length !== b.length) {
212 return false;
213 }
214
215 for (prop in a) {
216 aLength += 1;
217
218 if (!deepEqual(a[prop], b[prop])) {
219 return false;
220 }
221 }
222
223 for (prop in b) {
224 bLength += 1;
225 }
226
227 return aLength == bLength;
228 },
229
230 functionName: function functionName(func) {
231 var name = func.displayName || func.name;
232
233 // Use function decomposition as a last resort to get function
234 // name. Does not rely on function decomposition to work - if it
235 // doesn't debugging will be slightly less informative
236 // (i.e. toString will say 'spy' rather than 'myFunc').
237 if (!name) {
238 var matches = func.toString().match(/function ([^\s\(]+)/);
239 name = matches && matches[1];
240 }
241
242 return name;
243 },
244
245 functionToString: function toString() {
246 if (this.getCall && this.callCount) {
247 var thisValue, prop, i = this.callCount;
248
249 while (i--) {
250 thisValue = this.getCall(i).thisValue;
251
252 for (prop in thisValue) {
253 if (thisValue[prop] === this) {
254 return prop;
255 }
256 }
257 }
258 }
259
260 return this.displayName || "sinon fake";
261 },
262
263 getConfig: function (custom) {
264 var config = {};
265 custom = custom || {};
266 var defaults = sinon.defaultConfig;
267
268 for (var prop in defaults) {
269 if (defaults.hasOwnProperty(prop)) {
270 config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop];
271 }
272 }
273
274 return config;
275 },
276
277 format: function (val) {
278 return "" + val;
279 },
280
281 defaultConfig: {
282 injectIntoThis: true,
283 injectInto: null,
284 properties: ["spy", "stub", "mock", "clock", "server", "requests"],
285 useFakeTimers: true,
286 useFakeServer: true
287 },
288
289 timesInWords: function timesInWords(count) {
290 return count == 1 && "once" ||
291 count == 2 && "twice" ||
292 count == 3 && "thrice" ||
293 (count || 0) + " times";
294 },
295
296 calledInOrder: function (spies) {
297 for (var i = 1, l = spies.length; i < l; i++) {
298 if (!spies[i - 1].calledBefore(spies[i]) || !spies[i].called) {
299 return false;
300 }
301 }
302
303 return true;
304 },
305
306 orderByFirstCall: function (spies) {
307 return spies.sort(function (a, b) {
308 // uuid, won't ever be equal
309 var aCall = a.getCall(0);
310 var bCall = b.getCall(0);
311 var aId = aCall && aCall.callId || -1;
312 var bId = bCall && bCall.callId || -1;
313
314 return aId < bId ? -1 : 1;
315 });
316 },
317
318 log: function () {},
319
320 logError: function (label, err) {
321 var msg = label + " threw exception: ";
322 sinon.log(msg + "[" + err.name + "] " + err.message);
323 if (err.stack) { sinon.log(err.stack); }
324
325 setTimeout(function () {
326 err.message = msg + err.message;
327 throw err;
328 }, 0);
329 },
330
331 typeOf: function (value) {
332 if (value === null) {
333 return "null";
334 }
335 else if (value === undefined) {
336 return "undefined";
337 }
338 var string = Object.prototype.toString.call(value);
339 return string.substring(8, string.length - 1).toLowerCase();
340 },
341
342 createStubInstance: function (constructor) {
343 if (typeof constructor !== "function") {
344 throw new TypeError("The constructor should be a function.");
345 }
346 return sinon.stub(sinon.create(constructor.prototype));
347 },
348
349 restore: function (object) {
350 if (object !== null && typeof object === "object") {
351 for (var prop in object) {
352 if (isRestorable(object[prop])) {
353 object[prop].restore();
354 }
355 }
356 }
357 else if (isRestorable(object)) {
358 object.restore();
359 }
360 }
361 };
362
363 var isNode = typeof module !== "undefined" && module.exports;
364 var isAMD = typeof define === 'function' && typeof define.amd === 'object' && define.amd;
365
366 if (isAMD) {
367 define(function(){
368 return sinon;
369 });
370 } else if (isNode) {
371 try {
372 formatio = require("formatio");
373 } catch (e) {}
374 module.exports = sinon;
375 module.exports.spy = require("./sinon/spy");
376 module.exports.spyCall = require("./sinon/call");
377 module.exports.behavior = require("./sinon/behavior");
378 module.exports.stub = require("./sinon/stub");
379 module.exports.mock = require("./sinon/mock");
380 module.exports.collection = require("./sinon/collection");
381 module.exports.assert = require("./sinon/assert");
382 module.exports.sandbox = require("./sinon/sandbox");
383 module.exports.test = require("./sinon/test");
384 module.exports.testCase = require("./sinon/test_case");
385 module.exports.assert = require("./sinon/assert");
386 module.exports.match = require("./sinon/match");
387 }
388
389 if (formatio) {
390 var formatter = formatio.configure({ quoteStrings: false });
391 sinon.format = function () {
392 return formatter.ascii.apply(formatter, arguments);
393 };
394 } else if (isNode) {
395 try {
396 var util = require("util");
397 sinon.format = function (value) {
398 return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value;
399 };
400 } catch (e) {
401 /* Node, but no util module - would be very old, but better safe than
402 sorry */
403 }
404 }
405
406 return sinon;
407 }(typeof formatio == "object" && formatio));
408
409 /*jslint eqeqeq: false, onevar: false*/
410 /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
411 /**
412 * Minimal Event interface implementation
413 *
414 * Original implementation by Sven Fuchs: https://gist.github.com/995028
415 * Modifications and tests by Christian Johansen.
416 *
417 * @author Sven Fuchs (svenfuchs@artweb-design.de)
418 * @author Christian Johansen (christian@cjohansen.no)
419 * @license BSD
420 *
421 * Copyright (c) 2011 Sven Fuchs, Christian Johansen
422 */
423
424 if (typeof sinon == "undefined") {
425 this.sinon = {};
426 }
427
428 (function () {
429 var push = [].push;
430
431 sinon.Event = function Event(type, bubbles, cancelable, target) {
432 this.initEvent(type, bubbles, cancelable, target);
433 };
434
435 sinon.Event.prototype = {
436 initEvent: function(type, bubbles, cancelable, target) {
437 this.type = type;
438 this.bubbles = bubbles;
439 this.cancelable = cancelable;
440 this.target = target;
441 },
442
443 stopPropagation: function () {},
444
445 preventDefault: function () {
446 this.defaultPrevented = true;
447 }
448 };
449
450 sinon.ProgressEvent = function ProgressEvent(type, progressEventRaw, target) {
451 this.initEvent(type, false, false, target);
452 this.loaded = progressEventRaw.loaded || null;
453 this.total = progressEventRaw.total || null;
454 };
455
456 sinon.ProgressEvent.prototype = new sinon.Event();
457
458 sinon.ProgressEvent.prototype.constructor = sinon.ProgressEvent;
459
460 sinon.CustomEvent = function CustomEvent(type, customData, target) {
461 this.initEvent(type, false, false, target);
462 this.detail = customData.detail || null;
463 };
464
465 sinon.CustomEvent.prototype = new sinon.Event();
466
467 sinon.CustomEvent.prototype.constructor = sinon.CustomEvent;
468
469 sinon.EventTarget = {
470 addEventListener: function addEventListener(event, listener) {
471 this.eventListeners = this.eventListeners || {};
472 this.eventListeners[event] = this.eventListeners[event] || [];
473 push.call(this.eventListeners[event], listener);
474 },
475
476 removeEventListener: function removeEventListener(event, listener) {
477 var listeners = this.eventListeners && this.eventListeners[event] || [];
478
479 for (var i = 0, l = listeners.length; i < l; ++i) {
480 if (listeners[i] == listener) {
481 return listeners.splice(i, 1);
482 }
483 }
484 },
485
486 dispatchEvent: function dispatchEvent(event) {
487 var type = event.type;
488 var listeners = this.eventListeners && this.eventListeners[type] || [];
489
490 for (var i = 0; i < listeners.length; i++) {
491 if (typeof listeners[i] == "function") {
492 listeners[i].call(this, event);
493 } else {
494 listeners[i].handleEvent(event);
495 }
496 }
497
498 return !!event.defaultPrevented;
499 }
500 };
501 }());
502
503 /**
504 * @depend ../../sinon.js
505 * @depend event.js
506 */
507 /*jslint eqeqeq: false, onevar: false*/
508 /*global sinon, module, require, ActiveXObject, XMLHttpRequest, DOMParser*/
509 /**
510 * Fake XMLHttpRequest object
511 *
512 * @author Christian Johansen (christian@cjohansen.no)
513 * @license BSD
514 *
515 * Copyright (c) 2010-2013 Christian Johansen
516 */
517
518 // wrapper for global
519 (function(global) {
520 if (typeof sinon === "undefined") {
521 global.sinon = {};
522 }
523
524 var supportsProgress = typeof ProgressEvent !== "undefined";
525 var supportsCustomEvent = typeof CustomEvent !== "undefined";
526 sinon.xhr = { XMLHttpRequest: global.XMLHttpRequest };
527 var xhr = sinon.xhr;
528 xhr.GlobalXMLHttpRequest = global.XMLHttpRequest;
529 xhr.GlobalActiveXObject = global.ActiveXObject;
530 xhr.supportsActiveX = typeof xhr.GlobalActiveXObject != "undefined";
531 xhr.supportsXHR = typeof xhr.GlobalXMLHttpRequest != "undefined";
532 xhr.workingXHR = xhr.supportsXHR ? xhr.GlobalXMLHttpRequest : xhr.supportsActiveX
533 ? function() { return new xhr.GlobalActiveXObject("MSXML2.XMLHTTP.3.0") } : false;
534 xhr.supportsCORS = 'withCredentials' in (new sinon.xhr.GlobalXMLHttpRequest());
535
536 /*jsl:ignore*/
537 var unsafeHeaders = {
538 "Accept-Charset": true,
539 "Accept-Encoding": true,
540 "Connection": true,
541 "Content-Length": true,
542 "Cookie": true,
543 "Cookie2": true,
544 "Content-Transfer-Encoding": true,
545 "Date": true,
546 "Expect": true,
547 "Host": true,
548 "Keep-Alive": true,
549 "Referer": true,
550 "TE": true,
551 "Trailer": true,
552 "Transfer-Encoding": true,
553 "Upgrade": true,
554 "User-Agent": true,
555 "Via": true
556 };
557 /*jsl:end*/
558
559 function FakeXMLHttpRequest() {
560 this.readyState = FakeXMLHttpRequest.UNSENT;
561 this.requestHeaders = {};
562 this.requestBody = null;
563 this.status = 0;
564 this.statusText = "";
565 this.upload = new UploadProgress();
566 if (sinon.xhr.supportsCORS) {
567 this.withCredentials = false;
568 }
569
570
571 var xhr = this;
572 var events = ["loadstart", "load", "abort", "loadend"];
573
574 function addEventListener(eventName) {
575 xhr.addEventListener(eventName, function (event) {
576 var listener = xhr["on" + eventName];
577
578 if (listener && typeof listener == "function") {
579 listener.call(this, event);
580 }
581 });
582 }
583
584 for (var i = events.length - 1; i >= 0; i--) {
585 addEventListener(events[i]);
586 }
587
588 if (typeof FakeXMLHttpRequest.onCreate == "function") {
589 FakeXMLHttpRequest.onCreate(this);
590 }
591 }
592
593 // An upload object is created for each
594 // FakeXMLHttpRequest and allows upload
595 // events to be simulated using uploadProgress
596 // and uploadError.
597 function UploadProgress() {
598 this.eventListeners = {
599 "progress": [],
600 "load": [],
601 "abort": [],
602 "error": []
603 }
604 }
605
606 UploadProgress.prototype.addEventListener = function(event, listener) {
607 this.eventListeners[event].push(listener);
608 };
609
610 UploadProgress.prototype.removeEventListener = function(event, listener) {
611 var listeners = this.eventListeners[event] || [];
612
613 for (var i = 0, l = listeners.length; i < l; ++i) {
614 if (listeners[i] == listener) {
615 return listeners.splice(i, 1);
616 }
617 }
618 };
619
620 UploadProgress.prototype.dispatchEvent = function(event) {
621 var listeners = this.eventListeners[event.type] || [];
622
623 for (var i = 0, listener; (listener = listeners[i]) != null; i++) {
624 listener(event);
625 }
626 };
627
628 function verifyState(xhr) {
629 if (xhr.readyState !== FakeXMLHttpRequest.OPENED) {
630 throw new Error("INVALID_STATE_ERR");
631 }
632
633 if (xhr.sendFlag) {
634 throw new Error("INVALID_STATE_ERR");
635 }
636 }
637
638 // filtering to enable a white-list version of Sinon FakeXhr,
639 // where whitelisted requests are passed through to real XHR
640 function each(collection, callback) {
641 if (!collection) return;
642 for (var i = 0, l = collection.length; i < l; i += 1) {
643 callback(collection[i]);
644 }
645 }
646 function some(collection, callback) {
647 for (var index = 0; index < collection.length; index++) {
648 if(callback(collection[index]) === true) return true;
649 }
650 return false;
651 }
652 // largest arity in XHR is 5 - XHR#open
653 var apply = function(obj,method,args) {
654 switch(args.length) {
655 case 0: return obj[method]();
656 case 1: return obj[method](args[0]);
657 case 2: return obj[method](args[0],args[1]);
658 case 3: return obj[method](args[0],args[1],args[2]);
659 case 4: return obj[method](args[0],args[1],args[2],args[3]);
660 case 5: return obj[method](args[0],args[1],args[2],args[3],args[4]);
661 }
662 };
663
664 FakeXMLHttpRequest.filters = [];
665 FakeXMLHttpRequest.addFilter = function(fn) {
666 this.filters.push(fn)
667 };
668 var IE6Re = /MSIE 6/;
669 FakeXMLHttpRequest.defake = function(fakeXhr,xhrArgs) {
670 var xhr = new sinon.xhr.workingXHR();
671 each(["open","setRequestHeader","send","abort","getResponseHeader",
672 "getAllResponseHeaders","addEventListener","overrideMimeType","removeEventListener"],
673 function(method) {
674 fakeXhr[method] = function() {
675 return apply(xhr,method,arguments);
676 };
677 });
678
679 var copyAttrs = function(args) {
680 each(args, function(attr) {
681 try {
682 fakeXhr[attr] = xhr[attr]
683 } catch(e) {
684 if(!IE6Re.test(navigator.userAgent)) throw e;
685 }
686 });
687 };
688
689 var stateChange = function() {
690 fakeXhr.readyState = xhr.readyState;
691 if(xhr.readyState >= FakeXMLHttpRequest.HEADERS_RECEIVED) {
692 copyAttrs(["status","statusText"]);
693 }
694 if(xhr.readyState >= FakeXMLHttpRequest.LOADING) {
695 copyAttrs(["responseText"]);
696 }
697 if(xhr.readyState === FakeXMLHttpRequest.DONE) {
698 copyAttrs(["responseXML"]);
699 }
700 if(fakeXhr.onreadystatechange) fakeXhr.onreadystatechange.call(fakeXhr, { target: fakeXhr });
701 };
702 if(xhr.addEventListener) {
703 for(var event in fakeXhr.eventListeners) {
704 if(fakeXhr.eventListeners.hasOwnProperty(event)) {
705 each(fakeXhr.eventListeners[event],function(handler) {
706 xhr.addEventListener(event, handler);
707 });
708 }
709 }
710 xhr.addEventListener("readystatechange",stateChange);
711 } else {
712 xhr.onreadystatechange = stateChange;
713 }
714 apply(xhr,"open",xhrArgs);
715 };
716 FakeXMLHttpRequest.useFilters = false;
717
718 function verifyRequestOpened(xhr) {
719 if (xhr.readyState != FakeXMLHttpRequest.OPENED) {
720 throw new Error("INVALID_STATE_ERR - " + xhr.readyState);
721 }
722 }
723
724 function verifyRequestSent(xhr) {
725 if (xhr.readyState == FakeXMLHttpRequest.DONE) {
726 throw new Error("Request done");
727 }
728 }
729
730 function verifyHeadersReceived(xhr) {
731 if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) {
732 throw new Error("No headers received");
733 }
734 }
735
736 function verifyResponseBodyType(body) {
737 if (typeof body != "string") {
738 var error = new Error("Attempted to respond to fake XMLHttpRequest with " +
739 body + ", which is not a string.");
740 error.name = "InvalidBodyException";
741 throw error;
742 }
743 }
744
745 sinon.extend(FakeXMLHttpRequest.prototype, sinon.EventTarget, {
746 async: true,
747
748 open: function open(method, url, async, username, password) {
749 this.method = method;
750 this.url = url;
751 this.async = typeof async == "boolean" ? async : true;
752 this.username = username;
753 this.password = password;
754 this.responseText = null;
755 this.responseXML = null;
756 this.requestHeaders = {};
757 this.sendFlag = false;
758 if(sinon.FakeXMLHttpRequest.useFilters === true) {
759 var xhrArgs = arguments;
760 var defake = some(FakeXMLHttpRequest.filters,function(filter) {
761 return filter.apply(this,xhrArgs)
762 });
763 if (defake) {
764 return sinon.FakeXMLHttpRequest.defake(this,arguments);
765 }
766 }
767 this.readyStateChange(FakeXMLHttpRequest.OPENED);
768 },
769
770 readyStateChange: function readyStateChange(state) {
771 this.readyState = state;
772
773 if (typeof this.onreadystatechange == "function") {
774 try {
775 this.onreadystatechange();
776 } catch (e) {
777 sinon.logError("Fake XHR onreadystatechange handler", e);
778 }
779 }
780
781 this.dispatchEvent(new sinon.Event("readystatechange"));
782
783 switch (this.readyState) {
784 case FakeXMLHttpRequest.DONE:
785 this.dispatchEvent(new sinon.Event("load", false, false, this));
786 this.dispatchEvent(new sinon.Event("loadend", false, false, this));
787 this.upload.dispatchEvent(new sinon.Event("load", false, false, this));
788 if (supportsProgress) {
789 this.upload.dispatchEvent(new sinon.ProgressEvent('progress', {loaded: 100, total: 100}));
790 }
791 break;
792 }
793 },
794
795 setRequestHeader: function setRequestHeader(header, value) {
796 verifyState(this);
797
798 if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) {
799 throw new Error("Refused to set unsafe header \"" + header + "\"");
800 }
801
802 if (this.requestHeaders[header]) {
803 this.requestHeaders[header] += "," + value;
804 } else {
805 this.requestHeaders[header] = value;
806 }
807 },
808
809 // Helps testing
810 setResponseHeaders: function setResponseHeaders(headers) {
811 verifyRequestOpened(this);
812 this.responseHeaders = {};
813
814 for (var header in headers) {
815 if (headers.hasOwnProperty(header)) {
816 this.responseHeaders[header] = headers[header];
817 }
818 }
819
820 if (this.async) {
821 this.readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED);
822 } else {
823 this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED;
824 }
825 },
826
827 // Currently treats ALL data as a DOMString (i.e. no Document)
828 send: function send(data) {
829 verifyState(this);
830
831 if (!/^(get|head)$/i.test(this.method)) {
832 if (this.requestHeaders["Content-Type"]) {
833 var value = this.requestHeaders["Content-Type"].split(";");
834 this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8";
835 } else {
836 this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8";
837 }
838
839 this.requestBody = data;
840 }
841
842 this.errorFlag = false;
843 this.sendFlag = this.async;
844 this.readyStateChange(FakeXMLHttpRequest.OPENED);
845
846 if (typeof this.onSend == "function") {
847 this.onSend(this);
848 }
849
850 this.dispatchEvent(new sinon.Event("loadstart", false, false, this));
851 },
852
853 abort: function abort() {
854 this.aborted = true;
855 this.responseText = null;
856 this.errorFlag = true;
857 this.requestHeaders = {};
858
859 if (this.readyState > sinon.FakeXMLHttpRequest.UNSENT && this.sendFlag) {
860 this.readyStateChange(sinon.FakeXMLHttpRequest.DONE);
861 this.sendFlag = false;
862 }
863
864 this.readyState = sinon.FakeXMLHttpRequest.UNSENT;
865
866 this.dispatchEvent(new sinon.Event("abort", false, false, this));
867
868 this.upload.dispatchEvent(new sinon.Event("abort", false, false, this));
869
870 if (typeof this.onerror === "function") {
871 this.onerror();
872 }
873 },
874
875 getResponseHeader: function getResponseHeader(header) {
876 if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
877 return null;
878 }
879
880 if (/^Set-Cookie2?$/i.test(header)) {
881 return null;
882 }
883
884 header = header.toLowerCase();
885
886 for (var h in this.responseHeaders) {
887 if (h.toLowerCase() == header) {
888 return this.responseHeaders[h];
889 }
890 }
891
892 return null;
893 },
894
895 getAllResponseHeaders: function getAllResponseHeaders() {
896 if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) {
897 return "";
898 }
899
900 var headers = "";
901
902 for (var header in this.responseHeaders) {
903 if (this.responseHeaders.hasOwnProperty(header) &&
904 !/^Set-Cookie2?$/i.test(header)) {
905 headers += header + ": " + this.responseHeaders[header] + "\r\n";
906 }
907 }
908
909 return headers;
910 },
911
912 setResponseBody: function setResponseBody(body) {
913 verifyRequestSent(this);
914 verifyHeadersReceived(this);
915 verifyResponseBodyType(body);
916
917 var chunkSize = this.chunkSize || 10;
918 var index = 0;
919 this.responseText = "";
920
921 do {
922 if (this.async) {
923 this.readyStateChange(FakeXMLHttpRequest.LOADING);
924 }
925
926 this.responseText += body.substring(index, index + chunkSize);
927 index += chunkSize;
928 } while (index < body.length);
929
930 var type = this.getResponseHeader("Content-Type");
931
932 if (this.responseText &&
933 (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) {
934 try {
935 this.responseXML = FakeXMLHttpRequest.parseXML(this.responseText);
936 } catch (e) {
937 // Unable to parse XML - no biggie
938 }
939 }
940
941 if (this.async) {
942 this.readyStateChange(FakeXMLHttpRequest.DONE);
943 } else {
944 this.readyState = FakeXMLHttpRequest.DONE;
945 }
946 },
947
948 respond: function respond(status, headers, body) {
949 this.status = typeof status == "number" ? status : 200;
950 this.statusText = FakeXMLHttpRequest.statusCodes[this.status];
951 this.setResponseHeaders(headers || {});
952 this.setResponseBody(body || "");
953 },
954
955 uploadProgress: function uploadProgress(progressEventRaw) {
956 if (supportsProgress) {
957 this.upload.dispatchEvent(new sinon.ProgressEvent("progress", progressEventRaw));
958 }
959 },
960
961 uploadError: function uploadError(error) {
962 if (supportsCustomEvent) {
963 this.upload.dispatchEvent(new sinon.CustomEvent("error", {"detail": error}));
964 }
965 }
966 });
967
968 sinon.extend(FakeXMLHttpRequest, {
969 UNSENT: 0,
970 OPENED: 1,
971 HEADERS_RECEIVED: 2,
972 LOADING: 3,
973 DONE: 4
974 });
975
976 // Borrowed from JSpec
977 FakeXMLHttpRequest.parseXML = function parseXML(text) {
978 var xmlDoc;
979
980 if (typeof DOMParser != "undefined") {
981 var parser = new DOMParser();
982 xmlDoc = parser.parseFromString(text, "text/xml");
983 } else {
984 xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
985 xmlDoc.async = "false";
986 xmlDoc.loadXML(text);
987 }
988
989 return xmlDoc;
990 };
991
992 FakeXMLHttpRequest.statusCodes = {
993 100: "Continue",
994 101: "Switching Protocols",
995 200: "OK",
996 201: "Created",
997 202: "Accepted",
998 203: "Non-Authoritative Information",
999 204: "No Content",
1000 205: "Reset Content",
1001 206: "Partial Content",
1002 300: "Multiple Choice",
1003 301: "Moved Permanently",
1004 302: "Found",
1005 303: "See Other",
1006 304: "Not Modified",
1007 305: "Use Proxy",
1008 307: "Temporary Redirect",
1009 400: "Bad Request",
1010 401: "Unauthorized",
1011 402: "Payment Required",
1012 403: "Forbidden",
1013 404: "Not Found",
1014 405: "Method Not Allowed",
1015 406: "Not Acceptable",
1016 407: "Proxy Authentication Required",
1017 408: "Request Timeout",
1018 409: "Conflict",
1019 410: "Gone",
1020 411: "Length Required",
1021 412: "Precondition Failed",
1022 413: "Request Entity Too Large",
1023 414: "Request-URI Too Long",
1024 415: "Unsupported Media Type",
1025 416: "Requested Range Not Satisfiable",
1026 417: "Expectation Failed",
1027 422: "Unprocessable Entity",
1028 500: "Internal Server Error",
1029 501: "Not Implemented",
1030 502: "Bad Gateway",
1031 503: "Service Unavailable",
1032 504: "Gateway Timeout",
1033 505: "HTTP Version Not Supported"
1034 };
1035
1036 sinon.useFakeXMLHttpRequest = function () {
1037 sinon.FakeXMLHttpRequest.restore = function restore(keepOnCreate) {
1038 if (xhr.supportsXHR) {
1039 global.XMLHttpRequest = xhr.GlobalXMLHttpRequest;
1040 }
1041
1042 if (xhr.supportsActiveX) {
1043 global.ActiveXObject = xhr.GlobalActiveXObject;
1044 }
1045
1046 delete sinon.FakeXMLHttpRequest.restore;
1047
1048 if (keepOnCreate !== true) {
1049 delete sinon.FakeXMLHttpRequest.onCreate;
1050 }
1051 };
1052 if (xhr.supportsXHR) {
1053 global.XMLHttpRequest = sinon.FakeXMLHttpRequest;
1054 }
1055
1056 if (xhr.supportsActiveX) {
1057 global.ActiveXObject = function ActiveXObject(objId) {
1058 if (objId == "Microsoft.XMLHTTP" || /^Msxml2\.XMLHTTP/i.test(objId)) {
1059
1060 return new sinon.FakeXMLHttpRequest();
1061 }
1062
1063 return new xhr.GlobalActiveXObject(objId);
1064 };
1065 }
1066
1067 return sinon.FakeXMLHttpRequest;
1068 };
1069
1070 sinon.FakeXMLHttpRequest = FakeXMLHttpRequest;
1071
1072 })(typeof global === "object" ? global : this);
1073
1074 if (typeof module !== 'undefined' && module.exports) {
1075 module.exports = sinon;
1076 }
1077
1078 /**
1079 * @depend fake_xml_http_request.js
1080 */
1081 /*jslint eqeqeq: false, onevar: false, regexp: false, plusplus: false*/
1082 /*global module, require, window*/
1083 /**
1084 * The Sinon "server" mimics a web server that receives requests from
1085 * sinon.FakeXMLHttpRequest and provides an API to respond to those requests,
1086 * both synchronously and asynchronously. To respond synchronuously, canned
1087 * answers have to be provided upfront.
1088 *
1089 * @author Christian Johansen (christian@cjohansen.no)
1090 * @license BSD
1091 *
1092 * Copyright (c) 2010-2013 Christian Johansen
1093 */
1094
1095 if (typeof sinon == "undefined") {
1096 var sinon = {};
1097 }
1098
1099 sinon.fakeServer = (function () {
1100 var push = [].push;
1101 function F() {}
1102
1103 function create(proto) {
1104 F.prototype = proto;
1105 return new F();
1106 }
1107
1108 function responseArray(handler) {
1109 var response = handler;
1110
1111 if (Object.prototype.toString.call(handler) != "[object Array]") {
1112 response = [200, {}, handler];
1113 }
1114
1115 if (typeof response[2] != "string") {
1116 throw new TypeError("Fake server response body should be string, but was " +
1117 typeof response[2]);
1118 }
1119
1120 return response;
1121 }
1122
1123 var wloc = typeof window !== "undefined" ? window.location : {};
1124 var rCurrLoc = new RegExp("^" + wloc.protocol + "//" + wloc.host);
1125
1126 function matchOne(response, reqMethod, reqUrl) {
1127 var rmeth = response.method;
1128 var matchMethod = !rmeth || rmeth.toLowerCase() == reqMethod.toLowerCase();
1129 var url = response.url;
1130 var matchUrl = !url || url == reqUrl || (typeof url.test == "function" && url.test(reqUrl));
1131
1132 return matchMethod && matchUrl;
1133 }
1134
1135 function match(response, request) {
1136 var requestUrl = request.url;
1137
1138 if (!/^https?:\/\//.test(requestUrl) || rCurrLoc.test(requestUrl)) {
1139 requestUrl = requestUrl.replace(rCurrLoc, "");
1140 }
1141
1142 if (matchOne(response, this.getHTTPMethod(request), requestUrl)) {
1143 if (typeof response.response == "function") {
1144 var ru = response.url;
1145 var args = [request].concat(ru && typeof ru.exec == "function" ? ru.exec(requestUrl).slice(1) : []);
1146 return response.response.apply(response, args);
1147 }
1148
1149 return true;
1150 }
1151
1152 return false;
1153 }
1154
1155 function log(response, request) {
1156 var str;
1157
1158 str = "Request:\n" + sinon.format(request) + "\n\n";
1159 str += "Response:\n" + sinon.format(response) + "\n\n";
1160
1161 sinon.log(str);
1162 }
1163
1164 return {
1165 create: function () {
1166 var server = create(this);
1167 this.xhr = sinon.useFakeXMLHttpRequest();
1168 server.requests = [];
1169
1170 this.xhr.onCreate = function (xhrObj) {
1171 server.addRequest(xhrObj);
1172 };
1173
1174 return server;
1175 },
1176
1177 addRequest: function addRequest(xhrObj) {
1178 var server = this;
1179 push.call(this.requests, xhrObj);
1180
1181 xhrObj.onSend = function () {
1182 server.handleRequest(this);
1183
1184 if (server.autoRespond && !server.responding) {
1185 setTimeout(function () {
1186 server.responding = false;
1187 server.respond();
1188 }, server.autoRespondAfter || 10);
1189
1190 server.responding = true;
1191 }
1192 };
1193 },
1194
1195 getHTTPMethod: function getHTTPMethod(request) {
1196 if (this.fakeHTTPMethods && /post/i.test(request.method)) {
1197 var matches = (request.requestBody || "").match(/_method=([^\b;]+)/);
1198 return !!matches ? matches[1] : request.method;
1199 }
1200
1201 return request.method;
1202 },
1203
1204 handleRequest: function handleRequest(xhr) {
1205 if (xhr.async) {
1206 if (!this.queue) {
1207 this.queue = [];
1208 }
1209
1210 push.call(this.queue, xhr);
1211 } else {
1212 this.processRequest(xhr);
1213 }
1214 },
1215
1216 respondWith: function respondWith(method, url, body) {
1217 if (arguments.length == 1 && typeof method != "function") {
1218 this.response = responseArray(method);
1219 return;
1220 }
1221
1222 if (!this.responses) { this.responses = []; }
1223
1224 if (arguments.length == 1) {
1225 body = method;
1226 url = method = null;
1227 }
1228
1229 if (arguments.length == 2) {
1230 body = url;
1231 url = method;
1232 method = null;
1233 }
1234
1235 push.call(this.responses, {
1236 method: method,
1237 url: url,
1238 response: typeof body == "function" ? body : responseArray(body)
1239 });
1240 },
1241
1242 respond: function respond() {
1243 if (arguments.length > 0) this.respondWith.apply(this, arguments);
1244 var queue = this.queue || [];
1245 var requests = queue.splice(0);
1246 var request;
1247
1248 while(request = requests.shift()) {
1249 this.processRequest(request);
1250 }
1251 },
1252
1253 processRequest: function processRequest(request) {
1254 try {
1255 if (request.aborted) {
1256 return;
1257 }
1258
1259 var response = this.response || [404, {}, ""];
1260
1261 if (this.responses) {
1262 for (var l = this.responses.length, i = l - 1; i >= 0; i--) {
1263 if (match.call(this, this.responses[i], request)) {
1264 response = this.responses[i].response;
1265 break;
1266 }
1267 }
1268 }
1269
1270 if (request.readyState != 4) {
1271 log(response, request);
1272
1273 request.respond(response[0], response[1], response[2]);
1274 }
1275 } catch (e) {
1276 sinon.logError("Fake server request processing", e);
1277 }
1278 },
1279
1280 restore: function restore() {
1281 return this.xhr.restore && this.xhr.restore.apply(this.xhr, arguments);
1282 }
1283 };
1284 }());
1285
1286 if (typeof module !== 'undefined' && module.exports) {
1287 module.exports = sinon;
1288 }
1289
1290 /*jslint eqeqeq: false, plusplus: false, evil: true, onevar: false, browser: true, forin: false*/
1291 /*global module, require, window*/
1292 /**
1293 * Fake timer API
1294 * setTimeout
1295 * setInterval
1296 * clearTimeout
1297 * clearInterval
1298 * tick
1299 * reset
1300 * Date
1301 *
1302 * Inspired by jsUnitMockTimeOut from JsUnit
1303 *
1304 * @author Christian Johansen (christian@cjohansen.no)
1305 * @license BSD
1306 *
1307 * Copyright (c) 2010-2013 Christian Johansen
1308 */
1309
1310 if (typeof sinon == "undefined") {
1311 var sinon = {};
1312 }
1313
1314 (function (global) {
1315 var id = 1;
1316
1317 function addTimer(args, recurring) {
1318 if (args.length === 0) {
1319 throw new Error("Function requires at least 1 parameter");
1320 }
1321
1322 if (typeof args[0] === "undefined") {
1323 throw new Error("Callback must be provided to timer calls");
1324 }
1325
1326 var toId = id++;
1327 var delay = args[1] || 0;
1328
1329 if (!this.timeouts) {
1330 this.timeouts = {};
1331 }
1332
1333 this.timeouts[toId] = {
1334 id: toId,
1335 func: args[0],
1336 callAt: this.now + delay,
1337 invokeArgs: Array.prototype.slice.call(args, 2)
1338 };
1339
1340 if (recurring === true) {
1341 this.timeouts[toId].interval = delay;
1342 }
1343
1344 return toId;
1345 }
1346
1347 function parseTime(str) {
1348 if (!str) {
1349 return 0;
1350 }
1351
1352 var strings = str.split(":");
1353 var l = strings.length, i = l;
1354 var ms = 0, parsed;
1355
1356 if (l > 3 || !/^(\d\d:){0,2}\d\d?$/.test(str)) {
1357 throw new Error("tick only understands numbers and 'h:m:s'");
1358 }
1359
1360 while (i--) {
1361 parsed = parseInt(strings[i], 10);
1362
1363 if (parsed >= 60) {
1364 throw new Error("Invalid time " + str);
1365 }
1366
1367 ms += parsed * Math.pow(60, (l - i - 1));
1368 }
1369
1370 return ms * 1000;
1371 }
1372
1373 function createObject(object) {
1374 var newObject;
1375
1376 if (Object.create) {
1377 newObject = Object.create(object);
1378 } else {
1379 var F = function () {};
1380 F.prototype = object;
1381 newObject = new F();
1382 }
1383
1384 newObject.Date.clock = newObject;
1385 return newObject;
1386 }
1387
1388 sinon.clock = {
1389 now: 0,
1390
1391 create: function create(now) {
1392 var clock = createObject(this);
1393
1394 if (typeof now == "number") {
1395 clock.now = now;
1396 }
1397
1398 if (!!now && typeof now == "object") {
1399 throw new TypeError("now should be milliseconds since UNIX epoch");
1400 }
1401
1402 return clock;
1403 },
1404
1405 setTimeout: function setTimeout(callback, timeout) {
1406 return addTimer.call(this, arguments, false);
1407 },
1408
1409 clearTimeout: function clearTimeout(timerId) {
1410 if (!this.timeouts) {
1411 this.timeouts = [];
1412 }
1413
1414 if (timerId in this.timeouts) {
1415 delete this.timeouts[timerId];
1416 }
1417 },
1418
1419 setInterval: function setInterval(callback, timeout) {
1420 return addTimer.call(this, arguments, true);
1421 },
1422
1423 clearInterval: function clearInterval(timerId) {
1424 this.clearTimeout(timerId);
1425 },
1426
1427 setImmediate: function setImmediate(callback) {
1428 var passThruArgs = Array.prototype.slice.call(arguments, 1);
1429
1430 return addTimer.call(this, [callback, 0].concat(passThruArgs), false);
1431 },
1432
1433 clearImmediate: function clearImmediate(timerId) {
1434 this.clearTimeout(timerId);
1435 },
1436
1437 tick: function tick(ms) {
1438 ms = typeof ms == "number" ? ms : parseTime(ms);
1439 var tickFrom = this.now, tickTo = this.now + ms, previous = this.now;
1440 var timer = this.firstTimerInRange(tickFrom, tickTo);
1441
1442 var firstException;
1443 while (timer && tickFrom <= tickTo) {
1444 if (this.timeouts[timer.id]) {
1445 tickFrom = this.now = timer.callAt;
1446 try {
1447 this.callTimer(timer);
1448 } catch (e) {
1449 firstException = firstException || e;
1450 }
1451 }
1452
1453 timer = this.firstTimerInRange(previous, tickTo);
1454 previous = tickFrom;
1455 }
1456
1457 this.now = tickTo;
1458
1459 if (firstException) {
1460 throw firstException;
1461 }
1462
1463 return this.now;
1464 },
1465
1466 firstTimerInRange: function (from, to) {
1467 var timer, smallest = null, originalTimer;
1468
1469 for (var id in this.timeouts) {
1470 if (this.timeouts.hasOwnProperty(id)) {
1471 if (this.timeouts[id].callAt < from || this.timeouts[id].callAt > to) {
1472 continue;
1473 }
1474
1475 if (smallest === null || this.timeouts[id].callAt < smallest) {
1476 originalTimer = this.timeouts[id];
1477 smallest = this.timeouts[id].callAt;
1478
1479 timer = {
1480 func: this.timeouts[id].func,
1481 callAt: this.timeouts[id].callAt,
1482 interval: this.timeouts[id].interval,
1483 id: this.timeouts[id].id,
1484 invokeArgs: this.timeouts[id].invokeArgs
1485 };
1486 }
1487 }
1488 }
1489
1490 return timer || null;
1491 },
1492
1493 callTimer: function (timer) {
1494 if (typeof timer.interval == "number") {
1495 this.timeouts[timer.id].callAt += timer.interval;
1496 } else {
1497 delete this.timeouts[timer.id];
1498 }
1499
1500 try {
1501 if (typeof timer.func == "function") {
1502 timer.func.apply(null, timer.invokeArgs);
1503 } else {
1504 eval(timer.func);
1505 }
1506 } catch (e) {
1507 var exception = e;
1508 }
1509
1510 if (!this.timeouts[timer.id]) {
1511 if (exception) {
1512 throw exception;
1513 }
1514 return;
1515 }
1516
1517 if (exception) {
1518 throw exception;
1519 }
1520 },
1521
1522 reset: function reset() {
1523 this.timeouts = {};
1524 },
1525
1526 Date: (function () {
1527 var NativeDate = Date;
1528
1529 function ClockDate(year, month, date, hour, minute, second, ms) {
1530 // Defensive and verbose to avoid potential harm in passing
1531 // explicit undefined when user does not pass argument
1532 switch (arguments.length) {
1533 case 0:
1534 return new NativeDate(ClockDate.clock.now);
1535 case 1:
1536 return new NativeDate(year);
1537 case 2:
1538 return new NativeDate(year, month);
1539 case 3:
1540 return new NativeDate(year, month, date);
1541 case 4:
1542 return new NativeDate(year, month, date, hour);
1543 case 5:
1544 return new NativeDate(year, month, date, hour, minute);
1545 case 6:
1546 return new NativeDate(year, month, date, hour, minute, second);
1547 default:
1548 return new NativeDate(year, month, date, hour, minute, second, ms);
1549 }
1550 }
1551
1552 return mirrorDateProperties(ClockDate, NativeDate);
1553 }())
1554 };
1555
1556 function mirrorDateProperties(target, source) {
1557 if (source.now) {
1558 target.now = function now() {
1559 return target.clock.now;
1560 };
1561 } else {
1562 delete target.now;
1563 }
1564
1565 if (source.toSource) {
1566 target.toSource = function toSource() {
1567 return source.toSource();
1568 };
1569 } else {
1570 delete target.toSource;
1571 }
1572
1573 target.toString = function toString() {
1574 return source.toString();
1575 };
1576
1577 target.prototype = source.prototype;
1578 target.parse = source.parse;
1579 target.UTC = source.UTC;
1580 target.prototype.toUTCString = source.prototype.toUTCString;
1581
1582 for (var prop in source) {
1583 if (source.hasOwnProperty(prop)) {
1584 target[prop] = source[prop];
1585 }
1586 }
1587
1588 return target;
1589 }
1590
1591 var methods = ["Date", "setTimeout", "setInterval",
1592 "clearTimeout", "clearInterval"];
1593
1594 if (typeof global.setImmediate !== "undefined") {
1595 methods.push("setImmediate");
1596 }
1597
1598 if (typeof global.clearImmediate !== "undefined") {
1599 methods.push("clearImmediate");
1600 }
1601
1602 function restore() {
1603 var method;
1604
1605 for (var i = 0, l = this.methods.length; i < l; i++) {
1606 method = this.methods[i];
1607
1608 if (global[method].hadOwnProperty) {
1609 global[method] = this["_" + method];
1610 } else {
1611 try {
1612 delete global[method];
1613 } catch (e) {}
1614 }
1615 }
1616
1617 // Prevent multiple executions which will completely remove these props
1618 this.methods = [];
1619 }
1620
1621 function stubGlobal(method, clock) {
1622 clock[method].hadOwnProperty = Object.prototype.hasOwnProperty.call(global, method);
1623 clock["_" + method] = global[method];
1624
1625 if (method == "Date") {
1626 var date = mirrorDateProperties(clock[method], global[method]);
1627 global[method] = date;
1628 } else {
1629 global[method] = function () {
1630 return clock[method].apply(clock, arguments);
1631 };
1632
1633 for (var prop in clock[method]) {
1634 if (clock[method].hasOwnProperty(prop)) {
1635 global[method][prop] = clock[method][prop];
1636 }
1637 }
1638 }
1639
1640 global[method].clock = clock;
1641 }
1642
1643 sinon.useFakeTimers = function useFakeTimers(now) {
1644 var clock = sinon.clock.create(now);
1645 clock.restore = restore;
1646 clock.methods = Array.prototype.slice.call(arguments,
1647 typeof now == "number" ? 1 : 0);
1648
1649 if (clock.methods.length === 0) {
1650 clock.methods = methods;
1651 }
1652
1653 for (var i = 0, l = clock.methods.length; i < l; i++) {
1654 stubGlobal(clock.methods[i], clock);
1655 }
1656
1657 return clock;
1658 };
1659 }(typeof global != "undefined" && typeof global !== "function" ? global : this));
1660
1661 sinon.timers = {
1662 setTimeout: setTimeout,
1663 clearTimeout: clearTimeout,
1664 setImmediate: (typeof setImmediate !== "undefined" ? setImmediate : undefined),
1665 clearImmediate: (typeof clearImmediate !== "undefined" ? clearImmediate: undefined),
1666 setInterval: setInterval,
1667 clearInterval: clearInterval,
1668 Date: Date
1669 };
1670
1671 if (typeof module !== 'undefined' && module.exports) {
1672 module.exports = sinon;
1673 }
1674
1675 /**
1676 * @depend fake_server.js
1677 * @depend fake_timers.js
1678 */
1679 /*jslint browser: true, eqeqeq: false, onevar: false*/
1680 /*global sinon*/
1681 /**
1682 * Add-on for sinon.fakeServer that automatically handles a fake timer along with
1683 * the FakeXMLHttpRequest. The direct inspiration for this add-on is jQuery
1684 * 1.3.x, which does not use xhr object's onreadystatehandler at all - instead,
1685 * it polls the object for completion with setInterval. Dispite the direct
1686 * motivation, there is nothing jQuery-specific in this file, so it can be used
1687 * in any environment where the ajax implementation depends on setInterval or
1688 * setTimeout.
1689 *
1690 * @author Christian Johansen (christian@cjohansen.no)
1691 * @license BSD
1692 *
1693 * Copyright (c) 2010-2013 Christian Johansen
1694 */
1695
1696 (function () {
1697 function Server() {}
1698 Server.prototype = sinon.fakeServer;
1699
1700 sinon.fakeServerWithClock = new Server();
1701
1702 sinon.fakeServerWithClock.addRequest = function addRequest(xhr) {
1703 if (xhr.async) {
1704 if (typeof setTimeout.clock == "object") {
1705 this.clock = setTimeout.clock;
1706 } else {
1707 this.clock = sinon.useFakeTimers();
1708 this.resetClock = true;
1709 }
1710
1711 if (!this.longestTimeout) {
1712 var clockSetTimeout = this.clock.setTimeout;
1713 var clockSetInterval = this.clock.setInterval;
1714 var server = this;
1715
1716 this.clock.setTimeout = function (fn, timeout) {
1717 server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
1718
1719 return clockSetTimeout.apply(this, arguments);
1720 };
1721
1722 this.clock.setInterval = function (fn, timeout) {
1723 server.longestTimeout = Math.max(timeout, server.longestTimeout || 0);
1724
1725 return clockSetInterval.apply(this, arguments);
1726 };
1727 }
1728 }
1729
1730 return sinon.fakeServer.addRequest.call(this, xhr);
1731 };
1732
1733 sinon.fakeServerWithClock.respond = function respond() {
1734 var returnVal = sinon.fakeServer.respond.apply(this, arguments);
1735
1736 if (this.clock) {
1737 this.clock.tick(this.longestTimeout || 0);
1738 this.longestTimeout = 0;
1739
1740 if (this.resetClock) {
1741 this.clock.restore();
1742 this.resetClock = false;
1743 }
1744 }
1745
1746 return returnVal;
1747 };
1748
1749 sinon.fakeServerWithClock.restore = function restore() {
1750 if (this.clock) {
1751 this.clock.restore();
1752 }
1753
1754 return sinon.fakeServer.restore.apply(this, arguments);
1755 };
1756 }());
1757
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
3 <head> 3 <head>
4 <meta charset="utf-8"> 4 <meta charset="utf-8">
5 <title>video.js HLS Plugin Test Suite</title> 5 <title>video.js HLS Plugin Test Suite</title>
6 <!-- Load sinon server for fakeXHR -->
7 <script src="../libs/sinon/sinon-server-1.9.0.js"></script>
8
6 <!-- Load local QUnit. --> 9 <!-- Load local QUnit. -->
7 <link rel="stylesheet" href="../libs/qunit/qunit.css" media="screen"> 10 <link rel="stylesheet" href="../libs/qunit/qunit.css" media="screen">
8 <script src="../libs/qunit/qunit.js"></script> 11 <script src="../libs/qunit/qunit.js"></script>
......
...@@ -29,6 +29,33 @@ var ...@@ -29,6 +29,33 @@ var
29 oldSourceBuffer, 29 oldSourceBuffer,
30 oldSupportsNativeHls, 30 oldSupportsNativeHls,
31 xhrUrls, 31 xhrUrls,
32 requests,
33 xhr,
34
35 standardXHRResponse = function(request) {
36 if (!request.url) return;
37
38 var contentType = "application/json",
39 // contents off the global object
40 manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
41
42 if (manifestName) {
43 manifestName = manifestName[1];
44 } else {
45 request.url;
46 }
47
48 if (/\.m3u8?/.test(request.url)) {
49 contentType = 'application/vnd.apple.mpegurl';
50 } else if (/\.ts/.test(request.url)) {
51 contentType = 'video/MP2T';
52 }
53
54 request.response = new Uint8Array([1]).buffer;
55 request.respond(200,
56 {'Content-Type': contentType},
57 window.manifests[manifestName || request.url]);
58 },
32 59
33 mockSegmentParser = function(tags) { 60 mockSegmentParser = function(tags) {
34 if (tags === undefined) { 61 if (tags === undefined) {
...@@ -87,25 +114,10 @@ module('HLS', { ...@@ -87,25 +114,10 @@ module('HLS', {
87 oldSetTimeout = window.setTimeout; 114 oldSetTimeout = window.setTimeout;
88 115
89 // make XHRs synchronous 116 // make XHRs synchronous
90 oldXhr = window.XMLHttpRequest; 117 xhr = sinon.useFakeXMLHttpRequest();
91 window.XMLHttpRequest = function() { 118 requests = [];
92 this.open = function(method, url) { 119 xhr.onCreate = function(xhr) {
93 xhrUrls.push(url); 120 requests.push(xhr);
94 };
95 this.send = function() {
96 // if the request URL looks like one of the test manifests, grab the
97 // contents off the global object
98 var manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]);
99 if (manifestName) {
100 manifestName = manifestName[1];
101 }
102 this.responseText = window.manifests[manifestName || xhrUrls.slice(-1)[0]];
103 this.response = new Uint8Array([1]).buffer;
104
105 this.readyState = 4;
106 this.onreadystatechange();
107 };
108 this.abort = function() {};
109 }; 121 };
110 xhrUrls = []; 122 xhrUrls = [];
111 }, 123 },
...@@ -115,7 +127,8 @@ module('HLS', { ...@@ -115,7 +127,8 @@ module('HLS', {
115 videojs.hls.SegmentParser = oldSegmentParser; 127 videojs.hls.SegmentParser = oldSegmentParser;
116 videojs.SourceBuffer = oldSourceBuffer; 128 videojs.SourceBuffer = oldSourceBuffer;
117 window.setTimeout = oldSetTimeout; 129 window.setTimeout = oldSetTimeout;
118 window.XMLHttpRequest = oldXhr; 130 xhr.restore();
131 //window.XMLHttpRequest = oldXhr;
119 } 132 }
120 }); 133 });
121 134
...@@ -130,6 +143,7 @@ test('starts playing if autoplay is specified', function() { ...@@ -130,6 +143,7 @@ test('starts playing if autoplay is specified', function() {
130 type: 'sourceopen' 143 type: 'sourceopen'
131 }); 144 });
132 145
146 standardXHRResponse(requests[0]);
133 strictEqual(1, plays, 'play was called'); 147 strictEqual(1, plays, 'play was called');
134 }); 148 });
135 149
...@@ -147,6 +161,7 @@ test('loads the specified manifest URL on init', function() { ...@@ -147,6 +161,7 @@ test('loads the specified manifest URL on init', function() {
147 videojs.mediaSources[player.currentSrc()].trigger({ 161 videojs.mediaSources[player.currentSrc()].trigger({
148 type: 'sourceopen' 162 type: 'sourceopen'
149 }); 163 });
164 standardXHRResponse(requests[0]);
150 ok(loadedmanifest, 'loadedmanifest fires'); 165 ok(loadedmanifest, 'loadedmanifest fires');
151 ok(loadedmetadata, 'loadedmetadata fires'); 166 ok(loadedmetadata, 'loadedmetadata fires');
152 ok(player.hls.master, 'a master is inferred'); 167 ok(player.hls.master, 'a master is inferred');
...@@ -171,6 +186,8 @@ test('sets the duration if one is available on the playlist', function() { ...@@ -171,6 +186,8 @@ test('sets the duration if one is available on the playlist', function() {
171 type: 'sourceopen' 186 type: 'sourceopen'
172 }); 187 });
173 188
189 standardXHRResponse(requests[0]);
190 standardXHRResponse(requests[1]);
174 strictEqual(calls, 2, 'duration is set'); 191 strictEqual(calls, 2, 'duration is set');
175 }); 192 });
176 193
...@@ -187,6 +204,8 @@ test('calculates the duration if needed', function() { ...@@ -187,6 +204,8 @@ test('calculates the duration if needed', function() {
187 type: 'sourceopen' 204 type: 'sourceopen'
188 }); 205 });
189 206
207 standardXHRResponse(requests[0]);
208 standardXHRResponse(requests[1]);
190 strictEqual(durations.length, 2, 'duration is set'); 209 strictEqual(durations.length, 2, 'duration is set');
191 strictEqual(durations[0], 210 strictEqual(durations[0],
192 player.hls.media.segments.length * 10, 211 player.hls.media.segments.length * 10,
...@@ -202,7 +221,9 @@ test('starts downloading a segment on loadedmetadata', function() { ...@@ -202,7 +221,9 @@ test('starts downloading a segment on loadedmetadata', function() {
202 type: 'sourceopen' 221 type: 'sourceopen'
203 }); 222 });
204 223
205 strictEqual(xhrUrls[1], 224 standardXHRResponse(requests[0]);
225 standardXHRResponse(requests[1]);
226 strictEqual(requests[1].url,
206 window.location.origin + 227 window.location.origin +
207 window.location.pathname.split('/').slice(0, -1).join('/') + 228 window.location.pathname.split('/').slice(0, -1).join('/') +
208 '/manifest/00001.ts', 229 '/manifest/00001.ts',
...@@ -215,7 +236,9 @@ test('recognizes absolute URIs and requests them unmodified', function() { ...@@ -215,7 +236,9 @@ test('recognizes absolute URIs and requests them unmodified', function() {
215 type: 'sourceopen' 236 type: 'sourceopen'
216 }); 237 });
217 238
218 strictEqual(xhrUrls[1], 239 standardXHRResponse(requests[0]);
240 standardXHRResponse(requests[1]);
241 strictEqual(requests[1].url,
219 'http://example.com/00001.ts', 242 'http://example.com/00001.ts',
220 'the first segment is requested'); 243 'the first segment is requested');
221 }); 244 });
...@@ -226,7 +249,9 @@ test('recognizes domain-relative URLs', function() { ...@@ -226,7 +249,9 @@ test('recognizes domain-relative URLs', function() {
226 type: 'sourceopen' 249 type: 'sourceopen'
227 }); 250 });
228 251
229 strictEqual(xhrUrls[1], 252 standardXHRResponse(requests[0]);
253 standardXHRResponse(requests[1]);
254 strictEqual(requests[1].url,
230 window.location.origin + '/00001.ts', 255 window.location.origin + '/00001.ts',
231 'the first segment is requested'); 256 'the first segment is requested');
232 }); 257 });
...@@ -272,13 +297,17 @@ test('downloads media playlists after loading the master', function() { ...@@ -272,13 +297,17 @@ test('downloads media playlists after loading the master', function() {
272 type: 'sourceopen' 297 type: 'sourceopen'
273 }); 298 });
274 299
275 strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested'); 300 standardXHRResponse(requests[0]);
276 strictEqual(xhrUrls[1], 301 standardXHRResponse(requests[1]);
302 standardXHRResponse(requests[2]);
303
304 strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested');
305 strictEqual(requests[1].url,
277 window.location.origin + 306 window.location.origin +
278 window.location.pathname.split('/').slice(0, -1).join('/') + 307 window.location.pathname.split('/').slice(0, -1).join('/') +
279 '/manifest/media.m3u8', 308 '/manifest/media.m3u8',
280 'media playlist requested'); 309 'media playlist requested');
281 strictEqual(xhrUrls[2], 310 strictEqual(requests[2].url,
282 window.location.origin + 311 window.location.origin +
283 window.location.pathname.split('/').slice(0, -1).join('/') + 312 window.location.pathname.split('/').slice(0, -1).join('/') +
284 '/manifest/00001.ts', 313 '/manifest/00001.ts',
...@@ -309,6 +338,9 @@ test('calculates the bandwidth after downloading a segment', function() { ...@@ -309,6 +338,9 @@ test('calculates the bandwidth after downloading a segment', function() {
309 type: 'sourceopen' 338 type: 'sourceopen'
310 }); 339 });
311 340
341 standardXHRResponse(requests[0]);
342 standardXHRResponse(requests[1]);
343
312 ok(player.hls.bandwidth, 'bandwidth is calculated'); 344 ok(player.hls.bandwidth, 'bandwidth is calculated');
313 ok(player.hls.bandwidth > 0, 345 ok(player.hls.bandwidth > 0,
314 'bandwidth is positive: ' + player.hls.bandwidth); 346 'bandwidth is positive: ' + player.hls.bandwidth);
...@@ -327,6 +359,10 @@ test('selects a playlist after segment downloads', function() { ...@@ -327,6 +359,10 @@ test('selects a playlist after segment downloads', function() {
327 type: 'sourceopen' 359 type: 'sourceopen'
328 }); 360 });
329 361
362 standardXHRResponse(requests[0]);
363 standardXHRResponse(requests[1]);
364 standardXHRResponse(requests[2]);
365
330 strictEqual(calls, 1, 'selects after the initial segment'); 366 strictEqual(calls, 1, 'selects after the initial segment');
331 player.currentTime = function() { 367 player.currentTime = function() {
332 return 1; 368 return 1;
...@@ -335,34 +371,27 @@ test('selects a playlist after segment downloads', function() { ...@@ -335,34 +371,27 @@ test('selects a playlist after segment downloads', function() {
335 return videojs.createTimeRange(0, 2); 371 return videojs.createTimeRange(0, 2);
336 }; 372 };
337 player.trigger('timeupdate'); 373 player.trigger('timeupdate');
374
375 standardXHRResponse(requests[3]);
338 strictEqual(calls, 2, 'selects after additional segments'); 376 strictEqual(calls, 2, 'selects after additional segments');
339 }); 377 });
340 378
341 asyncTest('moves to the next segment if there is a network error', function() { 379 test('moves to the next segment if there is a network error', function() {
342 var mediaIndex; 380 var mediaIndex;
343 381
344 player.on('loadedmanifest', function() {
345 // fail the next segment request
346 window.XMLHttpRequest = function() {
347 this.open = function() {};
348 this.send = function() {
349 this.readyState = 4;
350 this.status = 400;
351 this.onreadystatechange();
352
353 strictEqual(mediaIndex + 1, player.hls.mediaIndex, 'media index is incremented');
354 start();
355 };
356 };
357
358 mediaIndex = player.hls.mediaIndex;
359 player.trigger('timeupdate');
360 });
361
362 player.hls('manifest/master.m3u8'); 382 player.hls('manifest/master.m3u8');
363 videojs.mediaSources[player.currentSrc()].trigger({ 383 videojs.mediaSources[player.currentSrc()].trigger({
364 type: 'sourceopen' 384 type: 'sourceopen'
365 }); 385 });
386
387 standardXHRResponse(requests[0]);
388 standardXHRResponse(requests[1]);
389
390 mediaIndex = player.hls.mediaIndex;
391 player.trigger('timeupdate');
392
393 requests[2].respond(400);
394 strictEqual(mediaIndex + 1, player.hls.mediaIndex, 'media index is incremented');
366 }); 395 });
367 396
368 test('updates the duration after switching playlists', function() { 397 test('updates the duration after switching playlists', function() {
...@@ -387,6 +416,10 @@ test('updates the duration after switching playlists', function() { ...@@ -387,6 +416,10 @@ test('updates the duration after switching playlists', function() {
387 type: 'sourceopen' 416 type: 'sourceopen'
388 }); 417 });
389 418
419 standardXHRResponse(requests[0]);
420 standardXHRResponse(requests[1]);
421 standardXHRResponse(requests[2]);
422 standardXHRResponse(requests[3]);
390 ok(selectedPlaylist, 'selected playlist'); 423 ok(selectedPlaylist, 'selected playlist');
391 strictEqual(calls, 1, 'updates the duration'); 424 strictEqual(calls, 1, 'updates the duration');
392 }); 425 });
...@@ -402,6 +435,8 @@ test('downloads additional playlists if required', function() { ...@@ -402,6 +435,8 @@ test('downloads additional playlists if required', function() {
402 type: 'sourceopen' 435 type: 'sourceopen'
403 }); 436 });
404 437
438 standardXHRResponse(requests[0]);
439 standardXHRResponse(requests[1]);
405 // before an m3u8 is downloaded, no segments are available 440 // before an m3u8 is downloaded, no segments are available
406 player.hls.selectPlaylist = function() { 441 player.hls.selectPlaylist = function() {
407 if (!called) { 442 if (!called) {
...@@ -411,13 +446,15 @@ test('downloads additional playlists if required', function() { ...@@ -411,13 +446,15 @@ test('downloads additional playlists if required', function() {
411 playlist.segments = [1, 1, 1]; 446 playlist.segments = [1, 1, 1];
412 return playlist; 447 return playlist;
413 }; 448 };
414 xhrUrls = [];
415 449
416 // the playlist selection is revisited after a new segment is downloaded 450 // the playlist selection is revisited after a new segment is downloaded
417 player.trigger('timeupdate'); 451 player.trigger('timeupdate');
418 452
419 strictEqual(2, xhrUrls.length, 'requests were made'); 453 standardXHRResponse(requests[2]);
420 strictEqual(xhrUrls[1], 454 standardXHRResponse(requests[3]);
455
456 strictEqual(4, requests.length, 'requests were made');
457 strictEqual(requests[3].url,
421 window.location.origin + 458 window.location.origin +
422 window.location.pathname.split('/').slice(0, -1).join('/') + 459 window.location.pathname.split('/').slice(0, -1).join('/') +
423 '/manifest/' + 460 '/manifest/' +
...@@ -434,6 +471,8 @@ test('selects a playlist below the current bandwidth', function() { ...@@ -434,6 +471,8 @@ test('selects a playlist below the current bandwidth', function() {
434 type: 'sourceopen' 471 type: 'sourceopen'
435 }); 472 });
436 473
474 standardXHRResponse(requests[0]);
475
437 // the default playlist has a really high bitrate 476 // the default playlist has a really high bitrate
438 player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10; 477 player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10;
439 // playlist 1 has a very low bitrate 478 // playlist 1 has a very low bitrate
...@@ -454,6 +493,8 @@ test('raises the minimum bitrate for a stream proportionially', function() { ...@@ -454,6 +493,8 @@ test('raises the minimum bitrate for a stream proportionially', function() {
454 type: 'sourceopen' 493 type: 'sourceopen'
455 }); 494 });
456 495
496 standardXHRResponse(requests[0]);
497
457 // the default playlist's bandwidth + 10% is equal to the current bandwidth 498 // the default playlist's bandwidth + 10% is equal to the current bandwidth
458 player.hls.master.playlists[0].attributes.BANDWIDTH = 10; 499 player.hls.master.playlists[0].attributes.BANDWIDTH = 10;
459 player.hls.bandwidth = 11; 500 player.hls.bandwidth = 11;
...@@ -474,6 +515,8 @@ test('uses the lowest bitrate if no other is suitable', function() { ...@@ -474,6 +515,8 @@ test('uses the lowest bitrate if no other is suitable', function() {
474 type: 'sourceopen' 515 type: 'sourceopen'
475 }); 516 });
476 517
518 standardXHRResponse(requests[0]);
519
477 // the lowest bitrate playlist is much greater than 1b/s 520 // the lowest bitrate playlist is much greater than 1b/s
478 player.hls.bandwidth = 1; 521 player.hls.bandwidth = 1;
479 playlist = player.hls.selectPlaylist(); 522 playlist = player.hls.selectPlaylist();
...@@ -493,6 +536,8 @@ test('selects the correct rendition by player dimensions', function() { ...@@ -493,6 +536,8 @@ test('selects the correct rendition by player dimensions', function() {
493 type: 'sourceopen' 536 type: 'sourceopen'
494 }); 537 });
495 538
539 standardXHRResponse(requests[0]);
540
496 player.width(640); 541 player.width(640);
497 player.height(360); 542 player.height(360);
498 player.hls.bandwidth = 3000000; 543 player.hls.bandwidth = 3000000;
...@@ -525,9 +570,12 @@ test('does not download the next segment if the buffer is full', function() { ...@@ -525,9 +570,12 @@ test('does not download the next segment if the buffer is full', function() {
525 videojs.mediaSources[player.currentSrc()].trigger({ 570 videojs.mediaSources[player.currentSrc()].trigger({
526 type: 'sourceopen' 571 type: 'sourceopen'
527 }); 572 });
573
574 standardXHRResponse(requests[0]);
575
528 player.trigger('timeupdate'); 576 player.trigger('timeupdate');
529 577
530 strictEqual(xhrUrls.length, 1, 'no segment request was made'); 578 strictEqual(requests.length, 1, 'no segment request was made');
531 }); 579 });
532 580
533 test('downloads the next segment if the buffer is getting low', function() { 581 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() { ...@@ -535,7 +583,11 @@ test('downloads the next segment if the buffer is getting low', function() {
535 videojs.mediaSources[player.currentSrc()].trigger({ 583 videojs.mediaSources[player.currentSrc()].trigger({
536 type: 'sourceopen' 584 type: 'sourceopen'
537 }); 585 });
538 strictEqual(xhrUrls.length, 2, 'did not make a request'); 586
587 standardXHRResponse(requests[0]);
588 standardXHRResponse(requests[1]);
589
590 strictEqual(requests.length, 2, 'did not make a request');
539 player.currentTime = function() { 591 player.currentTime = function() {
540 return 15; 592 return 15;
541 }; 593 };
...@@ -544,8 +596,10 @@ test('downloads the next segment if the buffer is getting low', function() { ...@@ -544,8 +596,10 @@ test('downloads the next segment if the buffer is getting low', function() {
544 }; 596 };
545 player.trigger('timeupdate'); 597 player.trigger('timeupdate');
546 598
547 strictEqual(xhrUrls.length, 3, 'made a request'); 599 standardXHRResponse(requests[2]);
548 strictEqual(xhrUrls[2], 600
601 strictEqual(requests.length, 3, 'made a request');
602 strictEqual(requests[2].url,
549 window.location.origin + 603 window.location.origin +
550 window.location.pathname.split('/').slice(0, -1).join('/') + 604 window.location.pathname.split('/').slice(0, -1).join('/') +
551 '/manifest/00002.ts', 605 '/manifest/00002.ts',
...@@ -557,7 +611,8 @@ test('stops downloading segments at the end of the playlist', function() { ...@@ -557,7 +611,8 @@ test('stops downloading segments at the end of the playlist', function() {
557 videojs.mediaSources[player.currentSrc()].trigger({ 611 videojs.mediaSources[player.currentSrc()].trigger({
558 type: 'sourceopen' 612 type: 'sourceopen'
559 }); 613 });
560 xhrUrls = []; 614 standardXHRResponse(requests[0]);
615 requests = [];
561 player.hls.mediaIndex = 4; 616 player.hls.mediaIndex = 4;
562 player.trigger('timeupdate'); 617 player.trigger('timeupdate');
563 618
...@@ -570,6 +625,8 @@ test('only makes one segment request at a time', function() { ...@@ -570,6 +625,8 @@ test('only makes one segment request at a time', function() {
570 videojs.mediaSources[player.currentSrc()].trigger({ 625 videojs.mediaSources[player.currentSrc()].trigger({
571 type: 'sourceopen' 626 type: 'sourceopen'
572 }); 627 });
628 xhr.restore();
629 var oldXHR = window.XMLHttpRequest;
573 // mock out a long-running XHR 630 // mock out a long-running XHR
574 window.XMLHttpRequest = function() { 631 window.XMLHttpRequest = function() {
575 this.send = function() {}; 632 this.send = function() {};
...@@ -577,11 +634,14 @@ test('only makes one segment request at a time', function() { ...@@ -577,11 +634,14 @@ test('only makes one segment request at a time', function() {
577 openedXhrs++; 634 openedXhrs++;
578 }; 635 };
579 }; 636 };
637 standardXHRResponse(requests[0]);
580 player.trigger('timeupdate'); 638 player.trigger('timeupdate');
581 639
582 strictEqual(1, openedXhrs, 'one XHR is made'); 640 strictEqual(1, openedXhrs, 'one XHR is made');
583 player.trigger('timeupdate'); 641 player.trigger('timeupdate');
584 strictEqual(1, openedXhrs, 'only one XHR is made'); 642 strictEqual(1, openedXhrs, 'only one XHR is made');
643 window.XMLHttpRequest = oldXHR;
644 xhr = sinon.useFakeXMLHttpRequest();
585 }); 645 });
586 646
587 test('uses the src attribute if no options are provided and it ends in ".m3u8"', function() { 647 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"', ...@@ -592,7 +652,7 @@ test('uses the src attribute if no options are provided and it ends in ".m3u8"',
592 type: 'sourceopen' 652 type: 'sourceopen'
593 }); 653 });
594 654
595 strictEqual(url, xhrUrls[0], 'currentSrc is used'); 655 strictEqual(requests[0].url, url, 'currentSrc is used');
596 }); 656 });
597 657
598 test('ignores src attribute if it doesn\'t have the "m3u8" extension', function() { 658 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( ...@@ -600,27 +660,27 @@ test('ignores src attribute if it doesn\'t have the "m3u8" extension', function(
600 tech.src = 'basdfasdfasdfliel//.m3u9'; 660 tech.src = 'basdfasdfasdfliel//.m3u9';
601 player.hls(); 661 player.hls();
602 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 662 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
603 strictEqual(xhrUrls.length, 0, 'no request is made'); 663 strictEqual(requests.length, 0, 'no request is made');
604 664
605 tech.src = ''; 665 tech.src = '';
606 player.hls(); 666 player.hls();
607 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 667 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
608 strictEqual(xhrUrls.length, 0, 'no request is made'); 668 strictEqual(requests.length, 0, 'no request is made');
609 669
610 tech.src = 'http://example.com/movie.mp4?q=why.m3u8'; 670 tech.src = 'http://example.com/movie.mp4?q=why.m3u8';
611 player.hls(); 671 player.hls();
612 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 672 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
613 strictEqual(xhrUrls.length, 0, 'no request is made'); 673 strictEqual(requests.length, 0, 'no request is made');
614 674
615 tech.src = 'http://example.m3u8/movie.mp4'; 675 tech.src = 'http://example.m3u8/movie.mp4';
616 player.hls(); 676 player.hls();
617 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 677 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
618 strictEqual(xhrUrls.length, 0, 'no request is made'); 678 strictEqual(requests.length, 0, 'no request is made');
619 679
620 tech.src = '//example.com/movie.mp4#http://tricky.com/master.m3u8'; 680 tech.src = '//example.com/movie.mp4#http://tricky.com/master.m3u8';
621 player.hls(); 681 player.hls();
622 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created'); 682 ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
623 strictEqual(xhrUrls.length, 0, 'no request is made'); 683 strictEqual(requests.length, 0, 'no request is made');
624 }); 684 });
625 685
626 test('activates if the first playable source is HLS', function() { 686 test('activates if the first playable source is HLS', function() {
...@@ -651,6 +711,7 @@ test('cancels outstanding XHRs when seeking', function() { ...@@ -651,6 +711,7 @@ test('cancels outstanding XHRs when seeking', function() {
651 videojs.mediaSources[player.currentSrc()].trigger({ 711 videojs.mediaSources[player.currentSrc()].trigger({
652 type: 'sourceopen' 712 type: 'sourceopen'
653 }); 713 });
714 standardXHRResponse(requests[0]);
654 player.hls.media = { 715 player.hls.media = {
655 segments: [{ 716 segments: [{
656 uri: '0.ts', 717 uri: '0.ts',
...@@ -661,27 +722,14 @@ test('cancels outstanding XHRs when seeking', function() { ...@@ -661,27 +722,14 @@ test('cancels outstanding XHRs when seeking', function() {
661 }] 722 }]
662 }; 723 };
663 724
664 // XHR requests will never complete
665 window.XMLHttpRequest = function() {
666 this.open = function() {
667 opened++;
668 };
669 this.send = function() {};
670 this.abort = function() {
671 aborted = true;
672 this.readyState = 4;
673 this.status = 0;
674 this.onreadystatechange();
675 };
676 };
677 // trigger a segment download request 725 // trigger a segment download request
678 player.trigger('timeupdate'); 726 player.trigger('timeupdate');
679 opened = 0; 727 opened = 0;
680 // attempt to seek while the download is in progress 728 // attempt to seek while the download is in progress
681 player.trigger('seeking'); 729 player.trigger('seeking');
682 730
683 ok(aborted, 'XHR aborted'); 731 ok(requests[1].aborted, 'XHR aborted');
684 strictEqual(1, opened, 'opened new XHR'); 732 strictEqual(requests.length, 3, 'opened new XHR');
685 }); 733 });
686 734
687 test('flushes the parser after each segment', function() { 735 test('flushes the parser after each segment', function() {
...@@ -703,7 +751,9 @@ test('flushes the parser after each segment', function() { ...@@ -703,7 +751,9 @@ test('flushes the parser after each segment', function() {
703 type: 'sourceopen' 751 type: 'sourceopen'
704 }); 752 });
705 753
706 strictEqual(1, flushes, 'tags are flushed at the end of a segment'); 754 standardXHRResponse(requests[0]);
755 standardXHRResponse(requests[1]);
756 strictEqual(flushes, 1, 'tags are flushed at the end of a segment');
707 }); 757 });
708 758
709 test('drops tags before the target timestamp when seeking', function() { 759 test('drops tags before the target timestamp when seeking', function() {
...@@ -711,7 +761,8 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -711,7 +761,8 @@ test('drops tags before the target timestamp when seeking', function() {
711 i = 10, 761 i = 10,
712 callbacks = [], 762 callbacks = [],
713 tags = [], 763 tags = [],
714 bytes = []; 764 bytes = [],
765 oldSetTimeout = window.setTimeout;
715 766
716 // mock out the parser and source buffer 767 // mock out the parser and source buffer
717 videojs.hls.SegmentParser = mockSegmentParser(tags); 768 videojs.hls.SegmentParser = mockSegmentParser(tags);
...@@ -732,6 +783,8 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -732,6 +783,8 @@ test('drops tags before the target timestamp when seeking', function() {
732 videojs.mediaSources[player.currentSrc()].trigger({ 783 videojs.mediaSources[player.currentSrc()].trigger({
733 type: 'sourceopen' 784 type: 'sourceopen'
734 }); 785 });
786 standardXHRResponse(requests[0]);
787 standardXHRResponse(requests[1]);
735 while (callbacks.length) { 788 while (callbacks.length) {
736 callbacks.shift()(); 789 callbacks.shift()();
737 } 790 }
...@@ -748,19 +801,23 @@ test('drops tags before the target timestamp when seeking', function() { ...@@ -748,19 +801,23 @@ test('drops tags before the target timestamp when seeking', function() {
748 return 7; 801 return 7;
749 }; 802 };
750 player.trigger('seeking'); 803 player.trigger('seeking');
804 standardXHRResponse(requests[2]);
751 805
752 while (callbacks.length) { 806 while (callbacks.length) {
753 callbacks.shift()(); 807 callbacks.shift()();
754 } 808 }
755 809
756 deepEqual(bytes, [7,8,9], 'three tags are appended'); 810 deepEqual(bytes, [7,8,9], 'three tags are appended');
811 window.setTimeout = oldSetTimeout;
757 }); 812 });
758 813
759 test('clears pending buffer updates when seeking', function() { 814 test('clears pending buffer updates when seeking', function() {
760 var 815 var
761 bytes = [], 816 bytes = [],
762 callbacks = [], 817 callbacks = [],
763 tags = [{ pts: 0, bytes: 0 }]; 818 tags = [{ pts: 0, bytes: 0 }],
819 oldSetTimeout = window.setTimeout;
820
764 // mock out the parser and source buffer 821 // mock out the parser and source buffer
765 videojs.hls.SegmentParser = mockSegmentParser(tags); 822 videojs.hls.SegmentParser = mockSegmentParser(tags);
766 window.videojs.SourceBuffer = function() { 823 window.videojs.SourceBuffer = function() {
...@@ -779,18 +836,23 @@ test('clears pending buffer updates when seeking', function() { ...@@ -779,18 +836,23 @@ test('clears pending buffer updates when seeking', function() {
779 type: 'sourceopen' 836 type: 'sourceopen'
780 }); 837 });
781 838
839 standardXHRResponse(requests[0]);
840 standardXHRResponse(requests[1]);
841
782 // seek to 7s 842 // seek to 7s
783 tags.push({ pts: 7000, bytes: 7 }); 843 tags.push({ pts: 7000, bytes: 7 });
784 player.currentTime = function() { 844 player.currentTime = function() {
785 return 7; 845 return 7;
786 }; 846 };
787 player.trigger('seeking'); 847 player.trigger('seeking');
848 standardXHRResponse(requests[2]);
788 849
789 while (callbacks.length) { 850 while (callbacks.length) {
790 callbacks.shift()(); 851 callbacks.shift()();
791 } 852 }
792 853
793 deepEqual(bytes, ['flv', 7], 'tags queued to be appended should be cancelled'); 854 deepEqual(bytes, ['flv', 7], 'tags queued to be appended should be cancelled');
855 window.setTimeout = oldSetTimeout;
794 }); 856 });
795 857
796 test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { 858 test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
...@@ -825,23 +887,12 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() { ...@@ -825,23 +887,12 @@ test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
825 test('segment 404 should trigger MEDIA_ERR_NETWORK', function () { 887 test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
826 player.hls('manifest/media.m3u8'); 888 player.hls('manifest/media.m3u8');
827 889
828 player.on('loadedmanifest', function () {
829 window.XMLHttpRequest = function () {
830 this.open = function (method, url) {
831 xhrUrls.push(url);
832 };
833 this.send = function () {
834 this.readyState = 4;
835 this.status = 404;
836 this.onreadystatechange();
837 };
838 };
839 });
840
841 videojs.mediaSources[player.currentSrc()].trigger({ 890 videojs.mediaSources[player.currentSrc()].trigger({
842 type: 'sourceopen' 891 type: 'sourceopen'
843 }); 892 });
844 893
894 standardXHRResponse(requests[0]);
895 requests[1].respond(404);
845 ok(player.hls.error.message, 'an error message is available'); 896 ok(player.hls.error.message, 'an error message is available');
846 equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK'); 897 equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
847 }); 898 });
...@@ -849,23 +900,12 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () { ...@@ -849,23 +900,12 @@ test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
849 test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { 900 test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
850 player.hls('manifest/media.m3u8'); 901 player.hls('manifest/media.m3u8');
851 902
852 player.on('loadedmanifest', function () {
853 window.XMLHttpRequest = function () {
854 this.open = function (method, url) {
855 xhrUrls.push(url);
856 };
857 this.send = function () {
858 this.readyState = 4;
859 this.status = 500;
860 this.onreadystatechange();
861 };
862 };
863 });
864
865 videojs.mediaSources[player.currentSrc()].trigger({ 903 videojs.mediaSources[player.currentSrc()].trigger({
866 type: 'sourceopen' 904 type: 'sourceopen'
867 }); 905 });
868 906
907 standardXHRResponse(requests[0]);
908 requests[1].respond(500);
869 ok(player.hls.error.message, 'an error message is available'); 909 ok(player.hls.error.message, 'an error message is available');
870 equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED'); 910 equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED');
871 }); 911 });
...@@ -879,7 +919,8 @@ test('has no effect if native HLS is available', function() { ...@@ -879,7 +919,8 @@ test('has no effect if native HLS is available', function() {
879 }); 919 });
880 920
881 test('reloads live playlists', function() { 921 test('reloads live playlists', function() {
882 var callbacks = []; 922 var callbacks = [],
923 oldSetTimeout = window.setTimeout;
883 // capture timeouts 924 // capture timeouts
884 window.setTimeout = function(callback, timeout) { 925 window.setTimeout = function(callback, timeout) {
885 callbacks.push({ callback: callback, timeout: timeout }); 926 callbacks.push({ callback: callback, timeout: timeout });
...@@ -888,11 +929,13 @@ test('reloads live playlists', function() { ...@@ -888,11 +929,13 @@ test('reloads live playlists', function() {
888 videojs.mediaSources[player.currentSrc()].trigger({ 929 videojs.mediaSources[player.currentSrc()].trigger({
889 type: 'sourceopen' 930 type: 'sourceopen'
890 }); 931 });
932 standardXHRResponse(requests[0]);
891 933
892 strictEqual(1, callbacks.length, 'refresh was scheduled'); 934 strictEqual(1, callbacks.length, 'refresh was scheduled');
893 strictEqual(player.hls.media.targetDuration * 1000, 935 strictEqual(player.hls.media.targetDuration * 1000,
894 callbacks[0].timeout, 936 callbacks[0].timeout,
895 'waited one target duration'); 937 'waited one target duration');
938 window.setTimeout = oldSetTimeout;
896 }); 939 });
897 940
898 test('duration is Infinity for live playlists', function() { 941 test('duration is Infinity for live playlists', function() {
...@@ -901,7 +944,9 @@ test('duration is Infinity for live playlists', function() { ...@@ -901,7 +944,9 @@ test('duration is Infinity for live playlists', function() {
901 type: 'sourceopen' 944 type: 'sourceopen'
902 }); 945 });
903 946
904 strictEqual(Infinity, player.duration(), 'duration is infinity'); 947 standardXHRResponse(requests[0]);
948
949 strictEqual(player.duration(), Infinity, 'duration is infinity');
905 }); 950 });
906 951
907 test('does not reload playlists with an endlist tag', function() { 952 test('does not reload playlists with an endlist tag', function() {
...@@ -920,7 +965,8 @@ test('does not reload playlists with an endlist tag', function() { ...@@ -920,7 +965,8 @@ test('does not reload playlists with an endlist tag', function() {
920 965
921 test('reloads a live playlist after half a target duration if it has not ' + 966 test('reloads a live playlist after half a target duration if it has not ' +
922 'changed since the last request', function() { 967 'changed since the last request', function() {
923 var callbacks = []; 968 var callbacks = [],
969 oldSetTimeout = window.setTimeout;
924 // capture timeouts 970 // capture timeouts
925 window.setTimeout = function(callback, timeout) { 971 window.setTimeout = function(callback, timeout) {
926 callbacks.push({ callback: callback, timeout: timeout }); 972 callbacks.push({ callback: callback, timeout: timeout });
...@@ -930,19 +976,25 @@ test('reloads a live playlist after half a target duration if it has not ' + ...@@ -930,19 +976,25 @@ test('reloads a live playlist after half a target duration if it has not ' +
930 type: 'sourceopen' 976 type: 'sourceopen'
931 }); 977 });
932 978
979 standardXHRResponse(requests[0]);
980 standardXHRResponse(requests[1]);
933 strictEqual(callbacks.length, 1, 'full-length refresh scheduled'); 981 strictEqual(callbacks.length, 1, 'full-length refresh scheduled');
934 callbacks.pop().callback(); 982 callbacks.pop().callback();
983 standardXHRResponse(requests[2]);
935 984
936 strictEqual(1, callbacks.length, 'half-length refresh was scheduled'); 985 strictEqual(callbacks.length, 1, 'half-length refresh was scheduled');
937 strictEqual(callbacks[0].timeout, 986 strictEqual(callbacks[0].timeout,
938 player.hls.media.targetDuration / 2 * 1000, 987 player.hls.media.targetDuration / 2 * 1000,
939 'waited half a target duration'); 988 'waited half a target duration');
989 window.setTimeout = oldSetTimeout;
940 }); 990 });
941 991
942 test('merges playlist reloads', function() { 992 test('merges playlist reloads', function() {
943 var 993 var
944 oldPlaylist, 994 oldPlaylist,
945 callback; 995 callback,
996 oldSetTimeout = window.setTimeout;
997
946 // capture timeouts 998 // capture timeouts
947 window.setTimeout = function(cb) { 999 window.setTimeout = function(cb) {
948 callback = cb; 1000 callback = cb;
...@@ -952,14 +1004,19 @@ test('merges playlist reloads', function() { ...@@ -952,14 +1004,19 @@ test('merges playlist reloads', function() {
952 videojs.mediaSources[player.currentSrc()].trigger({ 1004 videojs.mediaSources[player.currentSrc()].trigger({
953 type: 'sourceopen' 1005 type: 'sourceopen'
954 }); 1006 });
1007 standardXHRResponse(requests[0]);
1008 standardXHRResponse(requests[1]);
955 oldPlaylist = player.hls.media; 1009 oldPlaylist = player.hls.media;
956 1010
957 callback(); 1011 callback();
1012 standardXHRResponse(requests[2]);
958 ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated'); 1013 ok(oldPlaylist !== player.hls.media, 'player.hls.media was updated');
1014 window.setTimeout = oldSetTimeout;
959 }); 1015 });
960 1016
961 test('updates the media index when a playlist reloads', function() { 1017 test('updates the media index when a playlist reloads', function() {
962 var callback; 1018 var callback,
1019 oldSetTimeout = window.setTimeout;
963 window.setTimeout = function(cb) { 1020 window.setTimeout = function(cb) {
964 callback = cb; 1021 callback = cb;
965 }; 1022 };
...@@ -978,6 +1035,8 @@ test('updates the media index when a playlist reloads', function() { ...@@ -978,6 +1035,8 @@ test('updates the media index when a playlist reloads', function() {
978 type: 'sourceopen' 1035 type: 'sourceopen'
979 }); 1036 });
980 1037
1038 standardXHRResponse(requests[0]);
1039 standardXHRResponse(requests[1]);
981 // play the stream until 2.ts is playing 1040 // play the stream until 2.ts is playing
982 player.hls.mediaIndex = 3; 1041 player.hls.mediaIndex = 3;
983 1042
...@@ -991,8 +1050,10 @@ test('updates the media index when a playlist reloads', function() { ...@@ -991,8 +1050,10 @@ test('updates the media index when a playlist reloads', function() {
991 '#EXTINF:10,\n' + 1050 '#EXTINF:10,\n' +
992 '3.ts\n'; 1051 '3.ts\n';
993 callback(); 1052 callback();
1053 standardXHRResponse(requests[2]);
994 1054
995 strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload'); 1055 strictEqual(player.hls.mediaIndex, 2, 'mediaIndex is updated after the reload');
1056 window.setTimeout = oldSetTimeout;
996 }); 1057 });
997 1058
998 test('mediaIndex is zero before the first segment loads', function() { 1059 test('mediaIndex is zero before the first segment loads', function() {
...@@ -1063,7 +1124,22 @@ test('does not reload master playlists', function() { ...@@ -1063,7 +1124,22 @@ test('does not reload master playlists', function() {
1063 }); 1124 });
1064 1125
1065 test('only reloads the active media playlist', function() { 1126 test('only reloads the active media playlist', function() {
1066 var callbacks = [], urls = [], responses = []; 1127 var callbacks = [],
1128 i = 0,
1129 filteredRequests = [],
1130 oldSetTimeout = window.setTimeout,
1131 customResponse;
1132
1133 customResponse = function(request) {
1134 request.response = new Uint8Array([1]).buffer;
1135 request.respond(200,
1136 {'Content-Type': 'application/vnd.apple.mpegurl'},
1137 '#EXTM3U\n' +
1138 '#EXT-X-MEDIA-SEQUENCE:1\n' +
1139 '#EXTINF:10,\n' +
1140 '1.ts\n');
1141 };
1142
1067 window.setTimeout = function(callback) { 1143 window.setTimeout = function(callback) {
1068 callbacks.push(callback); 1144 callbacks.push(callback);
1069 }; 1145 };
...@@ -1072,24 +1148,9 @@ test('only reloads the active media playlist', function() { ...@@ -1072,24 +1148,9 @@ test('only reloads the active media playlist', function() {
1072 videojs.mediaSources[player.currentSrc()].trigger({ 1148 videojs.mediaSources[player.currentSrc()].trigger({
1073 type: 'sourceopen' 1149 type: 'sourceopen'
1074 }); 1150 });
1151 standardXHRResponse(requests[0]);
1152 standardXHRResponse(requests[1]);
1075 1153
1076 window.XMLHttpRequest = function() {
1077 this.open = function(method, url) {
1078 urls.push(url);
1079 };
1080 this.send = function() {
1081 var xhr = this;
1082 responses.push(function() {
1083 xhr.readyState = 4;
1084 xhr.responseText = '#EXTM3U\n' +
1085 '#EXT-X-MEDIA-SEQUENCE:1\n' +
1086 '#EXTINF:10,\n' +
1087 '1.ts\n';
1088 xhr.response = new Uint8Array([1]).buffer;
1089 xhr.onreadystatechange();
1090 });
1091 };
1092 };
1093 player.hls.selectPlaylist = function() { 1154 player.hls.selectPlaylist = function() {
1094 return player.hls.master.playlists[1]; 1155 return player.hls.master.playlists[1];
1095 }; 1156 };
...@@ -1099,19 +1160,24 @@ test('only reloads the active media playlist', function() { ...@@ -1099,19 +1160,24 @@ test('only reloads the active media playlist', function() {
1099 1160
1100 player.trigger('timeupdate'); 1161 player.trigger('timeupdate');
1101 strictEqual(callbacks.length, 1, 'a refresh is scheduled'); 1162 strictEqual(callbacks.length, 1, 'a refresh is scheduled');
1102 strictEqual(responses.length, 1, 'segment requested');
1103 1163
1104 responses.shift()(); // segment response 1164 standardXHRResponse(requests[2]); // segment response
1105 responses.shift()(); // loaded switched.m3u8 1165 customResponse(requests[3]); // loaded witched.m3u8
1106 1166
1107 urls = [];
1108 callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8 1167 callbacks.shift()(); // out-of-date refresh of missingEndlist.m3u8
1109 callbacks.shift()(); // refresh switched.m3u8 1168 callbacks.shift()(); // refresh switched.m3u8
1110 1169
1111 strictEqual(urls.length, 1, 'one refresh was made'); 1170 for (; i < requests.length; i++) {
1112 strictEqual(urls[0], 1171 if (/switched/.test(requests[i].url)) {
1172 filteredRequests.push(requests[i]);
1173 }
1174 }
1175 strictEqual(filteredRequests.length, 2, 'one refresh was made');
1176 strictEqual(filteredRequests[1].url,
1113 'http://example.com/switched.m3u8', 1177 'http://example.com/switched.m3u8',
1114 'refreshed the active playlist'); 1178 'refreshed the active playlist');
1179
1180 window.setTimeout = oldSetTimeout;
1115 }); 1181 });
1116 1182
1117 })(window, window.videojs); 1183 })(window, window.videojs);
......