diff options
| -rw-r--r-- | src/service/location.js | 646 | ||||
| -rw-r--r-- | test/service/locationSpec.js | 722 | 
2 files changed, 975 insertions, 393 deletions
diff --git a/src/service/location.js b/src/service/location.js index 2f53f520..9a1afc37 100644 --- a/src/service/location.js +++ b/src/service/location.js @@ -1,275 +1,505 @@  'use strict';  var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?(\/[^\?#]*)?(\?([^#]*))?(#(.*))?$/, -    HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/, -    DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}; +    PATH_MATCH = /^([^\?#]*)?(\?([^#]*))?(#(.*))?$/, +    HASH_MATCH = PATH_MATCH, +    DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; +  /** - * @workInProgress - * @ngdoc service - * @name angular.service.$location - * @requires $browser - * - * @property {string} href The full URL of the current location. - * @property {string} protocol The protocol part of the URL (e.g. http or https). - * @property {string} host The host name, ip address or FQDN of the current location. - * @property {number} port The port number of the current location (e.g. 80, 443, 8080). - * @property {string} path The path of the current location (e.g. /myapp/inbox). - * @property {Object.<string|boolean>} search Map of query parameters (e.g. {user:"foo", page:23}). - * @property {string} hash The fragment part of the URL of the current location (e.g. #foo). - * @property {string} hashPath Similar to `path`, but located in the `hash` fragment - *     (e.g. ../foo#/some/path  => /some/path). - * @property {Object.<string|boolean>} hashSearch Similar to `search` but located in `hash` - *     fragment (e.g. .../foo#/some/path?hashQuery=param  =>  {hashQuery: "param"}). + * Encode path using encodeUriSegment, ignoring forward slashes   * - * @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 in the browser location url. + * @param {string} path Path to encode + * @returns {string} + */ +function encodePath(path) { +  var segments = path.split('/'), +      i = segments.length; + +  while (i--) { +    segments[i] = encodeUriSegment(segments[i]); +  } + +  return segments.join('/'); +} + + +function matchUrl(url, obj) { +  var match = URL_MATCH.exec(url), + +  match = { +      protocol: match[1], +      host: match[3], +      port: parseInt(match[5]) || DEFAULT_PORTS[match[1]] || null, +      path: match[6] || '/', +      search: match[8], +      hash: match[10] +    }; + +  if (obj) { +    obj.$$protocol = match.protocol; +    obj.$$host = match.host; +    obj.$$port = match.port; +  } + +  return match; +} + + +function composeProtocolHostPort(protocol, host, port) { +  return protocol + '://' + host + (port == DEFAULT_PORTS[protocol] ? '' : ':' + port); +} + + +function pathPrefixFromBase(basePath) { +  return basePath.substr(0, basePath.lastIndexOf('/')); +} + + +function convertToHtml5Url(url, basePath, hashPrefix) { +  var match = matchUrl(url); + +  // already html5 url +  if (decodeURIComponent(match.path) != basePath || isUndefined(match.hash) || +      match.hash.indexOf(hashPrefix) != 0) { +    return url; +  // convert hashbang url -> html5 url +  } else { +    return composeProtocolHostPort(match.protocol, match.host, match.port) + +           pathPrefixFromBase(basePath) + match.hash.substr(hashPrefix.length); +  } +} + + +function convertToHashbangUrl(url, basePath, hashPrefix) { +  var match = matchUrl(url); + +  // already hashbang url +  if (decodeURIComponent(match.path) == basePath) { +    return url; +  // convert html5 url -> hashbang url +  } else { +    var search = match.search && '?' + match.search || '', +        hash = match.hash && '#' + match.hash || '', +        pathPrefix = pathPrefixFromBase(basePath), +        path = match.path.substr(pathPrefix.length); + +    if (match.path.indexOf(pathPrefix) != 0) { +      throw 'Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'; +    } + +    return composeProtocolHostPort(match.protocol, match.host, match.port) + basePath + +           '#' + hashPrefix + path + search + hash; +  } +} + + +/** + * LocationUrl represents an url + * This object is exposed as $location service when html5 is enabled and supported   * - * Notice that using browser's forward/back buttons changes the $location. + * @constructor + * @param {string} url Html5 url + * @param {string} pathPrefix + */ +function LocationUrl(url, pathPrefix) { +  pathPrefix = pathPrefix || ''; + +  /** +   * Parse given html5 (regular) url string into properties +   * @param {string} url Html5 url +   * @private +   */ +  this.$$parse = function(url) { +    var match = matchUrl(url, this); + +    if (match.path.indexOf(pathPrefix) != 0) { +      throw 'Invalid url "' + url + '", missing path prefix "' + pathPrefix + '" !'; +    } + +    this.$$path = decodeURIComponent(match.path.substr(pathPrefix.length)); +    this.$$search = parseKeyValue(match.search); +    this.$$hash = match.hash && decodeURIComponent(match.hash) || ''; + +    this.$$compose(); +  }, + +  /** +   * Compose url and update `absUrl` property +   * @private +   */ +  this.$$compose = function() { +    var search = toKeyValue(this.$$search), +        hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; + +    this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; +    this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + +                    pathPrefix + this.$$url; +  }; + +  this.$$parse(url); +} + + +/** + * LocationHashbangUrl represents url + * This object is exposed as $location service when html5 history api is disabled or not supported   * - * @example -   <doc:example> -     <doc:source> -       <div ng:init="$location = $service('$location')"> -         <a id="ex-test" href="#myPath?name=misko">test hash</a>| -         <a id="ex-reset" href="#!/api/angular.service.$location">reset hash</a><br/> -         <input type='text' name="$location.hash" size="30"> -         <pre>$location = {{$location}}</pre> -       </div> -     </doc:source> -     <doc:scenario> -       it('should initialize the input field', function() { -         expect(using('.doc-example-live').input('$location.hash').val()). -           toBe('!/api/angular.service.$location'); -       }); - - -       it('should bind $location.hash to the input field', function() { -         using('.doc-example-live').input('$location.hash').enter('foo'); -         expect(browser().location().hash()).toBe('foo'); -       }); - - -       it('should set the hash to a test string with test link is presed', function() { -         using('.doc-example-live').element('#ex-test').click(); -         expect(using('.doc-example-live').input('$location.hash').val()). -           toBe('myPath?name=misko'); -       }); - -       it('should reset $location when reset link is pressed', function() { -         using('.doc-example-live').input('$location.hash').enter('foo'); -         using('.doc-example-live').element('#ex-reset').click(); -         expect(using('.doc-example-live').input('$location.hash').val()). -           toBe('!/api/angular.service.$location'); -       }); - -     </doc:scenario> -    </doc:example> + * @constructor + * @param {string} url Legacy url + * @param {string} hashPrefix Prefix for hash part (containing path and search)   */ -angularServiceInject("$location", function($browser) { -  var location = {update: update, updateHash: updateHash}; -  var lastLocation = {}; // last state since last update(). +function LocationHashbangUrl(url, hashPrefix) { +  var basePath; + +  /** +   * Parse given hashbang url into properties +   * @param {string} url Hashbang url +   * @private +   */ +  this.$$parse = function(url) { +    var match = matchUrl(url, this); + +    if (match.hash && match.hash.indexOf(hashPrefix) != 0) { +      throw 'Invalid url "' + url + '", missing hash prefix "' + hashPrefix + '" !'; +    } + +    basePath = match.path + (match.search ? '?' + match.search : ''); +    match = HASH_MATCH.exec((match.hash || '').substr(hashPrefix.length)); +    if (match[1]) { +      this.$$path = (match[1].charAt(0) == '/' ? '' : '/') + decodeURIComponent(match[1]); +    } else { +      this.$$path = ''; +    } + +    this.$$search = parseKeyValue(match[3]); +    this.$$hash = match[5] && decodeURIComponent(match[5]) || ''; + +    this.$$compose(); +  }; + +  /** +   * Compose hashbang url and update `absUrl` property +   * @private +   */ +  this.$$compose = function() { +    var search = toKeyValue(this.$$search), +        hash = this.$$hash ? '#' + encodeUriSegment(this.$$hash) : ''; -  $browser.onUrlChange(bind(this, this.$apply, function() { //register -    update($browser.url()); -  }))(); //initialize +    this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash; +    this.$$absUrl = composeProtocolHostPort(this.$$protocol, this.$$host, this.$$port) + +                    basePath + (this.$$url ? '#' + hashPrefix + this.$$url : ''); +  }; -  this.$watch(sync); +  this.$$parse(url); +} -  return location; -  // PUBLIC METHODS +LocationUrl.prototype = LocationHashbangUrl.prototype = { + +  /** +   * Has any change been replacing ? +   * @private +   */ +  $$replace: false,    /** -   * @workInProgress     * @ngdoc method -   * @name angular.service.$location#update +   * @name angular.service.$location#absUrl     * @methodOf angular.service.$location     *     * @description -   * Updates the location object. -   * Does not immediately update the browser -   * Browser is updated at the end of $digest() +   * This method is getter only.     * -   * Does not immediately update the browser. Instead the browser is updated at the end of $eval() -   * cycle. +   * Return full url representation with all segments encoded according to rules specified in +   * {@link http://www.ietf.org/rfc/rfc3986.txt RFC 3986}.     * -   * <pre> -       $location.update('http://www.angularjs.org/path#hash?search=x'); -       $location.update({host: 'www.google.com', protocol: 'https'}); -       $location.update({hashPath: '/path', hashSearch: {a: 'b', x: true}}); -     </pre> -   * -   * @param {string|Object} href Full href as a string or object with properties +   * @return {string}     */ -  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); -    } -    $browser.url(location.href); -    copy(location, lastLocation); -  } +  absUrl: locationGetter('$$absUrl'),    /** -   * @workInProgress     * @ngdoc method -   * @name angular.service.$location#updateHash +   * @name angular.service.$location#url     * @methodOf angular.service.$location     *     * @description -   * Updates the hash fragment part of the url. +   * This method is getter / setter.     * -   * @see update() +   * Return url (e.g. `/path?a=b#hash`) when called without any parameter.     * -   * <pre> -       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}}) -     </pre> +   * Change path, search and hash, when called with parameter and return `$location`.     * -   * @param {string|Object} path A hashPath or hashSearch object -   * @param {Object=} search A hashSearch object +   * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`) +   * @return {string}     */ -  function updateHash(path, search) { -    var hash = {}; +  url: function(url, replace) { +    if (isUndefined(url)) +      return this.$$url; -    if (isString(path)) { -      hash.hashPath = path; -      hash.hashSearch = search || {}; -    } else -      hash.hashSearch = path; +    var match = PATH_MATCH.exec(url); +    this.path(decodeURIComponent(match[1] || '')).search(match[3] || '') +        .hash(match[5] || '', replace); -    hash.hash = composeHash(hash); +    return this; +  }, -    update({hash: hash}); -  } +  /** +   * @ngdoc method +   * @name angular.service.$location#protocol +   * @methodOf angular.service.$location +   * +   * @description +   * This method is getter only. +   * +   * Return protocol of current url. +   * +   * @return {string} +   */ +  protocol: locationGetter('$$protocol'), +  /** +   * @ngdoc method +   * @name angular.service.$location#host +   * @methodOf angular.service.$location +   * +   * @description +   * This method is getter only. +   * +   * Return host of current url. +   * +   * @return {string} +   */ +  host: locationGetter('$$host'), -  // INNER METHODS +  /** +   * @ngdoc method +   * @name angular.service.$location#port +   * @methodOf angular.service.$location +   * +   * @description +   * This method is getter only. +   * +   * Return port of current url. +   * +   * @return {Number} +   */ +  port: locationGetter('$$port'),    /** -   * Synchronizes all location object properties. +   * @ngdoc method +   * @name angular.service.$location#path +   * @methodOf angular.service.$location     * -   * User is allowed to change properties, so after property change, -   * location object is not in consistent state. +   * @description +   * This method is getter / setter.     * -   * Properties are synced with the following precedence order: +   * Return path of current url when called without any parameter.     * -   * - `$location.href` -   * - `$location.hash` -   * - everything else +   * Change path when called with parameter and return `$location`.     * -   * Keep in mind that if the following code is executed: +   * Note: Path should always begin with forward slash (/), this method will add the forward slash +   * if it is missing.     * -   * scope.$location.href = 'http://www.angularjs.org/path#a/b' +   * @param {string=} path New path +   * @return {string} +   */ +  path: locationGetterSetter('$$path', function(path) { +    return path.charAt(0) == '/' ? path : '/' + path; +  }), + +  /** +   * @ngdoc method +   * @name angular.service.$location#search +   * @methodOf angular.service.$location +   * +   * @description +   * This method is getter / setter. +   * +   * Return search part (as object) of current url when called without any parameter.     * -   * immediately afterwards all other properties are still the old ones... +   * Change search part when called with parameter and return `$location`.     * -   * This method checks the changes and update location to the consistent state +   * @param {string|object<string,string>=} search New search part - string or hash object +   * @return {string}     */ -  function sync() { -    if (!equals(location, lastLocation)) { -      if (location.href != lastLocation.href) { -        update(location.href); +  search: function(search, paramValue) { +    if (isUndefined(search)) +      return this.$$search; + +    if (isDefined(paramValue)) { +      if (paramValue === null) { +        delete this.$$search[search];        } else { -        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); +        this.$$search[search] = escape(paramValue);        } +    } else { +      this.$$search = isString(search) ? parseKeyValue(search) : search;      } -  } +    this.$$compose(); +    return this; +  },    /** -   * Compose href string from a location object +   * @ngdoc method +   * @name angular.service.$location#hash +   * @methodOf angular.service.$location +   * +   * @description +   * This method is getter / setter. +   * +   * Return hash fragment when called without any parameter.     * -   * @param {Object} loc The location object with all properties -   * @return {string} Composed href +   * Change hash fragment when called with parameter and return `$location`. +   * +   * @param {string=} hash New hash fragment +   * @return {string}     */ -  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 : ''); -  } +  hash: locationGetterSetter('$$hash', identity),    /** -   * Compose hash string from location object +   * @ngdoc method +   * @name angular.service.$location#replace +   * @methodOf angular.service.$location     * -   * @param {Object} loc Object with hashPath and hashSearch properties -   * @return {string} Hash string +   * @description +   * If called, all changes to $location during current `$digest` will be replacing current history +   * record, instead of adding new one.     */ -  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 : ''); +  replace: function() { +    this.$$replace = true; +    return this;    } +}; -  /** -   * 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)); + +function locationGetter(property) { +  return function() { +    return this[property]; +  }; +} + + +function locationGetterSetter(property, preprocess) { +  return function(value) { +    if (isUndefined(value)) +      return this[property]; + +    this[property] = preprocess(value); +    this.$$compose(); + +    return this; +  }; +} + + +/** + * @ngdoc service + * @name angular.service.$location + * + * @requires $browser + * @requires $sniffer + * @requires $config + * @requires $document + * + * @description + * The $location service parses the URL in the browser address bar (based on the {@link https://developer.mozilla.org/en/window.location window.location}) and makes the URL available to your application. Changes to the URL in the address bar are reflected into $location service and changes to $location are reflected into the browser address bar. + * + * **The $location service:** + * + * - Exposes the current URL in the browser address bar, so you can + *   - Watch and observe the URL. + *   - Change the URL. + * - Synchronizes the URL with the browser when the user + *   - Changes the address bar. + *   - Clicks the back or forward button (or clicks a History link). + *   - Clicks on a link. + * - Represents the URL object as a set of methods (protocol, host, port, path, search, hash). + * + * For more information see {@link guide/dev_guide.services.$location Developer Guide: Angular Services: Using $location} + */ +angularServiceInject('$location', function($browser, $sniffer, $config, $document) { +  var scope = this, currentUrl, +      basePath = $browser.baseHref() || '/', +      pathPrefix = pathPrefixFromBase(basePath), +      hashPrefix = $config.hashPrefix || '', +      initUrl = $browser.url(); + +  if ($config.html5Mode) { +    if ($sniffer.history) { +      currentUrl = new LocationUrl(convertToHtml5Url(initUrl, basePath, hashPrefix), pathPrefix); +    } else { +      currentUrl = new LocationHashbangUrl(convertToHashbangUrl(initUrl, basePath, hashPrefix), +                                           hashPrefix);      } -    return loc; +    // link rewriting +    var u = currentUrl, +        absUrlPrefix = composeProtocolHostPort(u.protocol(), u.host(), u.port()) + pathPrefix; + +    $document.bind('click', function(event) { +      // TODO(vojta): rewrite link when opening in new tab/window (in legacy browser) +      // currently we open nice url link and redirect then + +      if (uppercase(event.target.nodeName) != 'A' || event.ctrlKey || event.which == 2) return; + +      var elm = jqLite(event.target), +          href = elm.attr('href'); + +      if (!href || isDefined(elm.attr('ng:ext-link')) || elm.attr('target')) return; + +      // remove same domain from full url links (IE7 always returns full hrefs) +      href = href.replace(absUrlPrefix, ''); + +      // link to different domain (or base path) +      if (href.substr(0, 4) == 'http') return; + +      // remove pathPrefix from absolute links +      href = href.indexOf(pathPrefix) === 0 ? href.substr(pathPrefix.length) : href; + +      currentUrl.url(href); +      scope.$apply(); +      event.preventDefault(); +    }); +  } else { +    currentUrl = new LocationHashbangUrl(initUrl, hashPrefix);    } -  /** -   * 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]); +  // rewrite hashbang url <> html5 url +  if (currentUrl.absUrl() != initUrl) { +    $browser.url(currentUrl.absUrl(), true); +  } + +  // update $location when $browser url changes +  $browser.onUrlChange(function(newUrl) { +    if (currentUrl.absUrl() != newUrl) { +      currentUrl.$$parse(newUrl); +      scope.$apply(); +    } +  }); + +  // update browser +  var changeCounter = 0; +  scope.$watch(function() { +    if ($browser.url() != currentUrl.absUrl()) { +      changeCounter++; +      scope.$evalAsync(function() { +        $browser.url(currentUrl.absUrl(), currentUrl.$$replace); +        currentUrl.$$replace = false; +      });      } -    return h; -  } -}, ['$browser']); +    return changeCounter; +  }); + +  return currentUrl; +}, ['$browser', '$sniffer', '$locationConfig', '$document']); + + +angular.service('$locationConfig', function() { +  return { +    html5Mode: false, +    hashPrefix: '' +  }; +}); diff --git a/test/service/locationSpec.js b/test/service/locationSpec.js index a137149d..e798aa81 100644 --- a/test/service/locationSpec.js +++ b/test/service/locationSpec.js @@ -1,251 +1,463 @@  'use strict'; +/** + * Create jasmine.Spy on given method, but ignore calls without arguments + * This is helpful when need to spy only setter methods and ignore getters + */ +function spyOnlyCallsWithArgs(obj, method) { +  var spy = spyOn(obj, method); +  obj[method] = function() { +    if (arguments.length) return spy.apply(this, arguments); +    return spy.originalValue.apply(this); +  }; +  return spy; +} + +  describe('$location', function() { -  var scope, $location, $browser; +  var url; -  beforeEach(function(){ -    scope = angular.scope(); -    $location = scope.$service('$location'); -    $browser = scope.$service('$browser'); -  }); +  describe('NewUrl', function() { +    beforeEach(function() { +      url = new LocationUrl('http://www.domain.com:9877/path/b?search=a&b=c&d#hash'); +    }); -  afterEach(function(){ -    dealoc(scope); -  }); +    it('should provide common getters', function() { +      expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#hash'); +      expect(url.protocol()).toBe('http'); +      expect(url.host()).toBe('www.domain.com'); +      expect(url.port()).toBe(9877); +      expect(url.path()).toBe('/path/b'); +      expect(url.search()).toEqual({search: 'a', b: 'c', d: true}); +      expect(url.hash()).toBe('hash'); +      expect(url.url()).toBe('/path/b?search=a&b=c&d#hash'); +    }); -  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('path() should change path', function() { +      url.path('/new/path'); +      expect(url.path()).toBe('/new/path'); +      expect(url.absUrl()).toBe('http://www.domain.com:9877/new/path?search=a&b=c&d#hash'); +    }); -  it('should update location when browser url changed', function() { -    var origUrl = $location.href; -    expect(origUrl).toEqual($browser.url()); +    it('search() should accept string', function() { +      url.search('x=y&c'); +      expect(url.search()).toEqual({x: 'y', c: true}); +      expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?x=y&c#hash'); +    }); -    var newUrl = 'http://somenew/url#foo'; -    $browser.url(newUrl); -    $browser.poll(); -    expect($location.href).toEqual(newUrl); -  }); +    it('search() should accept object', function() { +      url.search({one: 1, two: true}); +      expect(url.search()).toEqual({one: 1, two: true}); +      expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two#hash'); +    }); -  it('should update browser at the end of $eval', function() { -    var origBrowserUrl = $browser.url(); -    $location.update('http://www.angularjs.org/'); -    $location.update({path: '/a/b'}); -    expect($location.href).toEqual('http://www.angularjs.org/a/b'); -    expect($browser.url()).toEqual('http://www.angularjs.org/a/b'); -    $location.path = '/c'; -    scope.$digest(); -    expect($browser.url()).toEqual('http://www.angularjs.org/c'); -  }); +    it('search() should change single parameter', function() { +      url.search({id: 'old', preserved: true}); +      url.search('id', 'new'); + +      expect(url.search()).toEqual({id: 'new', preserved: true}); +    }); -  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('search() should remove single parameter', function() { +      url.search({id: 'old', preserved: true}); +      url.search('id', null); +      expect(url.search()).toEqual({preserved: true}); +    }); -  it('should update hash on hashPath or hashSearch update', function() { -    $location.update('http://server/#path?a=b'); -    scope.$digest(); -    $location.update({hashPath: '', hashSearch: {}}); -    expect($location.hash).toEqual(''); -  }); +    it('hash() should change hash fragment', function() { +      url.hash('new-hash'); +      expect(url.hash()).toBe('new-hash'); +      expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#new-hash'); +    }); -  it('should update hashPath and hashSearch on $location.hash change upon eval', function(){ -    $location.update('http://server/#path?a=b'); -    scope.$digest(); +    it('url() should change the path, search and hash', function() { +      url.url('/some/path?a=b&c=d#hhh'); +      expect(url.url()).toBe('/some/path?a=b&c=d#hhh'); +      expect(url.absUrl()).toBe('http://www.domain.com:9877/some/path?a=b&c=d#hhh'); +      expect(url.path()).toBe('/some/path'); +      expect(url.search()).toEqual({a: 'b', c: 'd'}); +      expect(url.hash()).toBe('hhh'); +    }); -    $location.hash = ''; -    scope.$digest(); -    expect($location.href).toEqual('http://server/'); -    expect($location.hashPath).toEqual(''); -    expect($location.hashSearch).toEqual({}); -  }); +    it('replace should set $$replace flag and return itself', function() { +      expect(url.$$replace).toBe(false); +      url.replace(); +      expect(url.$$replace).toBe(true); +      expect(url.replace()).toBe(url); +    }); -  it('should update hash on $location.hashPath or $location.hashSearch change upon eval', -      function() { -    $location.update('http://server/#path?a=b'); -    expect($location.href).toEqual('http://server/#path?a=b'); -    expect($location.hashPath).toEqual('path'); -    expect($location.hashSearch).toEqual({a:'b'}); -    $location.hashPath = ''; -    $location.hashSearch = {}; -    scope.$digest(); +    it('should parse new url', function() { +      url = new LocationUrl('http://host.com/base'); +      expect(url.path()).toBe('/base'); -    expect($location.href).toEqual('http://server/'); -    expect($location.hash).toEqual(''); -  }); +      url = new LocationUrl('http://host.com/base#'); +      expect(url.path()).toBe('/base'); +    }); -  it('should sync $location upon eval before watches are fired', function(){ -    scope.$location = scope.$service('$location'); //publish to the scope for $watch +    it('should prefix path with forward-slash', function() { +      url = new LocationUrl('http://server/a'); +      url.path('b'); -    var log = ''; -    scope.$watch('$location.hash', function(scope){ -      log += scope.$location.hashPath + ';'; +      expect(url.path()).toBe('/b'); +      expect(url.absUrl()).toBe('http://server/b');      }); -    expect(log).toEqual(''); -    scope.$digest(); -    expect(log).toEqual(';'); -    log = ''; -    scope.$location.hash = '/abc'; -    scope.$digest(); -    expect(scope.$location.hash).toEqual('/abc'); -    expect(log).toEqual('/abc;'); -  }); +    it('should set path to forward-slash when empty', function() { +      url = new LocationUrl('http://server'); +      expect(url.path()).toBe('/'); +      expect(url.absUrl()).toBe('http://server/'); +    }); -  describe('sync', function() { -    it('should update hash with escaped hashPath', function() { -      $location.hashPath = 'foo=bar'; -      scope.$digest(); -      expect($location.hash).toBe('foo%3Dbar'); +    it('setters should return Url object to allow chaining', function() { +      expect(url.path('/any')).toBe(url); +      expect(url.search('')).toBe(url); +      expect(url.hash('aaa')).toBe(url); +      expect(url.url('/some')).toBe(url);      }); -    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'; +    it('should not preserve old properties when parsing new url', function() { +      url.$$parse('http://www.domain.com:9877/a'); -      scope.$digest(); +      expect(url.path()).toBe('/a'); +      expect(url.search()).toEqual({}); +      expect(url.hash()).toBe(''); +      expect(url.absUrl()).toBe('http://www.domain.com:9877/a'); +    }); -      expect($location).toEqualData({href: 'https://hrefhost:23/hrefpath', -                                     protocol: 'https', -                                     host: 'hrefhost', -                                     port: '23', -                                     path: '/hrefpath', -                                     search: {}, -                                     hash: '', -                                     hashPath: '', -                                     hashSearch: {} -                                    }); + +    it('should prepend path with basePath', function() { +      url = new LocationUrl('http://server/base/abc?a', '/base'); +      expect(url.path()).toBe('/abc'); +      expect(url.search()).toEqual({a: true}); + +      url.path('/new/path'); +      expect(url.absUrl()).toBe('http://server/base/new/path?a');      }); -    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'; +    it('should throw error when invalid url given', function() { +      url = new LocationUrl('http://server.org/base/abc', '/base'); -      scope.$digest(); +      expect(function() { +        url.$$parse('http://server.org/path#/path'); +      }).toThrow('Invalid url "http://server.org/path#/path", missing path prefix "/base" !'); +    }); + + +    describe('encoding', function() { + +      it('should encode special characters', function() { +        url.path('/a <>#'); +        url.search({'i j': '<>#'}); +        url.hash('<>#'); + +        expect(url.path()).toBe('/a <>#'); +        expect(url.search()).toEqual({'i j': '<>#'}); +        expect(url.hash()).toBe('<>#'); +        expect(url.absUrl()).toBe('http://www.domain.com:9877/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23'); +      }); + + +      it('should not encode !$:@', function() { +        url.path('/!$:@'); +        url.search(''); +        url.hash('!$:@'); -      expect($location).toEqualData({href: 'http://host:333/path#hash', -                                     protocol: 'http', -                                     host: 'host', -                                     port: '333', -                                     path: '/path', -                                     search: {}, -                                     hash: 'hash', -                                     hashPath: 'hash', -                                     hashSearch: {} -                                    }); +        expect(url.absUrl()).toBe('http://www.domain.com:9877/!$:@#!$:@'); +      }); + + +      it('should decode special characters', function() { +        url = new LocationUrl('http://host.com/a%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23'); +        expect(url.path()).toBe('/a <>#'); +        expect(url.search()).toEqual({'i j': '<>#'}); +        expect(url.hash()).toBe('x <>#'); +      });      });    }); -  describe('update()', function() { +  describe('HashbangUrl', function() { + +    beforeEach(function() { +      url = new LocationHashbangUrl('http://www.server.org:1234/base#!/path?a=b&c#hash', '!'); +    }); + + +    it('should parse hashband url into path and search', function() { +      expect(url.protocol()).toBe('http'); +      expect(url.host()).toBe('www.server.org'); +      expect(url.port()).toBe(1234); +      expect(url.path()).toBe('/path'); +      expect(url.search()).toEqual({a: 'b', c: true}); +      expect(url.hash()).toBe('hash'); +    }); + + +    it('absUrl() should return hashbang url', function() { +      expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/path?a=b&c#hash'); + +      url.path('/new/path'); +      url.search({one: 1}); +      url.hash('hhh'); +      expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/new/path?one=1#hhh'); +    }); + + +    it('should preserve query params in base', function() { +      url = new LocationHashbangUrl('http://www.server.org:1234/base?base=param#/path?a=b&c#hash', ''); +      expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/path?a=b&c#hash'); + +      url.path('/new/path'); +      url.search({one: 1}); +      url.hash('hhh'); +      expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/new/path?one=1#hhh'); +    }); + + +    it('should prefix path with forward-slash', function() { +      url = new LocationHashbangUrl('http://host.com/base#path', ''); +      expect(url.path()).toBe('/path'); +      expect(url.absUrl()).toBe('http://host.com/base#/path'); + +      url.path('wrong'); +      expect(url.path()).toBe('/wrong'); +      expect(url.absUrl()).toBe('http://host.com/base#/wrong'); +    }); + + +    it('should set path to forward-slash when empty', function() { +      url = new LocationHashbangUrl('http://server/base#!', '!'); +      url.path('aaa'); + +      expect(url.path()).toBe('/aaa'); +      expect(url.absUrl()).toBe('http://server/base#!/aaa'); +    }); -    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 not preserve old properties when parsing new url', function() { +      url.$$parse('http://www.server.org:1234/base#!/'); + +      expect(url.path()).toBe('/'); +      expect(url.search()).toEqual({}); +      expect(url.hash()).toBe(''); +      expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/');      }); -    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 throw error when invalid url given', function() { +      expect(function() { +        url.$$parse('http://server.org/path#/path'); +      }).toThrow('Invalid url "http://server.org/path#/path", missing hash prefix "!" !');      }); -    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('encoding', function() { + +      it('should encode special characters', function() { +        url.path('/a <>#'); +        url.search({'i j': '<>#'}); +        url.hash('<>#'); + +        expect(url.path()).toBe('/a <>#'); +        expect(url.search()).toEqual({'i j': '<>#'}); +        expect(url.hash()).toBe('<>#'); +        expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23'); +      }); + + +      it('should not encode !$:@', function() { +        url.path('/!$:@'); +        url.search(''); +        url.hash('!$:@'); + +        expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/!$:@#!$:@'); +      }); + + +      it('should decode special characters', function() { +        url = new LocationHashbangUrl('http://host.com/a#/%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23', ''); +        expect(url.path()).toBe('/ <>#'); +        expect(url.search()).toEqual({'i j': '<>#'}); +        expect(url.hash()).toBe('x <>#'); +      });      });    }); -  describe('updateHash()', function() { +  var $browser, $location, scope; + +  function init(url, html5Mode, basePath, hashPrefix, supportHistory) { +    scope = angular.scope(null, { +      $locationConfig: {html5Mode: html5Mode, hashPrefix: hashPrefix}, +      $sniffer: {history: supportHistory}}); + +    $browser = scope.$service('$browser'); +    $browser.url(url); +    $browser.$$baseHref = basePath; +    $location = scope.$service('$location'); +  } + +  function dealocRootElement() { +    dealoc(scope.$service('$document')); +  } + + +  describe('wiring', function() { + +    beforeEach(function() { +      init('http://new.com/a/b#!', false, '/a/b', '!', true); +    }); + -    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 update $location when browser url changes', function() { +      spyOn($location, '$$parse').andCallThrough(); +      $browser.url('http://new.com/a/b#!/aaa'); +      $browser.poll(); +      expect($location.absUrl()).toBe('http://new.com/a/b#!/aaa'); +      expect($location.path()).toBe('/aaa'); +      expect($location.$$parse).toHaveBeenCalledOnce();      }); -    it('should reset hashSearch when updating with a single string', function() { -      $location.updateHash({foo:'bar'}); //set some initial state for hashSearch +    it('should update browser when $location changes', function() { +      var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); +      $location.path('/new/path'); +      expect($browserUrl).not.toHaveBeenCalled(); +      scope.$apply(); -      $location.updateHash('path'); -      expect($location.hashPath).toEqual('path'); -      expect($location.hashSearch).toEqual({}); +      expect($browserUrl).toHaveBeenCalledOnce(); +      expect($browser.url()).toBe('http://new.com/a/b#!/new/path');      }); -    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 update browser only once per $apply cycle', function() { +      var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); +      $location.path('/new/path'); + +      scope.$watch(function() { +        $location.search('a=b'); +      }); + +      scope.$apply(); +      expect($browserUrl).toHaveBeenCalledOnce(); +      expect($browser.url()).toBe('http://new.com/a/b#!/new/path?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 replace browser url when url was replaced at least once', function() { +      var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough(); +      $location.path('/n/url').replace(); +      scope.$apply(); + +      expect($browserUrl).toHaveBeenCalledOnce(); +      expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true]);      }); -    it('should update href and hash when updating to empty string', function() { -      $location.updateHash(''); -      expect($location.href).toBe('http://server'); -      expect($location.hash).toBe(''); +    it('should update the browser if changed from within a watcher', function() { +      scope.$watch(function() { return true; }, function() { +        $location.path('/changed'); +      });        scope.$digest(); +      expect($browser.url()).toBe('http://new.com/a/b#!/changed'); +    }); +  }); + + +  // html5 history is disabled +  describe('disabled history', function() { + +    it('should use hashbang url with hash prefix', function() { +      init('http://domain.com/base/index.html#!/a/b', false, '/base/index.html', '!'); +      expect($browser.url()).toBe('http://domain.com/base/index.html#!/a/b'); +      $location.path('/new'); +      $location.search({a: true}); +      scope.$apply(); +      expect($browser.url()).toBe('http://domain.com/base/index.html#!/new?a'); +    }); + + +    it('should use hashbang url without hash prefix', function() { +      init('http://domain.com/base/index.html#/a/b', false, '/base/index.html', ''); +      expect($browser.url()).toBe('http://domain.com/base/index.html#/a/b'); +      $location.path('/new'); +      $location.search({a: true}); +      scope.$apply(); +      expect($browser.url()).toBe('http://domain.com/base/index.html#/new?a'); +    }); +  }); + + +  // html5 history enabled, but not supported by browser +  describe('history on old browser', function() { + +    afterEach(dealocRootElement); + +    it('should use hashbang url with hash prefix', function() { +      init('http://domain.com/base/index.html#!!/a/b', true, '/base/index.html', '!!', false); +      expect($browser.url()).toBe('http://domain.com/base/index.html#!!/a/b'); +      $location.path('/new'); +      $location.search({a: true}); +      scope.$apply(); +      expect($browser.url()).toBe('http://domain.com/base/index.html#!!/new?a'); +    }); + -      expect($location.href).toBe('http://server'); -      expect($location.hash).toBe(''); +    it('should redirect to hashbang url when new url given', function() { +      init('http://domain.com/base/new-path/index.html', true, '/base/index.html', '!'); +      expect($browser.url()).toBe('http://domain.com/base/index.html#!/new-path/index.html'); +    }); +  }); + + +  // html5 history enabled and supported by browser +  describe('history on new browser', function() { + +    afterEach(dealocRootElement); + +    it('should use new url', function() { +      init('http://domain.com/base/old/index.html#a', true, '/base/index.html', '', true); +      expect($browser.url()).toBe('http://domain.com/base/old/index.html#a'); +      $location.path('/new'); +      $location.search({a: true}); +      scope.$apply(); +      expect($browser.url()).toBe('http://domain.com/base/new?a#a'); +    }); + + +    it('should rewrite when hashbang url given', function() { +      init('http://domain.com/base/index.html#!/a/b', true, '/base/index.html', '!', true); +      expect($browser.url()).toBe('http://domain.com/base/a/b'); +      $location.path('/new'); +      $location.hash('abc'); +      scope.$apply(); +      expect($browser.url()).toBe('http://domain.com/base/new#abc'); +      expect($location.path()).toBe('/new'); +    }); + + +    it('should rewrite when hashbang url given (without hash prefix)', function() { +      init('http://domain.com/base/index.html#/a/b', true, '/base/index.html', '', true); +      expect($browser.url()).toBe('http://domain.com/base/a/b'); +      expect($location.path()).toBe('/a/b');      });    }); @@ -255,21 +467,21 @@ describe('$location', 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'); +      expect(match[1]).toBe('http'); +      expect(match[3]).toBe('www.angularjs.org'); +      expect(match[6]).toBe('/path'); +      expect(match[8]).toBe('search'); +      expect(match[10]).toBe('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[1]).toBe('file'); +      expect(match[3]).toBe('');        expect(match[5]).toBeFalsy(); -      expect(match[6]).toEqual('/Users/Shared/misko/work/angular.js/scenario/widgets.html'); +      expect(match[6]).toBe('/Users/Shared/misko/work/angular.js/scenario/widgets.html');        expect(match[8]).toBeFalsy();      }); @@ -277,30 +489,170 @@ describe('$location', function() {      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[1]).toBe('http'); +      expect(match[3]).toBe('a-b1.c-d.09');        expect(match[5]).toBeFalsy(); -      expect(match[6]).toEqual('/path'); +      expect(match[6]).toBe('/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'); +      expect(match[3]).toBe('host.org');        match = URL_MATCH.exec('http://host.org#'); -      expect(match[3]).toEqual('host.org'); +      expect(match[3]).toBe('host.org');        match = URL_MATCH.exec('http://host.org?'); -      expect(match[3]).toEqual('host.org'); +      expect(match[3]).toBe('host.org');      });      it('should match with just "/" path', function() {        var match = URL_MATCH.exec('http://server/#?book=moby'); -      expect(match[10]).toEqual('?book=moby'); +      expect(match[10]).toBe('?book=moby'); +    }); +  }); + + +  describe('PATH_MATCH', function() { + +    it('should parse just path', function() { +      var match = PATH_MATCH.exec('/path'); +      expect(match[1]).toBe('/path'); +    }); + + +    it('should parse path with search', function() { +      var match = PATH_MATCH.exec('/ppp/a?a=b&c'); +      expect(match[1]).toBe('/ppp/a'); +      expect(match[3]).toBe('a=b&c'); +    }); + + +    it('should parse path with hash', function() { +      var match = PATH_MATCH.exec('/ppp/a#abc?'); +      expect(match[1]).toBe('/ppp/a'); +      expect(match[5]).toBe('abc?'); +    }); + + +    it('should parse path with both search and hash', function() { +      var match = PATH_MATCH.exec('/ppp/a?a=b&c#abc/d?'); +      expect(match[3]).toBe('a=b&c'); +    }); +  }); + + +  describe('link rewriting', function() { + +    var root, link, extLink, $browser, originalBrowser, lastEventPreventDefault; + +    function init(linkHref, html5Mode, supportHist, attrs) { +      var jqRoot = jqLite('<div></div>'); +      attrs = attrs ? ' ' + attrs + ' ' : ''; +      link = jqLite('<a href="' + linkHref + '"' + attrs + '>link</a>')[0]; +      root = jqRoot.append(link)[0]; + +      jqLite(document.body).append(jqRoot); + +      var scope = angular.scope(null, { +        $document: jqRoot, +        $sniffer: {history: supportHist}, +        $locationConfig: {html5Mode: html5Mode, hashPrefix: '!'} +      }); + +      $browser = scope.$service('$browser'); +      $browser.url('http://host.com/base'); +      $browser.$$baseHref = '/base/index.html'; +      var $location = scope.$service('$location'); +      originalBrowser = $browser.url(); + +      // we have to prevent the default operation, as we need to test absolute links (http://...) +      // and navigating to these links would kill jstd +      jqRoot.bind('click', function(e) { +        lastEventPreventDefault = e.isDefaultPrevented(); +        e.preventDefault(); +      }); +    } + +    function triggerAndExpectRewriteTo(url) { +      browserTrigger(link, 'click'); +      expect(lastEventPreventDefault).toBe(true); +      expect($browser.url()).toBe(url); +    } + +    function triggerAndExpectNoRewrite() { +      browserTrigger(link, 'click'); +      expect(lastEventPreventDefault).toBe(false); +      expect($browser.url()).toBe(originalBrowser); +    } + +    afterEach(function() { +      dealoc(root); +      dealoc(document.body); +    }); + + +    it('should rewrite rel link to new url when history enabled on new browser', function() { +      init('link?a#b', true, true); +      triggerAndExpectRewriteTo('http://host.com/base/link?a#b'); +    }); + + +    it('should rewrite abs link to new url when history enabled on new browser', function() { +      init('/base/link?a#b', true, true); +      triggerAndExpectRewriteTo('http://host.com/base/link?a#b'); +    }); + + +    it('should rewrite rel link to hashbang url when history enabled on old browser', function() { +      init('link?a#b', true, false); +      triggerAndExpectRewriteTo('http://host.com/base/index.html#!/link?a#b'); +    }); + + +    it('should rewrite abs link to hashbang url when history enabled on old browser', function() { +      init('/base/link?a#b', true, false); +      triggerAndExpectRewriteTo('http://host.com/base/index.html#!/link?a#b'); +    }); + + +    it('should not rewrite when history disabled', function() { +      init('#new', false); +      triggerAndExpectNoRewrite(); +    }); + + +    it('should not rewrite ng:ext-link', function() { +      init('#new', true, true, 'ng:ext-link'); +      triggerAndExpectNoRewrite(); +    }); + + +    it('should not rewrite full url links do different domain', function() { +      init('http://www.dot.abc/a?b=c', true); +      triggerAndExpectNoRewrite(); +    }); + + +    it('should not rewrite links with target="_blank"', function() { +      init('/a?b=c', true, true, 'target="_blank"'); +      triggerAndExpectNoRewrite(); +    }); + + +    it('should not rewrite links with target specified', function() { +      init('/a?b=c', true, true, 'target="some-frame"'); +      triggerAndExpectNoRewrite(); +    }); + + +    it('should rewrite full url links to same domain and base path', function() { +      init('http://host.com/base/new', true); +      triggerAndExpectRewriteTo('http://host.com/base/index.html#!/new');      });    });  });  | 
