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:
- *
- * - Content-Type: application/x-www-form-urlencoded
- * - Accept: application/json, text/plain, */*
- * - X-Requested-With: XMLHttpRequest
- *
- *
- * @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:
- *
- * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it
- * - cookies(name, value) -> set name to value, if value is undefined delete the cookie
- * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)
- *
- *
- * @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:
+ *
+ * - Content-Type: application/x-www-form-urlencoded
+ * - Accept: application/json, text/plain, */*
+ * - X-Requested-With: XMLHttpRequest
+ *
+ *
+ * @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:
+ *
+ * - cookies() -> hash of all cookies, this is NOT a copy of the internal state, so do not modify it
+ * - cookies(name, value) -> set name to value, if value is undefined delete the cookie
+ * - cookies(name) -> the same as (name, undefined) == DELETES (no one calls it right now that way)
+ *
+ *
+ * @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