From 3e1a6688c37057014707b4b90551d5444ccc3f78 Mon Sep 17 00:00:00 2001 From: TEHEK Firefox Date: Fri, 28 Oct 2011 15:32:32 +0000 Subject: chore(browser): rename Browser.js -> browser.js, BrowserSpec.js -> browserSpec.js And move them to proper service subfolder...--- src/service/Browser.js | 479 ---------------------------- src/service/browser.js | 479 ++++++++++++++++++++++++++++ test/BrowserSpecs.js | 721 ------------------------------------------- test/service/browserSpecs.js | 721 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 1200 insertions(+), 1200 deletions(-) delete mode 100644 src/service/Browser.js create mode 100644 src/service/browser.js delete mode 100644 test/BrowserSpecs.js create mode 100644 test/service/browserSpecs.js diff --git a/src/service/Browser.js b/src/service/Browser.js deleted file mode 100644 index 2e2c07e8..00000000 --- a/src/service/Browser.js +++ /dev/null @@ -1,479 +0,0 @@ -'use strict'; - -////////////////////////////// -// Browser -////////////////////////////// -var XHR = window.XMLHttpRequest || function() { - try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} - try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} - try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} - throw new Error("This browser does not support XMLHttpRequest."); -}; - - -/** - * @ngdoc object - * @name angular.module.ng.$browser - * @requires $log - * @description - * This object has two goals: - * - * - hide all the global state in the browser caused by the window object - * - abstract away all the browser specific features and inconsistencies - * - * For tests we provide {@link angular.module.ngMock.$browser mock implementation} of the `$browser` - * service, which can be used for convenient testing of the application without the interaction with - * the real browser apis. - */ -/** - * @param {object} window The global window object. - * @param {object} document jQuery wrapped document. - * @param {object} body jQuery wrapped document.body. - * @param {function()} XHR XMLHttpRequest constructor. - * @param {object} $log console.log or an object with the same interface. - * @param {object} $sniffer $sniffer service - */ -function Browser(window, document, body, XHR, $log, $sniffer) { - var self = this, - rawDocument = document[0], - location = window.location, - history = window.history, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - pendingDeferIds = {}; - - self.isMock = false; - - ////////////////////////////////////////////////////////////// - // XHR API - ////////////////////////////////////////////////////////////// - var idCounter = 0; - var outstandingRequestCount = 0; - var outstandingRequestCallbacks = []; - - - /** - * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` - * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. - */ - function completeOutstandingRequest(fn) { - try { - fn.apply(null, sliceArgs(arguments, 1)); - } finally { - outstandingRequestCount--; - if (outstandingRequestCount === 0) { - while(outstandingRequestCallbacks.length) { - try { - outstandingRequestCallbacks.pop()(); - } catch (e) { - $log.error(e); - } - } - } - } - } - - /** - * @ngdoc method - * @name angular.module.ng.$browser#xhr - * @methodOf angular.module.ng.$browser - * - * @param {string} method Requested method (get|post|put|delete|head|json) - * @param {string} url Requested url - * @param {?string} post Post data to send (null if nothing to post) - * @param {function(number, string)} callback Function that will be called on response - * @param {object=} header additional HTTP headers to send with XHR. - * Standard headers are: - * - * - * @description - * Send ajax request - */ - self.xhr = function(method, url, post, callback, headers) { - outstandingRequestCount ++; - if (lowercase(method) == 'json') { - var callbackId = ("angular_" + Math.random() + '_' + (idCounter++)).replace(/\d\./, ''); - window[callbackId] = function(data) { - window[callbackId].data = data; - }; - - var script = self.addJs(url.replace('JSON_CALLBACK', callbackId), function() { - if (window[callbackId].data) { - completeOutstandingRequest(callback, 200, window[callbackId].data); - } else { - completeOutstandingRequest(callback); - } - delete window[callbackId]; - body[0].removeChild(script); - }); - } else { - var xhr = new XHR(); - xhr.open(method, url, true); - forEach(headers, function(value, key) { - if (value) xhr.setRequestHeader(key, value); - }); - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - // normalize IE bug (http://bugs.jquery.com/ticket/1450) - var status = xhr.status == 1223 ? 204 : xhr.status; - completeOutstandingRequest(callback, status, xhr.responseText); - } - }; - xhr.send(post || ''); - } - }; - - /** - * @private - * Note: this method is used only by scenario runner - * TODO(vojta): prefix this method with $$ ? - * @param {function()} callback Function that will be called when no outstanding request - */ - self.notifyWhenNoOutstandingRequests = function(callback) { - // force browser to execute all pollFns - this is needed so that cookies and other pollers fire - // at some deterministic time in respect to the test runner's actions. Leaving things up to the - // regular poller would result in flaky tests. - forEach(pollFns, function(pollFn){ pollFn(); }); - - if (outstandingRequestCount === 0) { - callback(); - } else { - outstandingRequestCallbacks.push(callback); - } - }; - - ////////////////////////////////////////////////////////////// - // Poll Watcher API - ////////////////////////////////////////////////////////////// - var pollFns = [], - pollTimeout; - - /** - * @ngdoc method - * @name angular.module.ng.$browser#addPollFn - * @methodOf angular.module.ng.$browser - * - * @param {function()} fn Poll function to add - * - * @description - * Adds a function to the list of functions that poller periodically executes, - * and starts polling if not started yet. - * - * @returns {function()} the added function - */ - self.addPollFn = function(fn) { - if (isUndefined(pollTimeout)) startPoller(100, setTimeout); - pollFns.push(fn); - return fn; - }; - - /** - * @param {number} interval How often should browser call poll functions (ms) - * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. - * - * @description - * Configures the poller to run in the specified intervals, using the specified - * setTimeout fn and kicks it off. - */ - function startPoller(interval, setTimeout) { - (function check() { - forEach(pollFns, function(pollFn){ pollFn(); }); - pollTimeout = setTimeout(check, interval); - })(); - } - - ////////////////////////////////////////////////////////////// - // URL API - ////////////////////////////////////////////////////////////// - - var lastBrowserUrl = location.href; - - /** - * @ngdoc method - * @name angular.module.ng.$browser#url - * @methodOf angular.module.ng.$browser - * - * @description - * GETTER: - * Without any argument, this method just returns current value of location.href. - * - * SETTER: - * With at least one argument, this method sets url to new value. - * If html5 history api supported, pushState/replaceState is used, otherwise - * location.href/location.replace is used. - * Returns its own instance to allow chaining - * - * NOTE: this api is intended for use only by the $location service. Please use the - * {@link angular.module.ng.$location $location service} to change url. - * - * @param {string} url New url (when used as setter) - * @param {boolean=} replace Should new url replace current history record ? - */ - self.url = function(url, replace) { - // setter - if (url) { - lastBrowserUrl = url; - if ($sniffer.history) { - if (replace) history.replaceState(null, '', url); - else history.pushState(null, '', url); - } else { - if (replace) location.replace(url); - else location.href = url; - } - return self; - // getter - } else { - return location.href; - } - }; - - var urlChangeListeners = [], - urlChangeInit = false; - - function fireUrlChange() { - if (lastBrowserUrl == self.url()) return; - - lastBrowserUrl = self.url(); - forEach(urlChangeListeners, function(listener) { - listener(self.url()); - }); - } - - /** - * @ngdoc method - * @name angular.module.ng.$browser#onUrlChange - * @methodOf angular.module.ng.$browser - * @TODO(vojta): refactor to use node's syntax for events - * - * @description - * Register callback function that will be called, when url changes. - * - * It's only called when the url is changed by outside of angular: - * - user types different url into address bar - * - user clicks on history (forward/back) button - * - user clicks on a link - * - * It's not called when url is changed by $browser.url() method - * - * The listener gets called with new url as parameter. - * - * NOTE: this api is intended for use only by the $location service. Please use the - * {@link angular.module.ng.$location $location service} to monitor url changes in angular apps. - * - * @param {function(string)} listener Listener function to be called when url changes. - * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. - */ - self.onUrlChange = function(callback) { - if (!urlChangeInit) { - // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) - // don't fire popstate when user change the address bar and don't fire hashchange when url - // changed by push/replaceState - - // html5 history api - popstate event - if ($sniffer.history) jqLite(window).bind('popstate', fireUrlChange); - // hashchange event - if ($sniffer.hashchange) jqLite(window).bind('hashchange', fireUrlChange); - // polling - else self.addPollFn(fireUrlChange); - - urlChangeInit = true; - } - - urlChangeListeners.push(callback); - return callback; - }; - - ////////////////////////////////////////////////////////////// - // Cookies API - ////////////////////////////////////////////////////////////// - var lastCookies = {}; - var lastCookieString = ''; - - /** - * @ngdoc method - * @name angular.module.ng.$browser#cookies - * @methodOf angular.module.ng.$browser - * - * @param {string=} name Cookie name - * @param {string=} value Cokkie value - * - * @description - * The cookies method provides a 'private' low level access to browser cookies. - * It is not meant to be used directly, use the $cookie service instead. - * - * The return values vary depending on the arguments that the method was called with as follows: - * - * - * @returns {Object} Hash of all cookies (if called without any parameter) - */ - self.cookies = function(name, value) { - var cookieLength, cookieArray, cookie, i, keyValue, index; - - if (name) { - if (value === undefined) { - rawDocument.cookie = escape(name) + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } else { - if (isString(value)) { - rawDocument.cookie = escape(name) + '=' + escape(value); - - cookieLength = name.length + value.length + 1; - if (cookieLength > 4096) { - $log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+ - cookieLength + " > 4096 bytes)!"); - } - if (lastCookies.length > 20) { - $log.warn("Cookie '"+ name +"' possibly not set or overflowed because too many cookies " + - "were already set (" + lastCookies.length + " > 20 )"); - } - } - } - } else { - if (rawDocument.cookie !== lastCookieString) { - lastCookieString = rawDocument.cookie; - cookieArray = lastCookieString.split("; "); - lastCookies = {}; - - for (i = 0; i < cookieArray.length; i++) { - cookie = cookieArray[i]; - index = cookie.indexOf('='); - if (index > 0) { //ignore nameless cookies - lastCookies[unescape(cookie.substring(0, index))] = unescape(cookie.substring(index + 1)); - } - } - } - return lastCookies; - } - }; - - - /** - * @ngdoc method - * @name angular.module.ng.$browser#defer - * @methodOf angular.module.ng.$browser - * @param {function()} fn A function, who's execution should be defered. - * @param {number=} [delay=0] of milliseconds to defer the function execution. - * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. - * - * @description - * Executes a fn asynchroniously via `setTimeout(fn, delay)`. - * - * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using - * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed - * via `$browser.defer.flush()`. - * - */ - self.defer = function(fn, delay) { - var timeoutId; - outstandingRequestCount++; - timeoutId = setTimeout(function() { - delete pendingDeferIds[timeoutId]; - completeOutstandingRequest(fn); - }, delay || 0); - pendingDeferIds[timeoutId] = true; - return timeoutId; - }; - - - /** - * THIS DOC IS NOT VISIBLE because ngdocs can't process docs for foo#method.method - * - * @name angular.module.ng.$browser#defer.cancel - * @methodOf angular.module.ng.$browser.defer - * - * @description - * Cancels a defered task identified with `deferId`. - * - * @param {*} deferId Token returned by the `$browser.defer` function. - * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. - */ - self.defer.cancel = function(deferId) { - if (pendingDeferIds[deferId]) { - delete pendingDeferIds[deferId]; - clearTimeout(deferId); - completeOutstandingRequest(noop); - return true; - } - return false; - }; - - - ////////////////////////////////////////////////////////////// - // Misc API - ////////////////////////////////////////////////////////////// - - /** - * @ngdoc method - * @name angular.module.ng.$browser#addCss - * @methodOf angular.module.ng.$browser - * - * @param {string} url Url to css file - * @description - * Adds a stylesheet tag to the head. - */ - self.addCss = function(url) { - var link = jqLite(rawDocument.createElement('link')); - link.attr('rel', 'stylesheet'); - link.attr('type', 'text/css'); - link.attr('href', url); - body.append(link); - }; - - - /** - * @ngdoc method - * @name angular.module.ng.$browser#addJs - * @methodOf angular.module.ng.$browser - * - * @param {string} url Url to js file - * - * @description - * Adds a script tag to the head. - */ - self.addJs = function(url, done) { - // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: - // - fetches local scripts via XHR and evals them - // - adds and immediately removes script elements from the document - var script = rawDocument.createElement('script'); - - script.type = 'text/javascript'; - script.src = url; - - if (msie) { - script.onreadystatechange = function() { - /loaded|complete/.test(script.readyState) && done && done(); - }; - } else { - if (done) script.onload = script.onerror = done; - } - - body[0].appendChild(script); - - return script; - }; - - /** - * Returns current - * (always relative - without domain) - * - * @returns {string=} - */ - self.baseHref = function() { - var href = document.find('base').attr('href'); - return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : href; - }; -} - -function $BrowserProvider(){ - this.$get = ['$window', '$log', '$sniffer', '$document', - function( $window, $log, $sniffer, $document){ - return new Browser($window, $document, $document.find('body'), XHR, $log, $sniffer); - }]; -} diff --git a/src/service/browser.js b/src/service/browser.js new file mode 100644 index 00000000..2e2c07e8 --- /dev/null +++ b/src/service/browser.js @@ -0,0 +1,479 @@ +'use strict'; + +////////////////////////////// +// Browser +////////////////////////////// +var XHR = window.XMLHttpRequest || function() { + try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {} + try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {} + try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {} + throw new Error("This browser does not support XMLHttpRequest."); +}; + + +/** + * @ngdoc object + * @name angular.module.ng.$browser + * @requires $log + * @description + * This object has two goals: + * + * - hide all the global state in the browser caused by the window object + * - abstract away all the browser specific features and inconsistencies + * + * For tests we provide {@link angular.module.ngMock.$browser mock implementation} of the `$browser` + * service, which can be used for convenient testing of the application without the interaction with + * the real browser apis. + */ +/** + * @param {object} window The global window object. + * @param {object} document jQuery wrapped document. + * @param {object} body jQuery wrapped document.body. + * @param {function()} XHR XMLHttpRequest constructor. + * @param {object} $log console.log or an object with the same interface. + * @param {object} $sniffer $sniffer service + */ +function Browser(window, document, body, XHR, $log, $sniffer) { + var self = this, + rawDocument = document[0], + location = window.location, + history = window.history, + setTimeout = window.setTimeout, + clearTimeout = window.clearTimeout, + pendingDeferIds = {}; + + self.isMock = false; + + ////////////////////////////////////////////////////////////// + // XHR API + ////////////////////////////////////////////////////////////// + var idCounter = 0; + var outstandingRequestCount = 0; + var outstandingRequestCallbacks = []; + + + /** + * Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks` + * counter. If the counter reaches 0, all the `outstandingRequestCallbacks` are executed. + */ + function completeOutstandingRequest(fn) { + try { + fn.apply(null, sliceArgs(arguments, 1)); + } finally { + outstandingRequestCount--; + if (outstandingRequestCount === 0) { + while(outstandingRequestCallbacks.length) { + try { + outstandingRequestCallbacks.pop()(); + } catch (e) { + $log.error(e); + } + } + } + } + } + + /** + * @ngdoc method + * @name angular.module.ng.$browser#xhr + * @methodOf angular.module.ng.$browser + * + * @param {string} method Requested method (get|post|put|delete|head|json) + * @param {string} url Requested url + * @param {?string} post Post data to send (null if nothing to post) + * @param {function(number, string)} callback Function that will be called on response + * @param {object=} header additional HTTP headers to send with XHR. + * Standard headers are: + * + * + * @description + * Send ajax request + */ + self.xhr = function(method, url, post, callback, headers) { + outstandingRequestCount ++; + if (lowercase(method) == 'json') { + var callbackId = ("angular_" + Math.random() + '_' + (idCounter++)).replace(/\d\./, ''); + window[callbackId] = function(data) { + window[callbackId].data = data; + }; + + var script = self.addJs(url.replace('JSON_CALLBACK', callbackId), function() { + if (window[callbackId].data) { + completeOutstandingRequest(callback, 200, window[callbackId].data); + } else { + completeOutstandingRequest(callback); + } + delete window[callbackId]; + body[0].removeChild(script); + }); + } else { + var xhr = new XHR(); + xhr.open(method, url, true); + forEach(headers, function(value, key) { + if (value) xhr.setRequestHeader(key, value); + }); + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + // normalize IE bug (http://bugs.jquery.com/ticket/1450) + var status = xhr.status == 1223 ? 204 : xhr.status; + completeOutstandingRequest(callback, status, xhr.responseText); + } + }; + xhr.send(post || ''); + } + }; + + /** + * @private + * Note: this method is used only by scenario runner + * TODO(vojta): prefix this method with $$ ? + * @param {function()} callback Function that will be called when no outstanding request + */ + self.notifyWhenNoOutstandingRequests = function(callback) { + // force browser to execute all pollFns - this is needed so that cookies and other pollers fire + // at some deterministic time in respect to the test runner's actions. Leaving things up to the + // regular poller would result in flaky tests. + forEach(pollFns, function(pollFn){ pollFn(); }); + + if (outstandingRequestCount === 0) { + callback(); + } else { + outstandingRequestCallbacks.push(callback); + } + }; + + ////////////////////////////////////////////////////////////// + // Poll Watcher API + ////////////////////////////////////////////////////////////// + var pollFns = [], + pollTimeout; + + /** + * @ngdoc method + * @name angular.module.ng.$browser#addPollFn + * @methodOf angular.module.ng.$browser + * + * @param {function()} fn Poll function to add + * + * @description + * Adds a function to the list of functions that poller periodically executes, + * and starts polling if not started yet. + * + * @returns {function()} the added function + */ + self.addPollFn = function(fn) { + if (isUndefined(pollTimeout)) startPoller(100, setTimeout); + pollFns.push(fn); + return fn; + }; + + /** + * @param {number} interval How often should browser call poll functions (ms) + * @param {function()} setTimeout Reference to a real or fake `setTimeout` function. + * + * @description + * Configures the poller to run in the specified intervals, using the specified + * setTimeout fn and kicks it off. + */ + function startPoller(interval, setTimeout) { + (function check() { + forEach(pollFns, function(pollFn){ pollFn(); }); + pollTimeout = setTimeout(check, interval); + })(); + } + + ////////////////////////////////////////////////////////////// + // URL API + ////////////////////////////////////////////////////////////// + + var lastBrowserUrl = location.href; + + /** + * @ngdoc method + * @name angular.module.ng.$browser#url + * @methodOf angular.module.ng.$browser + * + * @description + * GETTER: + * Without any argument, this method just returns current value of location.href. + * + * SETTER: + * With at least one argument, this method sets url to new value. + * If html5 history api supported, pushState/replaceState is used, otherwise + * location.href/location.replace is used. + * Returns its own instance to allow chaining + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@link angular.module.ng.$location $location service} to change url. + * + * @param {string} url New url (when used as setter) + * @param {boolean=} replace Should new url replace current history record ? + */ + self.url = function(url, replace) { + // setter + if (url) { + lastBrowserUrl = url; + if ($sniffer.history) { + if (replace) history.replaceState(null, '', url); + else history.pushState(null, '', url); + } else { + if (replace) location.replace(url); + else location.href = url; + } + return self; + // getter + } else { + return location.href; + } + }; + + var urlChangeListeners = [], + urlChangeInit = false; + + function fireUrlChange() { + if (lastBrowserUrl == self.url()) return; + + lastBrowserUrl = self.url(); + forEach(urlChangeListeners, function(listener) { + listener(self.url()); + }); + } + + /** + * @ngdoc method + * @name angular.module.ng.$browser#onUrlChange + * @methodOf angular.module.ng.$browser + * @TODO(vojta): refactor to use node's syntax for events + * + * @description + * Register callback function that will be called, when url changes. + * + * It's only called when the url is changed by outside of angular: + * - user types different url into address bar + * - user clicks on history (forward/back) button + * - user clicks on a link + * + * It's not called when url is changed by $browser.url() method + * + * The listener gets called with new url as parameter. + * + * NOTE: this api is intended for use only by the $location service. Please use the + * {@link angular.module.ng.$location $location service} to monitor url changes in angular apps. + * + * @param {function(string)} listener Listener function to be called when url changes. + * @return {function(string)} Returns the registered listener fn - handy if the fn is anonymous. + */ + self.onUrlChange = function(callback) { + if (!urlChangeInit) { + // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera) + // don't fire popstate when user change the address bar and don't fire hashchange when url + // changed by push/replaceState + + // html5 history api - popstate event + if ($sniffer.history) jqLite(window).bind('popstate', fireUrlChange); + // hashchange event + if ($sniffer.hashchange) jqLite(window).bind('hashchange', fireUrlChange); + // polling + else self.addPollFn(fireUrlChange); + + urlChangeInit = true; + } + + urlChangeListeners.push(callback); + return callback; + }; + + ////////////////////////////////////////////////////////////// + // Cookies API + ////////////////////////////////////////////////////////////// + var lastCookies = {}; + var lastCookieString = ''; + + /** + * @ngdoc method + * @name angular.module.ng.$browser#cookies + * @methodOf angular.module.ng.$browser + * + * @param {string=} name Cookie name + * @param {string=} value Cokkie value + * + * @description + * The cookies method provides a 'private' low level access to browser cookies. + * It is not meant to be used directly, use the $cookie service instead. + * + * The return values vary depending on the arguments that the method was called with as follows: + * + * + * @returns {Object} Hash of all cookies (if called without any parameter) + */ + self.cookies = function(name, value) { + var cookieLength, cookieArray, cookie, i, keyValue, index; + + if (name) { + if (value === undefined) { + rawDocument.cookie = escape(name) + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; + } else { + if (isString(value)) { + rawDocument.cookie = escape(name) + '=' + escape(value); + + cookieLength = name.length + value.length + 1; + if (cookieLength > 4096) { + $log.warn("Cookie '"+ name +"' possibly not set or overflowed because it was too large ("+ + cookieLength + " > 4096 bytes)!"); + } + if (lastCookies.length > 20) { + $log.warn("Cookie '"+ name +"' possibly not set or overflowed because too many cookies " + + "were already set (" + lastCookies.length + " > 20 )"); + } + } + } + } else { + if (rawDocument.cookie !== lastCookieString) { + lastCookieString = rawDocument.cookie; + cookieArray = lastCookieString.split("; "); + lastCookies = {}; + + for (i = 0; i < cookieArray.length; i++) { + cookie = cookieArray[i]; + index = cookie.indexOf('='); + if (index > 0) { //ignore nameless cookies + lastCookies[unescape(cookie.substring(0, index))] = unescape(cookie.substring(index + 1)); + } + } + } + return lastCookies; + } + }; + + + /** + * @ngdoc method + * @name angular.module.ng.$browser#defer + * @methodOf angular.module.ng.$browser + * @param {function()} fn A function, who's execution should be defered. + * @param {number=} [delay=0] of milliseconds to defer the function execution. + * @returns {*} DeferId that can be used to cancel the task via `$browser.defer.cancel()`. + * + * @description + * Executes a fn asynchroniously via `setTimeout(fn, delay)`. + * + * Unlike when calling `setTimeout` directly, in test this function is mocked and instead of using + * `setTimeout` in tests, the fns are queued in an array, which can be programmatically flushed + * via `$browser.defer.flush()`. + * + */ + self.defer = function(fn, delay) { + var timeoutId; + outstandingRequestCount++; + timeoutId = setTimeout(function() { + delete pendingDeferIds[timeoutId]; + completeOutstandingRequest(fn); + }, delay || 0); + pendingDeferIds[timeoutId] = true; + return timeoutId; + }; + + + /** + * THIS DOC IS NOT VISIBLE because ngdocs can't process docs for foo#method.method + * + * @name angular.module.ng.$browser#defer.cancel + * @methodOf angular.module.ng.$browser.defer + * + * @description + * Cancels a defered task identified with `deferId`. + * + * @param {*} deferId Token returned by the `$browser.defer` function. + * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfuly canceled. + */ + self.defer.cancel = function(deferId) { + if (pendingDeferIds[deferId]) { + delete pendingDeferIds[deferId]; + clearTimeout(deferId); + completeOutstandingRequest(noop); + return true; + } + return false; + }; + + + ////////////////////////////////////////////////////////////// + // Misc API + ////////////////////////////////////////////////////////////// + + /** + * @ngdoc method + * @name angular.module.ng.$browser#addCss + * @methodOf angular.module.ng.$browser + * + * @param {string} url Url to css file + * @description + * Adds a stylesheet tag to the head. + */ + self.addCss = function(url) { + var link = jqLite(rawDocument.createElement('link')); + link.attr('rel', 'stylesheet'); + link.attr('type', 'text/css'); + link.attr('href', url); + body.append(link); + }; + + + /** + * @ngdoc method + * @name angular.module.ng.$browser#addJs + * @methodOf angular.module.ng.$browser + * + * @param {string} url Url to js file + * + * @description + * Adds a script tag to the head. + */ + self.addJs = function(url, done) { + // we can't use jQuery/jqLite here because jQuery does crazy shit with script elements, e.g.: + // - fetches local scripts via XHR and evals them + // - adds and immediately removes script elements from the document + var script = rawDocument.createElement('script'); + + script.type = 'text/javascript'; + script.src = url; + + if (msie) { + script.onreadystatechange = function() { + /loaded|complete/.test(script.readyState) && done && done(); + }; + } else { + if (done) script.onload = script.onerror = done; + } + + body[0].appendChild(script); + + return script; + }; + + /** + * Returns current + * (always relative - without domain) + * + * @returns {string=} + */ + self.baseHref = function() { + var href = document.find('base').attr('href'); + return href ? href.replace(/^https?\:\/\/[^\/]*/, '') : href; + }; +} + +function $BrowserProvider(){ + this.$get = ['$window', '$log', '$sniffer', '$document', + function( $window, $log, $sniffer, $document){ + return new Browser($window, $document, $document.find('body'), XHR, $log, $sniffer); + }]; +} diff --git a/test/BrowserSpecs.js b/test/BrowserSpecs.js deleted file mode 100644 index 5234f0be..00000000 --- a/test/BrowserSpecs.js +++ /dev/null @@ -1,721 +0,0 @@ -'use strict'; - -function MockWindow() { - var events = {}; - var timeouts = this.timeouts = []; - - this.setTimeout = function(fn) { - return timeouts.push(fn) - 1; - }; - - this.clearTimeout = function(id) { - timeouts[id] = noop; - }; - - this.setTimeout.flush = function() { - var length = timeouts.length; - while (length-- > 0) timeouts.shift()(); - }; - - this.addEventListener = function(name, listener) { - if (isUndefined(events[name])) events[name] = []; - events[name].push(listener); - }; - - this.attachEvent = function(name, listener) { - this.addEventListener(name.substr(2), listener); - }; - - this.removeEventListener = noop; - this.detachEvent = noop; - - this.fire = function(name) { - forEach(events[name], function(fn) { - fn({type: name}); // type to make jQuery happy - }); - }; - - this.location = { - href: 'http://server', - replace: noop - }; - - this.history = { - replaceState: noop, - pushState: noop - }; -} - -describe('browser', function() { - - var browser, fakeWindow, xhr, logs, scripts, removedScripts, sniffer; - - beforeEach(function() { - scripts = []; - removedScripts = []; - xhr = null; - sniffer = {history: true, hashchange: true}; - fakeWindow = new MockWindow(); - - var fakeBody = [{appendChild: function(node){scripts.push(node);}, - removeChild: function(node){removedScripts.push(node);}}]; - - var FakeXhr = function() { - xhr = this; - this.open = function(method, url, async){ - xhr.method = method; - xhr.url = url; - xhr.async = async; - xhr.headers = {}; - }; - this.setRequestHeader = function(key, value){ - xhr.headers[key] = value; - }; - this.send = function(post){ - xhr.post = post; - }; - }; - - logs = {log:[], warn:[], info:[], error:[]}; - - var fakeLog = {log: function() { logs.log.push(slice.call(arguments)); }, - warn: function() { logs.warn.push(slice.call(arguments)); }, - info: function() { logs.info.push(slice.call(arguments)); }, - error: function() { logs.error.push(slice.call(arguments)); }}; - - browser = new Browser(fakeWindow, jqLite(window.document), fakeBody, FakeXhr, - fakeLog, sniffer); - }); - - it('should contain cookie cruncher', function() { - expect(browser.cookies).toBeDefined(); - }); - - describe('outstading requests', function() { - it('should process callbacks immedietly with no outstanding requests', function() { - var callback = jasmine.createSpy('callback'); - browser.notifyWhenNoOutstandingRequests(callback); - expect(callback).toHaveBeenCalled(); - }); - - it('should queue callbacks with outstanding requests', function() { - var callback = jasmine.createSpy('callback'); - browser.xhr('GET', '/url', null, noop); - browser.notifyWhenNoOutstandingRequests(callback); - expect(callback).not.toHaveBeenCalled(); - - xhr.readyState = 4; - xhr.onreadystatechange(); - expect(callback).toHaveBeenCalled(); - }); - }); - - describe('xhr', function() { - describe('JSON', function() { - var log; - - function callback(code, data) { - log += code + ':' + data + ';'; - } - - beforeEach(function() { - log = ""; - }); - - - // We don't have unit tests for IE because script.readyState is readOnly. - // Instead we run e2e tests on all browsers - see e2e for $xhr. - if (!msie) { - - it('should add script tag for JSONP request', function() { - var notify = jasmine.createSpy('notify'); - browser.xhr('JSON', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); - browser.notifyWhenNoOutstandingRequests(notify); - expect(notify).not.toHaveBeenCalled(); - expect(scripts.length).toEqual(1); - var script = scripts[0]; - var url = script.src.split('?cb='); - expect(url[0]).toEqual('http://example.org/path'); - expect(typeof fakeWindow[url[1]]).toEqual('function'); - fakeWindow[url[1]]('data'); - script.onload(); - - expect(notify).toHaveBeenCalled(); - expect(log).toEqual('200:data;'); - expect(scripts).toEqual(removedScripts); - expect(fakeWindow[url[1]]).toBeUndefined(); - }); - - - it('should call callback when script fails to load', function() { - browser.xhr('JSON', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); - var script = scripts[0]; - expect(typeof script.onload).toBe('function'); - expect(typeof script.onerror).toBe('function'); - script.onerror(); - - expect(log).toEqual('undefined:undefined;'); - }); - - - it('should update the outstandingRequests counter for successful requests', function() { - var notify = jasmine.createSpy('notify'); - browser.xhr('JSON', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); - browser.notifyWhenNoOutstandingRequests(notify); - expect(notify).not.toHaveBeenCalled(); - - var script = scripts[0]; - var url = script.src.split('?cb='); - fakeWindow[url[1]]('data'); - script.onload(); - - expect(notify).toHaveBeenCalled(); - }); - - - it('should update the outstandingRequests counter for failed requests', function() { - var notify = jasmine.createSpy('notify'); - browser.xhr('JSON', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); - browser.notifyWhenNoOutstandingRequests(notify); - expect(notify).not.toHaveBeenCalled(); - - scripts[0].onerror(); - - expect(notify).toHaveBeenCalled(); - }); - } - }); - - - it('should normalize IE\'s 1223 status code into 204', function() { - var callback = jasmine.createSpy('XHR'); - - browser.xhr('GET', 'URL', 'POST', callback); - - xhr.status = 1223; - xhr.readyState = 4; - xhr.onreadystatechange(); - - expect(callback).toHaveBeenCalled(); - expect(callback.argsForCall[0][0]).toEqual(204); - }); - - it('should set only the requested headers', function() { - var code, response, headers = {}; - browser.xhr('POST', 'URL', null, function(c,r){ - code = c; - response = r; - }, {'X-header1': 'value1', 'X-header2': 'value2'}); - - expect(xhr.method).toEqual('POST'); - expect(xhr.url).toEqual('URL'); - expect(xhr.post).toEqual(''); - expect(xhr.headers).toEqual({ - "X-header1":"value1", - "X-header2":"value2" - }); - - xhr.status = 202; - xhr.responseText = 'RESPONSE'; - xhr.readyState = 4; - xhr.onreadystatechange(); - - expect(code).toEqual(202); - expect(response).toEqual('RESPONSE'); - }); - }); - - describe('defer', function() { - it('should execute fn asynchroniously via setTimeout', function() { - var callback = jasmine.createSpy('deferred'); - - browser.defer(callback); - expect(callback).not.toHaveBeenCalled(); - - fakeWindow.setTimeout.flush(); - expect(callback).toHaveBeenCalledOnce(); - }); - - - it('should update outstandingRequests counter', function() { - var callback = jasmine.createSpy('deferred'); - - browser.defer(callback); - expect(callback).not.toHaveBeenCalled(); - - fakeWindow.setTimeout.flush(); - expect(callback).toHaveBeenCalledOnce(); - }); - - - it('should return unique deferId', function() { - var deferId1 = browser.defer(noop), - deferId2 = browser.defer(noop); - - expect(deferId1).toBeDefined(); - expect(deferId2).toBeDefined(); - expect(deferId1).not.toEqual(deferId2); - }); - - - describe('cancel', function() { - it('should allow tasks to be canceled with returned deferId', function() { - var log = [], - deferId1 = browser.defer(function() { log.push('cancel me'); }), - deferId2 = browser.defer(function() { log.push('ok'); }), - deferId3 = browser.defer(function() { log.push('cancel me, now!'); }); - - expect(log).toEqual([]); - expect(browser.defer.cancel(deferId1)).toBe(true); - expect(browser.defer.cancel(deferId3)).toBe(true); - fakeWindow.setTimeout.flush(); - expect(log).toEqual(['ok']); - expect(browser.defer.cancel(deferId2)).toBe(false); - }); - }); - }); - - - describe('cookies', function() { - - function deleteAllCookies() { - var cookies = document.cookie.split(";"); - - for (var i = 0; i < cookies.length; i++) { - var cookie = cookies[i]; - var eqPos = cookie.indexOf("="); - var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; - document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; - } - } - - beforeEach(function() { - deleteAllCookies(); - expect(document.cookie).toEqual(''); - }); - - - afterEach(function() { - deleteAllCookies(); - expect(document.cookie).toEqual(''); - }); - - - describe('remove all via (null)', function() { - - it('should do nothing when no cookies are set', function() { - browser.cookies(null); - expect(document.cookie).toEqual(''); - expect(browser.cookies()).toEqual({}); - }); - - }); - - describe('remove via cookies(cookieName, undefined)', function() { - - it('should remove a cookie when it is present', function() { - document.cookie = 'foo=bar'; - - browser.cookies('foo', undefined); - - expect(document.cookie).toEqual(''); - expect(browser.cookies()).toEqual({}); - }); - - - it('should do nothing when an nonexisting cookie is being removed', function() { - browser.cookies('doesntexist', undefined); - expect(document.cookie).toEqual(''); - expect(browser.cookies()).toEqual({}); - }); - }); - - - describe('put via cookies(cookieName, string)', function() { - - it('should create and store a cookie', function() { - browser.cookies('cookieName', 'cookie=Value'); - expect(document.cookie).toMatch(/cookieName=cookie%3DValue;? ?/); - expect(browser.cookies()).toEqual({'cookieName':'cookie=Value'}); - }); - - - it('should overwrite an existing unsynced cookie', function() { - document.cookie = "cookie=new"; - - var oldVal = browser.cookies('cookie', 'newer'); - - expect(document.cookie).toEqual('cookie=newer'); - expect(browser.cookies()).toEqual({'cookie':'newer'}); - expect(oldVal).not.toBeDefined(); - }); - - it('should escape both name and value', function() { - browser.cookies('cookie1=', 'val;ue'); - browser.cookies('cookie2=bar;baz', 'val=ue'); - - var rawCookies = document.cookie.split("; "); //order is not guaranteed, so we need to parse - expect(rawCookies.length).toEqual(2); - expect(rawCookies).toContain('cookie1%3D=val%3Bue'); - expect(rawCookies).toContain('cookie2%3Dbar%3Bbaz=val%3Due'); - }); - - it('should log warnings when 4kb per cookie storage limit is reached', function() { - var i, longVal = '', cookieStr; - - for(i=0; i<4091; i++) { - longVal += '+'; - } - - cookieStr = document.cookie; - browser.cookies('x', longVal); //total size 4093-4096, so it should go through - expect(document.cookie).not.toEqual(cookieStr); - expect(browser.cookies()['x']).toEqual(longVal); - expect(logs.warn).toEqual([]); - - browser.cookies('x', longVal + 'xxxx'); //total size 4097-4099, a warning should be logged - expect(logs.warn).toEqual( - [[ "Cookie 'x' possibly not set or overflowed because it was too large (4097 > 4096 " + - "bytes)!" ]]); - - //force browser to dropped a cookie and make sure that the cache is not out of sync - browser.cookies('x', 'shortVal'); - expect(browser.cookies().x).toEqual('shortVal'); //needed to prime the cache - cookieStr = document.cookie; - browser.cookies('x', longVal + longVal + longVal); //should be too long for all browsers - - if (document.cookie !== cookieStr) { - fail("browser didn't drop long cookie when it was expected. make the cookie in this " + - "test longer"); - } - - expect(browser.cookies().x).toEqual('shortVal'); - }); - - it('should log warnings when 20 cookies per domain storage limit is reached', function() { - var i, str, cookieStr; - - for (i=0; i<20; i++) { - str = '' + i; - browser.cookies(str, str); - } - - i=0; - for (str in browser.cookies()) { - i++; - } - expect(i).toEqual(20); - expect(logs.warn).toEqual([]); - cookieStr = document.cookie; - - browser.cookies('one', 'more'); - expect(logs.warn).toEqual([]); - - //if browser dropped a cookie (very likely), make sure that the cache is not out of sync - if (document.cookie === cookieStr) { - expect(size(browser.cookies())).toEqual(20); - } else { - expect(size(browser.cookies())).toEqual(21); - } - }); - }); - - - describe('get via cookies()[cookieName]', function() { - - it('should return undefined for nonexistent cookie', function() { - expect(browser.cookies().nonexistent).not.toBeDefined(); - }); - - - it ('should return a value for an existing cookie', function() { - document.cookie = "foo=bar=baz"; - expect(browser.cookies().foo).toEqual('bar=baz'); - }); - - - it ('should unescape cookie values that were escaped by puts', function() { - document.cookie = "cookie2%3Dbar%3Bbaz=val%3Due"; - expect(browser.cookies()['cookie2=bar;baz']).toEqual('val=ue'); - }); - - - it('should preserve leading & trailing spaces in names and values', function() { - browser.cookies(' cookie name ', ' cookie value '); - expect(browser.cookies()[' cookie name ']).toEqual(' cookie value '); - expect(browser.cookies()['cookie name']).not.toBeDefined(); - }); - }); - - - describe('getAll via cookies()', function() { - - it('should return cookies as hash', function() { - document.cookie = "foo1=bar1"; - document.cookie = "foo2=bar2"; - expect(browser.cookies()).toEqual({'foo1':'bar1', 'foo2':'bar2'}); - }); - - - it('should return empty hash if no cookies exist', function() { - expect(browser.cookies()).toEqual({}); - }); - }); - - - it('should pick up external changes made to browser cookies', function() { - browser.cookies('oatmealCookie', 'drool'); - expect(browser.cookies()).toEqual({'oatmealCookie':'drool'}); - - document.cookie = 'oatmealCookie=changed'; - expect(browser.cookies().oatmealCookie).toEqual('changed'); - }); - - - it('should initialize cookie cache with existing cookies', function() { - document.cookie = "existingCookie=existingValue"; - expect(browser.cookies()).toEqual({'existingCookie':'existingValue'}); - }); - - }); - - describe('poller', function() { - - it('should call functions in pollFns in regular intervals', function() { - var log = ''; - browser.addPollFn(function() {log+='a';}); - browser.addPollFn(function() {log+='b';}); - expect(log).toEqual(''); - fakeWindow.setTimeout.flush(); - expect(log).toEqual('ab'); - fakeWindow.setTimeout.flush(); - expect(log).toEqual('abab'); - }); - - it('should startPoller', function() { - expect(fakeWindow.timeouts.length).toEqual(0); - - browser.addPollFn(function() {}); - expect(fakeWindow.timeouts.length).toEqual(1); - - //should remain 1 as it is the check fn - browser.addPollFn(function() {}); - expect(fakeWindow.timeouts.length).toEqual(1); - }); - - it('should return fn that was passed into addPollFn', function() { - var fn = function() { return 1; }; - var returnedFn = browser.addPollFn(fn); - expect(returnedFn).toBe(fn); - }); - }); - - describe('url', function() { - var pushState, replaceState, locationReplace; - - beforeEach(function() { - pushState = spyOn(fakeWindow.history, 'pushState'); - replaceState = spyOn(fakeWindow.history, 'replaceState'); - locationReplace = spyOn(fakeWindow.location, 'replace'); - }); - - it('should return current location.href', function() { - fakeWindow.location.href = 'http://test.com'; - expect(browser.url()).toEqual('http://test.com'); - - fakeWindow.location.href = 'https://another.com'; - expect(browser.url()).toEqual('https://another.com'); - }); - - it('should use history.pushState when available', function() { - sniffer.history = true; - browser.url('http://new.org'); - - expect(pushState).toHaveBeenCalledOnce(); - expect(pushState.argsForCall[0][2]).toEqual('http://new.org'); - - expect(replaceState).not.toHaveBeenCalled(); - expect(locationReplace).not.toHaveBeenCalled(); - expect(fakeWindow.location.href).toEqual('http://server'); - }); - - it('should use history.replaceState when available', function() { - sniffer.history = true; - browser.url('http://new.org', true); - - expect(replaceState).toHaveBeenCalledOnce(); - expect(replaceState.argsForCall[0][2]).toEqual('http://new.org'); - - expect(pushState).not.toHaveBeenCalled(); - expect(locationReplace).not.toHaveBeenCalled(); - expect(fakeWindow.location.href).toEqual('http://server'); - }); - - it('should set location.href when pushState not available', function() { - sniffer.history = false; - browser.url('http://new.org'); - - expect(fakeWindow.location.href).toEqual('http://new.org'); - - expect(pushState).not.toHaveBeenCalled(); - expect(replaceState).not.toHaveBeenCalled(); - expect(locationReplace).not.toHaveBeenCalled(); - }); - - it('should use location.replace when history.replaceState not available', function() { - sniffer.history = false; - browser.url('http://new.org', true); - - expect(locationReplace).toHaveBeenCalledWith('http://new.org'); - - expect(pushState).not.toHaveBeenCalled(); - expect(replaceState).not.toHaveBeenCalled(); - expect(fakeWindow.location.href).toEqual('http://server'); - }); - - it('should return $browser to allow chaining', function() { - expect(browser.url('http://any.com')).toBe(browser); - }); - }); - - describe('urlChange', function() { - var callback; - - beforeEach(function() { - callback = jasmine.createSpy('onUrlChange'); - }); - - afterEach(function() { - if (!jQuery) jqLite(fakeWindow).dealoc(); - }); - - it('should return registered callback', function() { - expect(browser.onUrlChange(callback)).toBe(callback); - }); - - it('should forward popstate event with new url when history supported', function() { - sniffer.history = true; - browser.onUrlChange(callback); - fakeWindow.location.href = 'http://server/new'; - - fakeWindow.fire('popstate'); - expect(callback).toHaveBeenCalledWith('http://server/new'); - - fakeWindow.fire('hashchange'); - fakeWindow.setTimeout.flush(); - expect(callback).toHaveBeenCalledOnce(); - }); - - it('should forward only popstate event when both history and hashchange supported', function() { - sniffer.history = true; - sniffer.hashchange = true; - browser.onUrlChange(callback); - fakeWindow.location.href = 'http://server/new'; - - fakeWindow.fire('popstate'); - expect(callback).toHaveBeenCalledWith('http://server/new'); - - fakeWindow.fire('hashchange'); - fakeWindow.setTimeout.flush(); - expect(callback).toHaveBeenCalledOnce(); - }); - - it('should forward hashchange event with new url when only hashchange supported', function() { - sniffer.history = false; - sniffer.hashchange = true; - browser.onUrlChange(callback); - fakeWindow.location.href = 'http://server/new'; - - fakeWindow.fire('hashchange'); - expect(callback).toHaveBeenCalledWith('http://server/new'); - - fakeWindow.fire('popstate'); - fakeWindow.setTimeout.flush(); - expect(callback).toHaveBeenCalledOnce(); - }); - - it('should use polling when neither history nor hashchange supported', function() { - sniffer.history = false; - sniffer.hashchange = false; - browser.onUrlChange(callback); - - fakeWindow.location.href = 'http://server.new'; - fakeWindow.setTimeout.flush(); - expect(callback).toHaveBeenCalledWith('http://server.new'); - - fakeWindow.fire('popstate'); - fakeWindow.fire('hashchange'); - expect(callback).toHaveBeenCalledOnce(); - }); - - it('should not fire urlChange if changed by browser.url method (polling)', function() { - sniffer.history = false; - sniffer.hashchange = false; - browser.onUrlChange(callback); - browser.url('http://new.com'); - - fakeWindow.setTimeout.flush(); - expect(callback).not.toHaveBeenCalled(); - }); - - it('should not fire urlChange if changed by browser.url method (hashchange)', function() { - sniffer.history = false; - sniffer.hashchange = true; - browser.onUrlChange(callback); - browser.url('http://new.com'); - - fakeWindow.fire('hashchange'); - expect(callback).not.toHaveBeenCalled(); - }); - }); - - describe('addJs', function() { - it('should append a script tag to body', function() { - browser.addJs('http://localhost/bar.js'); - expect(scripts.length).toBe(1); - expect(scripts[0].src).toBe('http://localhost/bar.js'); - expect(scripts[0].id).toBe(''); - }); - - it('should return the appended script element', function() { - var script = browser.addJs('http://localhost/bar.js'); - expect(script).toBe(scripts[0]); - }); - }); - - describe('baseHref', function() { - var jqDocHead; - - function setDocumentBaseHrefTo(href) { - clearDocumentBaseHref(); - jqDocHead.append(''); - } - - function clearDocumentBaseHref() { - jqDocHead.find('base').remove(); - } - - beforeEach(function() { - jqDocHead = jqLite(document).find('head'); - }); - - afterEach(clearDocumentBaseHref); - - it('should return value from ', function() { - setDocumentBaseHrefTo('/base/path/'); - expect(browser.baseHref()).toEqual('/base/path/'); - }); - - it('should return undefined if no ', function() { - expect(browser.baseHref()).toBeUndefined(); - }); - - it('should remove domain from ', function() { - setDocumentBaseHrefTo('http://host.com/base/path/'); - expect(browser.baseHref()).toEqual('/base/path/'); - - setDocumentBaseHrefTo('http://host.com/base/path/index.html'); - expect(browser.baseHref()).toEqual('/base/path/index.html'); - }); - }); -}); diff --git a/test/service/browserSpecs.js b/test/service/browserSpecs.js new file mode 100644 index 00000000..5234f0be --- /dev/null +++ b/test/service/browserSpecs.js @@ -0,0 +1,721 @@ +'use strict'; + +function MockWindow() { + var events = {}; + var timeouts = this.timeouts = []; + + this.setTimeout = function(fn) { + return timeouts.push(fn) - 1; + }; + + this.clearTimeout = function(id) { + timeouts[id] = noop; + }; + + this.setTimeout.flush = function() { + var length = timeouts.length; + while (length-- > 0) timeouts.shift()(); + }; + + this.addEventListener = function(name, listener) { + if (isUndefined(events[name])) events[name] = []; + events[name].push(listener); + }; + + this.attachEvent = function(name, listener) { + this.addEventListener(name.substr(2), listener); + }; + + this.removeEventListener = noop; + this.detachEvent = noop; + + this.fire = function(name) { + forEach(events[name], function(fn) { + fn({type: name}); // type to make jQuery happy + }); + }; + + this.location = { + href: 'http://server', + replace: noop + }; + + this.history = { + replaceState: noop, + pushState: noop + }; +} + +describe('browser', function() { + + var browser, fakeWindow, xhr, logs, scripts, removedScripts, sniffer; + + beforeEach(function() { + scripts = []; + removedScripts = []; + xhr = null; + sniffer = {history: true, hashchange: true}; + fakeWindow = new MockWindow(); + + var fakeBody = [{appendChild: function(node){scripts.push(node);}, + removeChild: function(node){removedScripts.push(node);}}]; + + var FakeXhr = function() { + xhr = this; + this.open = function(method, url, async){ + xhr.method = method; + xhr.url = url; + xhr.async = async; + xhr.headers = {}; + }; + this.setRequestHeader = function(key, value){ + xhr.headers[key] = value; + }; + this.send = function(post){ + xhr.post = post; + }; + }; + + logs = {log:[], warn:[], info:[], error:[]}; + + var fakeLog = {log: function() { logs.log.push(slice.call(arguments)); }, + warn: function() { logs.warn.push(slice.call(arguments)); }, + info: function() { logs.info.push(slice.call(arguments)); }, + error: function() { logs.error.push(slice.call(arguments)); }}; + + browser = new Browser(fakeWindow, jqLite(window.document), fakeBody, FakeXhr, + fakeLog, sniffer); + }); + + it('should contain cookie cruncher', function() { + expect(browser.cookies).toBeDefined(); + }); + + describe('outstading requests', function() { + it('should process callbacks immedietly with no outstanding requests', function() { + var callback = jasmine.createSpy('callback'); + browser.notifyWhenNoOutstandingRequests(callback); + expect(callback).toHaveBeenCalled(); + }); + + it('should queue callbacks with outstanding requests', function() { + var callback = jasmine.createSpy('callback'); + browser.xhr('GET', '/url', null, noop); + browser.notifyWhenNoOutstandingRequests(callback); + expect(callback).not.toHaveBeenCalled(); + + xhr.readyState = 4; + xhr.onreadystatechange(); + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('xhr', function() { + describe('JSON', function() { + var log; + + function callback(code, data) { + log += code + ':' + data + ';'; + } + + beforeEach(function() { + log = ""; + }); + + + // We don't have unit tests for IE because script.readyState is readOnly. + // Instead we run e2e tests on all browsers - see e2e for $xhr. + if (!msie) { + + it('should add script tag for JSONP request', function() { + var notify = jasmine.createSpy('notify'); + browser.xhr('JSON', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); + browser.notifyWhenNoOutstandingRequests(notify); + expect(notify).not.toHaveBeenCalled(); + expect(scripts.length).toEqual(1); + var script = scripts[0]; + var url = script.src.split('?cb='); + expect(url[0]).toEqual('http://example.org/path'); + expect(typeof fakeWindow[url[1]]).toEqual('function'); + fakeWindow[url[1]]('data'); + script.onload(); + + expect(notify).toHaveBeenCalled(); + expect(log).toEqual('200:data;'); + expect(scripts).toEqual(removedScripts); + expect(fakeWindow[url[1]]).toBeUndefined(); + }); + + + it('should call callback when script fails to load', function() { + browser.xhr('JSON', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); + var script = scripts[0]; + expect(typeof script.onload).toBe('function'); + expect(typeof script.onerror).toBe('function'); + script.onerror(); + + expect(log).toEqual('undefined:undefined;'); + }); + + + it('should update the outstandingRequests counter for successful requests', function() { + var notify = jasmine.createSpy('notify'); + browser.xhr('JSON', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); + browser.notifyWhenNoOutstandingRequests(notify); + expect(notify).not.toHaveBeenCalled(); + + var script = scripts[0]; + var url = script.src.split('?cb='); + fakeWindow[url[1]]('data'); + script.onload(); + + expect(notify).toHaveBeenCalled(); + }); + + + it('should update the outstandingRequests counter for failed requests', function() { + var notify = jasmine.createSpy('notify'); + browser.xhr('JSON', 'http://example.org/path?cb=JSON_CALLBACK', null, callback); + browser.notifyWhenNoOutstandingRequests(notify); + expect(notify).not.toHaveBeenCalled(); + + scripts[0].onerror(); + + expect(notify).toHaveBeenCalled(); + }); + } + }); + + + it('should normalize IE\'s 1223 status code into 204', function() { + var callback = jasmine.createSpy('XHR'); + + browser.xhr('GET', 'URL', 'POST', callback); + + xhr.status = 1223; + xhr.readyState = 4; + xhr.onreadystatechange(); + + expect(callback).toHaveBeenCalled(); + expect(callback.argsForCall[0][0]).toEqual(204); + }); + + it('should set only the requested headers', function() { + var code, response, headers = {}; + browser.xhr('POST', 'URL', null, function(c,r){ + code = c; + response = r; + }, {'X-header1': 'value1', 'X-header2': 'value2'}); + + expect(xhr.method).toEqual('POST'); + expect(xhr.url).toEqual('URL'); + expect(xhr.post).toEqual(''); + expect(xhr.headers).toEqual({ + "X-header1":"value1", + "X-header2":"value2" + }); + + xhr.status = 202; + xhr.responseText = 'RESPONSE'; + xhr.readyState = 4; + xhr.onreadystatechange(); + + expect(code).toEqual(202); + expect(response).toEqual('RESPONSE'); + }); + }); + + describe('defer', function() { + it('should execute fn asynchroniously via setTimeout', function() { + var callback = jasmine.createSpy('deferred'); + + browser.defer(callback); + expect(callback).not.toHaveBeenCalled(); + + fakeWindow.setTimeout.flush(); + expect(callback).toHaveBeenCalledOnce(); + }); + + + it('should update outstandingRequests counter', function() { + var callback = jasmine.createSpy('deferred'); + + browser.defer(callback); + expect(callback).not.toHaveBeenCalled(); + + fakeWindow.setTimeout.flush(); + expect(callback).toHaveBeenCalledOnce(); + }); + + + it('should return unique deferId', function() { + var deferId1 = browser.defer(noop), + deferId2 = browser.defer(noop); + + expect(deferId1).toBeDefined(); + expect(deferId2).toBeDefined(); + expect(deferId1).not.toEqual(deferId2); + }); + + + describe('cancel', function() { + it('should allow tasks to be canceled with returned deferId', function() { + var log = [], + deferId1 = browser.defer(function() { log.push('cancel me'); }), + deferId2 = browser.defer(function() { log.push('ok'); }), + deferId3 = browser.defer(function() { log.push('cancel me, now!'); }); + + expect(log).toEqual([]); + expect(browser.defer.cancel(deferId1)).toBe(true); + expect(browser.defer.cancel(deferId3)).toBe(true); + fakeWindow.setTimeout.flush(); + expect(log).toEqual(['ok']); + expect(browser.defer.cancel(deferId2)).toBe(false); + }); + }); + }); + + + describe('cookies', function() { + + function deleteAllCookies() { + var cookies = document.cookie.split(";"); + + for (var i = 0; i < cookies.length; i++) { + var cookie = cookies[i]; + var eqPos = cookie.indexOf("="); + var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie; + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; + } + } + + beforeEach(function() { + deleteAllCookies(); + expect(document.cookie).toEqual(''); + }); + + + afterEach(function() { + deleteAllCookies(); + expect(document.cookie).toEqual(''); + }); + + + describe('remove all via (null)', function() { + + it('should do nothing when no cookies are set', function() { + browser.cookies(null); + expect(document.cookie).toEqual(''); + expect(browser.cookies()).toEqual({}); + }); + + }); + + describe('remove via cookies(cookieName, undefined)', function() { + + it('should remove a cookie when it is present', function() { + document.cookie = 'foo=bar'; + + browser.cookies('foo', undefined); + + expect(document.cookie).toEqual(''); + expect(browser.cookies()).toEqual({}); + }); + + + it('should do nothing when an nonexisting cookie is being removed', function() { + browser.cookies('doesntexist', undefined); + expect(document.cookie).toEqual(''); + expect(browser.cookies()).toEqual({}); + }); + }); + + + describe('put via cookies(cookieName, string)', function() { + + it('should create and store a cookie', function() { + browser.cookies('cookieName', 'cookie=Value'); + expect(document.cookie).toMatch(/cookieName=cookie%3DValue;? ?/); + expect(browser.cookies()).toEqual({'cookieName':'cookie=Value'}); + }); + + + it('should overwrite an existing unsynced cookie', function() { + document.cookie = "cookie=new"; + + var oldVal = browser.cookies('cookie', 'newer'); + + expect(document.cookie).toEqual('cookie=newer'); + expect(browser.cookies()).toEqual({'cookie':'newer'}); + expect(oldVal).not.toBeDefined(); + }); + + it('should escape both name and value', function() { + browser.cookies('cookie1=', 'val;ue'); + browser.cookies('cookie2=bar;baz', 'val=ue'); + + var rawCookies = document.cookie.split("; "); //order is not guaranteed, so we need to parse + expect(rawCookies.length).toEqual(2); + expect(rawCookies).toContain('cookie1%3D=val%3Bue'); + expect(rawCookies).toContain('cookie2%3Dbar%3Bbaz=val%3Due'); + }); + + it('should log warnings when 4kb per cookie storage limit is reached', function() { + var i, longVal = '', cookieStr; + + for(i=0; i<4091; i++) { + longVal += '+'; + } + + cookieStr = document.cookie; + browser.cookies('x', longVal); //total size 4093-4096, so it should go through + expect(document.cookie).not.toEqual(cookieStr); + expect(browser.cookies()['x']).toEqual(longVal); + expect(logs.warn).toEqual([]); + + browser.cookies('x', longVal + 'xxxx'); //total size 4097-4099, a warning should be logged + expect(logs.warn).toEqual( + [[ "Cookie 'x' possibly not set or overflowed because it was too large (4097 > 4096 " + + "bytes)!" ]]); + + //force browser to dropped a cookie and make sure that the cache is not out of sync + browser.cookies('x', 'shortVal'); + expect(browser.cookies().x).toEqual('shortVal'); //needed to prime the cache + cookieStr = document.cookie; + browser.cookies('x', longVal + longVal + longVal); //should be too long for all browsers + + if (document.cookie !== cookieStr) { + fail("browser didn't drop long cookie when it was expected. make the cookie in this " + + "test longer"); + } + + expect(browser.cookies().x).toEqual('shortVal'); + }); + + it('should log warnings when 20 cookies per domain storage limit is reached', function() { + var i, str, cookieStr; + + for (i=0; i<20; i++) { + str = '' + i; + browser.cookies(str, str); + } + + i=0; + for (str in browser.cookies()) { + i++; + } + expect(i).toEqual(20); + expect(logs.warn).toEqual([]); + cookieStr = document.cookie; + + browser.cookies('one', 'more'); + expect(logs.warn).toEqual([]); + + //if browser dropped a cookie (very likely), make sure that the cache is not out of sync + if (document.cookie === cookieStr) { + expect(size(browser.cookies())).toEqual(20); + } else { + expect(size(browser.cookies())).toEqual(21); + } + }); + }); + + + describe('get via cookies()[cookieName]', function() { + + it('should return undefined for nonexistent cookie', function() { + expect(browser.cookies().nonexistent).not.toBeDefined(); + }); + + + it ('should return a value for an existing cookie', function() { + document.cookie = "foo=bar=baz"; + expect(browser.cookies().foo).toEqual('bar=baz'); + }); + + + it ('should unescape cookie values that were escaped by puts', function() { + document.cookie = "cookie2%3Dbar%3Bbaz=val%3Due"; + expect(browser.cookies()['cookie2=bar;baz']).toEqual('val=ue'); + }); + + + it('should preserve leading & trailing spaces in names and values', function() { + browser.cookies(' cookie name ', ' cookie value '); + expect(browser.cookies()[' cookie name ']).toEqual(' cookie value '); + expect(browser.cookies()['cookie name']).not.toBeDefined(); + }); + }); + + + describe('getAll via cookies()', function() { + + it('should return cookies as hash', function() { + document.cookie = "foo1=bar1"; + document.cookie = "foo2=bar2"; + expect(browser.cookies()).toEqual({'foo1':'bar1', 'foo2':'bar2'}); + }); + + + it('should return empty hash if no cookies exist', function() { + expect(browser.cookies()).toEqual({}); + }); + }); + + + it('should pick up external changes made to browser cookies', function() { + browser.cookies('oatmealCookie', 'drool'); + expect(browser.cookies()).toEqual({'oatmealCookie':'drool'}); + + document.cookie = 'oatmealCookie=changed'; + expect(browser.cookies().oatmealCookie).toEqual('changed'); + }); + + + it('should initialize cookie cache with existing cookies', function() { + document.cookie = "existingCookie=existingValue"; + expect(browser.cookies()).toEqual({'existingCookie':'existingValue'}); + }); + + }); + + describe('poller', function() { + + it('should call functions in pollFns in regular intervals', function() { + var log = ''; + browser.addPollFn(function() {log+='a';}); + browser.addPollFn(function() {log+='b';}); + expect(log).toEqual(''); + fakeWindow.setTimeout.flush(); + expect(log).toEqual('ab'); + fakeWindow.setTimeout.flush(); + expect(log).toEqual('abab'); + }); + + it('should startPoller', function() { + expect(fakeWindow.timeouts.length).toEqual(0); + + browser.addPollFn(function() {}); + expect(fakeWindow.timeouts.length).toEqual(1); + + //should remain 1 as it is the check fn + browser.addPollFn(function() {}); + expect(fakeWindow.timeouts.length).toEqual(1); + }); + + it('should return fn that was passed into addPollFn', function() { + var fn = function() { return 1; }; + var returnedFn = browser.addPollFn(fn); + expect(returnedFn).toBe(fn); + }); + }); + + describe('url', function() { + var pushState, replaceState, locationReplace; + + beforeEach(function() { + pushState = spyOn(fakeWindow.history, 'pushState'); + replaceState = spyOn(fakeWindow.history, 'replaceState'); + locationReplace = spyOn(fakeWindow.location, 'replace'); + }); + + it('should return current location.href', function() { + fakeWindow.location.href = 'http://test.com'; + expect(browser.url()).toEqual('http://test.com'); + + fakeWindow.location.href = 'https://another.com'; + expect(browser.url()).toEqual('https://another.com'); + }); + + it('should use history.pushState when available', function() { + sniffer.history = true; + browser.url('http://new.org'); + + expect(pushState).toHaveBeenCalledOnce(); + expect(pushState.argsForCall[0][2]).toEqual('http://new.org'); + + expect(replaceState).not.toHaveBeenCalled(); + expect(locationReplace).not.toHaveBeenCalled(); + expect(fakeWindow.location.href).toEqual('http://server'); + }); + + it('should use history.replaceState when available', function() { + sniffer.history = true; + browser.url('http://new.org', true); + + expect(replaceState).toHaveBeenCalledOnce(); + expect(replaceState.argsForCall[0][2]).toEqual('http://new.org'); + + expect(pushState).not.toHaveBeenCalled(); + expect(locationReplace).not.toHaveBeenCalled(); + expect(fakeWindow.location.href).toEqual('http://server'); + }); + + it('should set location.href when pushState not available', function() { + sniffer.history = false; + browser.url('http://new.org'); + + expect(fakeWindow.location.href).toEqual('http://new.org'); + + expect(pushState).not.toHaveBeenCalled(); + expect(replaceState).not.toHaveBeenCalled(); + expect(locationReplace).not.toHaveBeenCalled(); + }); + + it('should use location.replace when history.replaceState not available', function() { + sniffer.history = false; + browser.url('http://new.org', true); + + expect(locationReplace).toHaveBeenCalledWith('http://new.org'); + + expect(pushState).not.toHaveBeenCalled(); + expect(replaceState).not.toHaveBeenCalled(); + expect(fakeWindow.location.href).toEqual('http://server'); + }); + + it('should return $browser to allow chaining', function() { + expect(browser.url('http://any.com')).toBe(browser); + }); + }); + + describe('urlChange', function() { + var callback; + + beforeEach(function() { + callback = jasmine.createSpy('onUrlChange'); + }); + + afterEach(function() { + if (!jQuery) jqLite(fakeWindow).dealoc(); + }); + + it('should return registered callback', function() { + expect(browser.onUrlChange(callback)).toBe(callback); + }); + + it('should forward popstate event with new url when history supported', function() { + sniffer.history = true; + browser.onUrlChange(callback); + fakeWindow.location.href = 'http://server/new'; + + fakeWindow.fire('popstate'); + expect(callback).toHaveBeenCalledWith('http://server/new'); + + fakeWindow.fire('hashchange'); + fakeWindow.setTimeout.flush(); + expect(callback).toHaveBeenCalledOnce(); + }); + + it('should forward only popstate event when both history and hashchange supported', function() { + sniffer.history = true; + sniffer.hashchange = true; + browser.onUrlChange(callback); + fakeWindow.location.href = 'http://server/new'; + + fakeWindow.fire('popstate'); + expect(callback).toHaveBeenCalledWith('http://server/new'); + + fakeWindow.fire('hashchange'); + fakeWindow.setTimeout.flush(); + expect(callback).toHaveBeenCalledOnce(); + }); + + it('should forward hashchange event with new url when only hashchange supported', function() { + sniffer.history = false; + sniffer.hashchange = true; + browser.onUrlChange(callback); + fakeWindow.location.href = 'http://server/new'; + + fakeWindow.fire('hashchange'); + expect(callback).toHaveBeenCalledWith('http://server/new'); + + fakeWindow.fire('popstate'); + fakeWindow.setTimeout.flush(); + expect(callback).toHaveBeenCalledOnce(); + }); + + it('should use polling when neither history nor hashchange supported', function() { + sniffer.history = false; + sniffer.hashchange = false; + browser.onUrlChange(callback); + + fakeWindow.location.href = 'http://server.new'; + fakeWindow.setTimeout.flush(); + expect(callback).toHaveBeenCalledWith('http://server.new'); + + fakeWindow.fire('popstate'); + fakeWindow.fire('hashchange'); + expect(callback).toHaveBeenCalledOnce(); + }); + + it('should not fire urlChange if changed by browser.url method (polling)', function() { + sniffer.history = false; + sniffer.hashchange = false; + browser.onUrlChange(callback); + browser.url('http://new.com'); + + fakeWindow.setTimeout.flush(); + expect(callback).not.toHaveBeenCalled(); + }); + + it('should not fire urlChange if changed by browser.url method (hashchange)', function() { + sniffer.history = false; + sniffer.hashchange = true; + browser.onUrlChange(callback); + browser.url('http://new.com'); + + fakeWindow.fire('hashchange'); + expect(callback).not.toHaveBeenCalled(); + }); + }); + + describe('addJs', function() { + it('should append a script tag to body', function() { + browser.addJs('http://localhost/bar.js'); + expect(scripts.length).toBe(1); + expect(scripts[0].src).toBe('http://localhost/bar.js'); + expect(scripts[0].id).toBe(''); + }); + + it('should return the appended script element', function() { + var script = browser.addJs('http://localhost/bar.js'); + expect(script).toBe(scripts[0]); + }); + }); + + describe('baseHref', function() { + var jqDocHead; + + function setDocumentBaseHrefTo(href) { + clearDocumentBaseHref(); + jqDocHead.append(''); + } + + function clearDocumentBaseHref() { + jqDocHead.find('base').remove(); + } + + beforeEach(function() { + jqDocHead = jqLite(document).find('head'); + }); + + afterEach(clearDocumentBaseHref); + + it('should return value from ', function() { + setDocumentBaseHrefTo('/base/path/'); + expect(browser.baseHref()).toEqual('/base/path/'); + }); + + it('should return undefined if no ', function() { + expect(browser.baseHref()).toBeUndefined(); + }); + + it('should remove domain from ', function() { + setDocumentBaseHrefTo('http://host.com/base/path/'); + expect(browser.baseHref()).toEqual('/base/path/'); + + setDocumentBaseHrefTo('http://host.com/base/path/index.html'); + expect(browser.baseHref()).toEqual('/base/path/index.html'); + }); + }); +}); -- cgit v1.2.3