diff options
| author | Igor Minar | 2011-02-15 01:12:45 -0500 | 
|---|---|---|
| committer | Igor Minar | 2011-02-15 11:01:53 -0500 | 
| commit | 1777110958f76ee4be5760e36c96702223385918 (patch) | |
| tree | 5aa03b246507e66877e5eac69e58e004e244d7a5 | |
| parent | d2089a16335276eecb8d81eb17332c2dff2cf1a2 (diff) | |
| download | angular.js-1777110958f76ee4be5760e36c96702223385918.tar.bz2 | |
split up services into individual files
- split up services into files under src/service
- split up specs into files under test/service
- rewrite all specs so that they don't depend on one global forEach
- get rid of obsolete code and tests in ng:switch
- rename mock $log spec from "$log" to "$log mock"
46 files changed, 2842 insertions, 2642 deletions
| @@ -15,7 +15,23 @@ ANGULAR = [    'src/filters.js',    'src/formatters.js',    'src/validators.js', -  'src/services.js', +  'src/service/cookieStore.js', +  'src/service/cookies.js', +  'src/service/defer.js', +  'src/service/document.js', +  'src/service/exceptionHandler.js', +  'src/service/hover.js', +  'src/service/invalidWidgets.js', +  'src/service/location.js', +  'src/service/log.js', +  'src/service/resource.js', +  'src/service/route.js', +  'src/service/updateView.js', +  'src/service/window.js', +  'src/service/xhr.bulk.js', +  'src/service/xhr.cache.js', +  'src/service/xhr.error.js', +  'src/service/xhr.js',    'src/directives.js',    'src/markups.js',    'src/widgets.js', diff --git a/jsTestDriver-coverage.conf b/jsTestDriver-coverage.conf index 21ee8a2e..b8989811 100644 --- a/jsTestDriver-coverage.conf +++ b/jsTestDriver-coverage.conf @@ -8,6 +8,7 @@ load:    - src/Angular.js    - src/JSON.js    - src/*.js +  - src/service/*.js    - example/personalLog/*.js    - test/testabilityPatch.js    - src/scenario/Scenario.js @@ -18,6 +19,7 @@ load:    - test/scenario/*.js    - test/scenario/output/*.js    - test/*.js +  - test/service/*.js    - example/personalLog/test/*.js  exclude: diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf index 217a354a..9ae54022 100644 --- a/jsTestDriver-jquery.conf +++ b/jsTestDriver-jquery.conf @@ -8,6 +8,7 @@ load:    - src/Angular.js    - src/JSON.js    - src/*.js +  - src/service/*.js    - example/personalLog/*.js    - test/testabilityPatch.js    - src/scenario/Scenario.js @@ -18,6 +19,7 @@ load:    - test/scenario/*.js    - test/scenario/output/*.js    - test/*.js +  - test/service/*.js    - example/personalLog/test/*.js  exclude: diff --git a/jsTestDriver.conf b/jsTestDriver.conf index 8bb59ef1..204594d4 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -8,6 +8,7 @@ load:    - src/Angular.js    - src/JSON.js    - src/*.js +  - src/service/*.js    - example/personalLog/*.js    - test/testabilityPatch.js    - src/scenario/Scenario.js @@ -18,6 +19,7 @@ load:    - test/scenario/*.js    - test/scenario/output/*.js    - test/*.js +  - test/service/*.js    - example/personalLog/test/*.js  exclude: diff --git a/lib/jsl/jsl.default.conf b/lib/jsl/jsl.default.conf index d3cfe4fe..f4e2af16 100755 --- a/lib/jsl/jsl.default.conf +++ b/lib/jsl/jsl.default.conf @@ -122,7 +122,9 @@  # or "+process Folder\Path\*.htm".  #  +process src/*.js ++process src/service/*.js  +process src/scenario/*.js  +process test/*.js ++process test/service/*.js  +process test/scenario/*.js diff --git a/src/Injector.js b/src/Injector.js index c7ee6f82..295928bf 100644 --- a/src/Injector.js +++ b/src/Injector.js @@ -76,3 +76,7 @@ function injectService(services, fn) {  function injectUpdateView(fn) {    return injectService(['$updateView'], fn);  } + +function angularServiceInject(name, fn, inject, eager) { +  angularService(name, fn, {$inject:inject, $eager:eager}); +} diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index 427c93c8..d6297ccd 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -118,7 +118,25 @@               'AngularPublic.js',               // Extension points -             'services.js', + +             'service/cookieStore.js', +             'service/cookies.js', +             'service/defer.js', +             'service/document.js', +             'service/exceptionHandler.js', +             'service/hover.js', +             'service/invalidWidgets.js', +             'service/location.js', +             'service/log.js', +             'service/resource.js', +             'service/route.js', +             'service/updateView.js', +             'service/window.js', +             'service/xhr.bulk.js', +             'service/xhr.cache.js', +             'service/xhr.error.js', +             'service/xhr.js', +               'apis.js',               'filters.js',               'formatters.js', diff --git a/src/service/cookieStore.js b/src/service/cookieStore.js new file mode 100644 index 00000000..089e4578 --- /dev/null +++ b/src/service/cookieStore.js @@ -0,0 +1,64 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$cookieStore + * @requires $cookies + * + * @description + * Provides a key-value (string-object) storage, that is backed by session cookies. + * Objects put or retrieved from this storage are automatically serialized or + * deserialized by angular's toJson/fromJson. + * @example + */ +angularServiceInject('$cookieStore', function($store) { + +  return { +    /** +     * @workInProgress +     * @ngdoc method +     * @name angular.service.$cookieStore#get +     * @methodOf angular.service.$cookieStore +     * +     * @description +     * Returns the value of given cookie key +     * +     * @param {string} key Id to use for lookup. +     * @returns {Object} Deserialized cookie value. +     */ +    get: function(key) { +      return fromJson($store[key]); +    }, + +    /** +     * @workInProgress +     * @ngdoc method +     * @name angular.service.$cookieStore#put +     * @methodOf angular.service.$cookieStore +     * +     * @description +     * Sets a value for given cookie key +     * +     * @param {string} key Id for the `value`. +     * @param {Object} value Value to be stored. +     */ +    put: function(key, value) { +      $store[key] = toJson(value); +    }, + +    /** +     * @workInProgress +     * @ngdoc method +     * @name angular.service.$cookieStore#remove +     * @methodOf angular.service.$cookieStore +     * +     * @description +     * Remove given cookie +     * +     * @param {string} key Id of the key-value pair to delete. +     */ +    remove: function(key) { +      delete $store[key]; +    } +  }; + +}, ['$cookies']); diff --git a/src/service/cookies.js b/src/service/cookies.js new file mode 100644 index 00000000..082b73ab --- /dev/null +++ b/src/service/cookies.js @@ -0,0 +1,89 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$cookies + * @requires $browser + * + * @description + * Provides read/write access to browser's cookies. + * + * Only a simple Object is exposed and by adding or removing properties to/from + * this object, new cookies are created/deleted at the end of current $eval. + * + * @example + */ +angularServiceInject('$cookies', function($browser) { +  var rootScope = this, +      cookies = {}, +      lastCookies = {}, +      lastBrowserCookies; + +  //creates a poller fn that copies all cookies from the $browser to service & inits the service +  $browser.addPollFn(function() { +    var currentCookies = $browser.cookies(); +    if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl +      lastBrowserCookies = currentCookies; +      copy(currentCookies, lastCookies); +      copy(currentCookies, cookies); +      rootScope.$eval(); +    } +  })(); + +  //at the end of each eval, push cookies +  //TODO: this should happen before the "delayed" watches fire, because if some cookies are not +  //      strings or browser refuses to store some cookies, we update the model in the push fn. +  this.$onEval(PRIORITY_LAST, push); + +  return cookies; + + +  /** +   * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. +   */ +  function push(){ +    var name, +        value, +        browserCookies, +        updated; + +    //delete any cookies deleted in $cookies +    for (name in lastCookies) { +      if (isUndefined(cookies[name])) { +        $browser.cookies(name, _undefined); +      } +    } + +    //update all cookies updated in $cookies +    for(name in cookies) { +      value = cookies[name]; +      if (!isString(value)) { +        if (isDefined(lastCookies[name])) { +          cookies[name] = lastCookies[name]; +        } else { +          delete cookies[name]; +        } +      } else if (value !== lastCookies[name]) { +        $browser.cookies(name, value); +        updated = true; +      } +    } + +    //verify what was actually stored +    if (updated){ +      updated = false; +      browserCookies = $browser.cookies(); + +      for (name in cookies) { +        if (cookies[name] !== browserCookies[name]) { +          //delete or reset all cookies that the browser dropped from $cookies +          if (isUndefined(browserCookies[name])) { +            delete cookies[name]; +          } else { +            cookies[name] = browserCookies[name]; +          } +          updated = true; +        } +      } +    } +  } +}, ['$browser']); diff --git a/src/service/defer.js b/src/service/defer.js new file mode 100644 index 00000000..21bf139b --- /dev/null +++ b/src/service/defer.js @@ -0,0 +1,32 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$defer + * @requires $browser + * @requires $exceptionHandler + * @requires $updateView + * + * @description + * Delegates to {@link angular.service.$browser.defer $browser.defer}, but wraps the `fn` function + * into a try/catch block and delegates any exceptions to + * {@link angular.services.$exceptionHandler $exceptionHandler} service. + * + * In tests you can use `$browser.defer.flush()` to flush the queue of deferred functions. + * + * @param {function()} fn A function, who's execution should be deferred. + */ +angularServiceInject('$defer', function($browser, $exceptionHandler, $updateView) { +  var scope = this; + +  return function(fn) { +    $browser.defer(function() { +      try { +        fn(); +      } catch(e) { +        $exceptionHandler(e); +      } finally { +        $updateView(); +      } +    }); +  }; +}, ['$browser', '$exceptionHandler', '$updateView']); diff --git a/src/service/document.js b/src/service/document.js new file mode 100644 index 00000000..93d4d9a5 --- /dev/null +++ b/src/service/document.js @@ -0,0 +1,12 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$document + * @requires $window + * + * @description + * Reference to the browser window.document, but wrapped into angular.element(). + */ +angularServiceInject("$document", function(window){ +  return jqLite(window.document); +}, ['$window'], true); diff --git a/src/service/exceptionHandler.js b/src/service/exceptionHandler.js new file mode 100644 index 00000000..dd99a373 --- /dev/null +++ b/src/service/exceptionHandler.js @@ -0,0 +1,22 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$exceptionHandler + * @requires $log + * + * @description + * Any uncaught exception in angular expressions is delegated to this service. + * The default implementation simply delegates to `$log.error` which logs it into + * the browser console. + * + * In unit tests, if `angular-mocks.js` is loaded, this service is overriden by + * {@link angular.mock.service.$exceptionHandler mock $exceptionHandler} + * + * @example + */ +var $exceptionHandlerFactory; //reference to be used only in tests +angularServiceInject('$exceptionHandler', $exceptionHandlerFactory = function($log){ +  return function(e) { +    $log.error(e); +  }; +}, ['$log'], true); diff --git a/src/service/hover.js b/src/service/hover.js new file mode 100644 index 00000000..a7cef71a --- /dev/null +++ b/src/service/hover.js @@ -0,0 +1,56 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$hover + * @requires $browser + * @requires $document + * + * @description + * + * @example + */ +angularServiceInject("$hover", function(browser, document) { +  var tooltip, self = this, error, width = 300, arrowWidth = 10, body = jqLite(document[0].body); +  browser.hover(function(element, show){ +    if (show && (error = element.attr(NG_EXCEPTION) || element.attr(NG_VALIDATION_ERROR))) { +      if (!tooltip) { +        tooltip = { +            callout: jqLite('<div id="ng-callout"></div>'), +            arrow: jqLite('<div></div>'), +            title: jqLite('<div class="ng-title"></div>'), +            content: jqLite('<div class="ng-content"></div>') +        }; +        tooltip.callout.append(tooltip.arrow); +        tooltip.callout.append(tooltip.title); +        tooltip.callout.append(tooltip.content); +        body.append(tooltip.callout); +      } +      var docRect = body[0].getBoundingClientRect(), +          elementRect = element[0].getBoundingClientRect(), +          leftSpace = docRect.right - elementRect.right - arrowWidth; +      tooltip.title.text(element.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error..."); +      tooltip.content.text(error); +      if (leftSpace < width) { +        tooltip.arrow.addClass('ng-arrow-right'); +        tooltip.arrow.css({left: (width + 1)+'px'}); +        tooltip.callout.css({ +          position: 'fixed', +          left: (elementRect.left - arrowWidth - width - 4) + "px", +          top: (elementRect.top - 3) + "px", +          width: width + "px" +        }); +      } else { +        tooltip.arrow.addClass('ng-arrow-left'); +        tooltip.callout.css({ +          position: 'fixed', +          left: (elementRect.right + arrowWidth) + "px", +          top: (elementRect.top - 3) + "px", +          width: width + "px" +        }); +      } +    } else if (tooltip) { +      tooltip.callout.remove(); +      tooltip = _null; +    } +  }); +}, ['$browser', '$document'], true); diff --git a/src/service/invalidWidgets.js b/src/service/invalidWidgets.js new file mode 100644 index 00000000..af31d61d --- /dev/null +++ b/src/service/invalidWidgets.js @@ -0,0 +1,67 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$invalidWidgets + * + * @description + * Keeps references to all invalid widgets found during validation. + * Can be queried to find whether there are any invalid widgets currently displayed. + * + * @example + */ +angularServiceInject("$invalidWidgets", function(){ +  var invalidWidgets = []; + + +  /** Remove an element from the array of invalid widgets */ +  invalidWidgets.markValid = function(element){ +    var index = indexOf(invalidWidgets, element); +    if (index != -1) +      invalidWidgets.splice(index, 1); +  }; + + +  /** Add an element to the array of invalid widgets */ +  invalidWidgets.markInvalid = function(element){ +    var index = indexOf(invalidWidgets, element); +    if (index === -1) +      invalidWidgets.push(element); +  }; + + +  /** Return count of all invalid widgets that are currently visible */ +  invalidWidgets.visible = function() { +    var count = 0; +    forEach(invalidWidgets, function(widget){ +      count = count + (isVisible(widget) ? 1 : 0); +    }); +    return count; +  }; + + +  /* At the end of each eval removes all invalid widgets that are not part of the current DOM. */ +  this.$onEval(PRIORITY_LAST, function() { +    for(var i = 0; i < invalidWidgets.length;) { +      var widget = invalidWidgets[i]; +      if (isOrphan(widget[0])) { +        invalidWidgets.splice(i, 1); +        if (widget.dealoc) widget.dealoc(); +      } else { +        i++; +      } +    } +  }); + + +  /** +   * Traverses DOM element's (widget's) parents and considers the element to be an orphant if one of +   * it's parents isn't the current window.document. +   */ +  function isOrphan(widget) { +    if (widget == window.document) return false; +    var parent = widget.parentNode; +    return !parent || isOrphan(parent); +  } + +  return invalidWidgets; +}, [], true); diff --git a/src/service/location.js b/src/service/location.js new file mode 100644 index 00000000..31323284 --- /dev/null +++ b/src/service/location.js @@ -0,0 +1,264 @@ +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, +    HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/, +    DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}; + +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$location + * @requires $browser + * + * @property {string} href + * @property {string} protocol + * @property {string} host + * @property {number} port + * @property {string} path + * @property {Object.<string|boolean>} search + * @property {string} hash + * @property {string} hashPath + * @property {Object.<string|boolean>} hashSearch + * + * @description + * Parses the browser location url and makes it available to your application. + * Any changes to the url are reflected into $location service and changes to + * $location are reflected to url. + * Notice that using browser's forward/back buttons changes the $location. + * + * @example +   <doc:example> +     <doc:source> +       <a href="#">clear hash</a> | +       <a href="#myPath?name=misko">test hash</a><br/> +       <input type='text' name="$location.hash"/> +       <pre>$location = {{$location}}</pre> +     </doc:source> +     <doc:scenario> +     </doc:scenario> +    </doc:example> + */ +angularServiceInject("$location", function($browser) { +  var scope = this, +      location = {update:update, updateHash: updateHash}, +      lastLocation = {}; + +  $browser.onHashChange(function() { //register +    update($browser.getUrl()); +    copy(location, lastLocation); +    scope.$eval(); +  })(); //initialize + +  this.$onEval(PRIORITY_FIRST, sync); +  this.$onEval(PRIORITY_LAST, updateBrowser); + +  return location; + +  // PUBLIC METHODS + +  /** +   * @workInProgress +   * @ngdoc method +   * @name angular.service.$location#update +   * @methodOf angular.service.$location +   * +   * @description +   * Update location object +   * Does not immediately update the browser +   * Browser is updated at the end of $eval() +   * +   * @example +    <doc:example> +      <doc:source> +        scope.$location.update('http://www.angularjs.org/path#hash?search=x'); +        scope.$location.update({host: 'www.google.com', protocol: 'https'}); +        scope.$location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}}); +      </doc:source> +      <doc:scenario> +      </doc:scenario> +    </doc:example> +   * +   * @param {(string|Object)} href Full href as a string or object with properties +   */ +  function update(href) { +    if (isString(href)) { +      extend(location, parseHref(href)); +    } else { +      if (isDefined(href.hash)) { +        extend(href, isString(href.hash) ? parseHash(href.hash) : href.hash); +      } + +      extend(location, href); + +      if (isDefined(href.hashPath || href.hashSearch)) { +        location.hash = composeHash(location); +      } + +      location.href = composeHref(location); +    } +  } + +  /** +   * @workInProgress +   * @ngdoc method +   * @name angular.service.$location#updateHash +   * @methodOf angular.service.$location +   * +   * @description +   * Update location hash part +   * @see update() +   * +   * @example +    <doc:example> +      <doc:source> +        scope.$location.updateHash('/hp') +         ==> update({hashPath: '/hp'}) +        scope.$location.updateHash({a: true, b: 'val'}) +         ==> update({hashSearch: {a: true, b: 'val'}}) +        scope.$location.updateHash('/hp', {a: true}) +         ==> update({hashPath: '/hp', hashSearch: {a: true}}) +      </doc:source> +      <doc:scenario> +      </doc:scenario> +    </doc:example> +   * +   * @param {(string|Object)} path A hashPath or hashSearch object +   * @param {Object=} search A hashSearch object +   */ +  function updateHash(path, search) { +    var hash = {}; + +    if (isString(path)) { +      hash.hashPath = path; +      hash.hashSearch = search || {}; +    } else +      hash.hashSearch = path; + +    hash.hash = composeHash(hash); + +    update({hash: hash}); +  } + + +  // INNER METHODS + +  /** +   * Synchronizes all location object properties. +   * +   * User is allowed to change properties, so after property change, +   * location object is not in consistent state. +   * +   * Properties are synced with the following precedence order: +   * +   * - `$location.href` +   * - `$location.hash` +   * - everything else +   * +   * @example +   * <pre> +   *   scope.$location.href = 'http://www.angularjs.org/path#a/b' +   * </pre> +   * immediately after this call, other properties are still the old ones... +   * +   * This method checks the changes and update location to the consistent state +   */ +  function sync() { +    if (!equals(location, lastLocation)) { +      if (location.href != lastLocation.href) { +        update(location.href); +        return; +      } +      if (location.hash != lastLocation.hash) { +        var hash = parseHash(location.hash); +        updateHash(hash.hashPath, hash.hashSearch); +      } else { +        location.hash = composeHash(location); +        location.href = composeHref(location); +      } +      update(location.href); +    } +  } + + +  /** +   * If location has changed, update the browser +   * This method is called at the end of $eval() phase +   */ +  function updateBrowser() { +    sync(); + +    if ($browser.getUrl() != location.href) { +      $browser.setUrl(location.href); +      copy(location, lastLocation); +    } +  } + +  /** +   * Compose href string from a location object +   * +   * @param {Object} loc The location object with all properties +   * @return {string} Composed href +   */ +  function composeHref(loc) { +    var url = toKeyValue(loc.search); +    var port = (loc.port == DEFAULT_PORTS[loc.protocol] ? _null : loc.port); + +    return loc.protocol  + '://' + loc.host + +          (port ? ':' + port : '') + loc.path + +          (url ? '?' + url : '') + (loc.hash ? '#' + loc.hash : ''); +  } + +  /** +   * Compose hash string from location object +   * +   * @param {Object} loc Object with hashPath and hashSearch properties +   * @return {string} Hash string +   */ +  function composeHash(loc) { +    var hashSearch = toKeyValue(loc.hashSearch); +    //TODO: temporary fix for issue #158 +    return escape(loc.hashPath).replace(/%21/gi, '!').replace(/%3A/gi, ':').replace(/%24/gi, '$') + +          (hashSearch ? '?' + hashSearch : ''); +  } + +  /** +   * Parse href string into location object +   * +   * @param {string} href +   * @return {Object} The location object +   */ +  function parseHref(href) { +    var loc = {}; +    var match = URL_MATCH.exec(href); + +    if (match) { +      loc.href = href.replace(/#$/, ''); +      loc.protocol = match[1]; +      loc.host = match[3] || ''; +      loc.port = match[5] || DEFAULT_PORTS[loc.protocol] || _null; +      loc.path = match[6] || ''; +      loc.search = parseKeyValue(match[8]); +      loc.hash = match[10] || ''; + +      extend(loc, parseHash(loc.hash)); +    } + +    return loc; +  } + +  /** +   * Parse hash string into object +   * +   * @param {string} hash +   */ +  function parseHash(hash) { +    var h = {}; +    var match = HASH_MATCH.exec(hash); + +    if (match) { +      h.hash = hash; +      h.hashPath = unescape(match[1] || ''); +      h.hashSearch = parseKeyValue(match[3]); +    } + +    return h; +  } +}, ['$browser']); diff --git a/src/service/log.js b/src/service/log.js new file mode 100644 index 00000000..e1e3f2e6 --- /dev/null +++ b/src/service/log.js @@ -0,0 +1,92 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$log + * @requires $window + * + * @description + * Simple service for logging. Default implementation writes the message + * into the browser's console (if present). + * + * The main purpose of this service is to simplify debugging and troubleshooting. + * + * @example +    <doc:example> +      <doc:source> +         <p>Reload this page with open console, enter text and hit the log button...</p> +         Message: +         <input type="text" name="message" value="Hello World!"/> +         <button ng:click="$log.log(message)">log</button> +         <button ng:click="$log.warn(message)">warn</button> +         <button ng:click="$log.info(message)">info</button> +         <button ng:click="$log.error(message)">error</button> +      </doc:source> +      <doc:scenario> +      </doc:scenario> +    </doc:example> + */ +var $logFactory; //reference to be used only in tests +angularServiceInject("$log", $logFactory = function($window){ +  return { +    /** +     * @workInProgress +     * @ngdoc method +     * @name angular.service.$log#log +     * @methodOf angular.service.$log +     * +     * @description +     * Write a log message +     */ +    log: consoleLog('log'), + +    /** +     * @workInProgress +     * @ngdoc method +     * @name angular.service.$log#warn +     * @methodOf angular.service.$log +     * +     * @description +     * Write a warning message +     */ +    warn: consoleLog('warn'), + +    /** +     * @workInProgress +     * @ngdoc method +     * @name angular.service.$log#info +     * @methodOf angular.service.$log +     * +     * @description +     * Write an information message +     */ +    info: consoleLog('info'), + +    /** +     * @workInProgress +     * @ngdoc method +     * @name angular.service.$log#error +     * @methodOf angular.service.$log +     * +     * @description +     * Write an error message +     */ +    error: consoleLog('error') +  }; + +  function consoleLog(type) { +    var console = $window.console || {}; +    var logFn = console[type] || console.log || noop; +    if (logFn.apply) { +      return function(){ +        var args = []; +        forEach(arguments, function(arg){ +          args.push(formatError(arg)); +        }); +        return logFn.apply(console, args); +      }; +    } else { +      // we are IE, in which case there is nothing we can do +      return logFn; +    } +  } +}, ['$window'], true); diff --git a/src/service/resource.js b/src/service/resource.js new file mode 100644 index 00000000..9e86caa7 --- /dev/null +++ b/src/service/resource.js @@ -0,0 +1,204 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$resource + * @requires $xhr.cache + * + * @description + * Is a factory which creates a resource object that lets you interact with + * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. + * + * The returned resource object has action methods which provide high-level behaviors without + * the need to interact with the low level {@link angular.service.$xhr $xhr} service or + * raw XMLHttpRequest. + * + * @param {string} url A parameterized URL template with parameters prefixed by `:` as in + *   `/user/:username`. + * + * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in + *   `actions` methods. + * + *   Each key value in the parameter object is first bound to url template if present and then any + *   excess keys are appended to the url search query after the `?`. + * + *   Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + *   URL `/path/greet?salutation=Hello`. + * + *   If the parameter value is prefixed with `@` then the value of that parameter is extracted from + *   the data object (useful for non-GET operations). + * + * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the + *   default set of resource actions. The declaration should be created in the following format: + * + *       {action1: {method:?, params:?, isArray:?, verifyCache:?}, + *        action2: {method:?, params:?, isArray:?, verifyCache:?}, + *        ...} + * + *   Where: + * + *   - `action` – {string} – The name of action. This name becomes the name of the method on your + *     resource object. + *   - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, + *     and `JSON` (also known as JSONP). + *   - `params` – {object=} – Optional set of pre-bound parameters for this action. + *   - isArray – {boolean=} – If true then the returned object for this action is an array, see + *     `returns` section. + *   - verifyCache – {boolean=} – If true then whenever cache hit occurs, the object is returned and + *     an async request will be made to the server and the resources as well as the cache will be + *     updated when the response is received. + * + * @returns {Object} A resource "class" object with methods for the default set of resource actions + *   optionally extended with custom `actions`. The default set contains these actions: + * + *       { 'get':    {method:'GET'}, + *         'save':   {method:'POST'}, + *         'query':  {method:'GET', isArray:true}, + *         'remove': {method:'DELETE'}, + *         'delete': {method:'DELETE'} }; + * + *   Calling these methods invoke an {@link angular.service.$xhr} with the specified http method, + *   destination and parameters. When the data is returned from the server then the object is an + *   instance of the resource class `save`, `remove` and `delete` actions are available on it as + *   methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read, + *   update, delete) on server-side data like this: + *   <pre> +        var User = $resource('/user/:userId', {userId:'@id'}); +        var user = User.get({userId:123}, function(){ +          user.abc = true; +          user.$save(); +        }); +     </pre> + * + *   It is important to realize that invoking a $resource object method immediately returns an + *   empty reference (object or array depending on `isArray`). Once the data is returned from the + *   server the existing reference is populated with the actual data. This is a useful trick since + *   usually the resource is assigned to a model which is then rendered by the view. Having an empty + *   object results in no rendering, once the data arrives from the server then the object is + *   populated with the data and the view automatically re-renders itself showing the new data. This + *   means that in most case one never has to write a callback function for the action methods. + * + *   The action methods on the class object or instance object can be invoked with the following + *   parameters: + * + *   - HTTP GET "class" actions: `Resource.action([parameters], [callback])` + *   - non-GET "class" actions: `Resource.action(postData, [parameters], [callback])` + *   - non-GET instance actions:  `instance.$action([parameters], [callback])` + * + * + * @example + * + * # Credit card resource + * + * <pre> +     // Define CreditCard class +     var CreditCard = $resource('/user/:userId/card/:cardId', +      {userId:123, cardId:'@id'}, { +       charge: {method:'POST', params:{charge:true}} +      }); + +     // We can retrieve a collection from the server +     var cards = CreditCard.query(); +     // GET: /user/123/card +     // server returns: [ {id:456, number:'1234', name:'Smith'} ]; + +     var card = cards[0]; +     // each item is an instance of CreditCard +     expect(card instanceof CreditCard).toEqual(true); +     card.name = "J. Smith"; +     // non GET methods are mapped onto the instances +     card.$save(); +     // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} +     // server returns: {id:456, number:'1234', name: 'J. Smith'}; + +     // our custom method is mapped as well. +     card.$charge({amount:9.99}); +     // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} +     // server returns: {id:456, number:'1234', name: 'J. Smith'}; + +     // we can create an instance as well +     var newCard = new CreditCard({number:'0123'}); +     newCard.name = "Mike Smith"; +     newCard.$save(); +     // POST: /user/123/card {number:'0123', name:'Mike Smith'} +     // server returns: {id:789, number:'01234', name: 'Mike Smith'}; +     expect(newCard.id).toEqual(789); + * </pre> + * + * The object returned from this function execution is a resource "class" which has "static" method + * for each action in the definition. + * + * Calling these methods invoke `$xhr` on the `url` template with the given `method` and `params`. + * When the data is returned from the server then the object is an instance of the resource type and + * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD + * operations (create, read, update, delete) on server-side data. + +   <pre> +     var User = $resource('/user/:userId', {userId:'@id'}); +     var user = User.get({userId:123}, function(){ +       user.abc = true; +       user.$save(); +     }); +   </pre> + * + *     It's worth noting that the callback for `get`, `query` and other method gets passed in the + *     response that came from the server, so one could rewrite the above example as: + * +   <pre> +     var User = $resource('/user/:userId', {userId:'@id'}); +     User.get({userId:123}, function(u){ +       u.abc = true; +       u.$save(); +     }); +   </pre> + + * # Buzz client + +   Let's look at what a buzz client created with the `$resource` service looks like: +    <doc:example> +      <doc:source> +       <script> +         function BuzzController($resource) { +           this.Activity = $resource( +             'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments', +             {alt:'json', callback:'JSON_CALLBACK'}, +             {get:{method:'JSON', params:{visibility:'@self'}}, replies: {method:'JSON', params:{visibility:'@self', comments:'@comments'}}} +           ); +         } + +         BuzzController.prototype = { +           fetch: function() { +             this.activities = this.Activity.get({userId:this.userId}); +           }, +           expandReplies: function(activity) { +             activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id}); +           } +         }; +         BuzzController.$inject = ['$resource']; +       </script> + +       <div ng:controller="BuzzController"> +         <input name="userId" value="googlebuzz"/> +         <button ng:click="fetch()">fetch</button> +         <hr/> +         <div ng:repeat="item in activities.data.items"> +           <h1 style="font-size: 15px;"> +             <img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/> +             <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a> +             <a href ng:click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a> +           </h1> +           {{item.object.content | html}} +           <div ng:repeat="reply in item.replies.data.items" style="margin-left: 20px;"> +             <img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/> +             <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}} +           </div> +         </div> +       </div> +      </doc:source> +      <doc:scenario> +      </doc:scenario> +    </doc:example> + */ +angularServiceInject('$resource', function($xhr){ +  var resource = new ResourceFactory($xhr); +  return bind(resource, resource.route); +}, ['$xhr.cache']); diff --git a/src/service/route.js b/src/service/route.js new file mode 100644 index 00000000..2de484f6 --- /dev/null +++ b/src/service/route.js @@ -0,0 +1,266 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$route + * @requires $location + * + * @property {Object} current Reference to the current route definition. + * @property {Array.<Object>} routes Array of all configured routes. + * + * @description + * Watches `$location.hashPath` and tries to map the hash to an existing route + * definition. It is used for deep-linking URLs to controllers and views (HTML partials). + * + * The `$route` service is typically used in conjunction with {@link angular.widget.ng:view ng:view} + * widget. + * + * @example +   This example shows how changing the URL hash causes the <tt>$route</tt> +   to match a route against the URL, and the <tt>[[ng:include]]</tt> pulls in the partial. +   Try changing the URL in the input box to see changes. + +    <doc:example> +      <doc:source> +        <script> +          angular.service('myApp', function($route) { +            $route.when('/Book/:bookId', {template:'rsrc/book.html', controller:BookCntl}); +            $route.when('/Book/:bookId/ch/:chapterId', {template:'rsrc/chapter.html', controller:ChapterCntl}); +            $route.onChange(function() { +              $route.current.scope.params = $route.current.params; +            }); +          }, {$inject: ['$route']}); + +          function BookCntl() { +            this.name = "BookCntl"; +          } + +          function ChapterCntl() { +            this.name = "ChapterCntl"; +          } +        </script> + +        Chose: +        <a href="#/Book/Moby">Moby</a> | +        <a href="#/Book/Moby/ch/1">Moby: Ch1</a> | +        <a href="#/Book/Gatsby">Gatsby</a> | +        <a href="#/Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a><br/> +        <input type="text" name="$location.hashPath" size="80" /> +        <pre>$location={{$location}}</pre> +        <pre>$route.current.template={{$route.current.template}}</pre> +        <pre>$route.current.params={{$route.current.params}}</pre> +        <pre>$route.current.scope.name={{$route.current.scope.name}}</pre> +        <hr/> +        <ng:include src="$route.current.template" scope="$route.current.scope"/> +      </doc:source> +      <doc:scenario> +      </doc:scenario> +    </doc:example> + */ +angularServiceInject('$route', function(location, $updateView) { +  var routes = {}, +      onChange = [], +      matcher = switchRouteMatcher, +      parentScope = this, +      dirty = 0, +      $route = { +        routes: routes, + +        /** +         * @workInProgress +         * @ngdoc method +         * @name angular.service.$route#onChange +         * @methodOf angular.service.$route +         * +         * @param {function()} fn Function that will be called when `$route.current` changes. +         * @returns {function()} The registered function. +         * +         * @description +         * Register a handler function that will be called when route changes +         */ +        onChange: function(fn) { +          onChange.push(fn); +          return fn; +        }, + +        /** +         * @workInProgress +         * @ngdoc method +         * @name angular.service.$route#parent +         * @methodOf angular.service.$route +         * +         * @param {Scope} [scope=rootScope] Scope to be used as parent for newly created +         *    `$route.current.scope` scopes. +         * +         * @description +         * Sets a scope to be used as the parent scope for scopes created on route change. If not +         * set, defaults to the root scope. +         */ +        parent: function(scope) { +          if (scope) parentScope = scope; +        }, + +        /** +         * @workInProgress +         * @ngdoc method +         * @name angular.service.$route#when +         * @methodOf angular.service.$route +         * +         * @param {string} path Route path (matched against `$location.hash`) +         * @param {Object} params Mapping information to be assigned to `$route.current` on route +         *    match. +         * +         *    Object properties: +         * +         *    - `controller` – `{function()=}` – Controller fn that should be associated with newly +         *      created scope. +         *    - `template` – `{string=}` – path to an html template that should be used by +         *      {@link angular.widget.ng:view ng:view} or +         *      {@link angular.widget.ng:include ng:include} widgets. +         *    - `redirectTo` – {(string|function())=} – value to update +         *      {@link angular.service.$location $location} hash with and trigger route redirection. +         * +         *      If `redirectTo` is a function, it will be called with the following parameters: +         * +         *      - `{Object.<string>}` - route parameters extracted from the current +         *        `$location.hashPath` by applying the current route template. +         *      - `{string}` - current `$location.hash` +         *      - `{string}` - current `$location.hashPath` +         *      - `{string}` - current `$location.hashSearch` +         * +         *      The custom `redirectTo` function is expected to return a string which will be used +         *      to update `$location.hash`. +         * +         * @returns {Object} route object +         * +         * @description +         * Adds a new route definition to the `$route` service. +         */ +        when:function (path, params) { +          if (isUndefined(path)) return routes; //TODO(im): remove - not needed! +          var route = routes[path]; +          if (!route) route = routes[path] = {}; +          if (params) extend(route, params); +          dirty++; +          return route; +        }, + +        /** +         * @workInProgress +         * @ngdoc method +         * @name angular.service.$route#otherwise +         * @methodOf angular.service.$route +         * +         * @description +         * Sets route definition that will be used on route change when no other route definition +         * is matched. +         * +         * @param {Object} params Mapping information to be assigned to `$route.current`. +         */ +        otherwise: function(params) { +          $route.when(null, params); +        }, + +        /** +         * @workInProgress +         * @ngdoc method +         * @name angular.service.$route#reload +         * @methodOf angular.service.$route +         * +         * @description +         * Causes `$route` service to reload (and recreate the `$route.current` scope) upon the next +         * eval even if {@link angular.service.$location $location} hasn't changed. +         */ +        reload: function() { +          dirty++; +        } +      }; + + +  function switchRouteMatcher(on, when, dstName) { +    var regex = '^' + when.replace(/[\.\\\(\)\^\$]/g, "\$1") + '$', +        params = [], +        dst = {}; +    forEach(when.split(/\W/), function(param){ +      if (param) { +        var paramRegExp = new RegExp(":" + param + "([\\W])"); +        if (regex.match(paramRegExp)) { +          regex = regex.replace(paramRegExp, "([^\/]*)$1"); +          params.push(param); +        } +      } +    }); +    var match = on.match(new RegExp(regex)); +    if (match) { +      forEach(params, function(name, index){ +        dst[name] = match[index + 1]; +      }); +      if (dstName) this.$set(dstName, dst); +    } +    return match ? dst : _null; +  } + + +  function updateRoute(){ +    var childScope, routeParams, pathParams, segmentMatch, key, redir; + +    $route.current = _null; +    forEach(routes, function(rParams, rPath) { +      if (!pathParams) { +        if (pathParams = matcher(location.hashPath, rPath)) { +          routeParams = rParams; +        } +      } +    }); + +    // "otherwise" fallback +    routeParams = routeParams || routes[_null]; + +    if(routeParams) { +      if (routeParams.redirectTo) { +        if (isString(routeParams.redirectTo)) { +          // interpolate the redirectTo string +          redir = {hashPath: '', +                   hashSearch: extend({}, location.hashSearch, pathParams)}; + +          forEach(routeParams.redirectTo.split(':'), function(segment, i) { +            if (i==0) { +              redir.hashPath += segment; +            } else { +              segmentMatch = segment.match(/(\w+)(.*)/); +              key = segmentMatch[1]; +              redir.hashPath += pathParams[key] || location.hashSearch[key]; +              redir.hashPath += segmentMatch[2] || ''; +              delete redir.hashSearch[key]; +            } +          }); +        } else { +          // call custom redirectTo function +          redir = {hash: routeParams.redirectTo(pathParams, location.hash, location.hashPath, +                                                location.hashSearch)}; +        } + +        location.update(redir); +        $updateView(); //TODO this is to work around the $location<=>$browser issues +        return; +      } + +      childScope = createScope(parentScope); +      $route.current = extend({}, routeParams, { +        scope: childScope, +        params: extend({}, location.hashSearch, pathParams) +      }); +    } + +    //fire onChange callbacks +    forEach(onChange, parentScope.$tryEval); + +    if (childScope) { +      childScope.$become($route.current.controller); +    } +  } + + +  this.$watch(function(){return dirty + location.hash;}, updateRoute); + +  return $route; +}, ['$location', '$updateView']); diff --git a/src/service/updateView.js b/src/service/updateView.js new file mode 100644 index 00000000..603cfa5a --- /dev/null +++ b/src/service/updateView.js @@ -0,0 +1,59 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$updateView + * @requires $browser + * + * @description + * Calling `$updateView` enqueues the eventual update of the view. (Update the DOM to reflect the + * model). The update is eventual, since there are often multiple updates to the model which may + * be deferred. The default update delayed is 25 ms. This means that the view lags the model by + * that time. (25ms is small enough that it is perceived as instantaneous by the user). The delay + * can be adjusted by setting the delay property of the service. + * + * <pre>angular.service('$updateView').delay = 10</pre> + * + * The delay is there so that multiple updates to the model which occur sufficiently close + * together can be merged into a single update. + * + * You don't usually call '$updateView' directly since angular does it for you in most cases, + * but there are some cases when you need to call it. + * + *  - `$updateView()` called automatically by angular: + *    - Your Application Controllers: Your controller code is called by angular and hence + *      angular is aware that you may have changed the model. + *    - Your Services: Your service is usually called by your controller code, hence same rules + *      apply. + *  - May need to call `$updateView()` manually: + *    - Widgets / Directives: If you listen to any DOM events or events on any third party + *      libraries, then angular is not aware that you may have changed state state of the + *      model, and hence you need to call '$updateView()' manually. + *    - 'setTimeout'/'XHR':  If you call 'setTimeout' (instead of {@link angular.service.$defer}) + *      or 'XHR' (instead of {@link angular.service.$xhr}) then you may be changing the model + *      without angular knowledge and you may need to call '$updateView()' directly. + * + * NOTE: if you wish to update the view immediately (without delay), you can do so by calling + * {@link scope.$eval} at any time from your code: + * <pre>scope.$root.$eval()</pre> + * + * In unit-test mode the update is instantaneous and synchronous to simplify writing tests. + * + */ + +function serviceUpdateViewFactory($browser){ +  var rootScope = this; +  var scheduled; +  function update(){ +    scheduled = false; +    rootScope.$eval(); +  } +  return $browser.isMock ? update : function(){ +    if (!scheduled) { +      scheduled = true; +      $browser.defer(update, serviceUpdateViewFactory.delay); +    } +  }; +} +serviceUpdateViewFactory.delay = 25; + +angularServiceInject('$updateView', serviceUpdateViewFactory, ['$browser']); diff --git a/src/service/window.js b/src/service/window.js new file mode 100644 index 00000000..2392e7f9 --- /dev/null +++ b/src/service/window.js @@ -0,0 +1,25 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$window + * + * @description + * Is reference to the browser's `window` object. While `window` + * is globally available in JavaScript, it causes testability problems, because + * it is a global variable. In angular we always refer to it through the + * `$window` service, so it may be overriden, removed or mocked for testing. + * + * All expressions are evaluated with respect to current scope so they don't + * suffer from window globality. + * + * @example +   <doc:example> +     <doc:source> +       <input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" name="greeting" /> +       <button ng:click="$window.alert(greeting)">ALERT</button> +     </doc:source> +     <doc:scenario> +     </doc:scenario> +   </doc:example> + */ +angularServiceInject("$window", bind(window, identity, window), [], true); diff --git a/src/service/xhr.bulk.js b/src/service/xhr.bulk.js new file mode 100644 index 00000000..9933aa7e --- /dev/null +++ b/src/service/xhr.bulk.js @@ -0,0 +1,61 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr.bulk + * @requires $xhr + * @requires $xhr.error + * @requires $log + * + * @description + * + * @example + */ +angularServiceInject('$xhr.bulk', function($xhr, $error, $log){ +  var requests = [], +      scope = this; +  function bulkXHR(method, url, post, callback) { +    if (isFunction(post)) { +      callback = post; +      post = _null; +    } +    var currentQueue; +    forEach(bulkXHR.urls, function(queue){ +      if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) { +        currentQueue = queue; +      } +    }); +    if (currentQueue) { +      if (!currentQueue.requests) currentQueue.requests = []; +      currentQueue.requests.push({method: method, url: url, data:post, callback:callback}); +    } else { +      $xhr(method, url, post, callback); +    } +  } +  bulkXHR.urls = {}; +  bulkXHR.flush = function(callback){ +    forEach(bulkXHR.urls, function(queue, url){ +      var currentRequests = queue.requests; +      if (currentRequests && currentRequests.length) { +        queue.requests = []; +        queue.callbacks = []; +        $xhr('POST', url, {requests:currentRequests}, function(code, response){ +          forEach(response, function(response, i){ +            try { +              if (response.status == 200) { +                (currentRequests[i].callback || noop)(response.status, response.response); +              } else { +                $error(currentRequests[i], response); +              } +            } catch(e) { +              $log.error(e); +            } +          }); +          (callback || noop)(); +        }); +        scope.$eval(); +      } +    }); +  }; +  this.$onEval(PRIORITY_LAST, bulkXHR.flush); +  return bulkXHR; +}, ['$xhr', '$xhr.error', '$log']); diff --git a/src/service/xhr.cache.js b/src/service/xhr.cache.js new file mode 100644 index 00000000..e87b127b --- /dev/null +++ b/src/service/xhr.cache.js @@ -0,0 +1,66 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr.cache + * @function + * @requires $xhr + * + * @description + * Acts just like the {@link angular.service.$xhr $xhr} service but caches responses for `GET` + * requests. All cache misses are delegated to the $xhr service. + * + * @property {function()} delegate Function to delegate all the cache misses to. Defaults to + *   the {@link angular.service.$xhr $xhr} service. + * @property {object} data The hashmap where all cached entries are stored. + * + * @param {string} method HTTP method. + * @param {string} url Destination URL. + * @param {(string|Object)=} post Request body. + * @param {function(number, (string|Object))} callback Response callback. + * @param {boolean=} [verifyCache=false] If `true` then a result is immediately returned from cache + *   (if present) while a request is sent to the server for a fresh response that will update the + *   cached entry. The `callback` function will be called when the response is received. + */ +angularServiceInject('$xhr.cache', function($xhr, $defer, $log){ +  var inflight = {}, self = this; +  function cache(method, url, post, callback, verifyCache){ +    if (isFunction(post)) { +      callback = post; +      post = _null; +    } +    if (method == 'GET') { +      var data, dataCached; +      if (dataCached = cache.data[url]) { +        $defer(function() { callback(200, copy(dataCached.value)); }); +        if (!verifyCache) +          return; +      } + +      if (data = inflight[url]) { +        data.callbacks.push(callback); +      } else { +        inflight[url] = {callbacks: [callback]}; +        cache.delegate(method, url, post, function(status, response){ +          if (status == 200) +            cache.data[url] = { value: response }; +          var callbacks = inflight[url].callbacks; +          delete inflight[url]; +          forEach(callbacks, function(callback){ +            try { +              (callback||noop)(status, copy(response)); +            } catch(e) { +              $log.error(e); +            } +          }); +        }); +      } + +    } else { +      cache.data = {}; +      cache.delegate(method, url, post, callback); +    } +  } +  cache.data = {}; +  cache.delegate = $xhr; +  return cache; +}, ['$xhr.bulk', '$defer', '$log']); diff --git a/src/service/xhr.error.js b/src/service/xhr.error.js new file mode 100644 index 00000000..7f8e4a19 --- /dev/null +++ b/src/service/xhr.error.js @@ -0,0 +1,41 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr.error + * @function + * @requires $log + * + * @description + * Error handler for {@link angular.service.$xhr $xhr service}. An application can replaces this + * service with one specific for the application. The default implementation logs the error to + * {@link angular.service.$log $log.error}. + * + * @param {Object} request Request object. + * + *   The object has the following properties + * + *   - `method` – `{string}` – The http request method. + *   - `url` – `{string}` – The request destination. + *   - `data` – `{(string|Object)=} – An optional request body. + *   - `callback` – `{function()}` – The callback function + * + * @param {Object} response Response object. + * + *   The response object has the following properties: + * + *   - status – {number} – Http status code. + *   - body – {string|Object} – Body of the response. + * + * @example +    <doc:example> +      <doc:source> +        fetch a non-existent file and log an error in the console: +        <button ng:click="$service('$xhr')('GET', '/DOESNT_EXIST')">fetch</button> +      </doc:source> +    </doc:example> + */ +angularServiceInject('$xhr.error', function($log){ +  return function(request, response){ +    $log.error('ERROR: XHR: ' + request.url, request, response); +  }; +}, ['$log']); diff --git a/src/service/xhr.js b/src/service/xhr.js new file mode 100644 index 00000000..2f003398 --- /dev/null +++ b/src/service/xhr.js @@ -0,0 +1,99 @@ +/** + * @workInProgress + * @ngdoc service + * @name angular.service.$xhr + * @function + * @requires $browser + * @requires $xhr.error + * @requires $log + * + * @description + * Generates an XHR request. The $xhr service adds error handling then delegates all requests to + * {@link angular.service.$browser $browser.xhr()}. + * + * @param {string} method HTTP method to use. Valid values are: `GET`, `POST`, `PUT`, `DELETE`, and + *   `JSON`. `JSON` is a special case which causes a + *   [JSONP](http://en.wikipedia.org/wiki/JSON#JSONP) cross domain request using script tag + *   insertion. + * @param {string} url Relative or absolute URL specifying the destination of the request.  For + *   `JSON` requests, `url` should include `JSON_CALLBACK` string to be replaced with a name of an + *   angular generated callback function. + * @param {(string|Object)=} post Request content as either a string or an object to be stringified + *   as JSON before sent to the server. + * @param {function(number, (string|Object))} callback A function to be called when the response is + *   received. The callback will be called with: + * + *   - {number} code [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) of + *     the response. This will currently always be 200, since all non-200 responses are routed to + *     {@link angular.service.$xhr.error} service. + *   - {string|Object} response Response object as string or an Object if the response was in JSON + *     format. + * + * @example +   <doc:example> +     <doc:source> +       <script> +         function FetchCntl($xhr) { +           var self = this; + +           this.fetch = function() { +             self.clear(); +             $xhr(self.method, self.url, function(code, response) { +               self.code = code; +               self.response = response; +             }); +           }; + +           this.clear = function() { +             self.code = null; +             self.response = null; +           }; +         } +         FetchCntl.$inject = ['$xhr']; +       </script> +       <div ng:controller="FetchCntl"> +         <select name="method"> +           <option>GET</option> +           <option>JSON</option> +         </select> +         <input type="text" name="url" value="index.html" size="80"/><br/> +         <button ng:click="fetch()">fetch</button> +         <button ng:click="clear()">clear</button> +         <a href="" ng:click="method='GET'; url='index.html'">sample</a> +         <a href="" ng:click="method='JSON'; url='https://www.googleapis.com/buzz/v1/activities/googlebuzz/@self?alt=json&callback=JSON_CALLBACK'">buzz</a> +         <pre>code={{code}}</pre> +         <pre>response={{response}}</pre> +       </div> +     </doc:source> +   </doc:example> + */ +angularServiceInject('$xhr', function($browser, $error, $log){ +  var self = this; +  return function(method, url, post, callback){ +    if (isFunction(post)) { +      callback = post; +      post = _null; +    } +    if (post && isObject(post)) { +      post = toJson(post); +    } +    $browser.xhr(method, url, post, function(code, response){ +      try { +        if (isString(response) && /^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) { +          response = fromJson(response, true); +        } +        if (code == 200) { +          callback(code, response); +        } else { +          $error( +            {method: method, url:url, data:post, callback:callback}, +            {status: code, body:response}); +        } +      } catch (e) { +        $log.error(e); +      } finally { +        self.$eval(); +      } +    }); +  }; +}, ['$browser', '$xhr.error', '$log']); diff --git a/src/services.js b/src/services.js deleted file mode 100644 index be5189d1..00000000 --- a/src/services.js +++ /dev/null @@ -1,1541 +0,0 @@ -var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, -    HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/, -    DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}, -    EAGER = true; - -function angularServiceInject(name, fn, inject, eager) { -  angularService(name, fn, {$inject:inject, $eager:eager}); -} - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$window - * - * @description - * Is reference to the browser's `window` object. While `window` - * is globally available in JavaScript, it causes testability problems, because - * it is a global variable. In angular we always refer to it through the - * `$window` service, so it may be overriden, removed or mocked for testing. - * - * All expressions are evaluated with respect to current scope so they don't - * suffer from window globality. - * - * @example -   <doc:example> -     <doc:source> -       <input ng:init="$window = $service('$window'); greeting='Hello World!'" type="text" name="greeting" /> -       <button ng:click="$window.alert(greeting)">ALERT</button> -     </doc:source> -     <doc:scenario> -     </doc:scenario> -   </doc:example> - */ -angularServiceInject("$window", bind(window, identity, window), [], EAGER); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$document - * @requires $window - * - * @description - * Reference to the browser window.document, but wrapped into angular.element(). - */ -angularServiceInject("$document", function(window){ -  return jqLite(window.document); -}, ['$window'], EAGER); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$location - * @requires $browser - * - * @property {string} href - * @property {string} protocol - * @property {string} host - * @property {number} port - * @property {string} path - * @property {Object.<string|boolean>} search - * @property {string} hash - * @property {string} hashPath - * @property {Object.<string|boolean>} hashSearch - * - * @description - * Parses the browser location url and makes it available to your application. - * Any changes to the url are reflected into $location service and changes to - * $location are reflected to url. - * Notice that using browser's forward/back buttons changes the $location. - * - * @example -   <doc:example> -     <doc:source> -       <a href="#">clear hash</a> | -       <a href="#myPath?name=misko">test hash</a><br/> -       <input type='text' name="$location.hash"/> -       <pre>$location = {{$location}}</pre> -     </doc:source> -     <doc:scenario> -     </doc:scenario> -    </doc:example> - */ -angularServiceInject("$location", function($browser) { -  var scope = this, -      location = {update:update, updateHash: updateHash}, -      lastLocation = {}; - -  $browser.onHashChange(function() { //register -    update($browser.getUrl()); -    copy(location, lastLocation); -    scope.$eval(); -  })(); //initialize - -  this.$onEval(PRIORITY_FIRST, sync); -  this.$onEval(PRIORITY_LAST, updateBrowser); - -  return location; - -  // PUBLIC METHODS - -  /** -   * @workInProgress -   * @ngdoc method -   * @name angular.service.$location#update -   * @methodOf angular.service.$location -   * -   * @description -   * Update location object -   * Does not immediately update the browser -   * Browser is updated at the end of $eval() -   * -   * @example -    <doc:example> -      <doc:source> -        scope.$location.update('http://www.angularjs.org/path#hash?search=x'); -        scope.$location.update({host: 'www.google.com', protocol: 'https'}); -        scope.$location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}}); -      </doc:source> -      <doc:scenario> -      </doc:scenario> -    </doc:example> -   * -   * @param {(string|Object)} href Full href as a string or object with properties -   */ -  function update(href) { -    if (isString(href)) { -      extend(location, parseHref(href)); -    } else { -      if (isDefined(href.hash)) { -        extend(href, isString(href.hash) ? parseHash(href.hash) : href.hash); -      } - -      extend(location, href); - -      if (isDefined(href.hashPath || href.hashSearch)) { -        location.hash = composeHash(location); -      } - -      location.href = composeHref(location); -    } -  } - -  /** -   * @workInProgress -   * @ngdoc method -   * @name angular.service.$location#updateHash -   * @methodOf angular.service.$location -   * -   * @description -   * Update location hash part -   * @see update() -   * -   * @example -    <doc:example> -      <doc:source> -        scope.$location.updateHash('/hp') -         ==> update({hashPath: '/hp'}) -        scope.$location.updateHash({a: true, b: 'val'}) -         ==> update({hashSearch: {a: true, b: 'val'}}) -        scope.$location.updateHash('/hp', {a: true}) -         ==> update({hashPath: '/hp', hashSearch: {a: true}}) -      </doc:source> -      <doc:scenario> -      </doc:scenario> -    </doc:example> -   * -   * @param {(string|Object)} path A hashPath or hashSearch object -   * @param {Object=} search A hashSearch object -   */ -  function updateHash(path, search) { -    var hash = {}; - -    if (isString(path)) { -      hash.hashPath = path; -      hash.hashSearch = search || {}; -    } else -      hash.hashSearch = path; - -    hash.hash = composeHash(hash); - -    update({hash: hash}); -  } - - -  // INNER METHODS - -  /** -   * Synchronizes all location object properties. -   * -   * User is allowed to change properties, so after property change, -   * location object is not in consistent state. -   * -   * Properties are synced with the following precedence order: -   * -   * - `$location.href` -   * - `$location.hash` -   * - everything else -   * -   * @example -   * <pre> -   *   scope.$location.href = 'http://www.angularjs.org/path#a/b' -   * </pre> -   * immediately after this call, other properties are still the old ones... -   * -   * This method checks the changes and update location to the consistent state -   */ -  function sync() { -    if (!equals(location, lastLocation)) { -      if (location.href != lastLocation.href) { -        update(location.href); -        return; -      } -      if (location.hash != lastLocation.hash) { -        var hash = parseHash(location.hash); -        updateHash(hash.hashPath, hash.hashSearch); -      } else { -        location.hash = composeHash(location); -        location.href = composeHref(location); -      } -      update(location.href); -    } -  } - - -  /** -   * If location has changed, update the browser -   * This method is called at the end of $eval() phase -   */ -  function updateBrowser() { -    sync(); - -    if ($browser.getUrl() != location.href) { -      $browser.setUrl(location.href); -      copy(location, lastLocation); -    } -  } - -  /** -   * Compose href string from a location object -   * -   * @param {Object} loc The location object with all properties -   * @return {string} Composed href -   */ -  function composeHref(loc) { -    var url = toKeyValue(loc.search); -    var port = (loc.port == DEFAULT_PORTS[loc.protocol] ? _null : loc.port); - -    return loc.protocol  + '://' + loc.host + -          (port ? ':' + port : '') + loc.path + -          (url ? '?' + url : '') + (loc.hash ? '#' + loc.hash : ''); -  } - -  /** -   * Compose hash string from location object -   * -   * @param {Object} loc Object with hashPath and hashSearch properties -   * @return {string} Hash string -   */ -  function composeHash(loc) { -    var hashSearch = toKeyValue(loc.hashSearch); -    //TODO: temporary fix for issue #158 -    return escape(loc.hashPath).replace(/%21/gi, '!').replace(/%3A/gi, ':').replace(/%24/gi, '$') + -          (hashSearch ? '?' + hashSearch : ''); -  } - -  /** -   * Parse href string into location object -   * -   * @param {string} href -   * @return {Object} The location object -   */ -  function parseHref(href) { -    var loc = {}; -    var match = URL_MATCH.exec(href); - -    if (match) { -      loc.href = href.replace(/#$/, ''); -      loc.protocol = match[1]; -      loc.host = match[3] || ''; -      loc.port = match[5] || DEFAULT_PORTS[loc.protocol] || _null; -      loc.path = match[6] || ''; -      loc.search = parseKeyValue(match[8]); -      loc.hash = match[10] || ''; - -      extend(loc, parseHash(loc.hash)); -    } - -    return loc; -  } - -  /** -   * Parse hash string into object -   * -   * @param {string} hash -   */ -  function parseHash(hash) { -    var h = {}; -    var match = HASH_MATCH.exec(hash); - -    if (match) { -      h.hash = hash; -      h.hashPath = unescape(match[1] || ''); -      h.hashSearch = parseKeyValue(match[3]); -    } - -    return h; -  } -}, ['$browser']); - - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$log - * @requires $window - * - * @description - * Simple service for logging. Default implementation writes the message - * into the browser's console (if present). - * - * The main purpose of this service is to simplify debugging and troubleshooting. - * - * @example -    <doc:example> -      <doc:source> -         <p>Reload this page with open console, enter text and hit the log button...</p> -         Message: -         <input type="text" name="message" value="Hello World!"/> -         <button ng:click="$log.log(message)">log</button> -         <button ng:click="$log.warn(message)">warn</button> -         <button ng:click="$log.info(message)">info</button> -         <button ng:click="$log.error(message)">error</button> -      </doc:source> -      <doc:scenario> -      </doc:scenario> -    </doc:example> - */ -var $logFactory; //reference to be used only in tests -angularServiceInject("$log", $logFactory = function($window){ -  return { -    /** -     * @workInProgress -     * @ngdoc method -     * @name angular.service.$log#log -     * @methodOf angular.service.$log -     * -     * @description -     * Write a log message -     */ -    log: consoleLog('log'), - -    /** -     * @workInProgress -     * @ngdoc method -     * @name angular.service.$log#warn -     * @methodOf angular.service.$log -     * -     * @description -     * Write a warning message -     */ -    warn: consoleLog('warn'), - -    /** -     * @workInProgress -     * @ngdoc method -     * @name angular.service.$log#info -     * @methodOf angular.service.$log -     * -     * @description -     * Write an information message -     */ -    info: consoleLog('info'), - -    /** -     * @workInProgress -     * @ngdoc method -     * @name angular.service.$log#error -     * @methodOf angular.service.$log -     * -     * @description -     * Write an error message -     */ -    error: consoleLog('error') -  }; - -  function consoleLog(type) { -    var console = $window.console || {}; -    var logFn = console[type] || console.log || noop; -    if (logFn.apply) { -      return function(){ -        var args = []; -        forEach(arguments, function(arg){ -          args.push(formatError(arg)); -        }); -        return logFn.apply(console, args); -      }; -    } else { -      // we are IE, in which case there is nothing we can do -      return logFn; -    } -  } -}, ['$window'], EAGER); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$exceptionHandler - * @requires $log - * - * @description - * Any uncaught exception in angular expressions is delegated to this service. - * The default implementation simply delegates to `$log.error` which logs it into - * the browser console. - * - * In unit tests, if `angular-mocks.js` is loaded, this service is overriden by - * {@link angular.mock.service.$exceptionHandler mock $exceptionHandler} - * - * @example - */ -var $exceptionHandlerFactory; //reference to be used only in tests -angularServiceInject('$exceptionHandler', $exceptionHandlerFactory = function($log){ -  return function(e) { -    $log.error(e); -  }; -}, ['$log'], EAGER); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$updateView - * @requires $browser - * - * @description - * Calling `$updateView` enqueues the eventual update of the view. (Update the DOM to reflect the - * model). The update is eventual, since there are often multiple updates to the model which may - * be deferred. The default update delayed is 25 ms. This means that the view lags the model by - * that time. (25ms is small enough that it is perceived as instantaneous by the user). The delay - * can be adjusted by setting the delay property of the service. - * - * <pre>angular.service('$updateView').delay = 10</pre> - * - * The delay is there so that multiple updates to the model which occur sufficiently close - * together can be merged into a single update. - * - * You don't usually call '$updateView' directly since angular does it for you in most cases, - * but there are some cases when you need to call it. - * - *  - `$updateView()` called automatically by angular: - *    - Your Application Controllers: Your controller code is called by angular and hence - *      angular is aware that you may have changed the model. - *    - Your Services: Your service is usually called by your controller code, hence same rules - *      apply. - *  - May need to call `$updateView()` manually: - *    - Widgets / Directives: If you listen to any DOM events or events on any third party - *      libraries, then angular is not aware that you may have changed state state of the - *      model, and hence you need to call '$updateView()' manually. - *    - 'setTimeout'/'XHR':  If you call 'setTimeout' (instead of {@link angular.service.$defer}) - *      or 'XHR' (instead of {@link angular.service.$xhr}) then you may be changing the model - *      without angular knowledge and you may need to call '$updateView()' directly. - * - * NOTE: if you wish to update the view immediately (without delay), you can do so by calling - * {@link scope.$eval} at any time from your code: - * <pre>scope.$root.$eval()</pre> - * - * In unit-test mode the update is instantaneous and synchronous to simplify writing tests. - * - */ - -function serviceUpdateViewFactory($browser){ -  var rootScope = this; -  var scheduled; -  function update(){ -    scheduled = false; -    rootScope.$eval(); -  } -  return $browser.isMock ? update : function(){ -    if (!scheduled) { -      scheduled = true; -      $browser.defer(update, serviceUpdateViewFactory.delay); -    } -  }; -} -serviceUpdateViewFactory.delay = 25; - -angularServiceInject('$updateView', serviceUpdateViewFactory, ['$browser']); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$hover - * @requires $browser - * @requires $document - * - * @description - * - * @example - */ -angularServiceInject("$hover", function(browser, document) { -  var tooltip, self = this, error, width = 300, arrowWidth = 10, body = jqLite(document[0].body); -  browser.hover(function(element, show){ -    if (show && (error = element.attr(NG_EXCEPTION) || element.attr(NG_VALIDATION_ERROR))) { -      if (!tooltip) { -        tooltip = { -            callout: jqLite('<div id="ng-callout"></div>'), -            arrow: jqLite('<div></div>'), -            title: jqLite('<div class="ng-title"></div>'), -            content: jqLite('<div class="ng-content"></div>') -        }; -        tooltip.callout.append(tooltip.arrow); -        tooltip.callout.append(tooltip.title); -        tooltip.callout.append(tooltip.content); -        body.append(tooltip.callout); -      } -      var docRect = body[0].getBoundingClientRect(), -          elementRect = element[0].getBoundingClientRect(), -          leftSpace = docRect.right - elementRect.right - arrowWidth; -      tooltip.title.text(element.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error..."); -      tooltip.content.text(error); -      if (leftSpace < width) { -        tooltip.arrow.addClass('ng-arrow-right'); -        tooltip.arrow.css({left: (width + 1)+'px'}); -        tooltip.callout.css({ -          position: 'fixed', -          left: (elementRect.left - arrowWidth - width - 4) + "px", -          top: (elementRect.top - 3) + "px", -          width: width + "px" -        }); -      } else { -        tooltip.arrow.addClass('ng-arrow-left'); -        tooltip.callout.css({ -          position: 'fixed', -          left: (elementRect.right + arrowWidth) + "px", -          top: (elementRect.top - 3) + "px", -          width: width + "px" -        }); -      } -    } else if (tooltip) { -      tooltip.callout.remove(); -      tooltip = _null; -    } -  }); -}, ['$browser', '$document'], EAGER); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$invalidWidgets - * - * @description - * Keeps references to all invalid widgets found during validation. - * Can be queried to find whether there are any invalid widgets currently displayed. - * - * @example - */ -angularServiceInject("$invalidWidgets", function(){ -  var invalidWidgets = []; - - -  /** Remove an element from the array of invalid widgets */ -  invalidWidgets.markValid = function(element){ -    var index = indexOf(invalidWidgets, element); -    if (index != -1) -      invalidWidgets.splice(index, 1); -  }; - - -  /** Add an element to the array of invalid widgets */ -  invalidWidgets.markInvalid = function(element){ -    var index = indexOf(invalidWidgets, element); -    if (index === -1) -      invalidWidgets.push(element); -  }; - - -  /** Return count of all invalid widgets that are currently visible */ -  invalidWidgets.visible = function() { -    var count = 0; -    forEach(invalidWidgets, function(widget){ -      count = count + (isVisible(widget) ? 1 : 0); -    }); -    return count; -  }; - - -  /* At the end of each eval removes all invalid widgets that are not part of the current DOM. */ -  this.$onEval(PRIORITY_LAST, function() { -    for(var i = 0; i < invalidWidgets.length;) { -      var widget = invalidWidgets[i]; -      if (isOrphan(widget[0])) { -        invalidWidgets.splice(i, 1); -        if (widget.dealoc) widget.dealoc(); -      } else { -        i++; -      } -    } -  }); - - -  /** -   * Traverses DOM element's (widget's) parents and considers the element to be an orphant if one of -   * it's parents isn't the current window.document. -   */ -  function isOrphan(widget) { -    if (widget == window.document) return false; -    var parent = widget.parentNode; -    return !parent || isOrphan(parent); -  } - -  return invalidWidgets; -}, [], EAGER); - - - -function switchRouteMatcher(on, when, dstName) { -  var regex = '^' + when.replace(/[\.\\\(\)\^\$]/g, "\$1") + '$', -      params = [], -      dst = {}; -  forEach(when.split(/\W/), function(param){ -    if (param) { -      var paramRegExp = new RegExp(":" + param + "([\\W])"); -      if (regex.match(paramRegExp)) { -        regex = regex.replace(paramRegExp, "([^\/]*)$1"); -        params.push(param); -      } -    } -  }); -  var match = on.match(new RegExp(regex)); -  if (match) { -    forEach(params, function(name, index){ -      dst[name] = match[index + 1]; -    }); -    if (dstName) this.$set(dstName, dst); -  } -  return match ? dst : _null; -} - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$route - * @requires $location - * - * @property {Object} current Reference to the current route definition. - * @property {Array.<Object>} routes Array of all configured routes. - * - * @description - * Watches `$location.hashPath` and tries to map the hash to an existing route - * definition. It is used for deep-linking URLs to controllers and views (HTML partials). - * - * The `$route` service is typically used in conjunction with {@link angular.widget.ng:view ng:view} - * widget. - * - * @example -   This example shows how changing the URL hash causes the <tt>$route</tt> -   to match a route against the URL, and the <tt>[[ng:include]]</tt> pulls in the partial. -   Try changing the URL in the input box to see changes. - -    <doc:example> -      <doc:source> -        <script> -          angular.service('myApp', function($route) { -            $route.when('/Book/:bookId', {template:'rsrc/book.html', controller:BookCntl}); -            $route.when('/Book/:bookId/ch/:chapterId', {template:'rsrc/chapter.html', controller:ChapterCntl}); -            $route.onChange(function() { -              $route.current.scope.params = $route.current.params; -            }); -          }, {$inject: ['$route']}); - -          function BookCntl() { -            this.name = "BookCntl"; -          } - -          function ChapterCntl() { -            this.name = "ChapterCntl"; -          } -        </script> - -        Chose: -        <a href="#/Book/Moby">Moby</a> | -        <a href="#/Book/Moby/ch/1">Moby: Ch1</a> | -        <a href="#/Book/Gatsby">Gatsby</a> | -        <a href="#/Book/Gatsby/ch/4?key=value">Gatsby: Ch4</a><br/> -        <input type="text" name="$location.hashPath" size="80" /> -        <pre>$location={{$location}}</pre> -        <pre>$route.current.template={{$route.current.template}}</pre> -        <pre>$route.current.params={{$route.current.params}}</pre> -        <pre>$route.current.scope.name={{$route.current.scope.name}}</pre> -        <hr/> -        <ng:include src="$route.current.template" scope="$route.current.scope"/> -      </doc:source> -      <doc:scenario> -      </doc:scenario> -    </doc:example> - */ -angularServiceInject('$route', function(location, $updateView) { -  var routes = {}, -      onChange = [], -      matcher = switchRouteMatcher, -      parentScope = this, -      dirty = 0, -      $route = { -        routes: routes, - -        /** -         * @workInProgress -         * @ngdoc method -         * @name angular.service.$route#onChange -         * @methodOf angular.service.$route -         * -         * @param {function()} fn Function that will be called when `$route.current` changes. -         * @returns {function()} The registered function. -         * -         * @description -         * Register a handler function that will be called when route changes -         */ -        onChange: function(fn) { -          onChange.push(fn); -          return fn; -        }, - -        /** -         * @workInProgress -         * @ngdoc method -         * @name angular.service.$route#parent -         * @methodOf angular.service.$route -         * -         * @param {Scope} [scope=rootScope] Scope to be used as parent for newly created -         *    `$route.current.scope` scopes. -         * -         * @description -         * Sets a scope to be used as the parent scope for scopes created on route change. If not -         * set, defaults to the root scope. -         */ -        parent: function(scope) { -          if (scope) parentScope = scope; -        }, - -        /** -         * @workInProgress -         * @ngdoc method -         * @name angular.service.$route#when -         * @methodOf angular.service.$route -         * -         * @param {string} path Route path (matched against `$location.hash`) -         * @param {Object} params Mapping information to be assigned to `$route.current` on route -         *    match. -         * -         *    Object properties: -         * -         *    - `controller` – `{function()=}` – Controller fn that should be associated with newly -         *      created scope. -         *    - `template` – `{string=}` – path to an html template that should be used by -         *      {@link angular.widget.ng:view ng:view} or -         *      {@link angular.widget.ng:include ng:include} widgets. -         *    - `redirectTo` – {(string|function())=} – value to update -         *      {@link angular.service.$location $location} hash with and trigger route redirection. -         * -         *      If `redirectTo` is a function, it will be called with the following parameters: -         * -         *      - `{Object.<string>}` - route parameters extracted from the current -         *        `$location.hashPath` by applying the current route template. -         *      - `{string}` - current `$location.hash` -         *      - `{string}` - current `$location.hashPath` -         *      - `{string}` - current `$location.hashSearch` -         * -         *      The custom `redirectTo` function is expected to return a string which will be used -         *      to update `$location.hash`. -         * -         * @returns {Object} route object -         * -         * @description -         * Adds a new route definition to the `$route` service. -         */ -        when:function (path, params) { -          if (isUndefined(path)) return routes; //TODO(im): remove - not needed! -          var route = routes[path]; -          if (!route) route = routes[path] = {}; -          if (params) extend(route, params); -          dirty++; -          return route; -        }, - -        /** -         * @workInProgress -         * @ngdoc method -         * @name angular.service.$route#otherwise -         * @methodOf angular.service.$route -         * -         * @description -         * Sets route definition that will be used on route change when no other route definition -         * is matched. -         * -         * @param {Object} params Mapping information to be assigned to `$route.current`. -         */ -        otherwise: function(params) { -          $route.when(null, params); -        }, - -        /** -         * @workInProgress -         * @ngdoc method -         * @name angular.service.$route#reload -         * @methodOf angular.service.$route -         * -         * @description -         * Causes `$route` service to reload (and recreate the `$route.current` scope) upon the next -         * eval even if {@link angular.service.$location $location} hasn't changed. -         */ -        reload: function() { -          dirty++; -        } -      }; -  function updateRoute(){ -    var childScope, routeParams, pathParams, segmentMatch, key, redir; - -    $route.current = _null; -    forEach(routes, function(rParams, rPath) { -      if (!pathParams) { -        if (pathParams = matcher(location.hashPath, rPath)) { -          routeParams = rParams; -        } -      } -    }); - -    // "otherwise" fallback -    routeParams = routeParams || routes[_null]; - -    if(routeParams) { -      if (routeParams.redirectTo) { -        if (isString(routeParams.redirectTo)) { -          // interpolate the redirectTo string -          redir = {hashPath: '', -                   hashSearch: extend({}, location.hashSearch, pathParams)}; - -          forEach(routeParams.redirectTo.split(':'), function(segment, i) { -            if (i==0) { -              redir.hashPath += segment; -            } else { -              segmentMatch = segment.match(/(\w+)(.*)/); -              key = segmentMatch[1]; -              redir.hashPath += pathParams[key] || location.hashSearch[key]; -              redir.hashPath += segmentMatch[2] || ''; -              delete redir.hashSearch[key]; -            } -          }); -        } else { -          // call custom redirectTo function -          redir = {hash: routeParams.redirectTo(pathParams, location.hash, location.hashPath, -                                                location.hashSearch)}; -        } - -        location.update(redir); -        $updateView(); //TODO this is to work around the $location<=>$browser issues -        return; -      } - -      childScope = createScope(parentScope); -      $route.current = extend({}, routeParams, { -        scope: childScope, -        params: extend({}, location.hashSearch, pathParams) -      }); -    } - -    //fire onChange callbacks -    forEach(onChange, parentScope.$tryEval); - -    if (childScope) { -      childScope.$become($route.current.controller); -    } -  } - -  this.$watch(function(){return dirty + location.hash;}, updateRoute); - -  return $route; -}, ['$location', '$updateView']); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$xhr - * @function - * @requires $browser - * @requires $xhr.error - * @requires $log - * - * @description - * Generates an XHR request. The $xhr service adds error handling then delegates all requests to - * {@link angular.service.$browser $browser.xhr()}. - * - * @param {string} method HTTP method to use. Valid values are: `GET`, `POST`, `PUT`, `DELETE`, and - *   `JSON`. `JSON` is a special case which causes a - *   [JSONP](http://en.wikipedia.org/wiki/JSON#JSONP) cross domain request using script tag - *   insertion. - * @param {string} url Relative or absolute URL specifying the destination of the request.  For - *   `JSON` requests, `url` should include `JSON_CALLBACK` string to be replaced with a name of an - *   angular generated callback function. - * @param {(string|Object)=} post Request content as either a string or an object to be stringified - *   as JSON before sent to the server. - * @param {function(number, (string|Object))} callback A function to be called when the response is - *   received. The callback will be called with: - * - *   - {number} code [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) of - *     the response. This will currently always be 200, since all non-200 responses are routed to - *     {@link angular.service.$xhr.error} service. - *   - {string|Object} response Response object as string or an Object if the response was in JSON - *     format. - * - * @example -   <doc:example> -     <doc:source> -       <script> -         function FetchCntl($xhr) { -           var self = this; - -           this.fetch = function() { -             self.clear(); -             $xhr(self.method, self.url, function(code, response) { -               self.code = code; -               self.response = response; -             }); -           }; - -           this.clear = function() { -             self.code = null; -             self.response = null; -           }; -         } -         FetchCntl.$inject = ['$xhr']; -       </script> -       <div ng:controller="FetchCntl"> -         <select name="method"> -           <option>GET</option> -           <option>JSON</option> -         </select> -         <input type="text" name="url" value="index.html" size="80"/><br/> -         <button ng:click="fetch()">fetch</button> -         <button ng:click="clear()">clear</button> -         <a href="" ng:click="method='GET'; url='index.html'">sample</a> -         <a href="" ng:click="method='JSON'; url='https://www.googleapis.com/buzz/v1/activities/googlebuzz/@self?alt=json&callback=JSON_CALLBACK'">buzz</a> -         <pre>code={{code}}</pre> -         <pre>response={{response}}</pre> -       </div> -     </doc:source> -   </doc:example> - */ -angularServiceInject('$xhr', function($browser, $error, $log){ -  var self = this; -  return function(method, url, post, callback){ -    if (isFunction(post)) { -      callback = post; -      post = _null; -    } -    if (post && isObject(post)) { -      post = toJson(post); -    } -    $browser.xhr(method, url, post, function(code, response){ -      try { -        if (isString(response) && /^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) { -          response = fromJson(response, true); -        } -        if (code == 200) { -          callback(code, response); -        } else { -          $error( -            {method: method, url:url, data:post, callback:callback}, -            {status: code, body:response}); -        } -      } catch (e) { -        $log.error(e); -      } finally { -        self.$eval(); -      } -    }); -  }; -}, ['$browser', '$xhr.error', '$log']); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$xhr.error - * @function - * @requires $log - * - * @description - * Error handler for {@link angular.service.$xhr $xhr service}. An application can replaces this - * service with one specific for the application. The default implementation logs the error to - * {@link angular.service.$log $log.error}. - * - * @param {Object} request Request object. - * - *   The object has the following properties - * - *   - `method` – `{string}` – The http request method. - *   - `url` – `{string}` – The request destination. - *   - `data` – `{(string|Object)=} – An optional request body. - *   - `callback` – `{function()}` – The callback function - * - * @param {Object} response Response object. - * - *   The response object has the following properties: - * - *   - status – {number} – Http status code. - *   - body – {string|Object} – Body of the response. - * - * @example -    <doc:example> -      <doc:source> -        fetch a non-existent file and log an error in the console: -        <button ng:click="$service('$xhr')('GET', '/DOESNT_EXIST')">fetch</button> -      </doc:source> -    </doc:example> - */ -angularServiceInject('$xhr.error', function($log){ -  return function(request, response){ -    $log.error('ERROR: XHR: ' + request.url, request, response); -  }; -}, ['$log']); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$xhr.bulk - * @requires $xhr - * @requires $xhr.error - * @requires $log - * - * @description - * - * @example - */ -angularServiceInject('$xhr.bulk', function($xhr, $error, $log){ -  var requests = [], -      scope = this; -  function bulkXHR(method, url, post, callback) { -    if (isFunction(post)) { -      callback = post; -      post = _null; -    } -    var currentQueue; -    forEach(bulkXHR.urls, function(queue){ -      if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) { -        currentQueue = queue; -      } -    }); -    if (currentQueue) { -      if (!currentQueue.requests) currentQueue.requests = []; -      currentQueue.requests.push({method: method, url: url, data:post, callback:callback}); -    } else { -      $xhr(method, url, post, callback); -    } -  } -  bulkXHR.urls = {}; -  bulkXHR.flush = function(callback){ -    forEach(bulkXHR.urls, function(queue, url){ -      var currentRequests = queue.requests; -      if (currentRequests && currentRequests.length) { -        queue.requests = []; -        queue.callbacks = []; -        $xhr('POST', url, {requests:currentRequests}, function(code, response){ -          forEach(response, function(response, i){ -            try { -              if (response.status == 200) { -                (currentRequests[i].callback || noop)(response.status, response.response); -              } else { -                $error(currentRequests[i], response); -              } -            } catch(e) { -              $log.error(e); -            } -          }); -          (callback || noop)(); -        }); -        scope.$eval(); -      } -    }); -  }; -  this.$onEval(PRIORITY_LAST, bulkXHR.flush); -  return bulkXHR; -}, ['$xhr', '$xhr.error', '$log']); - - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$defer - * @requires $browser - * @requires $log - * - * @description - * Delegates to {@link angular.service.$browser.defer $browser.defer}, but wraps the `fn` function - * into a try/catch block and delegates any exceptions to - * {@link angular.services.$exceptionHandler $exceptionHandler} service. - * - * In tests you can use `$browser.defer.flush()` to flush the queue of deferred functions. - * - * @param {function()} fn A function, who's execution should be deferred. - */ -angularServiceInject('$defer', function($browser, $exceptionHandler, $updateView) { -  var scope = this; - -  return function(fn) { -    $browser.defer(function() { -      try { -        fn(); -      } catch(e) { -        $exceptionHandler(e); -      } finally { -        $updateView(); -      } -    }); -  }; -}, ['$browser', '$exceptionHandler', '$updateView']); - - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$xhr.cache - * @function - * @requires $xhr - * - * @description - * Acts just like the {@link angular.service.$xhr $xhr} service but caches responses for `GET` - * requests. All cache misses are delegated to the $xhr service. - * - * @property {function()} delegate Function to delegate all the cache misses to. Defaults to - *   the {@link angular.service.$xhr $xhr} service. - * @property {object} data The hashmap where all cached entries are stored. - * - * @param {string} method HTTP method. - * @param {string} url Destination URL. - * @param {(string|Object)=} post Request body. - * @param {function(number, (string|Object))} callback Response callback. - * @param {boolean=} [verifyCache=false] If `true` then a result is immediately returned from cache - *   (if present) while a request is sent to the server for a fresh response that will update the - *   cached entry. The `callback` function will be called when the response is received. - */ -angularServiceInject('$xhr.cache', function($xhr, $defer, $log){ -  var inflight = {}, self = this; -  function cache(method, url, post, callback, verifyCache){ -    if (isFunction(post)) { -      callback = post; -      post = _null; -    } -    if (method == 'GET') { -      var data, dataCached; -      if (dataCached = cache.data[url]) { -        $defer(function() { callback(200, copy(dataCached.value)); }); -        if (!verifyCache) -          return; -      } - -      if (data = inflight[url]) { -        data.callbacks.push(callback); -      } else { -        inflight[url] = {callbacks: [callback]}; -        cache.delegate(method, url, post, function(status, response){ -          if (status == 200) -            cache.data[url] = { value: response }; -          var callbacks = inflight[url].callbacks; -          delete inflight[url]; -          forEach(callbacks, function(callback){ -            try { -              (callback||noop)(status, copy(response)); -            } catch(e) { -              $log.error(e); -            } -          }); -        }); -      } - -    } else { -      cache.data = {}; -      cache.delegate(method, url, post, callback); -    } -  } -  cache.data = {}; -  cache.delegate = $xhr; -  return cache; -}, ['$xhr.bulk', '$defer', '$log']); - - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$resource - * @requires $xhr.cache - * - * @description - * Is a factory which creates a resource object that lets you interact with - * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. - * - * The returned resource object has action methods which provide high-level behaviors without - * the need to interact with the low level {@link angular.service.$xhr $xhr} service or - * raw XMLHttpRequest. - * - * @param {string} url A parameterized URL template with parameters prefixed by `:` as in - *   `/user/:username`. - * - * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in - *   `actions` methods. - * - *   Each key value in the parameter object is first bound to url template if present and then any - *   excess keys are appended to the url search query after the `?`. - * - *   Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in - *   URL `/path/greet?salutation=Hello`. - * - *   If the parameter value is prefixed with `@` then the value of that parameter is extracted from - *   the data object (useful for non-GET operations). - * - * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the - *   default set of resource actions. The declaration should be created in the following format: - * - *       {action1: {method:?, params:?, isArray:?, verifyCache:?}, - *        action2: {method:?, params:?, isArray:?, verifyCache:?}, - *        ...} - * - *   Where: - * - *   - `action` – {string} – The name of action. This name becomes the name of the method on your - *     resource object. - *   - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`, - *     and `JSON` (also known as JSONP). - *   - `params` – {object=} – Optional set of pre-bound parameters for this action. - *   - isArray – {boolean=} – If true then the returned object for this action is an array, see - *     `returns` section. - *   - verifyCache – {boolean=} – If true then whenever cache hit occurs, the object is returned and - *     an async request will be made to the server and the resources as well as the cache will be - *     updated when the response is received. - * - * @returns {Object} A resource "class" object with methods for the default set of resource actions - *   optionally extended with custom `actions`. The default set contains these actions: - * - *       { 'get':    {method:'GET'}, - *         'save':   {method:'POST'}, - *         'query':  {method:'GET', isArray:true}, - *         'remove': {method:'DELETE'}, - *         'delete': {method:'DELETE'} }; - * - *   Calling these methods invoke an {@link angular.service.$xhr} with the specified http method, - *   destination and parameters. When the data is returned from the server then the object is an - *   instance of the resource class `save`, `remove` and `delete` actions are available on it as - *   methods with the `$` prefix. This allows you to easily perform CRUD operations (create, read, - *   update, delete) on server-side data like this: - *   <pre> -        var User = $resource('/user/:userId', {userId:'@id'}); -        var user = User.get({userId:123}, function(){ -          user.abc = true; -          user.$save(); -        }); -     </pre> - * - *   It is important to realize that invoking a $resource object method immediately returns an - *   empty reference (object or array depending on `isArray`). Once the data is returned from the - *   server the existing reference is populated with the actual data. This is a useful trick since - *   usually the resource is assigned to a model which is then rendered by the view. Having an empty - *   object results in no rendering, once the data arrives from the server then the object is - *   populated with the data and the view automatically re-renders itself showing the new data. This - *   means that in most case one never has to write a callback function for the action methods. - * - *   The action methods on the class object or instance object can be invoked with the following - *   parameters: - * - *   - HTTP GET "class" actions: `Resource.action([parameters], [callback])` - *   - non-GET "class" actions: `Resource.action(postData, [parameters], [callback])` - *   - non-GET instance actions:  `instance.$action([parameters], [callback])` - * - * - * @example - * - * # Credit card resource - * - * <pre> -     // Define CreditCard class -     var CreditCard = $resource('/user/:userId/card/:cardId', -      {userId:123, cardId:'@id'}, { -       charge: {method:'POST', params:{charge:true}} -      }); - -     // We can retrieve a collection from the server -     var cards = CreditCard.query(); -     // GET: /user/123/card -     // server returns: [ {id:456, number:'1234', name:'Smith'} ]; - -     var card = cards[0]; -     // each item is an instance of CreditCard -     expect(card instanceof CreditCard).toEqual(true); -     card.name = "J. Smith"; -     // non GET methods are mapped onto the instances -     card.$save(); -     // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} -     // server returns: {id:456, number:'1234', name: 'J. Smith'}; - -     // our custom method is mapped as well. -     card.$charge({amount:9.99}); -     // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} -     // server returns: {id:456, number:'1234', name: 'J. Smith'}; - -     // we can create an instance as well -     var newCard = new CreditCard({number:'0123'}); -     newCard.name = "Mike Smith"; -     newCard.$save(); -     // POST: /user/123/card {number:'0123', name:'Mike Smith'} -     // server returns: {id:789, number:'01234', name: 'Mike Smith'}; -     expect(newCard.id).toEqual(789); - * </pre> - * - * The object returned from this function execution is a resource "class" which has "static" method - * for each action in the definition. - * - * Calling these methods invoke `$xhr` on the `url` template with the given `method` and `params`. - * When the data is returned from the server then the object is an instance of the resource type and - * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD - * operations (create, read, update, delete) on server-side data. - -   <pre> -     var User = $resource('/user/:userId', {userId:'@id'}); -     var user = User.get({userId:123}, function(){ -       user.abc = true; -       user.$save(); -     }); -   </pre> - * - *     It's worth noting that the callback for `get`, `query` and other method gets passed in the - *     response that came from the server, so one could rewrite the above example as: - * -   <pre> -     var User = $resource('/user/:userId', {userId:'@id'}); -     User.get({userId:123}, function(u){ -       u.abc = true; -       u.$save(); -     }); -   </pre> - - * # Buzz client - -   Let's look at what a buzz client created with the `$resource` service looks like: -    <doc:example> -      <doc:source> -       <script> -         function BuzzController($resource) { -           this.Activity = $resource( -             'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments', -             {alt:'json', callback:'JSON_CALLBACK'}, -             {get:{method:'JSON', params:{visibility:'@self'}}, replies: {method:'JSON', params:{visibility:'@self', comments:'@comments'}}} -           ); -         } - -         BuzzController.prototype = { -           fetch: function() { -             this.activities = this.Activity.get({userId:this.userId}); -           }, -           expandReplies: function(activity) { -             activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id}); -           } -         }; -         BuzzController.$inject = ['$resource']; -       </script> - -       <div ng:controller="BuzzController"> -         <input name="userId" value="googlebuzz"/> -         <button ng:click="fetch()">fetch</button> -         <hr/> -         <div ng:repeat="item in activities.data.items"> -           <h1 style="font-size: 15px;"> -             <img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/> -             <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a> -             <a href ng:click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a> -           </h1> -           {{item.object.content | html}} -           <div ng:repeat="reply in item.replies.data.items" style="margin-left: 20px;"> -             <img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/> -             <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}} -           </div> -         </div> -       </div> -      </doc:source> -      <doc:scenario> -      </doc:scenario> -    </doc:example> - */ -angularServiceInject('$resource', function($xhr){ -  var resource = new ResourceFactory($xhr); -  return bind(resource, resource.route); -}, ['$xhr.cache']); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$cookies - * @requires $browser - * - * @description - * Provides read/write access to browser's cookies. - * - * Only a simple Object is exposed and by adding or removing properties to/from - * this object, new cookies are created/deleted at the end of current $eval. - * - * @example - */ -angularServiceInject('$cookies', function($browser) { -  var rootScope = this, -      cookies = {}, -      lastCookies = {}, -      lastBrowserCookies; - -  //creates a poller fn that copies all cookies from the $browser to service & inits the service -  $browser.addPollFn(function() { -    var currentCookies = $browser.cookies(); -    if (lastBrowserCookies != currentCookies) { //relies on browser.cookies() impl -      lastBrowserCookies = currentCookies; -      copy(currentCookies, lastCookies); -      copy(currentCookies, cookies); -      rootScope.$eval(); -    } -  })(); - -  //at the end of each eval, push cookies -  //TODO: this should happen before the "delayed" watches fire, because if some cookies are not -  //      strings or browser refuses to store some cookies, we update the model in the push fn. -  this.$onEval(PRIORITY_LAST, push); - -  return cookies; - - -  /** -   * Pushes all the cookies from the service to the browser and verifies if all cookies were stored. -   */ -  function push(){ -    var name, -        value, -        browserCookies, -        updated; - -    //delete any cookies deleted in $cookies -    for (name in lastCookies) { -      if (isUndefined(cookies[name])) { -        $browser.cookies(name, _undefined); -      } -    } - -    //update all cookies updated in $cookies -    for(name in cookies) { -      value = cookies[name]; -      if (!isString(value)) { -        if (isDefined(lastCookies[name])) { -          cookies[name] = lastCookies[name]; -        } else { -          delete cookies[name]; -        } -      } else if (value !== lastCookies[name]) { -        $browser.cookies(name, value); -        updated = true; -      } -    } - -    //verify what was actually stored -    if (updated){ -      updated = false; -      browserCookies = $browser.cookies(); - -      for (name in cookies) { -        if (cookies[name] !== browserCookies[name]) { -          //delete or reset all cookies that the browser dropped from $cookies -          if (isUndefined(browserCookies[name])) { -            delete cookies[name]; -          } else { -            cookies[name] = browserCookies[name]; -          } -          updated = true; -        } -      } -    } -  } -}, ['$browser']); - -/** - * @workInProgress - * @ngdoc service - * @name angular.service.$cookieStore - * @requires $cookies - * - * @description - * Provides a key-value (string-object) storage, that is backed by session cookies. - * Objects put or retrieved from this storage are automatically serialized or - * deserialized by angular's toJson/fromJson. - * @example - */ -angularServiceInject('$cookieStore', function($store) { - -  return { -    /** -     * @workInProgress -     * @ngdoc method -     * @name angular.service.$cookieStore#get -     * @methodOf angular.service.$cookieStore -     * -     * @description -     * Returns the value of given cookie key -     * -     * @param {string} key Id to use for lookup. -     * @returns {Object} Deserialized cookie value. -     */ -    get: function(key) { -      return fromJson($store[key]); -    }, - -    /** -     * @workInProgress -     * @ngdoc method -     * @name angular.service.$cookieStore#put -     * @methodOf angular.service.$cookieStore -     * -     * @description -     * Sets a value for given cookie key -     * -     * @param {string} key Id for the `value`. -     * @param {Object} value Value to be stored. -     */ -    put: function(key, value) { -      $store[key] = toJson(value); -    }, - -    /** -     * @workInProgress -     * @ngdoc method -     * @name angular.service.$cookieStore#remove -     * @methodOf angular.service.$cookieStore -     * -     * @description -     * Remove given cookie -     * -     * @param {string} key Id of the key-value pair to delete. -     */ -    remove: function(key) { -      delete $store[key]; -    } -  }; - -}, ['$cookies']); diff --git a/src/widgets.js b/src/widgets.js index 6482f455..461684ac 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -743,6 +743,7 @@ angularWidget('ng:include', function(element){        </doc:scenario>      </doc:example>   */ +//TODO(im): remove all the code related to using and inline equals  var ngSwitch = angularWidget('ng:switch', function (element){    var compiler = this,        watchExpr = element.attr("on"), @@ -805,8 +806,7 @@ var ngSwitch = angularWidget('ng:switch', function (element){  }, {    equals: function(on, when) {      return ''+on == when; -  }, -  route: switchRouteMatcher +  }  }); diff --git a/test/angular-mocksSpec.js b/test/angular-mocksSpec.js index fabc47ea..f49c53b1 100644 --- a/test/angular-mocksSpec.js +++ b/test/angular-mocksSpec.js @@ -120,7 +120,7 @@ describe('TzDate', function() {    });  }); -describe('$log', function() { +describe('$log mock', function() {    var $log;    beforeEach(function() {      $log = MockLogFactory(); diff --git a/test/service/cookieStoreSpec.js b/test/service/cookieStoreSpec.js new file mode 100644 index 00000000..0a493470 --- /dev/null +++ b/test/service/cookieStoreSpec.js @@ -0,0 +1,39 @@ +describe('$cookieStore', function() { +  var scope, $browser, $cookieStore; + +  beforeEach(function() { +    scope = angular.scope(); +    $cookieStore = scope.$service('$cookieStore'); +    $browser = scope.$service('$browser'); +  }); + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it('should serialize objects to json', function() { +    $cookieStore.put('objectCookie', {id: 123, name: 'blah'}); +    scope.$eval(); //force eval in test +    expect($browser.cookies()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'}); +  }); + + +  it('should deserialize json to object', function() { +    $browser.cookies('objectCookie', '{"id":123,"name":"blah"}'); +    $browser.poll(); +    expect($cookieStore.get('objectCookie')).toEqual({id: 123, name: 'blah'}); +  }); + + +  it('should delete objects from the store when remove is called', function() { +    $cookieStore.put('gonner', { "I'll":"Be Back"}); +    scope.$eval(); //force eval in test +    $browser.poll(); +    expect($browser.cookies()).toEqual({'gonner': '{"I\'ll":"Be Back"}'}); + +    $cookieStore.remove('gonner'); +    scope.$eval(); +    expect($browser.cookies()).toEqual({}); +  }); +}); diff --git a/test/service/cookiesSpec.js b/test/service/cookiesSpec.js new file mode 100644 index 00000000..11551393 --- /dev/null +++ b/test/service/cookiesSpec.js @@ -0,0 +1,98 @@ +describe('$cookies', function() { +  var scope, $browser; + +  beforeEach(function() { +    $browser = new MockBrowser(); +    $browser.cookieHash['preexisting'] = 'oldCookie'; +    scope = angular.scope(null, angular.service, {$browser: $browser}); +    scope.$cookies = scope.$service('$cookies'); +  }); + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it('should provide access to existing cookies via object properties and keep them in sync', +      function(){ +    expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); + +    // access internal cookie storage of the browser mock directly to simulate behavior of +    // document.cookie +    $browser.cookieHash['brandNew'] = 'cookie'; +    $browser.poll(); + +    expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie'}); + +    $browser.cookieHash['brandNew'] = 'cookie2'; +    $browser.poll(); +    expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie2'}); + +    delete $browser.cookieHash['brandNew']; +    $browser.poll(); +    expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); +  }); + + +  it('should create or update a cookie when a value is assigned to a property', function() { +    scope.$cookies.oatmealCookie = 'nom nom'; +    scope.$eval(); + +    expect($browser.cookies()). +      toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); + +    scope.$cookies.oatmealCookie = 'gone'; +    scope.$eval(); + +    expect($browser.cookies()). +      toEqual({'preexisting': 'oldCookie', 'oatmealCookie': 'gone'}); +  }); + + +  it('should drop or reset any cookie that was set to a non-string value', function() { +    scope.$cookies.nonString = [1, 2, 3]; +    scope.$cookies.nullVal = null; +    scope.$cookies.undefVal = undefined; +    scope.$cookies.preexisting = function(){}; +    scope.$eval(); +    expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'}); +    expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); +  }); + + +  it('should remove a cookie when a $cookies property is deleted', function() { +    scope.$cookies.oatmealCookie = 'nom nom'; +    scope.$eval(); +    $browser.poll(); +    expect($browser.cookies()). +      toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); + +    delete scope.$cookies.oatmealCookie; +    scope.$eval(); + +    expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'}); +  }); + + +  it('should drop or reset cookies that browser refused to store', function() { +    var i, longVal; + +    for (i=0; i<5000; i++) { +      longVal += '*'; +    } + +    //drop if no previous value +    scope.$cookies.longCookie = longVal; +    scope.$eval(); +    expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); + + +    //reset if previous value existed +    scope.$cookies.longCookie = 'shortVal'; +    scope.$eval(); +    expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'}); +    scope.$cookies.longCookie = longVal; +    scope.$eval(); +    expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'}); +  }); +}); diff --git a/test/service/deferSpec.js b/test/service/deferSpec.js new file mode 100644 index 00000000..932c3661 --- /dev/null +++ b/test/service/deferSpec.js @@ -0,0 +1,69 @@ +describe('$defer', function() { +  var scope, $browser, $defer, $exceptionHandler; + +  beforeEach(function(){ +    scope = angular.scope({}, angular.service, +                          {'$exceptionHandler': jasmine.createSpy('$exceptionHandler')}); +    $browser = scope.$service('$browser'); +    $defer = scope.$service('$defer'); +    $exceptionHandler = scope.$service('$exceptionHandler'); +  }); + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it('should delegate functions to $browser.defer', function() { +    var counter = 0; +    $defer(function() { counter++; }); + +    expect(counter).toBe(0); + +    $browser.defer.flush(); +    expect(counter).toBe(1); + +    $browser.defer.flush(); //does nothing +    expect(counter).toBe(1); + +    expect($exceptionHandler).not.toHaveBeenCalled(); +  }); + + +  it('should delegate exception to the $exceptionHandler service', function() { +    $defer(function() {throw "Test Error";}); +    expect($exceptionHandler).not.toHaveBeenCalled(); + +    $browser.defer.flush(); +    expect($exceptionHandler).toHaveBeenCalledWith("Test Error"); +  }); + + +  it('should call eval after each callback is executed', function() { +    var eval = this.spyOn(scope, '$eval').andCallThrough(); + +    $defer(function() {}); +    expect(eval).wasNotCalled(); + +    $browser.defer.flush(); +    expect(eval).wasCalled(); + +    eval.reset(); //reset the spy; + +    $defer(function() {}); +    $defer(function() {}); +    $browser.defer.flush(); +    expect(eval.callCount).toBe(2); +  }); + + +  it('should call eval even if an exception is thrown in callback', function() { +    var eval = this.spyOn(scope, '$eval').andCallThrough(); + +    $defer(function() {throw "Test Error";}); +    expect(eval).wasNotCalled(); + +    $browser.defer.flush(); +    expect(eval).wasCalled(); +  }); +}); diff --git a/test/service/documentSpec.js b/test/service/documentSpec.js new file mode 100644 index 00000000..bd92023d --- /dev/null +++ b/test/service/documentSpec.js @@ -0,0 +1,17 @@ +describe('$document', function() { +  var scope; + +  beforeEach(function(){ +    scope = angular.scope(); +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it("should inject $document", function(){ +    expect(scope.$service('$document')).toEqual(jqLite(document)); +  }); +}); diff --git a/test/service/exceptionHandlerSpec.js b/test/service/exceptionHandlerSpec.js new file mode 100644 index 00000000..59349065 --- /dev/null +++ b/test/service/exceptionHandlerSpec.js @@ -0,0 +1,23 @@ +describe('$exceptionHandler', function() { +  var scope; + +  beforeEach(function(){ +    scope = angular.scope(); +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it('should log errors', function(){ +    var scope = createScope({}, {$exceptionHandler: $exceptionHandlerFactory}, +                                {$log: $logMock}), +        $log = scope.$service('$log'), +        $exceptionHandler = scope.$service('$exceptionHandler'); + +    $exceptionHandler('myError'); +    expect($log.error.logs.shift()).toEqual(['myError']); +  }); +}); diff --git a/test/service/hoverSpec.js b/test/service/hoverSpec.js new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/service/hoverSpec.js @@ -0,0 +1 @@ + diff --git a/test/service/invalidWidgetsSpec.js b/test/service/invalidWidgetsSpec.js new file mode 100644 index 00000000..b6b2da61 --- /dev/null +++ b/test/service/invalidWidgetsSpec.js @@ -0,0 +1,39 @@ +describe('$invalidWidgets', function() { +  var scope; + +  beforeEach(function(){ +    scope = angular.scope(); +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it("should count number of invalid widgets", function(){ +    scope = compile('<input name="price" ng:required ng:validate="number"></input>'); +    jqLite(document.body).append(scope.$element); +    scope.$init(); +    var $invalidWidgets = scope.$service('$invalidWidgets'); +    expect($invalidWidgets.length).toEqual(1); + +    scope.price = 123; +    scope.$eval(); +    expect($invalidWidgets.length).toEqual(0); + +    scope.$element.remove(); +    scope.price = 'abc'; +    scope.$eval(); +    expect($invalidWidgets.length).toEqual(0); + +    jqLite(document.body).append(scope.$element); +    scope.price = 'abcd'; //force revalidation, maybe this should be done automatically? +    scope.$eval(); +    expect($invalidWidgets.length).toEqual(1); + +    jqLite(document.body).html(''); +    scope.$eval(); +    expect($invalidWidgets.length).toEqual(0); +  }); +}); diff --git a/test/service/locationSpec.js b/test/service/locationSpec.js new file mode 100644 index 00000000..050875b1 --- /dev/null +++ b/test/service/locationSpec.js @@ -0,0 +1,299 @@ +describe('$location', function() { +  var scope, $location, $browser; + +  beforeEach(function(){ +    scope = angular.scope(); +    $location = scope.$service('$location'); +    $browser = scope.$service('$browser'); +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it("should update location object immediately when update is called", function() { +    var href = 'http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2='; +    $location.update(href); +    expect($location.href).toEqual(href); +    expect($location.protocol).toEqual("http"); +    expect($location.host).toEqual("host"); +    expect($location.port).toEqual("123"); +    expect($location.path).toEqual("/p/a/t/h.html"); +    expect($location.search).toEqual({query:'value'}); +    expect($location.hash).toEqual('path?key=value&flag&key2='); +    expect($location.hashPath).toEqual('path'); +    expect($location.hashSearch).toEqual({key: 'value', flag: true, key2: ''}); +  }); + + +  it('should update location when browser url changed', function() { +    var origUrl = $location.href; +    expect(origUrl).toEqual($browser.getUrl()); + +    var newUrl = 'http://somenew/url#foo'; +    $browser.setUrl(newUrl); +    $browser.poll(); +    expect($location.href).toEqual(newUrl); +  }); + + +  it('should update browser at the end of $eval', function() { +    var origBrowserUrl = $browser.getUrl(); +    $location.update('http://www.angularjs.org/'); +    $location.update({path: '/a/b'}); +    expect($location.href).toEqual('http://www.angularjs.org/a/b'); +    expect($browser.getUrl()).toEqual(origBrowserUrl); +    scope.$eval(); +    expect($browser.getUrl()).toEqual('http://www.angularjs.org/a/b'); +  }); + + +  it('should update hashPath and hashSearch on hash update', function(){ +    $location.update('http://server/#path?a=b'); +    expect($location.hashPath).toEqual('path'); +    expect($location.hashSearch).toEqual({a:'b'}); + +    $location.update({hash: ''}); +    expect($location.hashPath).toEqual(''); +    expect($location.hashSearch).toEqual({}); +  }); + + +  it('should update hash on hashPath or hashSearch update', function() { +    $location.update('http://server/#path?a=b'); +    scope.$eval(); +    $location.update({hashPath: '', hashSearch: {}}); + +    expect($location.hash).toEqual(''); +  }); + + +  it('should update hashPath and hashSearch on $location.hash change upon eval', function(){ +    $location.update('http://server/#path?a=b'); +    scope.$eval(); + +    $location.hash = ''; +    scope.$eval(); + +    expect($location.href).toEqual('http://server/'); +    expect($location.hashPath).toEqual(''); +    expect($location.hashSearch).toEqual({}); +  }); + + +  it('should update hash on $location.hashPath or $location.hashSearch change upon eval', +      function() { +    $location.update('http://server/#path?a=b'); +    scope.$eval(); +    $location.hashPath = ''; +    $location.hashSearch = {}; + +    scope.$eval(); + +    expect($location.href).toEqual('http://server/'); +    expect($location.hash).toEqual(''); +  }); + + +  it('should sync $location upon eval before watches are fired', function(){ +    scope.$location = scope.$service('$location'); //publish to the scope for $watch + +    var log = ''; +    scope.$watch('$location.hash', function(){ +      log += this.$location.hashPath + ';'; +    }); +    expect(log).toEqual(';'); + +    log = ''; +    scope.$location.hash = '/abc'; +    scope.$eval(); +    expect(scope.$location.hash).toEqual('/abc'); +    expect(log).toEqual('/abc;'); +  }); + + +  describe('sync', function() { + +    it('should update hash with escaped hashPath', function() { +      $location.hashPath = 'foo=bar'; +      scope.$eval(); +      expect($location.hash).toBe('foo%3Dbar'); +    }); + + +    it('should give $location.href the highest precedence', function() { +      $location.hashPath = 'hashPath'; +      $location.hashSearch = {hash:'search'}; +      $location.hash = 'hash'; +      $location.port = '333'; +      $location.host = 'host'; +      $location.href = 'https://hrefhost:23/hrefpath'; + +      scope.$eval(); + +      expect($location).toEqualData({href: 'https://hrefhost:23/hrefpath', +                                     protocol: 'https', +                                     host: 'hrefhost', +                                     port: '23', +                                     path: '/hrefpath', +                                     search: {}, +                                     hash: '', +                                     hashPath: '', +                                     hashSearch: {} +                                    }); +    }); + + +    it('should give $location.hash second highest precedence', function() { +      $location.hashPath = 'hashPath'; +      $location.hashSearch = {hash:'search'}; +      $location.hash = 'hash'; +      $location.port = '333'; +      $location.host = 'host'; +      $location.path = '/path'; + +      scope.$eval(); + +      expect($location).toEqualData({href: 'http://host:333/path#hash', +                                     protocol: 'http', +                                     host: 'host', +                                     port: '333', +                                     path: '/path', +                                     search: {}, +                                     hash: 'hash', +                                     hashPath: 'hash', +                                     hashSearch: {} +                                    }); +    }); +  }); + + +  describe('update()', function() { + +    it('should accept hash object and update only given properties', function() { +      $location.update("http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2="); +      $location.update({host: 'new', port: 24}); + +      expect($location.host).toEqual('new'); +      expect($location.port).toEqual(24); +      expect($location.protocol).toEqual('http'); +      expect($location.href).toEqual("http://new:24/p/a/t/h.html?query=value#path?key=value&flag&key2="); +    }); + + +    it('should remove # if hash is empty', function() { +      $location.update('http://www.angularjs.org/index.php#'); +      expect($location.href).toEqual('http://www.angularjs.org/index.php'); +    }); + + +    it('should clear hash when updating to hash-less URL', function() { +      $location.update('http://server'); +      expect($location.href).toBe('http://server'); +      expect($location.hash).toBe(''); +    }); +  }); + + +  describe('updateHash()', function() { + +    it('should accept single string argument to update path', function() { +      $location.updateHash('path'); +      expect($location.hash).toEqual('path'); +      expect($location.hashPath).toEqual('path'); +    }); + + +    it('should reset hashSearch when updating with a single string', function() { +      $location.updateHash({foo:'bar'}); //set some initial state for hashSearch + +      $location.updateHash('path'); +      expect($location.hashPath).toEqual('path'); +      expect($location.hashSearch).toEqual({}); +    }); + + +    it('should accept single object argument to update search', function() { +      $location.updateHash({a: 'b'}); +      expect($location.hash).toEqual('?a=b'); +      expect($location.hashSearch).toEqual({a: 'b'}); +    }); + + +    it('should accept path string and search object arguments to update both', function() { +      $location.updateHash('path', {a: 'b'}); +      expect($location.hash).toEqual('path?a=b'); +      expect($location.hashSearch).toEqual({a: 'b'}); +      expect($location.hashPath).toEqual('path'); +    }); + + +    it('should update href and hash when updating to empty string', function() { +      $location.updateHash(''); +      expect($location.href).toBe('http://server'); +      expect($location.hash).toBe(''); + +      scope.$eval(); + +      expect($location.href).toBe('http://server'); +      expect($location.hash).toBe(''); +    }); +  }); + + +  describe('URL_MATCH', function() { + +    it('should parse basic url', function() { +      var match = URL_MATCH.exec('http://www.angularjs.org/path?search#hash?x=x'); + +      expect(match[1]).toEqual('http'); +      expect(match[3]).toEqual('www.angularjs.org'); +      expect(match[6]).toEqual('/path'); +      expect(match[8]).toEqual('search'); +      expect(match[10]).toEqual('hash?x=x'); +    }); + + +    it('should parse file://', function(){ +      var match = URL_MATCH.exec('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); + +      expect(match[1]).toEqual('file'); +      expect(match[3]).toEqual(''); +      expect(match[5]).toBeFalsy(); +      expect(match[6]).toEqual('/Users/Shared/misko/work/angular.js/scenario/widgets.html'); +      expect(match[8]).toBeFalsy(); +    }); + + +    it('should parse url with "-" in host', function(){ +      var match = URL_MATCH.exec('http://a-b1.c-d.09/path'); + +      expect(match[1]).toEqual('http'); +      expect(match[3]).toEqual('a-b1.c-d.09'); +      expect(match[5]).toBeFalsy(); +      expect(match[6]).toEqual('/path'); +      expect(match[8]).toBeFalsy(); +    }); + + +    it('should parse host without "/" at the end', function() { +      var match = URL_MATCH.exec('http://host.org'); +      expect(match[3]).toEqual('host.org'); + +      match = URL_MATCH.exec('http://host.org#'); +      expect(match[3]).toEqual('host.org'); + +      match = URL_MATCH.exec('http://host.org?'); +      expect(match[3]).toEqual('host.org'); +    }); + + +    it('should match with just "/" path', function() { +      var match = URL_MATCH.exec('http://server/#?book=moby'); + +      expect(match[10]).toEqual('?book=moby'); +    }); +  }); +}); diff --git a/test/service/logSpec.js b/test/service/logSpec.js new file mode 100644 index 00000000..5d8fa0db --- /dev/null +++ b/test/service/logSpec.js @@ -0,0 +1,100 @@ +describe('$log', function() { +  var scope; + +  beforeEach(function(){ +    scope = angular.scope(); +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it('should use console if present', function(){ +    var logger = ""; +    function log(){ logger+= 'log;'; } +    function warn(){ logger+= 'warn;'; } +    function info(){ logger+= 'info;'; } +    function error(){ logger+= 'error;'; } +    var scope = createScope({}, {$log: $logFactory}, +                                {$exceptionHandler: rethrow, +                                 $window: {console: {log: log, +                                                     warn: warn, +                                                     info: info, +                                                     error: error}}}), +        $log = scope.$service('$log'); + +    $log.log(); +    $log.warn(); +    $log.info(); +    $log.error(); +    expect(logger).toEqual('log;warn;info;error;'); +  }); + + +  it('should use console.log() if other not present', function(){ +    var logger = ""; +    function log(){ logger+= 'log;'; } +    var scope = createScope({}, {$log: $logFactory}, +                                {$window: {console:{log:log}}, +                                 $exceptionHandler: rethrow}); +    var $log = scope.$service('$log'); +    $log.log(); +    $log.warn(); +    $log.info(); +    $log.error(); +    expect(logger).toEqual('log;log;log;log;'); +  }); + + +  it('should use noop if no console', function(){ +    var scope = createScope({}, {$log: $logFactory}, +                                {$window: {}, +                                 $exceptionHandler: rethrow}), +        $log = scope.$service('$log'); +    $log.log(); +    $log.warn(); +    $log.info(); +    $log.error(); +  }); + + +  describe('$log.error', function(){ +    var e, $log, errorArgs; + +    beforeEach(function(){ +      e = new Error(''); +      e.message = undefined; +      e.sourceURL = undefined; +      e.line = undefined; +      e.stack = undefined; + +      $log = $logFactory({console:{error:function(){ +        errorArgs = arguments; +      }}}); +    }); + + +    it('should pass error if does not have trace', function(){ +      $log.error('abc', e); +      expect(errorArgs).toEqual(['abc', e]); +    }); + + +    it('should print stack', function(){ +      e.stack = 'stack'; +      $log.error('abc', e); +      expect(errorArgs).toEqual(['abc', 'stack']); +    }); + + +    it('should print line', function(){ +      e.message = 'message'; +      e.sourceURL = 'sourceURL'; +      e.line = '123'; +      $log.error('abc', e); +      expect(errorArgs).toEqual(['abc', 'message\nsourceURL:123']); +    }); +  }); +}); diff --git a/test/service/resourceSpec.js b/test/service/resourceSpec.js new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/service/resourceSpec.js @@ -0,0 +1 @@ + diff --git a/test/service/routeSpec.js b/test/service/routeSpec.js new file mode 100644 index 00000000..95258cc8 --- /dev/null +++ b/test/service/routeSpec.js @@ -0,0 +1,228 @@ +describe('$route', function() { +  var scope; + +  beforeEach(function(){ +    scope = angular.scope(); +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it('should route and fire change event', function(){ +    var log = '', +        $location, $route; + +    function BookChapter() { +      this.log = '<init>'; +    } +    scope = compile('<div></div>').$init(); +    $location = scope.$service('$location'); +    $route = scope.$service('$route'); +    $route.when('/Book/:book/Chapter/:chapter', {controller: BookChapter, template:'Chapter.html'}); +    $route.when('/Blank'); +    $route.onChange(function(){ +      log += 'onChange();'; +    }); +    $location.update('http://server#/Book/Moby/Chapter/Intro?p=123'); +    scope.$eval(); +    expect(log).toEqual('onChange();'); +    expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'}); +    expect($route.current.scope.log).toEqual('<init>'); +    var lastId = $route.current.scope.$id; + +    log = ''; +    $location.update('http://server#/Blank?ignore'); +    scope.$eval(); +    expect(log).toEqual('onChange();'); +    expect($route.current.params).toEqual({ignore:true}); +    expect($route.current.scope.$id).not.toEqual(lastId); + +    log = ''; +    $location.update('http://server#/NONE'); +    scope.$eval(); +    expect(log).toEqual('onChange();'); +    expect($route.current).toEqual(null); + +    $route.when('/NONE', {template:'instant update'}); +    scope.$eval(); +    expect($route.current.template).toEqual('instant update'); +  }); + + +  it('should return fn registered with onChange()', function() { +    var scope = angular.scope(), +        $route = scope.$service('$route'), +        fn = function() {}; + +    expect($route.onChange(fn)).toBe(fn); +  }); + + +  it('should allow routes to be defined with just templates without controllers', function() { +    var scope = angular.scope(), +        $location = scope.$service('$location'), +        $route = scope.$service('$route'), +        onChangeSpy = jasmine.createSpy('onChange'); + +    $route.when('/foo', {template: 'foo.html'}); +    $route.onChange(onChangeSpy); +    expect($route.current).toBeNull(); +    expect(onChangeSpy).not.toHaveBeenCalled(); + +    $location.updateHash('/foo'); +    scope.$eval(); + +    expect($route.current.template).toEqual('foo.html'); +    expect($route.current.controller).toBeUndefined(); +    expect(onChangeSpy).toHaveBeenCalled(); +  }); + + +  it('should handle unknown routes with "otherwise" route definition', function() { +    var scope = angular.scope(), +        $location = scope.$service('$location'), +        $route = scope.$service('$route'), +        onChangeSpy = jasmine.createSpy('onChange'); + +    function NotFoundCtrl() {this.notFoundProp = 'not found!'} + +    $route.when('/foo', {template: 'foo.html'}); +    $route.otherwise({template: '404.html', controller: NotFoundCtrl}); +    $route.onChange(onChangeSpy); +    expect($route.current).toBeNull(); +    expect(onChangeSpy).not.toHaveBeenCalled(); + +    $location.updateHash('/unknownRoute'); +    scope.$eval(); + +    expect($route.current.template).toBe('404.html'); +    expect($route.current.controller).toBe(NotFoundCtrl); +    expect($route.current.scope.notFoundProp).toBe('not found!'); +    expect(onChangeSpy).toHaveBeenCalled(); + +    onChangeSpy.reset(); +    $location.updateHash('/foo'); +    scope.$eval(); + +    expect($route.current.template).toEqual('foo.html'); +    expect($route.current.controller).toBeUndefined(); +    expect($route.current.scope.notFoundProp).toBeUndefined(); +    expect(onChangeSpy).toHaveBeenCalled(); +  }); + + +  describe('redirection', function() { + +    it('should support redirection via redirectTo property by updating $location', function() { +      var scope = angular.scope(), +          $location = scope.$service('$location'), +          $browser = scope.$service('$browser'), +          $route = scope.$service('$route'), +          onChangeSpy = jasmine.createSpy('onChange'); + +      $route.when('', {redirectTo: '/foo'}); +      $route.when('/foo', {template: 'foo.html'}); +      $route.when('/bar', {template: 'bar.html'}); +      $route.when('/baz', {redirectTo: '/bar'}); +      $route.otherwise({template: '404.html'}); +      $route.onChange(onChangeSpy); +      expect($route.current).toBeNull(); +      expect(onChangeSpy).not.toHaveBeenCalled(); + +      scope.$eval(); //triggers initial route change - match the redirect route +      $browser.defer.flush(); //triger route change - match the route we redirected to + +      expect($location.hash).toBe('/foo'); +      expect($route.current.template).toBe('foo.html'); +      expect(onChangeSpy.callCount).toBe(1); + +      onChangeSpy.reset(); +      $location.updateHash(''); +      scope.$eval(); //match the redirect route + update $browser +      $browser.defer.flush(); //match the route we redirected to + +      expect($location.hash).toBe('/foo'); +      expect($route.current.template).toBe('foo.html'); +      expect(onChangeSpy.callCount).toBe(1); + +      onChangeSpy.reset(); +      $location.updateHash('/baz'); +      scope.$eval(); //match the redirect route + update $browser +      $browser.defer.flush(); //match the route we redirected to + +      expect($location.hash).toBe('/bar'); +      expect($route.current.template).toBe('bar.html'); +      expect(onChangeSpy.callCount).toBe(1); +    }); + + +    it('should interpolate route variables in the redirected hashPath from the original hashPath', +        function() { +      var scope = angular.scope(), +          $location = scope.$service('$location'), +          $browser = scope.$service('$browser'), +          $route = scope.$service('$route'); + +      $route.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'}); +      $route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'}); +      scope.$eval(); + +      $location.updateHash('/foo/id1/foo/subid3/gah'); +      scope.$eval(); //triggers initial route change - match the redirect route +      $browser.defer.flush(); //triger route change - match the route we redirected to + +      expect($location.hash).toBe('/bar/id1/subid3/23?extraId=gah'); +      expect($route.current.template).toBe('bar.html'); +    }); + + +    it('should interpolate route variables in the redirected hashPath from the original hashSearch', +        function() { +      var scope = angular.scope(), +          $location = scope.$service('$location'), +          $browser = scope.$service('$browser'), +          $route = scope.$service('$route'); + +      $route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'}); +      $route.when('/foo/:id/:extra', {redirectTo: '/bar/:id/:subid/99'}); +      scope.$eval(); + +      $location.hash = '/foo/id3/eId?subid=sid1&appended=true'; +      scope.$eval(); //triggers initial route change - match the redirect route +      $browser.defer.flush(); //triger route change - match the route we redirected to + +      expect($location.hash).toBe('/bar/id3/sid1/99?appended=true&extra=eId'); +      expect($route.current.template).toBe('bar.html'); +    }); + + +    it('should allow custom redirectTo function to be used', function() { +      var scope = angular.scope(), +          $location = scope.$service('$location'), +          $browser = scope.$service('$browser'), +          $route = scope.$service('$route'); + +      $route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'}); +      $route.when('/foo/:id', +                  {redirectTo: customRedirectFn}); +      scope.$eval(); + +      $location.hash = '/foo/id3?subid=sid1&appended=true'; +      scope.$eval(); //triggers initial route change - match the redirect route +      $browser.defer.flush(); //triger route change - match the route we redirected to + +      expect($location.hash).toBe('custom'); + +      function customRedirectFn(routePathParams, hash, hashPath, hashSearch) { +        expect(routePathParams).toEqual({id: 'id3'}); +        expect(hash).toEqual($location.hash); +        expect(hashPath).toEqual($location.hashPath); +        expect(hashSearch).toEqual($location.hashSearch); +        return 'custom'; +      } +    }); +  }); +}); diff --git a/test/service/updateViewSpec.js b/test/service/updateViewSpec.js new file mode 100644 index 00000000..beca355e --- /dev/null +++ b/test/service/updateViewSpec.js @@ -0,0 +1,61 @@ +describe('$updateView', function() { +  var scope, browser, evalCount, $updateView; + +  beforeEach(function(){ +    browser = new MockBrowser(); +    // Pretend that you are real Browser so that we see the delays +    browser.isMock = false; +    browser.defer = jasmine.createSpy('defer'); + +    scope = angular.scope(null, null, {$browser:browser}); +    $updateView = scope.$service('$updateView'); +    scope.$onEval(function(){ evalCount++; }); +    evalCount = 0; +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it('should eval root scope after a delay', function(){ +    $updateView(); +    expect(evalCount).toEqual(0); +    expect(browser.defer).toHaveBeenCalled(); +    expect(browser.defer.mostRecentCall.args[1]).toEqual(25); +    browser.defer.mostRecentCall.args[0](); +    expect(evalCount).toEqual(1); +  }); + + +  it('should allow changing of delay time', function(){ +    var oldValue = angular.service('$updateView').delay; +    angular.service('$updateView').delay = 50; +    $updateView(); +    expect(evalCount).toEqual(0); +    expect(browser.defer).toHaveBeenCalled(); +    expect(browser.defer.mostRecentCall.args[1]).toEqual(50); +    angular.service('$updateView').delay = oldValue; +  }); + + +  it('should ignore multiple requests for update', function(){ +    $updateView(); +    $updateView(); +    expect(evalCount).toEqual(0); +    expect(browser.defer).toHaveBeenCalled(); +    expect(browser.defer.callCount).toEqual(1); +    browser.defer.mostRecentCall.args[0](); +    expect(evalCount).toEqual(1); +  }); + + +  it('should update immediatelly in test/mock mode', function(){ +    scope = angular.scope(); +    scope.$onEval(function(){ evalCount++; }); +    expect(evalCount).toEqual(0); +    scope.$service('$updateView')(); +    expect(evalCount).toEqual(1); +  }); +}); diff --git a/test/service/windowSpec.js b/test/service/windowSpec.js new file mode 100644 index 00000000..e968f560 --- /dev/null +++ b/test/service/windowSpec.js @@ -0,0 +1,17 @@ +describe('$window', function() { +  var scope; + +  beforeEach(function(){ +    scope = angular.scope(); +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  it("should inject $window", function(){ +    expect(scope.$service('$window')).toBe(window); +  }); +}); diff --git a/test/service/xhr.bulkSpec.js b/test/service/xhr.bulkSpec.js new file mode 100644 index 00000000..89429a91 --- /dev/null +++ b/test/service/xhr.bulkSpec.js @@ -0,0 +1,69 @@ +describe('$xhr.bulk', function() { +  var scope, $browser, $browserXhr, $log, $xhrBulk, $xhrError, log; + +  beforeEach(function(){ +    scope = angular.scope({}, angular.service, { +      '$xhr.error': $xhrError = jasmine.createSpy('$xhr.error'), +      '$log': $log = {} +    }); +    $browser = scope.$service('$browser'); +    $browserXhr = $browser.xhr; +    $xhrBulk = scope.$service('$xhr.bulk'); +    log = ''; +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  function callback(code, response) { +    expect(code).toEqual(200); +    log = log + toJson(response) + ';'; +  } + + +  it('should collect requests', function(){ +    $xhrBulk.urls["/"] = {match:/.*/}; +    $xhrBulk('GET', '/req1', null, callback); +    $xhrBulk('POST', '/req2', {post:'data'}, callback); + +    $browserXhr.expectPOST('/', { +      requests:[{method:'GET',  url:'/req1', data: null}, +                {method:'POST', url:'/req2', data:{post:'data'} }] +    }).respond([ +      {status:200, response:'first'}, +      {status:200, response:'second'} +    ]); +    $xhrBulk.flush(function(){ log += 'DONE';}); +    $browserXhr.flush(); +    expect(log).toEqual('"first";"second";DONE'); +  }); + + +  it('should handle non 200 status code by forwarding to error handler', function(){ +    $xhrBulk.urls['/'] = {match:/.*/}; +    $xhrBulk('GET', '/req1', null, callback); +    $xhrBulk('POST', '/req2', {post:'data'}, callback); + +    $browserXhr.expectPOST('/', { +      requests:[{method:'GET',  url:'/req1', data: null}, +                {method:'POST', url:'/req2', data:{post:'data'} }] +    }).respond([ +      {status:404, response:'NotFound'}, +      {status:200, response:'second'} +    ]); +    $xhrBulk.flush(function(){ log += 'DONE';}); +    $browserXhr.flush(); + +    expect($xhrError).wasCalled(); +    var cb = $xhrError.mostRecentCall.args[0].callback; +    expect(typeof cb).toEqual($function); +    expect($xhrError).wasCalledWith( +        {url:'/req1', method:'GET', data:null, callback:cb}, +        {status:404, response:'NotFound'}); + +    expect(log).toEqual('"second";DONE'); +  }); +}); diff --git a/test/service/xhr.cacheSpec.js b/test/service/xhr.cacheSpec.js new file mode 100644 index 00000000..82b33b72 --- /dev/null +++ b/test/service/xhr.cacheSpec.js @@ -0,0 +1,128 @@ +describe('$xhr.cache', function() { +  var scope, $browser, $browserXhr, cache, log; + +  beforeEach(function(){ +    scope = angular.scope(); +    $browser = scope.$service('$browser'); +    $browserXhr = $browser.xhr; +    cache = scope.$service('$xhr.cache'); +    log = ''; +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  function callback(code, response) { +    expect(code).toEqual(200); +    log = log + toJson(response) + ';'; +  } + + +  it('should cache requests', function(){ +    $browserXhr.expectGET('/url').respond('first'); +    cache('GET', '/url', null, callback); +    $browserXhr.flush(); + +    $browserXhr.expectGET('/url').respond('ERROR'); +    cache('GET', '/url', null, callback); +    $browser.defer.flush(); +    expect(log).toEqual('"first";"first";'); + +    cache('GET', '/url', null, callback, false); +    $browser.defer.flush(); +    expect(log).toEqual('"first";"first";"first";'); +  }); + + +  it('should first return cache request, then return server request', function(){ +    $browserXhr.expectGET('/url').respond('first'); +    cache('GET', '/url', null, callback, true); +    $browserXhr.flush(); + +    $browserXhr.expectGET('/url').respond('ERROR'); +    cache('GET', '/url', null, callback, true); +    $browser.defer.flush(); +    expect(log).toEqual('"first";"first";'); + +    $browserXhr.flush(); +    expect(log).toEqual('"first";"first";"ERROR";'); +  }); + + +  it('should serve requests from cache', function(){ +    cache.data.url = {value:'123'}; +    cache('GET', 'url', null, callback); +    $browser.defer.flush(); +    expect(log).toEqual('"123";'); + +    cache('GET', 'url', null, callback, false); +    $browser.defer.flush(); +    expect(log).toEqual('"123";"123";'); +  }); + + +  it('should keep track of in flight requests and request only once', function(){ +    scope.$service('$xhr.bulk').urls['/bulk'] = { +      match:function(url){ +        return url == '/url'; +      } +    }; +    $browserXhr.expectPOST('/bulk', { +      requests:[{method:'GET',  url:'/url', data: null}] +    }).respond([ +      {status:200, response:'123'} +    ]); +    cache('GET', '/url', null, callback); +    cache('GET', '/url', null, callback); +    cache.delegate.flush(); +    $browserXhr.flush(); +    expect(log).toEqual('"123";"123";'); +  }); + + +  it('should clear cache on non GET', function(){ +    $browserXhr.expectPOST('abc', {}).respond({}); +    cache.data.url = {value:123}; +    cache('POST', 'abc', {}); +    expect(cache.data.url).toBeUndefined(); +  }); + + +  it('should call callback asynchronously for both cache hit and cache miss', function() { +    $browserXhr.expectGET('/url').respond('+'); +    cache('GET', '/url', null, callback); +    expect(log).toEqual(''); //callback hasn't executed + +    $browserXhr.flush(); +    expect(log).toEqual('"+";'); //callback has executed + +    cache('GET', '/url', null, callback); +    expect(log).toEqual('"+";'); //callback hasn't executed + +    $browser.defer.flush(); +    expect(log).toEqual('"+";"+";'); //callback has executed +  }); + + +  it('should call eval after callbacks for both cache hit and cache miss execute', function() { +    var eval = this.spyOn(scope, '$eval').andCallThrough(); + +    $browserXhr.expectGET('/url').respond('+'); +    cache('GET', '/url', null, callback); +    expect(eval).wasNotCalled(); + +    $browserXhr.flush(); +    expect(eval).wasCalled(); + +    eval.reset(); //reset the spy + +    cache('GET', '/url', null, callback); +    expect(eval).wasNotCalled(); + +    $browser.defer.flush(); +    expect(eval).wasCalled(); +  }); +}); diff --git a/test/service/xhr.errorSpec.js b/test/service/xhr.errorSpec.js new file mode 100644 index 00000000..da1b102e --- /dev/null +++ b/test/service/xhr.errorSpec.js @@ -0,0 +1,36 @@ +describe('$xhr.error', function() { +  var scope, $browser, $browserXhr, $xhr, $xhrError, log; + +  beforeEach(function(){ +    scope = angular.scope({}, angular.service, { +      '$xhr.error': $xhrError = jasmine.createSpy('$xhr.error') +    }); +    $browser = scope.$service('$browser'); +    $browserXhr = $browser.xhr; +    $xhr = scope.$service('$xhr'); +    log = ''; +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  function callback(code, response) { +    expect(code).toEqual(200); +    log = log + toJson(response) + ';'; +  } + + +  it('should handle non 200 status codes by forwarding to error handler', function(){ +    $browserXhr.expectPOST('/req', 'MyData').respond(500, 'MyError'); +    $xhr('POST', '/req', 'MyData', callback); +    $browserXhr.flush(); +    var cb = $xhrError.mostRecentCall.args[0].callback; +    expect(typeof cb).toEqual($function); +    expect($xhrError).wasCalledWith( +        {url:'/req', method:'POST', data:'MyData', callback:cb}, +        {status:500, body:'MyError'}); +  }); +}); diff --git a/test/service/xhrSpec.js b/test/service/xhrSpec.js new file mode 100644 index 00000000..35861a92 --- /dev/null +++ b/test/service/xhrSpec.js @@ -0,0 +1,47 @@ +describe('$xhr', function() { +  var scope, $browser, $browserXhr, $log, $xhr, log; + +  beforeEach(function(){ +    scope = angular.scope({}, angular.service, { '$log': $log = {} }); +    $browser = scope.$service('$browser'); +    $browserXhr = $browser.xhr; +    $xhr = scope.$service('$xhr'); +    log = ''; +  }); + + +  afterEach(function(){ +    dealoc(scope); +  }); + + +  function callback(code, response) { +    expect(code).toEqual(200); +    log = log + toJson(response) + ';'; +  } + + +  it('should forward the request to $browser and decode JSON', function(){ +    $browserXhr.expectGET('/reqGET').respond('first'); +    $browserXhr.expectGET('/reqGETjson').respond('["second"]'); +    $browserXhr.expectPOST('/reqPOST', {post:'data'}).respond('third'); + +    $xhr('GET', '/reqGET', null, callback); +    $xhr('GET', '/reqGETjson', null, callback); +    $xhr('POST', '/reqPOST', {post:'data'}, callback); + +    $browserXhr.flush(); + +    expect(log).toEqual('"third";["second"];"first";'); +  }); + + +  it('should handle exceptions in callback', function(){ +    $log.error = jasmine.createSpy('$log.error'); +    $browserXhr.expectGET('/reqGET').respond('first'); +    $xhr('GET', '/reqGET', null, function(){ throw "MyException"; }); +    $browserXhr.flush(); + +    expect($log.error).wasCalledWith("MyException"); +  }); +}); diff --git a/test/servicesSpec.js b/test/servicesSpec.js deleted file mode 100644 index 0186684e..00000000 --- a/test/servicesSpec.js +++ /dev/null @@ -1,1081 +0,0 @@ -describe("service", function(){ -  var scope, $xhrError, $log, mockServices, $browser, $browserXhr, $xhrBulk, $xhr; - -  beforeEach(function(){ -    $xhrError = jasmine.createSpy('$xhr.error'); -    $log = {}; -    scope = createScope({}, angularService, { -      '$xhr.error': $xhrError, -      '$log': $log -    }); -    $browser = scope.$service('$browser'); -    $browserXhr = $browser.xhr; -    $xhrBulk = scope.$service('$xhr.bulk'); -    $xhr = scope.$service('$xhr'); -  }); - -  afterEach(function(){ -    dealoc(scope); -  }); - - - -  it("should inject $window", function(){ -    expect(scope.$service('$window')).toEqual(window); -  }); - -  describe("$log", function(){ -    it('should use console if present', function(){ -      var logger = ""; -      function log(){ logger+= 'log;'; } -      function warn(){ logger+= 'warn;'; } -      function info(){ logger+= 'info;'; } -      function error(){ logger+= 'error;'; } -      var scope = createScope({}, {$log: $logFactory}, -                                  {$exceptionHandler: rethrow, -                                   $window: {console: {log: log, -                                                       warn: warn, -                                                       info: info, -                                                       error: error}}}), -          $log = scope.$service('$log'); - -      $log.log(); -      $log.warn(); -      $log.info(); -      $log.error(); -      expect(logger).toEqual('log;warn;info;error;'); -    }); - -    it('should use console.log() if other not present', function(){ -      var logger = ""; -      function log(){ logger+= 'log;'; } -      var scope = createScope({}, {$log: $logFactory}, -                                  {$window: {console:{log:log}}, -                                   $exceptionHandler: rethrow}); -      var $log = scope.$service('$log'); -      $log.log(); -      $log.warn(); -      $log.info(); -      $log.error(); -      expect(logger).toEqual('log;log;log;log;'); -    }); - -    it('should use noop if no console', function(){ -      var scope = createScope({}, {$log: $logFactory}, -                                  {$window: {}, -                                   $exceptionHandler: rethrow}), -          $log = scope.$service('$log'); -      $log.log(); -      $log.warn(); -      $log.info(); -      $log.error(); -    }); - -    describe('error', function(){ -      var e, $log, errorArgs; -      beforeEach(function(){ -        e = new Error(''); -        e.message = undefined; -        e.sourceURL = undefined; -        e.line = undefined; -        e.stack = undefined; - -        $log = $logFactory({console:{error:function(){ -          errorArgs = arguments; -        }}}); -      }); - -      it('should pass error if does not have trace', function(){ -        $log.error('abc', e); -        expect(errorArgs).toEqual(['abc', e]); -      }); - -      it('should print stack', function(){ -        e.stack = 'stack'; -        $log.error('abc', e); -        expect(errorArgs).toEqual(['abc', 'stack']); -      }); - -      it('should print line', function(){ -        e.message = 'message'; -        e.sourceURL = 'sourceURL'; -        e.line = '123'; -        $log.error('abc', e); -        expect(errorArgs).toEqual(['abc', 'message\nsourceURL:123']); -      }); - -    }); - -  }); - -  describe("$exceptionHandler", function(){ -    it('should log errors', function(){ -      var scope = createScope({}, {$exceptionHandler: $exceptionHandlerFactory}, -                                  {$log: $logMock}), -          $log = scope.$service('$log'), -          $exceptionHandler = scope.$service('$exceptionHandler'); - -      $exceptionHandler('myError'); -      expect($log.error.logs.shift()).toEqual(['myError']); -    }); -  }); - -  describe("$location", function(){ -    var $location; - -    beforeEach(function() { -      $location = scope.$service('$location'); -    }); - - -    it("should update location object immediately when update is called", function() { -      var href = 'http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2='; -      $location.update(href); -      expect($location.href).toEqual(href); -      expect($location.protocol).toEqual("http"); -      expect($location.host).toEqual("host"); -      expect($location.port).toEqual("123"); -      expect($location.path).toEqual("/p/a/t/h.html"); -      expect($location.search).toEqual({query:'value'}); -      expect($location.hash).toEqual('path?key=value&flag&key2='); -      expect($location.hashPath).toEqual('path'); -      expect($location.hashSearch).toEqual({key: 'value', flag: true, key2: ''}); -    }); - - -    it('should update location when browser url changed', function() { -      var origUrl = $location.href; -      expect(origUrl).toEqual($browser.getUrl()); - -      var newUrl = 'http://somenew/url#foo'; -      $browser.setUrl(newUrl); -      $browser.poll(); -      expect($location.href).toEqual(newUrl); -    }); - - -    it('should update browser at the end of $eval', function() { -      var origBrowserUrl = $browser.getUrl(); -      $location.update('http://www.angularjs.org/'); -      $location.update({path: '/a/b'}); -      expect($location.href).toEqual('http://www.angularjs.org/a/b'); -      expect($browser.getUrl()).toEqual(origBrowserUrl); -      scope.$eval(); -      expect($browser.getUrl()).toEqual('http://www.angularjs.org/a/b'); -    }); - - -    it('should update hashPath and hashSearch on hash update', function(){ -      $location.update('http://server/#path?a=b'); -      expect($location.hashPath).toEqual('path'); -      expect($location.hashSearch).toEqual({a:'b'}); - -      $location.update({hash: ''}); -      expect($location.hashPath).toEqual(''); -      expect($location.hashSearch).toEqual({}); -    }); - - -    it('should update hash on hashPath or hashSearch update', function() { -      $location.update('http://server/#path?a=b'); -      scope.$eval(); -      $location.update({hashPath: '', hashSearch: {}}); - -      expect($location.hash).toEqual(''); -    }); - - -    it('should update hashPath and hashSearch on $location.hash change upon eval', function(){ -      $location.update('http://server/#path?a=b'); -      scope.$eval(); - -      $location.hash = ''; -      scope.$eval(); - -      expect($location.href).toEqual('http://server/'); -      expect($location.hashPath).toEqual(''); -      expect($location.hashSearch).toEqual({}); -    }); - - -    it('should update hash on $location.hashPath or $location.hashSearch change upon eval', -        function() { -      $location.update('http://server/#path?a=b'); -      scope.$eval(); -      $location.hashPath = ''; -      $location.hashSearch = {}; - -      scope.$eval(); - -      expect($location.href).toEqual('http://server/'); -      expect($location.hash).toEqual(''); -    }); - - -    it('should sync $location upon eval before watches are fired', function(){ -      scope.$location = scope.$service('$location'); //publish to the scope for $watch - -      var log = ''; -      scope.$watch('$location.hash', function(){ -        log += this.$location.hashPath + ';'; -      }); -      expect(log).toEqual(';'); - -      log = ''; -      scope.$location.hash = '/abc'; -      scope.$eval(); -      expect(scope.$location.hash).toEqual('/abc'); -      expect(log).toEqual('/abc;'); -    }); - - -    describe('sync', function() { -      it('should update hash with escaped hashPath', function() { -        $location.hashPath = 'foo=bar'; -        scope.$eval(); -        expect($location.hash).toBe('foo%3Dbar'); -      }); - - -      it('should give $location.href the highest precedence', function() { -        $location.hashPath = 'hashPath'; -        $location.hashSearch = {hash:'search'}; -        $location.hash = 'hash'; -        $location.port = '333'; -        $location.host = 'host'; -        $location.href = 'https://hrefhost:23/hrefpath'; - -        scope.$eval(); - -        expect($location).toEqualData({href: 'https://hrefhost:23/hrefpath', -                                       protocol: 'https', -                                       host: 'hrefhost', -                                       port: '23', -                                       path: '/hrefpath', -                                       search: {}, -                                       hash: '', -                                       hashPath: '', -                                       hashSearch: {} -                                      }); -      }); - - -      it('should give $location.hash second highest precedence', function() { -        $location.hashPath = 'hashPath'; -        $location.hashSearch = {hash:'search'}; -        $location.hash = 'hash'; -        $location.port = '333'; -        $location.host = 'host'; -        $location.path = '/path'; - -        scope.$eval(); - -        expect($location).toEqualData({href: 'http://host:333/path#hash', -                                       protocol: 'http', -                                       host: 'host', -                                       port: '333', -                                       path: '/path', -                                       search: {}, -                                       hash: 'hash', -                                       hashPath: 'hash', -                                       hashSearch: {} -                                      }); -      }); -    }); - -    describe('update()', function() { -      it('should accept hash object and update only given properties', function() { -        $location.update("http://host:123/p/a/t/h.html?query=value#path?key=value&flag&key2="); -        $location.update({host: 'new', port: 24}); - -        expect($location.host).toEqual('new'); -        expect($location.port).toEqual(24); -        expect($location.protocol).toEqual('http'); -        expect($location.href).toEqual("http://new:24/p/a/t/h.html?query=value#path?key=value&flag&key2="); -      }); - -      it('should remove # if hash is empty', function() { -        $location.update('http://www.angularjs.org/index.php#'); -        expect($location.href).toEqual('http://www.angularjs.org/index.php'); -      }); - -      it('should clear hash when updating to hash-less URL', function() { -        $location.update('http://server'); -        expect($location.href).toBe('http://server'); -        expect($location.hash).toBe(''); -      }); -    }); - - -    describe('updateHash()', function() { -      it('should accept single string argument to update path', function() { -        $location.updateHash('path'); -        expect($location.hash).toEqual('path'); -        expect($location.hashPath).toEqual('path'); -      }); - -      it('should reset hashSearch when updating with a single string', function() { -        $location.updateHash({foo:'bar'}); //set some initial state for hashSearch - -        $location.updateHash('path'); -        expect($location.hashPath).toEqual('path'); -        expect($location.hashSearch).toEqual({}); -      }); - -      it('should accept single object argument to update search', function() { -        $location.updateHash({a: 'b'}); -        expect($location.hash).toEqual('?a=b'); -        expect($location.hashSearch).toEqual({a: 'b'}); -      }); - -      it('should accept path string and search object arguments to update both', function() { -        $location.updateHash('path', {a: 'b'}); -        expect($location.hash).toEqual('path?a=b'); -        expect($location.hashSearch).toEqual({a: 'b'}); -        expect($location.hashPath).toEqual('path'); -      }); - -      it('should update href and hash when updating to empty string', function() { -        $location.updateHash(''); -        expect($location.href).toBe('http://server'); -        expect($location.hash).toBe(''); - -        scope.$eval(); - -        expect($location.href).toBe('http://server'); -        expect($location.hash).toBe(''); -      }); -    }); -  }); - -  describe("$invalidWidgets", function(){ -    it("should count number of invalid widgets", function(){ -      scope = compile('<input name="price" ng:required ng:validate="number"></input>'); -      jqLite(document.body).append(scope.$element); -      scope.$init(); -      var $invalidWidgets = scope.$service('$invalidWidgets'); -      expect($invalidWidgets.length).toEqual(1); - -      scope.price = 123; -      scope.$eval(); -      expect($invalidWidgets.length).toEqual(0); - -      scope.$element.remove(); -      scope.price = 'abc'; -      scope.$eval(); -      expect($invalidWidgets.length).toEqual(0); - -      jqLite(document.body).append(scope.$element); -      scope.price = 'abcd'; //force revalidation, maybe this should be done automatically? -      scope.$eval(); -      expect($invalidWidgets.length).toEqual(1); - -      jqLite(document.body).html(''); -      scope.$eval(); -      expect($invalidWidgets.length).toEqual(0); -    }); -  }); - - -  describe("$route", function(){ -    it('should route and fire change event', function(){ -      var log = '', -          $location, $route; - -      function BookChapter() { -        this.log = '<init>'; -      } -      scope = compile('<div></div>').$init(); -      $location = scope.$service('$location'); -      $route = scope.$service('$route'); -      $route.when('/Book/:book/Chapter/:chapter', {controller: BookChapter, template:'Chapter.html'}); -      $route.when('/Blank'); -      $route.onChange(function(){ -        log += 'onChange();'; -      }); -      $location.update('http://server#/Book/Moby/Chapter/Intro?p=123'); -      scope.$eval(); -      expect(log).toEqual('onChange();'); -      expect($route.current.params).toEqual({book:'Moby', chapter:'Intro', p:'123'}); -      expect($route.current.scope.log).toEqual('<init>'); -      var lastId = $route.current.scope.$id; - -      log = ''; -      $location.update('http://server#/Blank?ignore'); -      scope.$eval(); -      expect(log).toEqual('onChange();'); -      expect($route.current.params).toEqual({ignore:true}); -      expect($route.current.scope.$id).not.toEqual(lastId); - -      log = ''; -      $location.update('http://server#/NONE'); -      scope.$eval(); -      expect(log).toEqual('onChange();'); -      expect($route.current).toEqual(null); - -      $route.when('/NONE', {template:'instant update'}); -      scope.$eval(); -      expect($route.current.template).toEqual('instant update'); -    }); - -    it('should return fn registered with onChange()', function() { -      var scope = angular.scope(), -          $route = scope.$service('$route'), -          fn = function() {}; - -      expect($route.onChange(fn)).toBe(fn); -    }); - -    it('should allow routes to be defined with just templates without controllers', function() { -      var scope = angular.scope(), -          $location = scope.$service('$location'), -          $route = scope.$service('$route'), -          onChangeSpy = jasmine.createSpy('onChange'); - -      $route.when('/foo', {template: 'foo.html'}); -      $route.onChange(onChangeSpy); -      expect($route.current).toBeNull(); -      expect(onChangeSpy).not.toHaveBeenCalled(); - -      $location.updateHash('/foo'); -      scope.$eval(); - -      expect($route.current.template).toEqual('foo.html'); -      expect($route.current.controller).toBeUndefined(); -      expect(onChangeSpy).toHaveBeenCalled(); -    }); - -    it('should handle unknown routes with "otherwise" route definition', function() { -      var scope = angular.scope(), -          $location = scope.$service('$location'), -          $route = scope.$service('$route'), -          onChangeSpy = jasmine.createSpy('onChange'); - -      function NotFoundCtrl() {this.notFoundProp = 'not found!'} - -      $route.when('/foo', {template: 'foo.html'}); -      $route.otherwise({template: '404.html', controller: NotFoundCtrl}); -      $route.onChange(onChangeSpy); -      expect($route.current).toBeNull(); -      expect(onChangeSpy).not.toHaveBeenCalled(); - -      $location.updateHash('/unknownRoute'); -      scope.$eval(); - -      expect($route.current.template).toBe('404.html'); -      expect($route.current.controller).toBe(NotFoundCtrl); -      expect($route.current.scope.notFoundProp).toBe('not found!'); -      expect(onChangeSpy).toHaveBeenCalled(); - -      onChangeSpy.reset(); -      $location.updateHash('/foo'); -      scope.$eval(); - -      expect($route.current.template).toEqual('foo.html'); -      expect($route.current.controller).toBeUndefined(); -      expect($route.current.scope.notFoundProp).toBeUndefined(); -      expect(onChangeSpy).toHaveBeenCalled(); -    }); -  }); - - -  describe('redirection', function() { - -    it('should support redirection via redirectTo property by updating $location', function() { -      var scope = angular.scope(), -          $location = scope.$service('$location'), -          $browser = scope.$service('$browser'), -          $route = scope.$service('$route'), -          onChangeSpy = jasmine.createSpy('onChange'); - -      $route.when('', {redirectTo: '/foo'}); -      $route.when('/foo', {template: 'foo.html'}); -      $route.when('/bar', {template: 'bar.html'}); -      $route.when('/baz', {redirectTo: '/bar'}); -      $route.otherwise({template: '404.html'}); -      $route.onChange(onChangeSpy); -      expect($route.current).toBeNull(); -      expect(onChangeSpy).not.toHaveBeenCalled(); - -      scope.$eval(); //triggers initial route change - match the redirect route -      $browser.defer.flush(); //triger route change - match the route we redirected to - -      expect($location.hash).toBe('/foo'); -      expect($route.current.template).toBe('foo.html'); -      expect(onChangeSpy.callCount).toBe(1); - -      onChangeSpy.reset(); -      $location.updateHash(''); -      scope.$eval(); //match the redirect route + update $browser -      $browser.defer.flush(); //match the route we redirected to - -      expect($location.hash).toBe('/foo'); -      expect($route.current.template).toBe('foo.html'); -      expect(onChangeSpy.callCount).toBe(1); - -      onChangeSpy.reset(); -      $location.updateHash('/baz'); -      scope.$eval(); //match the redirect route + update $browser -      $browser.defer.flush(); //match the route we redirected to - -      expect($location.hash).toBe('/bar'); -      expect($route.current.template).toBe('bar.html'); -      expect(onChangeSpy.callCount).toBe(1); -    }); - -    it('should interpolate route variables in the redirected hashPath from the original hashPath', -        function() { -      var scope = angular.scope(), -          $location = scope.$service('$location'), -          $browser = scope.$service('$browser'), -          $route = scope.$service('$route'); - -      $route.when('/foo/:id/foo/:subid/:extraId', {redirectTo: '/bar/:id/:subid/23'}); -      $route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'}); -      scope.$eval(); - -      $location.updateHash('/foo/id1/foo/subid3/gah'); -      scope.$eval(); //triggers initial route change - match the redirect route -      $browser.defer.flush(); //triger route change - match the route we redirected to - -      expect($location.hash).toBe('/bar/id1/subid3/23?extraId=gah'); -      expect($route.current.template).toBe('bar.html'); -    }); - -    it('should interpolate route variables in the redirected hashPath from the original hashSearch', -        function() { -      var scope = angular.scope(), -          $location = scope.$service('$location'), -          $browser = scope.$service('$browser'), -          $route = scope.$service('$route'); - -      $route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'}); -      $route.when('/foo/:id/:extra', {redirectTo: '/bar/:id/:subid/99'}); -      scope.$eval(); - -      $location.hash = '/foo/id3/eId?subid=sid1&appended=true'; -      scope.$eval(); //triggers initial route change - match the redirect route -      $browser.defer.flush(); //triger route change - match the route we redirected to - -      expect($location.hash).toBe('/bar/id3/sid1/99?appended=true&extra=eId'); -      expect($route.current.template).toBe('bar.html'); -    }); - -    it('should allow custom redirectTo function to be used', function() { -      var scope = angular.scope(), -          $location = scope.$service('$location'), -          $browser = scope.$service('$browser'), -          $route = scope.$service('$route'); - -      $route.when('/bar/:id/:subid/:subsubid', {template: 'bar.html'}); -      $route.when('/foo/:id', -                  {redirectTo: customRedirectFn}); -      scope.$eval(); - -      $location.hash = '/foo/id3?subid=sid1&appended=true'; -      scope.$eval(); //triggers initial route change - match the redirect route -      $browser.defer.flush(); //triger route change - match the route we redirected to - -      expect($location.hash).toBe('custom'); - -      function customRedirectFn(routePathParams, hash, hashPath, hashSearch) { -        expect(routePathParams).toEqual({id: 'id3'}); -        expect(hash).toEqual($location.hash); -        expect(hashPath).toEqual($location.hashPath); -        expect(hashSearch).toEqual($location.hashSearch); -        return 'custom'; -      } -    }); -  }); - - -  describe('$defer', function() { -    var $defer, $exceptionHandler; - -    beforeEach(function(){ -      scope = createScope({}, angularService, { -        '$exceptionHandler': jasmine.createSpy('$exceptionHandler') -      }); - -      $browser = scope.$service('$browser'); -      $defer = scope.$service('$defer'); -      $exceptionHandler = scope.$service('$exceptionHandler'); -    }); - - -    it('should delegate functions to $browser.defer', function() { -      var counter = 0; -      $defer(function() { counter++; }); - -      expect(counter).toBe(0); - -      $browser.defer.flush(); -      expect(counter).toBe(1); - -      $browser.defer.flush(); //does nothing -      expect(counter).toBe(1); - -      expect($exceptionHandler).not.toHaveBeenCalled(); -    }); - - -    it('should delegate exception to the $exceptionHandler service', function() { -      $defer(function() {throw "Test Error";}); -      expect($exceptionHandler).not.toHaveBeenCalled(); - -      $browser.defer.flush(); -      expect($exceptionHandler).toHaveBeenCalledWith("Test Error"); -    }); - - -    it('should call eval after each callback is executed', function() { -      var eval = this.spyOn(scope, '$eval').andCallThrough(); - -      $defer(function() {}); -      expect(eval).wasNotCalled(); - -      $browser.defer.flush(); -      expect(eval).wasCalled(); - -      eval.reset(); //reset the spy; - -      $defer(function() {}); -      $defer(function() {}); -      $browser.defer.flush(); -      expect(eval.callCount).toBe(2); -    }); - - -    it('should call eval even if an exception is thrown in callback', function() { -      var eval = this.spyOn(scope, '$eval').andCallThrough(); - -      $defer(function() {throw "Test Error";}); -      expect(eval).wasNotCalled(); - -      $browser.defer.flush(); -      expect(eval).wasCalled(); -    }); -  }); - - -  describe('$xhr', function(){ -    var log; -    function callback(code, response) { -      expect(code).toEqual(200); -      log = log + toJson(response) + ';'; -    } - -    beforeEach(function(){ -      log = ''; -    }); - -    it('should forward the request to $browser and decode JSON', function(){ -      $browserXhr.expectGET('/reqGET').respond('first'); -      $browserXhr.expectGET('/reqGETjson').respond('["second"]'); -      $browserXhr.expectPOST('/reqPOST', {post:'data'}).respond('third'); - -      $xhr('GET', '/reqGET', null, callback); -      $xhr('GET', '/reqGETjson', null, callback); -      $xhr('POST', '/reqPOST', {post:'data'}, callback); - -      $browserXhr.flush(); - -      expect(log).toEqual('"third";["second"];"first";'); -    }); - -    it('should handle non 200 status codes by forwarding to error handler', function(){ -      $browserXhr.expectPOST('/req', 'MyData').respond(500, 'MyError'); -      $xhr('POST', '/req', 'MyData', callback); -      $browserXhr.flush(); -      var cb = $xhrError.mostRecentCall.args[0].callback; -      expect(typeof cb).toEqual($function); -      expect($xhrError).wasCalledWith( -          {url:'/req', method:'POST', data:'MyData', callback:cb}, -          {status:500, body:'MyError'}); -    }); - -    it('should handle exceptions in callback', function(){ -      $log.error = jasmine.createSpy('$log.error'); -      $browserXhr.expectGET('/reqGET').respond('first'); -      $xhr('GET', '/reqGET', null, function(){ throw "MyException"; }); -      $browserXhr.flush(); - -      expect($log.error).wasCalledWith("MyException"); -    }); - -    describe('bulk', function(){ -      it('should collect requests', function(){ -        $xhrBulk.urls["/"] = {match:/.*/}; -        $xhrBulk('GET', '/req1', null, callback); -        $xhrBulk('POST', '/req2', {post:'data'}, callback); - -        $browserXhr.expectPOST('/', { -          requests:[{method:'GET',  url:'/req1', data: null}, -                    {method:'POST', url:'/req2', data:{post:'data'} }] -        }).respond([ -          {status:200, response:'first'}, -          {status:200, response:'second'} -        ]); -        $xhrBulk.flush(function(){ log += 'DONE';}); -        $browserXhr.flush(); -        expect(log).toEqual('"first";"second";DONE'); -      }); - -      it('should handle non 200 status code by forwarding to error handler', function(){ -        $xhrBulk.urls['/'] = {match:/.*/}; -        $xhrBulk('GET', '/req1', null, callback); -        $xhrBulk('POST', '/req2', {post:'data'}, callback); - -        $browserXhr.expectPOST('/', { -          requests:[{method:'GET',  url:'/req1', data: null}, -                    {method:'POST', url:'/req2', data:{post:'data'} }] -        }).respond([ -          {status:404, response:'NotFound'}, -          {status:200, response:'second'} -        ]); -        $xhrBulk.flush(function(){ log += 'DONE';}); -        $browserXhr.flush(); - -        expect($xhrError).wasCalled(); -        var cb = $xhrError.mostRecentCall.args[0].callback; -        expect(typeof cb).toEqual($function); -        expect($xhrError).wasCalledWith( -            {url:'/req1', method:'GET', data:null, callback:cb}, -            {status:404, response:'NotFound'}); - -        expect(log).toEqual('"second";DONE'); -      }); -    }); - -    describe('cache', function(){ -      var cache; -      beforeEach(function(){ cache = scope.$service('$xhr.cache'); }); - -      it('should cache requests', function(){ -        $browserXhr.expectGET('/url').respond('first'); -        cache('GET', '/url', null, callback); -        $browserXhr.flush(); - -        $browserXhr.expectGET('/url').respond('ERROR'); -        cache('GET', '/url', null, callback); -        $browser.defer.flush(); -        expect(log).toEqual('"first";"first";'); - -        cache('GET', '/url', null, callback, false); -        $browser.defer.flush(); -        expect(log).toEqual('"first";"first";"first";'); -      }); - -      it('should first return cache request, then return server request', function(){ -        $browserXhr.expectGET('/url').respond('first'); -        cache('GET', '/url', null, callback, true); -        $browserXhr.flush(); - -        $browserXhr.expectGET('/url').respond('ERROR'); -        cache('GET', '/url', null, callback, true); -        $browser.defer.flush(); -        expect(log).toEqual('"first";"first";'); - -        $browserXhr.flush(); -        expect(log).toEqual('"first";"first";"ERROR";'); -      }); - -      it('should serve requests from cache', function(){ -        cache.data.url = {value:'123'}; -        cache('GET', 'url', null, callback); -        $browser.defer.flush(); -        expect(log).toEqual('"123";'); - -        cache('GET', 'url', null, callback, false); -        $browser.defer.flush(); -        expect(log).toEqual('"123";"123";'); -      }); - -      it('should keep track of in flight requests and request only once', function(){ -        scope.$service('$xhr.bulk').urls['/bulk'] = { -          match:function(url){ -            return url == '/url'; -          } -        }; -        $browserXhr.expectPOST('/bulk', { -          requests:[{method:'GET',  url:'/url', data: null}] -        }).respond([ -          {status:200, response:'123'} -        ]); -        cache('GET', '/url', null, callback); -        cache('GET', '/url', null, callback); -        cache.delegate.flush(); -        $browserXhr.flush(); -        expect(log).toEqual('"123";"123";'); -      }); - -      it('should clear cache on non GET', function(){ -        $browserXhr.expectPOST('abc', {}).respond({}); -        cache.data.url = {value:123}; -        cache('POST', 'abc', {}); -        expect(cache.data.url).toBeUndefined(); -      }); - -      it('should call callback asynchronously for both cache hit and cache miss', function() { -        $browserXhr.expectGET('/url').respond('+'); -        cache('GET', '/url', null, callback); -        expect(log).toEqual(''); //callback hasn't executed - -        $browserXhr.flush(); -        expect(log).toEqual('"+";'); //callback has executed - -        cache('GET', '/url', null, callback); -        expect(log).toEqual('"+";'); //callback hasn't executed - -        $browser.defer.flush(); -        expect(log).toEqual('"+";"+";'); //callback has executed -      }); - -      it('should call eval after callbacks for both cache hit and cache miss execute', function() { -        var eval = this.spyOn(scope, '$eval').andCallThrough(); - -        $browserXhr.expectGET('/url').respond('+'); -        cache('GET', '/url', null, callback); -        expect(eval).wasNotCalled(); - -        $browserXhr.flush(); -        expect(eval).wasCalled(); - -        eval.reset(); //reset the spy - -        cache('GET', '/url', null, callback); -        expect(eval).wasNotCalled(); - -        $browser.defer.flush(); -        expect(eval).wasCalled(); -      }); -    }); - -  }); - - -  describe('$cookies', function() { - -    var scope, $browser; - -    beforeEach(function() { -      $browser = new MockBrowser(); -      $browser.cookieHash['preexisting'] = 'oldCookie'; -      scope = createScope(null, angularService, {$browser: $browser}); -      scope.$cookies = scope.$service('$cookies'); -    }); - - -    it('should provide access to existing cookies via object properties and keep them in sync', -        function(){ -      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); - -      // access internal cookie storage of the browser mock directly to simulate behavior of -      // document.cookie -      $browser.cookieHash['brandNew'] = 'cookie'; -      $browser.poll(); - -      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie'}); - -      $browser.cookieHash['brandNew'] = 'cookie2'; -      $browser.poll(); -      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'brandNew':'cookie2'}); - -      delete $browser.cookieHash['brandNew']; -      $browser.poll(); -      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); -    }); - - -    it('should create or update a cookie when a value is assigned to a property', function() { -      scope.$cookies.oatmealCookie = 'nom nom'; -      scope.$eval(); - -      expect($browser.cookies()). -        toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); - -      scope.$cookies.oatmealCookie = 'gone'; -      scope.$eval(); - -      expect($browser.cookies()). -        toEqual({'preexisting': 'oldCookie', 'oatmealCookie': 'gone'}); -    }); - - -    it('should drop or reset any cookie that was set to a non-string value', function() { -      scope.$cookies.nonString = [1, 2, 3]; -      scope.$cookies.nullVal = null; -      scope.$cookies.undefVal = undefined; -      scope.$cookies.preexisting = function(){}; -      scope.$eval(); -      expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'}); -      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); -    }); - - -    it('should remove a cookie when a $cookies property is deleted', function() { -      scope.$cookies.oatmealCookie = 'nom nom'; -      scope.$eval(); -      $browser.poll(); -      expect($browser.cookies()). -        toEqual({'preexisting': 'oldCookie', 'oatmealCookie':'nom nom'}); - -      delete scope.$cookies.oatmealCookie; -      scope.$eval(); - -      expect($browser.cookies()).toEqual({'preexisting': 'oldCookie'}); -    }); - - -    it('should drop or reset cookies that browser refused to store', function() { -      var i, longVal; - -      for (i=0; i<5000; i++) { -        longVal += '*'; -      } - -      //drop if no previous value -      scope.$cookies.longCookie = longVal; -      scope.$eval(); -      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie'}); - - -      //reset if previous value existed -      scope.$cookies.longCookie = 'shortVal'; -      scope.$eval(); -      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'}); -      scope.$cookies.longCookie = longVal; -      scope.$eval(); -      expect(scope.$cookies).toEqual({'preexisting': 'oldCookie', 'longCookie': 'shortVal'}); -    }); -  }); - - -  describe('$cookieStore', function() { - -    it('should serialize objects to json', function() { -      scope.$service('$cookieStore').put('objectCookie', {id: 123, name: 'blah'}); -      scope.$eval(); //force eval in test -      expect($browser.cookies()).toEqual({'objectCookie': '{"id":123,"name":"blah"}'}); -    }); - - -    it('should deserialize json to object', function() { -      $browser.cookies('objectCookie', '{"id":123,"name":"blah"}'); -      $browser.poll(); -      expect(scope.$service('$cookieStore').get('objectCookie')).toEqual({id: 123, name: 'blah'}); -    }); - - -    it('should delete objects from the store when remove is called', function() { -      scope.$service('$cookieStore').put('gonner', { "I'll":"Be Back"}); -      scope.$eval(); //force eval in test -      expect($browser.cookies()).toEqual({'gonner': '{"I\'ll":"Be Back"}'}); -    }); - -  }); - - -  describe('URL_MATCH', function() { - -    it('should parse basic url', function() { -      var match = URL_MATCH.exec('http://www.angularjs.org/path?search#hash?x=x'); - -      expect(match[1]).toEqual('http'); -      expect(match[3]).toEqual('www.angularjs.org'); -      expect(match[6]).toEqual('/path'); -      expect(match[8]).toEqual('search'); -      expect(match[10]).toEqual('hash?x=x'); -    }); - -    it('should parse file://', function(){ -      var match = URL_MATCH.exec('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); - -      expect(match[1]).toEqual('file'); -      expect(match[3]).toEqual(''); -      expect(match[5]).toBeFalsy(); -      expect(match[6]).toEqual('/Users/Shared/misko/work/angular.js/scenario/widgets.html'); -      expect(match[8]).toBeFalsy(); -    }); - -    it('should parse url with "-" in host', function(){ -      var match = URL_MATCH.exec('http://a-b1.c-d.09/path'); - -      expect(match[1]).toEqual('http'); -      expect(match[3]).toEqual('a-b1.c-d.09'); -      expect(match[5]).toBeFalsy(); -      expect(match[6]).toEqual('/path'); -      expect(match[8]).toBeFalsy(); -    }); - -    it('should parse host without "/" at the end', function() { -      var match = URL_MATCH.exec('http://host.org'); -      expect(match[3]).toEqual('host.org'); - -      match = URL_MATCH.exec('http://host.org#'); -      expect(match[3]).toEqual('host.org'); - -      match = URL_MATCH.exec('http://host.org?'); -      expect(match[3]).toEqual('host.org'); -    }); - -    it('should match with just "/" path', function() { -      var match = URL_MATCH.exec('http://server/#?book=moby'); - -      expect(match[10]).toEqual('?book=moby'); -    }); -  }); - -  describe('$updateView', function(){ -    var scope, browser, evalCount, $updateView; - -    beforeEach(function(){ -      browser = new MockBrowser(); -      // Pretend that you are real Browser so that we see the delays -      browser.isMock = false; -      browser.defer = jasmine.createSpy('defer'); - -      scope = angular.scope(null, null, {$browser:browser}); -      $updateView = scope.$service('$updateView'); -      scope.$onEval(function(){ evalCount++; }); -      evalCount = 0; -    }); - -    it('should eval root scope after a delay', function(){ -      $updateView(); -      expect(evalCount).toEqual(0); -      expect(browser.defer).toHaveBeenCalled(); -      expect(browser.defer.mostRecentCall.args[1]).toEqual(25); -      browser.defer.mostRecentCall.args[0](); -      expect(evalCount).toEqual(1); -    }); - -    it('should allow changing of delay time', function(){ -      var oldValue = angular.service('$updateView').delay; -      angular.service('$updateView').delay = 50; -      $updateView(); -      expect(evalCount).toEqual(0); -      expect(browser.defer).toHaveBeenCalled(); -      expect(browser.defer.mostRecentCall.args[1]).toEqual(50); -      angular.service('$updateView').delay = oldValue; -    }); - -    it('should ignore multiple requests for update', function(){ -      $updateView(); -      $updateView(); -      expect(evalCount).toEqual(0); -      expect(browser.defer).toHaveBeenCalled(); -      expect(browser.defer.callCount).toEqual(1); -      browser.defer.mostRecentCall.args[0](); -      expect(evalCount).toEqual(1); -    }); - -    it('should update immediatelly in test/mock mode', function(){ -      scope = angular.scope(); -      scope.$onEval(function(){ evalCount++; }); -      expect(evalCount).toEqual(0); -      scope.$service('$updateView')(); -      expect(evalCount).toEqual(1); -    }); -  }); -}); diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 12fe7b64..9aa3b95d 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -601,23 +601,8 @@ describe("widget", function(){        expect(element.text()).toEqual('one');      }); -    it("should match urls", function(){ -      var scope = angular.compile('<ng:switch on="url" using="route:params"><div ng:switch-when="/Book/:name">{{params.name}}</div></ng:switch>'); -      scope.url = '/Book/Moby'; -      scope.$init(); -      expect(scope.$element.text()).toEqual('Moby'); -      dealoc(scope); -    }); - -    it("should match sandwich ids", function(){ -      var scope = {}; -      var match = angular.widget('NG:SWITCH').route.call(scope, '/a/123/b', '/a/:id'); -      expect(match).toBeFalsy(); -    }); -      it('should call change on switch', function(){        var scope = angular.compile('<ng:switch on="url" change="name=\'works\'"><div ng:switch-when="a">{{name}}</div></ng:switch>'); -      var cleared = false;        scope.url = 'a';        scope.$init();        expect(scope.name).toEqual(undefined); | 
