<NME> nav_test.js
<BEF> // Copyright 2013 Google Inc. All rights reserved.
//
// Use of this source code is governed by The MIT License.
// See the LICENSE file for details.

/**
 * @fileoverview Tests for navigation-related core functions.
 */

goog.require('spf');
goog.require('spf.config');
goog.require('spf.history');
goog.require('spf.nav');
goog.require('spf.nav.request');
goog.require('spf.state');
goog.require('spf.url');


describe('spf.nav', function() {

  var MOCK_DELAY = 10;
  var objFunc = function() { return {}; };
  var nullFunc = function() { return null; };
  var trueFunc = function() { return true; };
  var falseFunc = function() { return false; };
  var createFakeBrowserEvent = function() {
    var target = {
      href: 'SPF_HREF'
    };
    var evt = {
      metaKey: false,
      altKey: false,
      ctrlKey: false,
      shiftKey: false,
      target: target,
      defaultPrevented: false,
      preventDefault: function() { evt.defaultPrevented = true; }
    };
    return evt;
  };
  var createFakeRequest = function(response1, opt_response2, opt_shouldFail) {
    var counter = 0;
    var xhr = {
      abort: function() { },
      readyState: 1
    };
    var callOnSuccessDelayed = function(url, opts) {
      setTimeout(function() {
        if (counter == 0) {
          if (opt_response2) {
            opts.onPart(url, response1);
          } else {
            opts.onSuccess(url, response1);
          }
          counter++;
        } else {
          opts.onPart(url, opt_response2);
          var fullResponse = {
            'type': 'multipart',
            'parts': [response1, opt_response2]
          };
          opts.onSuccess(url, fullResponse);
        }
        xhr.readyState = 4;
      }, MOCK_DELAY);
    };
    var callOnErrorDelayed = function(url, opts) {
      setTimeout(function() {
        opts.onError(url, new Error());
        xhr.readyState = 4;
      }, MOCK_DELAY);
    };
    return function(url, options) {
      if (opt_shouldFail) {
        callOnErrorDelayed(url, options);
      } else {
        callOnSuccessDelayed(url, options);
      }
      return xhr;
    };
  };
  var fakeHistoryReplace = function(url, state, doCallback) {
    if (doCallback) {
      var callback = spf.state.get(spf.state.Key.HISTORY_CALLBACK);
      if (callback) {
        callback(url, state);
      }
    }
  };


  beforeEach(function() {
    spyOn(spf.history, 'add');
    spyOn(spf.history, 'replace').and.callFake(fakeHistoryReplace);
    if (!document.addEventListener) {
      document.addEventListener = jasmine.createSpy('addEventListener');
    }

    spf.nav.init();

    jasmine.clock().install();
  });


  afterEach(function() {
    jasmine.clock().uninstall();
    spf.nav.dispose();
    spf.config.clear();
    spf.state.values_ = {};
  });


  describe('prefetch', function() {


    it('prefetches a page', function() {
      var url = '/page';
      var fake = createFakeRequest({'foobar': true});
      spyOn(spf.nav.request, 'send').and.callFake(fake);

      var absoluteUrl = spf.url.absolute(url);
      spf.nav.prefetch(url);
      var prefetches = spf.nav.prefetches_();
      expect(prefetches[absoluteUrl]).toBeTruthy();

      jasmine.clock().tick(MOCK_DELAY + 1);
      expect(prefetches[absoluteUrl]).not.toBeTruthy();
    });


    it('promotes a prefetch', function() {
      spyOn(spf.nav, 'handleNavigateSuccess_');

      var url = '/page';
      var fake = createFakeRequest({'foobar': true});
      spyOn(spf.nav.request, 'send').and.callFake(fake);


      var absoluteUrl = spf.url.absolute(url);
      spf.nav.prefetch(url);
      spf.nav.navigate(url);
      expect(spf.state.get(spf.state.Key.NAV_PROMOTE)).toEqual(url);

      jasmine.clock().tick(MOCK_DELAY + 1);
      expect(spf.nav.handleNavigateSuccess_).toHaveBeenCalled();
    });


    it('prefetch singlepart response with redirects', function() {
      spyOn(spf.nav, 'prefetch_').and.callThrough();

      var url = '/page';
      var url2 = '/page2';
      var fake = createFakeRequest({'redirect': url2});
      spyOn(spf.nav.request, 'send').and.callFake(fake);

      var absoluteUrl = spf.url.absolute(url);
      spf.nav.prefetch(url);

      jasmine.clock().tick(MOCK_DELAY + 1);
      expect(spf.nav.prefetch_.calls.argsFor(1)[0]).toEqual(url2);
      expect(spf.nav.prefetch_.calls.count()).toEqual(2);
    });


    it('prefetch multipart response with redirects', function() {
      spyOn(spf.nav, 'prefetch_').and.callThrough();

      var url = '/page';
      var url2 = '/page2';
      var fake = createFakeRequest({'redirect': url2}, {'foobar': true});
      spyOn(spf.nav.request, 'send').and.callFake(fake);

      var absoluteUrl = spf.url.absolute(url);
      spf.nav.prefetch(url);

      jasmine.clock().tick(2 * MOCK_DELAY + 1);
      expect(spf.nav.prefetch_.calls.argsFor(1)[0]).toEqual(url2);
      expect(spf.nav.prefetch_.calls.count()).toEqual(2);
    });


    it('promotes a prefetch with redirects', function() {
      spyOn(spf.nav, 'prefetch').and.callThrough();
      spyOn(spf.nav, 'handleNavigateSuccess_');

      var url = '/page';
      var url2 = '/page2';
      var fake = createFakeRequest({'redirect': url2}, {'foobar': true});
      spyOn(spf.nav.request, 'send').and.callFake(fake);

      var absoluteUrl = spf.url.absolute(url);
      spf.nav.prefetch(url);
      jasmine.clock().tick(MOCK_DELAY + 1);
      spf.nav.navigate(url);

      jasmine.clock().tick(MOCK_DELAY + 1);
      expect(spf.nav.handleNavigateSuccess_).toHaveBeenCalled();
    });


    it('handles promoted prefetch errors', function() {
      spyOn(spf.nav, 'handleNavigateError_');

      var url = '/page';
      var fake = createFakeRequest({'foobar': true}, null, true);
      spyOn(spf.nav.request, 'send').and.callFake(fake);


      var absoluteUrl = spf.url.absolute(url);
      spf.nav.prefetch(url);
      spf.nav.navigate(url);
      expect(spf.state.get(spf.state.Key.NAV_PROMOTE)).toEqual(url);

      jasmine.clock().tick(MOCK_DELAY + 1);
      expect(spf.nav.handleNavigateError_).toHaveBeenCalled();
    });
  });


  describe('handleClick', function() {


    beforeEach(function() {
      spyOn(spf.nav, 'navigate_');
    });


    it('ignores click with defaultPrevented', function() {
      var evt = createFakeBrowserEvent();

      evt.preventDefault();
      spf.nav.handleClick_(evt);

      expect(evt.defaultPrevented).toEqual(true);
      expect(spf.nav.navigate_).not.toHaveBeenCalled();
    });


    it('ignores click with modifier keys', function() {
      var evt = createFakeBrowserEvent();

      evt.metaKey = true;
      spf.nav.handleClick_(evt);

      expect(evt.defaultPrevented).toEqual(false);
      expect(spf.nav.navigate_).not.toHaveBeenCalled();
    });


    it('ignores click with alternative button', function() {
      var evt = createFakeBrowserEvent();

      evt.button = 1;
      spf.nav.handleClick_(evt);

      expect(evt.defaultPrevented).toEqual(false);
      expect(spf.nav.navigate_).not.toHaveBeenCalled();
    });


    it('ignores click with nolink class', function() {
      var evt = createFakeBrowserEvent();
      spyOn(spf.nav, 'getAncestorWithNoLinkClass_').and.callFake(objFunc);
      spf.config.set('nolink-class', 'NOLINK_CLASS');

      spf.nav.handleClick_(evt);

      expect(evt.defaultPrevented).toEqual(false);
      expect(spf.nav.navigate_).not.toHaveBeenCalled();
    });


    it('ignores click without href', function() {
      var evt = createFakeBrowserEvent();
      spyOn(spf.nav, 'getAncestorWithHref_').and.callFake(nullFunc);
      spyOn(spf.nav, 'getAncestorWithLinkClass_').and.callFake(objFunc);

      spf.nav.handleClick_(evt);

      expect(spf.nav.getAncestorWithHref_).toHaveBeenCalled();
      expect(evt.defaultPrevented).toEqual(false);
      expect(spf.nav.navigate_).not.toHaveBeenCalled();
    });


    it('ignores click without link class', function() {
      var evt = createFakeBrowserEvent();
      spyOn(spf.nav, 'getAncestorWithLinkClass_').and.callFake(nullFunc);

      spf.nav.handleClick_(evt);

      expect(spf.nav.getAncestorWithLinkClass_).toHaveBeenCalled();
      expect(evt.defaultPrevented).toEqual(false);
      expect(spf.nav.navigate_).not.toHaveBeenCalled();
    });


    it('ignores click if not eligible', function() {
      var evt = createFakeBrowserEvent();
      spyOn(spf.nav, 'getAncestorWithHref_').and.callFake(function() {
          return {href: evt.target.href}; });
      spyOn(spf.nav, 'getAncestorWithLinkClass_').and.callFake(objFunc);
      spyOn(spf.nav, 'isEligible_').and.callFake(falseFunc);

      spf.nav.handleClick_(evt);

      expect(spf.nav.isEligible_).toHaveBeenCalled();
      expect(evt.defaultPrevented).toEqual(false);
      expect(spf.nav.navigate_).not.toHaveBeenCalled();
    });


    it('handles spf click', function() {
      var evt = createFakeBrowserEvent();
      spyOn(spf.nav, 'getAncestorWithHref_').and.callFake(function() {
          return {href: evt.target.href}; });
      spyOn(spf.nav, 'getAncestorWithLinkClass_').and.callFake(objFunc);

      spf.nav.handleClick_(evt);

      expect(evt.defaultPrevented).toEqual(true);
      expect(spf.nav.navigate_).toHaveBeenCalled();
      expect(spf.nav.navigate_.calls.argsFor(0)[0]).toEqual(evt.target.href);
    });


  });


  describe('isAllowed', function() {


    it('respects same-origin security', function() {
      var sameDomainUrl = '/page';
      var crossDomainUrl = 'https://www.google.com/';
      expect(spf.nav.isAllowed_(sameDomainUrl)).toBe(true);
      expect(spf.nav.isAllowed_(crossDomainUrl)).toBe(false);
    });


  });


  describe('isEligible', function() {


    it('respects initialization', function() {
      var url = '/page';
      spf.state.set(spf.state.Key.NAV_INIT, false);
      expect(spf.nav.isEligible_(url)).toBe(false);
      spf.state.set(spf.state.Key.NAV_INIT, true);
      expect(spf.nav.isEligible_(url)).toBe(true);
    });


    it('respects session navigation limit', function() {
      var url = '/page';
      spf.config.set('navigate-limit', 5);
      spf.state.set(spf.state.Key.NAV_COUNTER, 5);
      expect(spf.nav.isEligible_(url)).toBe(false);
      spf.state.set(spf.state.Key.NAV_COUNTER, 4);
      expect(spf.nav.isEligible_(url)).toBe(true);
      spf.config.set('navigate-limit', null);
      spf.state.set(spf.state.Key.NAV_COUNTER, 999999);
      expect(spf.nav.isEligible_(url)).toBe(true);
    });


    it('respects session lifetime', function() {
      var url = '/page';
      // Lifetime at 1s, age at 1s should be expired.
      spf.config.set('navigate-lifetime', 1000);
      spf.state.set(spf.state.Key.NAV_INIT_TIME, spf.now() - 1000);
      expect(spf.nav.isEligible_(url)).toBe(false);
      // Lifetime at 1s, age at 0.5s should be unexpired.
      spf.state.set(spf.state.Key.NAV_INIT_TIME, spf.now() - 500);
      expect(spf.nav.isEligible_(url)).toBe(true);
      // Lifetime at unlimited, age at 10d should be unexpired.
      spf.config.set('navigate-lifetime', null);
      // 864000000ms = 10d * 24hr/d * 60min/hr * 60s/min * 1000ms/s.
      spf.state.set(spf.state.Key.NAV_INIT_TIME, spf.now() - 864000000);
      expect(spf.nav.isEligible_(url)).toBe(true);
    });


  });


  describe('isNavigable', function() {


    it('allows standard to standard across pages', function() {
      var current = window.location.href;
      var url = '/page';
      var crossDomainUrl = 'https://www.google.com/page';
      expect(spf.nav.isNavigable_(url, current)).toBe(true);
      expect(spf.nav.isNavigable_(crossDomainUrl, current)).toBe(true);
    });


    it('allows standard to hash across pages', function() {
      var current = window.location.href;
      var urlWithTargetHash = '/page#target';
      var urlWithEmptyHash = '/page#';
      var crossDomainUrlWithTargetHash = 'https://www.google.com/page#target';
      var crossDomainUrlWithEmptyHash = 'https://www.google.com/page#';
      expect(spf.nav.isNavigable_(urlWithTargetHash, current)).toBe(true);
      expect(spf.nav.isNavigable_(urlWithEmptyHash, current)).toBe(true);
      expect(spf.nav.isNavigable_(
                 crossDomainUrlWithTargetHash, current)).toBe(true);
      expect(spf.nav.isNavigable_(
                 crossDomainUrlWithEmptyHash, current)).toBe(true);
    });


    it('allows hash to standard across pages', function() {
      var current = window.location.href;
      var currentWithTargetHash = spf.url.absolute(current) + '#target';
      var currentWithEmptyHash = spf.url.absolute(current) + '#';
      var url = '/page';
      var crossDomainUrl = 'https://www.google.com/page';
      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(true);
      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(true);
      expect(spf.nav.isNavigable_(
                 crossDomainUrl, currentWithTargetHash)).toBe(true);
      expect(spf.nav.isNavigable_(
                 crossDomainUrl, currentWithEmptyHash)).toBe(true);
    });


    it('allows hash to hash across pages', function() {
      var current = window.location.href;
      var currentWithTargetHash = spf.url.absolute(current) + '#target';
      var currentWithEmptyHash = spf.url.absolute(current) + '#';
      var urlWithTargetHash = '/page#target';
      var urlWithEmptyHash = '/page#';
      var crossDomainUrlWithTargetHash = 'https://www.google.com/page#target';
      var crossDomainUrlWithEmptyHash = 'https://www.google.com/page#';
      expect(spf.nav.isNavigable_(
                 urlWithTargetHash, currentWithTargetHash)).toBe(true);
      expect(spf.nav.isNavigable_(
                 urlWithTargetHash, currentWithEmptyHash)).toBe(true);
      expect(spf.nav.isNavigable_(
                 urlWithEmptyHash, currentWithTargetHash)).toBe(true);
      expect(spf.nav.isNavigable_(
                 urlWithEmptyHash, currentWithEmptyHash)).toBe(true);
      expect(spf.nav.isNavigable_(
                 crossDomainUrlWithTargetHash, currentWithTargetHash)
             ).toBe(true);
      expect(spf.nav.isNavigable_(
                 crossDomainUrlWithTargetHash, currentWithEmptyHash)
             ).toBe(true);
      expect(spf.nav.isNavigable_(
                 crossDomainUrlWithEmptyHash, currentWithTargetHash)
             ).toBe(true);
      expect(spf.nav.isNavigable_(
                 crossDomainUrlWithEmptyHash, currentWithEmptyHash)
             ).toBe(true);
    });


    it('allows standard to standard for same page', function() {
      var current = window.location.href;
      var url = current;
      expect(spf.nav.isNavigable_(url, current)).toBe(true);
    });


    it('denies standard to hash for same page', function() {
      var current = window.location.href;
      var urlWithTargetHash = spf.url.absolute(current) + '#target';
      var urlWithEmptyHash = spf.url.absolute(current) + '#';
      expect(spf.nav.isNavigable_(urlWithTargetHash, current)).toBe(false);
      expect(spf.nav.isNavigable_(urlWithEmptyHash, current)).toBe(false);
    });


    it('denies hash to standard for same page', function() {
      var current = window.location.href;
      var currentWithTargetHash = spf.url.absolute(current) + '#target';
      var currentWithEmptyHash = spf.url.absolute(current) + '#';
      var url = current;
      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(false);
      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(false);
    });


    it('denies hash to hash for same page', function() {
      var current = window.location.href;
      var currentWithTargetHash = spf.url.absolute(current) + '#target';
      var currentWithEmptyHash = spf.url.absolute(current) + '#';
      var urlWithTargetHash = currentWithTargetHash;
      var urlWithEmptyHash = currentWithEmptyHash;
      expect(spf.nav.isNavigable_(
                 urlWithTargetHash, currentWithTargetHash)).toBe(false);
      expect(spf.nav.isNavigable_(
                 urlWithTargetHash, currentWithEmptyHash)).toBe(false);
      expect(spf.nav.isNavigable_(
                 urlWithEmptyHash, currentWithTargetHash)).toBe(false);
      expect(spf.nav.isNavigable_(
                 urlWithEmptyHash, currentWithEmptyHash)).toBe(false);
    });


  });


  describe('navigate', function() {


    it('handles singlepart redirect response', function() {
      // Skip history tests for browsers which don't support the history apis.
      // TODO(dcphillips): Figure out a way to control this in the BUILD files.
      if (!window.history.pushState || !window.history.replaceState) {
        return;
      }

      spyOn(spf.nav, 'navigate_').and.callThrough();

      var url = '/page';
      var url2 = '/page2';
      var fake = createFakeRequest({'redirect': url2});
      spyOn(spf.nav.request, 'send').and.callFake(fake);

      var absoluteUrl = spf.url.absolute(url);
      spf.nav.navigate(url);

      jasmine.clock().tick(MOCK_DELAY + 1);
      expect(spf.nav.navigate_.calls.argsFor(1)[0]).toEqual(url2);
      expect(spf.nav.navigate_.calls.count()).toEqual(2);
    });


    it('handles multipart redirect response', function() {
      // Skip history tests for browsers which don't support the history apis.
      // TODO(dcphillips): Figure out a way to control this in the BUILD files.
      if (!window.history.pushState || !window.history.replaceState) {
        return;
      }

      spyOn(spf.nav, 'navigate_').and.callThrough();

      var url = '/page';
      var url2 = '/page2';
      var fake = createFakeRequest({'redirect': url2}, {'foobar': true});
      spyOn(spf.nav.request, 'send').and.callFake(fake);

      var absoluteUrl = spf.url.absolute(url);
      spf.nav.navigate(url);

      jasmine.clock().tick(2 * MOCK_DELAY + 1);
      expect(spf.nav.navigate_.calls.argsFor(1)[0]).toEqual(url2);
      expect(spf.nav.navigate_.calls.count()).toEqual(2);
    });


  });


  describe('dispatchError', function() {

    var url = '/page';
    var err = new Error();


    it('dispatches an event', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var proceed = spf.nav.dispatchError_(url, err);
      var evtName = spf.dispatch.calls.argsFor(0)[0];
      expect(spf.dispatch).toHaveBeenCalled();
      expect(evtName).toEqual(spf.EventName.ERROR);
      expect(proceed).toBe(true);
    });


    it('dispatches an event and propagates cancellation', function() {
      spyOn(spf, 'dispatch').and.returnValue(false);
      var proceed = spf.nav.dispatchError_(url, err);
      expect(spf.dispatch).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('executes a callback', function() {
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchError_(url, err,
                                           {'onError': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('executes a callback and propagates cancellation', function() {
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchError_(url, err,
                                           {'onError': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('does not dispatch an event if events are skipped', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchError_(url, err,
                                           {'onError': fn}, true);
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('does not dispatch an event if a callback is canceled', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchError_(url, err,
                                           {'onError': fn});
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('passes the same data to both events and callbacks', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchError_(url, err,
                                           {'onError': fn});
      var evtData = spf.dispatch.calls.argsFor(0)[1];
      var fnData = fn.calls.argsFor(0)[0];
      expect(evtData).toEqual(fnData);
      expect(proceed).toBe(true);
    });


  });


  describe('dispatchReload', function() {

    var url = '/page';

    it('dispatches an event', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      spf.nav.dispatchReload_(url);
      var evtName = spf.dispatch.calls.argsFor(0)[0];
      expect(spf.dispatch).toHaveBeenCalled();
      expect(evtName).toEqual(spf.EventName.RELOAD);
    });


  });


  describe('dispatchClick', function() {

    var url = '/page';
    var target = document.createElement('a');


    it('dispatches an event', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var proceed = spf.nav.dispatchClick_(url, target);
      var evtName = spf.dispatch.calls.argsFor(0)[0];
      expect(spf.dispatch).toHaveBeenCalled();
      expect(evtName).toEqual(spf.EventName.CLICK);
      expect(proceed).toBe(true);
    });


    it('dispatches an event and propagates cancellation', function() {
      spyOn(spf, 'dispatch').and.returnValue(false);
      var proceed = spf.nav.dispatchClick_(url, target);
      expect(spf.dispatch).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


  });


  describe('dispatchHistory', function() {

    var url = '/page';


    it('dispatches an event', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var proceed = spf.nav.dispatchHistory_(url);
      var evtName = spf.dispatch.calls.argsFor(0)[0];
      expect(spf.dispatch).toHaveBeenCalled();
      expect(evtName).toEqual(spf.EventName.HISTORY);
      expect(proceed).toBe(true);
    });


    it('dispatches an event and propagates cancellation', function() {
      spyOn(spf, 'dispatch').and.returnValue(false);
      var proceed = spf.nav.dispatchHistory_(url);
      expect(spf.dispatch).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


  });


  describe('dispatchRequest', function() {

    var url = '/page';
    var referer = '/other';
    var previous = '/previous';


    it('dispatches an event', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var proceed = spf.nav.dispatchRequest_(url, referer, previous);
      var evtName = spf.dispatch.calls.argsFor(0)[0];
      expect(spf.dispatch).toHaveBeenCalled();
      expect(evtName).toEqual(spf.EventName.REQUEST);
      expect(proceed).toBe(true);
    });


    it('dispatches an event and propagates cancellation', function() {
      spyOn(spf, 'dispatch').and.returnValue(false);
      var proceed = spf.nav.dispatchRequest_(url, referer, previous);
      expect(spf.dispatch).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('executes a callback', function() {
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchRequest_(url, referer, previous,
                                             {'onRequest': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('executes a callback and propagates cancellation', function() {
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchRequest_(url, referer, previous,
                                             {'onRequest': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('does not dispatch an event if events are skipped', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchRequest_(url, referer, previous,
                                             {'onRequest': fn}, true);
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('does not dispatch an event if a callback is canceled', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchRequest_(url, referer, previous,
                                             {'onRequest': fn});
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('passes the same data to both events and callbacks', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchRequest_(url, referer, previous,
                                             {'onRequest': fn});
      var evtData = spf.dispatch.calls.argsFor(0)[1];
      var fnData = fn.calls.argsFor(0)[0];
      expect(evtData).toEqual(fnData);
      expect(proceed).toBe(true);
    });


  });


  describe('dispatchPartProcess', function() {

    var url = '/page';
    var partial = {};


    it('dispatches an event', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var proceed = spf.nav.dispatchPartProcess_(url, partial);
      var evtName = spf.dispatch.calls.argsFor(0)[0];
      expect(spf.dispatch).toHaveBeenCalled();
      expect(evtName).toEqual(spf.EventName.PART_PROCESS);
      expect(proceed).toBe(true);
    });


    it('dispatches an event and propagates cancellation', function() {
      spyOn(spf, 'dispatch').and.returnValue(false);
      var proceed = spf.nav.dispatchPartProcess_(url, partial);
      expect(spf.dispatch).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('executes a callback', function() {
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchPartProcess_(url, partial,
                                                 {'onPartProcess': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('executes a callback and propagates cancellation', function() {
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchPartProcess_(url, partial,
                                                 {'onPartProcess': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('does not dispatch an event if events are skipped', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchPartProcess_(url, partial,
                                                 {'onPartProcess': fn}, true);
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('does not dispatch an event if a callback is canceled', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchPartProcess_(url, partial,
                                                 {'onPartProcess': fn});
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('passes the same data to both events and callbacks', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchPartProcess_(url, partial,
                                                 {'onPartProcess': fn});
      var evtData = spf.dispatch.calls.argsFor(0)[1];
      var fnData = fn.calls.argsFor(0)[0];
      expect(evtData).toEqual(fnData);
      expect(proceed).toBe(true);
    });


  });


  describe('dispatchPartDone', function() {

    var url = '/page';
    var partial = {};


    it('dispatches an event', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var proceed = spf.nav.dispatchPartDone_(url, partial);
      var evtName = spf.dispatch.calls.argsFor(0)[0];
      expect(spf.dispatch).toHaveBeenCalled();
      expect(evtName).toEqual(spf.EventName.PART_DONE);
      expect(proceed).toBe(true);
    });


    it('dispatches an event and propagates cancellation', function() {
      spyOn(spf, 'dispatch').and.returnValue(false);
      var proceed = spf.nav.dispatchPartDone_(url, partial);
      expect(spf.dispatch).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('executes a callback', function() {
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchPartDone_(url, partial,
                                              {'onPartDone': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('executes a callback and propagates cancellation', function() {
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchPartDone_(url, partial,
                                              {'onPartDone': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('does not dispatch an event if events are skipped', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchPartDone_(url, partial,
                                              {'onPartDone': fn}, true);
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('does not dispatch an event if a callback is canceled', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchPartDone_(url, partial,
                                              {'onPartDone': fn});
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('passes the same data to both events and callbacks', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchPartDone_(url, partial,
                                              {'onPartDone': fn});
      var evtData = spf.dispatch.calls.argsFor(0)[1];
      var fnData = fn.calls.argsFor(0)[0];
      expect(evtData).toEqual(fnData);
      expect(proceed).toBe(true);
    });


  });


  describe('dispatchProcess', function() {

    var url = '/page';
    var response = {};


    it('dispatches an event', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var proceed = spf.nav.dispatchProcess_(url, response);
      var evtName = spf.dispatch.calls.argsFor(0)[0];
      expect(spf.dispatch).toHaveBeenCalled();
      expect(evtName).toEqual(spf.EventName.PROCESS);
      expect(proceed).toBe(true);
    });


    it('dispatches an event and propagates cancellation', function() {
      spyOn(spf, 'dispatch').and.returnValue(false);
      var proceed = spf.nav.dispatchProcess_(url, response);
      expect(spf.dispatch).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('executes a callback', function() {
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchProcess_(url, response,
                                             {'onProcess': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('executes a callback and propagates cancellation', function() {
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchProcess_(url, response,
                                             {'onProcess': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('does not dispatch an event if events are skipped', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchProcess_(url, response,
                                             {'onProcess': fn}, true);
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('does not dispatch an event if a callback is canceled', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchProcess_(url, response,
                                             {'onProcess': fn});
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('passes the same data to both events and callbacks', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchProcess_(url, response,
                                             {'onProcess': fn});
      var evtData = spf.dispatch.calls.argsFor(0)[1];
      var fnData = fn.calls.argsFor(0)[0];
      expect(evtData).toEqual(fnData);
      expect(proceed).toBe(true);
    });


  });


  describe('dispatchDone', function() {

    var url = '/page';
    var response = {};


    it('dispatches an event', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var proceed = spf.nav.dispatchDone_(url, response);
      var evtName = spf.dispatch.calls.argsFor(0)[0];
      expect(spf.dispatch).toHaveBeenCalled();
      expect(evtName).toEqual(spf.EventName.DONE);
      expect(proceed).toBe(true);
    });


    it('dispatches an event and propagates cancellation', function() {
      spyOn(spf, 'dispatch').and.returnValue(false);
      var proceed = spf.nav.dispatchDone_(url, response);
      expect(spf.dispatch).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('executes a callback', function() {
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchDone_(url, response,
                                          {'onDone': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('executes a callback and propagates cancellation', function() {
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchDone_(url, response,
                                          {'onDone': fn});
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('does not dispatch an event if events are skipped', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchDone_(url, response,
                                          {'onDone': fn}, true);
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(true);
    });


    it('does not dispatch an event if a callback is canceled', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn').and.returnValue(false);
      var proceed = spf.nav.dispatchDone_(url, response,
                                          {'onDone': fn});
      expect(spf.dispatch).not.toHaveBeenCalled();
      expect(fn).toHaveBeenCalled();
      expect(proceed).toBe(false);
    });


    it('passes the same data to both events and callbacks', function() {
      spyOn(spf, 'dispatch').and.callThrough();
      var fn = jasmine.createSpy('fn');
      var proceed = spf.nav.dispatchDone_(url, response,
                                          {'onDone': fn});
      var evtData = spf.dispatch.calls.argsFor(0)[1];
      var fnData = fn.calls.argsFor(0)[0];
      expect(evtData).toEqual(fnData);
      expect(proceed).toBe(true);
    });


  });


});

<MSG> Merge pull request #381 from nicksay/fix-377-again

Fix reloads when the URL contains a hash
<DFF> @@ -476,13 +476,13 @@ describe('spf.nav', function() {
     });
 
 
-    it('denies hash to standard for same page', function() {
+    it('allows hash to standard for same page', function() {
       var current = window.location.href;
       var currentWithTargetHash = spf.url.absolute(current) + '#target';
       var currentWithEmptyHash = spf.url.absolute(current) + '#';
       var url = current;
-      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(false);
-      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(false);
+      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(true);
+      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(true);
     });
 
 

{'commit': "diff --git a/src/client/nav/nav.js b/src/client/nav/nav.js\nindex f271ba2..fdc74e0 100644\n--- a/src/client/nav/nav.js\n+++ b/src/client/nav/nav.js\n@@ -252,10 +252,9 @@ spf.nav.isEligible_ = function(url) {\n  */\n spf.nav.isNavigable_ = function(url, opt_current) {\n   var current = opt_current || window.location.href;\n-  // Check for transitions between hash URLs.  If the source or destination\n-  // is a hash and the page is the same, navigation is not handled.\n-  if (spf.string.contains(url, '#') ||\n-      spf.string.contains(current, '#')) {\n+  // Check for transitions between hash URLs.  If the destination\n+  // contains a hash and the page is the same, navigation is not handled.\n+  if (spf.string.contains(url, '#')) {\n     var absoluteUrl = spf.url.absolute(url);\n     var absoluteCurrent = spf.url.absolute(current);\n     if (absoluteUrl == absoluteCurrent) {\n@@ -896,13 +895,13 @@ spf.nav.reload = function(url, reason, opt_err) {\n     logReason += ' Message: ' + err;\n   }\n   spf.nav.dispatchReload_(url, logReason);\n+  var current = window.location.href;\n   // If the url has already changed, clear its entry to prevent browser\n   // inconsistency with history management for 301 responses on reloads. Chrome\n   // will identify that the starting url was the same, and replace the current\n   // history state, whereas Firefox will set a new state with the post 301\n   // value.\n-  if (spf.config.get('experimental-remove-history') &&\n-      window.location.href == url) {\n+  if (spf.config.get('experimental-remove-history') && current == url) {\n     spf.history.removeCurrentEntry();\n   }\n   // Delay the reload until after the history state has had time to clear.\n@@ -914,10 +913,16 @@ spf.nav.reload = function(url, reason, opt_err) {\n       url = spf.url.appendParameters(url, params);\n     }\n     window.location.href = url;\n-    // If the new url contains a hash and the path is the same for both the new\n-    // and the old urls, then just assigning to `location.href` is not enough to\n-    // trigger a reload.  Explicitly call `location.reload()` to be sure.\n-    window.location.reload();\n+    // If the new url only differs by a hash then just assigning to\n+    // `location.href` is not enough to trigger a reload.  If this is the case,\n+    // explicitly calling `location.reload()` is required, but it can't be done\n+    // every time because an immediate call to `location.reload()` will cancel\n+    // the navgation started by the assignment to `location.href`.  The\n+    // `isNavigable_` function checks for hash-based navgiation that won't\n+    // trigger, so use it here to determine whether to call `location.reload()`.\n+    if (!spf.nav.isNavigable_(url, current)) {\n+      window.location.reload();\n+    }\n   }, 0);\n };\n \ndiff --git a/src/client/nav/nav_test.js b/src/client/nav/nav_test.js\nindex 49542b8..29a2006 100644\n--- a/src/client/nav/nav_test.js\n+++ b/src/client/nav/nav_test.js\n@@ -476,13 +476,13 @@ describe('spf.nav', function() {\n     });\n \n \n-    it('denies hash to standard for same page', function() {\n+    it('allows hash to standard for same page', function() {\n       var current = window.location.href;\n       var currentWithTargetHash = spf.url.absolute(current) + '#target';\n       var currentWithEmptyHash = spf.url.absolute(current) + '#';\n       var url = current;\n-      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(false);\n-      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(false);\n+      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(true);\n+      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(true);\n     });\n \n \n", 'commit_subject': 'Merge pull request #381 from nicksay/fix-377-again', 'repo_name': 'youtube/spfjs', 'committer': {'name': 'Alex Nicksay', 'email': '7c2b58e326787f100693e0a135d10e022088ae3e@gmail.com', 'time_sec': '1452290629', 'tz_offset': '-300', 'date': {'seconds': '1452290629'}}, 'commit_tree': 'ebe0591eb6388507daba475968166b67db463b33', 'commit_message': 'Merge pull request #381 from nicksay/fix-377-again\n\nFix reloads when the URL contains a hash', 'language_name': 'JavaScript', 'license': 'mit', 'hunks': "@@ -476,13 +476,13 @@ describe('spf.nav', function() {\n     });\n \n \n-    it('denies hash to standard for same page', function() {\n+    it('allows hash to standard for same page', function() {\n       var current = window.location.href;\n       var currentWithTargetHash = spf.url.absolute(current) + '#target';\n       var currentWithEmptyHash = spf.url.absolute(current) + '#';\n       var url = current;\n-      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(false);\n-      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(false);\n+      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(true);\n+      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(true);\n     });\n \n \n", 'addition_count': 3, 'deletion_count': 3, 'src_file': 'a/src/client/nav/nav_test.js', 'tgt_file': 'b/src/client/nav/nav_test.js', 'file_extension': '.js', 'before_file': "// Copyright 2013 Google Inc. All rights reserved.\n//\n// Use of this source code is governed by The MIT License.\n// See the LICENSE file for details.\n\n/**\n * @fileoverview Tests for navigation-related core functions.\n */\n\ngoog.require('spf');\ngoog.require('spf.config');\ngoog.require('spf.history');\ngoog.require('spf.nav');\ngoog.require('spf.nav.request');\ngoog.require('spf.state');\ngoog.require('spf.url');\n\n\ndescribe('spf.nav', function() {\n\n  var MOCK_DELAY = 10;\n  var objFunc = function() { return {}; };\n  var nullFunc = function() { return null; };\n  var trueFunc = function() { return true; };\n  var falseFunc = function() { return false; };\n  var createFakeBrowserEvent = function() {\n    var target = {\n      href: 'SPF_HREF'\n    };\n    var evt = {\n      metaKey: false,\n      altKey: false,\n      ctrlKey: false,\n      shiftKey: false,\n      target: target,\n      defaultPrevented: false,\n      preventDefault: function() { evt.defaultPrevented = true; }\n    };\n    return evt;\n  };\n  var createFakeRequest = function(response1, opt_response2, opt_shouldFail) {\n    var counter = 0;\n    var xhr = {\n      abort: function() { },\n      readyState: 1\n    };\n    var callOnSuccessDelayed = function(url, opts) {\n      setTimeout(function() {\n        if (counter == 0) {\n          if (opt_response2) {\n            opts.onPart(url, response1);\n          } else {\n            opts.onSuccess(url, response1);\n          }\n          counter++;\n        } else {\n          opts.onPart(url, opt_response2);\n          var fullResponse = {\n            'type': 'multipart',\n            'parts': [response1, opt_response2]\n          };\n          opts.onSuccess(url, fullResponse);\n        }\n        xhr.readyState = 4;\n      }, MOCK_DELAY);\n    };\n    var callOnErrorDelayed = function(url, opts) {\n      setTimeout(function() {\n        opts.onError(url, new Error());\n        xhr.readyState = 4;\n      }, MOCK_DELAY);\n    };\n    return function(url, options) {\n      if (opt_shouldFail) {\n        callOnErrorDelayed(url, options);\n      } else {\n        callOnSuccessDelayed(url, options);\n      }\n      return xhr;\n    };\n  };\n  var fakeHistoryReplace = function(url, state, doCallback) {\n    if (doCallback) {\n      var callback = spf.state.get(spf.state.Key.HISTORY_CALLBACK);\n      if (callback) {\n        callback(url, state);\n      }\n    }\n  };\n\n\n  beforeEach(function() {\n    spyOn(spf.history, 'add');\n    spyOn(spf.history, 'replace').and.callFake(fakeHistoryReplace);\n    if (!document.addEventListener) {\n      document.addEventListener = jasmine.createSpy('addEventListener');\n    }\n\n    spf.nav.init();\n\n    jasmine.clock().install();\n  });\n\n\n  afterEach(function() {\n    jasmine.clock().uninstall();\n    spf.nav.dispose();\n    spf.config.clear();\n    spf.state.values_ = {};\n  });\n\n\n  describe('prefetch', function() {\n\n\n    it('prefetches a page', function() {\n      var url = '/page';\n      var fake = createFakeRequest({'foobar': true});\n      spyOn(spf.nav.request, 'send').and.callFake(fake);\n\n      var absoluteUrl = spf.url.absolute(url);\n      spf.nav.prefetch(url);\n      var prefetches = spf.nav.prefetches_();\n      expect(prefetches[absoluteUrl]).toBeTruthy();\n\n      jasmine.clock().tick(MOCK_DELAY + 1);\n      expect(prefetches[absoluteUrl]).not.toBeTruthy();\n    });\n\n\n    it('promotes a prefetch', function() {\n      spyOn(spf.nav, 'handleNavigateSuccess_');\n\n      var url = '/page';\n      var fake = createFakeRequest({'foobar': true});\n      spyOn(spf.nav.request, 'send').and.callFake(fake);\n\n\n      var absoluteUrl = spf.url.absolute(url);\n      spf.nav.prefetch(url);\n      spf.nav.navigate(url);\n      expect(spf.state.get(spf.state.Key.NAV_PROMOTE)).toEqual(url);\n\n      jasmine.clock().tick(MOCK_DELAY + 1);\n      expect(spf.nav.handleNavigateSuccess_).toHaveBeenCalled();\n    });\n\n\n    it('prefetch singlepart response with redirects', function() {\n      spyOn(spf.nav, 'prefetch_').and.callThrough();\n\n      var url = '/page';\n      var url2 = '/page2';\n      var fake = createFakeRequest({'redirect': url2});\n      spyOn(spf.nav.request, 'send').and.callFake(fake);\n\n      var absoluteUrl = spf.url.absolute(url);\n      spf.nav.prefetch(url);\n\n      jasmine.clock().tick(MOCK_DELAY + 1);\n      expect(spf.nav.prefetch_.calls.argsFor(1)[0]).toEqual(url2);\n      expect(spf.nav.prefetch_.calls.count()).toEqual(2);\n    });\n\n\n    it('prefetch multipart response with redirects', function() {\n      spyOn(spf.nav, 'prefetch_').and.callThrough();\n\n      var url = '/page';\n      var url2 = '/page2';\n      var fake = createFakeRequest({'redirect': url2}, {'foobar': true});\n      spyOn(spf.nav.request, 'send').and.callFake(fake);\n\n      var absoluteUrl = spf.url.absolute(url);\n      spf.nav.prefetch(url);\n\n      jasmine.clock().tick(2 * MOCK_DELAY + 1);\n      expect(spf.nav.prefetch_.calls.argsFor(1)[0]).toEqual(url2);\n      expect(spf.nav.prefetch_.calls.count()).toEqual(2);\n    });\n\n\n    it('promotes a prefetch with redirects', function() {\n      spyOn(spf.nav, 'prefetch').and.callThrough();\n      spyOn(spf.nav, 'handleNavigateSuccess_');\n\n      var url = '/page';\n      var url2 = '/page2';\n      var fake = createFakeRequest({'redirect': url2}, {'foobar': true});\n      spyOn(spf.nav.request, 'send').and.callFake(fake);\n\n      var absoluteUrl = spf.url.absolute(url);\n      spf.nav.prefetch(url);\n      jasmine.clock().tick(MOCK_DELAY + 1);\n      spf.nav.navigate(url);\n\n      jasmine.clock().tick(MOCK_DELAY + 1);\n      expect(spf.nav.handleNavigateSuccess_).toHaveBeenCalled();\n    });\n\n\n    it('handles promoted prefetch errors', function() {\n      spyOn(spf.nav, 'handleNavigateError_');\n\n      var url = '/page';\n      var fake = createFakeRequest({'foobar': true}, null, true);\n      spyOn(spf.nav.request, 'send').and.callFake(fake);\n\n\n      var absoluteUrl = spf.url.absolute(url);\n      spf.nav.prefetch(url);\n      spf.nav.navigate(url);\n      expect(spf.state.get(spf.state.Key.NAV_PROMOTE)).toEqual(url);\n\n      jasmine.clock().tick(MOCK_DELAY + 1);\n      expect(spf.nav.handleNavigateError_).toHaveBeenCalled();\n    });\n  });\n\n\n  describe('handleClick', function() {\n\n\n    beforeEach(function() {\n      spyOn(spf.nav, 'navigate_');\n    });\n\n\n    it('ignores click with defaultPrevented', function() {\n      var evt = createFakeBrowserEvent();\n\n      evt.preventDefault();\n      spf.nav.handleClick_(evt);\n\n      expect(evt.defaultPrevented).toEqual(true);\n      expect(spf.nav.navigate_).not.toHaveBeenCalled();\n    });\n\n\n    it('ignores click with modifier keys', function() {\n      var evt = createFakeBrowserEvent();\n\n      evt.metaKey = true;\n      spf.nav.handleClick_(evt);\n\n      expect(evt.defaultPrevented).toEqual(false);\n      expect(spf.nav.navigate_).not.toHaveBeenCalled();\n    });\n\n\n    it('ignores click with alternative button', function() {\n      var evt = createFakeBrowserEvent();\n\n      evt.button = 1;\n      spf.nav.handleClick_(evt);\n\n      expect(evt.defaultPrevented).toEqual(false);\n      expect(spf.nav.navigate_).not.toHaveBeenCalled();\n    });\n\n\n    it('ignores click with nolink class', function() {\n      var evt = createFakeBrowserEvent();\n      spyOn(spf.nav, 'getAncestorWithNoLinkClass_').and.callFake(objFunc);\n      spf.config.set('nolink-class', 'NOLINK_CLASS');\n\n      spf.nav.handleClick_(evt);\n\n      expect(evt.defaultPrevented).toEqual(false);\n      expect(spf.nav.navigate_).not.toHaveBeenCalled();\n    });\n\n\n    it('ignores click without href', function() {\n      var evt = createFakeBrowserEvent();\n      spyOn(spf.nav, 'getAncestorWithHref_').and.callFake(nullFunc);\n      spyOn(spf.nav, 'getAncestorWithLinkClass_').and.callFake(objFunc);\n\n      spf.nav.handleClick_(evt);\n\n      expect(spf.nav.getAncestorWithHref_).toHaveBeenCalled();\n      expect(evt.defaultPrevented).toEqual(false);\n      expect(spf.nav.navigate_).not.toHaveBeenCalled();\n    });\n\n\n    it('ignores click without link class', function() {\n      var evt = createFakeBrowserEvent();\n      spyOn(spf.nav, 'getAncestorWithLinkClass_').and.callFake(nullFunc);\n\n      spf.nav.handleClick_(evt);\n\n      expect(spf.nav.getAncestorWithLinkClass_).toHaveBeenCalled();\n      expect(evt.defaultPrevented).toEqual(false);\n      expect(spf.nav.navigate_).not.toHaveBeenCalled();\n    });\n\n\n    it('ignores click if not eligible', function() {\n      var evt = createFakeBrowserEvent();\n      spyOn(spf.nav, 'getAncestorWithHref_').and.callFake(function() {\n          return {href: evt.target.href}; });\n      spyOn(spf.nav, 'getAncestorWithLinkClass_').and.callFake(objFunc);\n      spyOn(spf.nav, 'isEligible_').and.callFake(falseFunc);\n\n      spf.nav.handleClick_(evt);\n\n      expect(spf.nav.isEligible_).toHaveBeenCalled();\n      expect(evt.defaultPrevented).toEqual(false);\n      expect(spf.nav.navigate_).not.toHaveBeenCalled();\n    });\n\n\n    it('handles spf click', function() {\n      var evt = createFakeBrowserEvent();\n      spyOn(spf.nav, 'getAncestorWithHref_').and.callFake(function() {\n          return {href: evt.target.href}; });\n      spyOn(spf.nav, 'getAncestorWithLinkClass_').and.callFake(objFunc);\n\n      spf.nav.handleClick_(evt);\n\n      expect(evt.defaultPrevented).toEqual(true);\n      expect(spf.nav.navigate_).toHaveBeenCalled();\n      expect(spf.nav.navigate_.calls.argsFor(0)[0]).toEqual(evt.target.href);\n    });\n\n\n  });\n\n\n  describe('isAllowed', function() {\n\n\n    it('respects same-origin security', function() {\n      var sameDomainUrl = '/page';\n      var crossDomainUrl = 'https://www.google.com/';\n      expect(spf.nav.isAllowed_(sameDomainUrl)).toBe(true);\n      expect(spf.nav.isAllowed_(crossDomainUrl)).toBe(false);\n    });\n\n\n  });\n\n\n  describe('isEligible', function() {\n\n\n    it('respects initialization', function() {\n      var url = '/page';\n      spf.state.set(spf.state.Key.NAV_INIT, false);\n      expect(spf.nav.isEligible_(url)).toBe(false);\n      spf.state.set(spf.state.Key.NAV_INIT, true);\n      expect(spf.nav.isEligible_(url)).toBe(true);\n    });\n\n\n    it('respects session navigation limit', function() {\n      var url = '/page';\n      spf.config.set('navigate-limit', 5);\n      spf.state.set(spf.state.Key.NAV_COUNTER, 5);\n      expect(spf.nav.isEligible_(url)).toBe(false);\n      spf.state.set(spf.state.Key.NAV_COUNTER, 4);\n      expect(spf.nav.isEligible_(url)).toBe(true);\n      spf.config.set('navigate-limit', null);\n      spf.state.set(spf.state.Key.NAV_COUNTER, 999999);\n      expect(spf.nav.isEligible_(url)).toBe(true);\n    });\n\n\n    it('respects session lifetime', function() {\n      var url = '/page';\n      // Lifetime at 1s, age at 1s should be expired.\n      spf.config.set('navigate-lifetime', 1000);\n      spf.state.set(spf.state.Key.NAV_INIT_TIME, spf.now() - 1000);\n      expect(spf.nav.isEligible_(url)).toBe(false);\n      // Lifetime at 1s, age at 0.5s should be unexpired.\n      spf.state.set(spf.state.Key.NAV_INIT_TIME, spf.now() - 500);\n      expect(spf.nav.isEligible_(url)).toBe(true);\n      // Lifetime at unlimited, age at 10d should be unexpired.\n      spf.config.set('navigate-lifetime', null);\n      // 864000000ms = 10d * 24hr/d * 60min/hr * 60s/min * 1000ms/s.\n      spf.state.set(spf.state.Key.NAV_INIT_TIME, spf.now() - 864000000);\n      expect(spf.nav.isEligible_(url)).toBe(true);\n    });\n\n\n  });\n\n\n  describe('isNavigable', function() {\n\n\n    it('allows standard to standard across pages', function() {\n      var current = window.location.href;\n      var url = '/page';\n      var crossDomainUrl = 'https://www.google.com/page';\n      expect(spf.nav.isNavigable_(url, current)).toBe(true);\n      expect(spf.nav.isNavigable_(crossDomainUrl, current)).toBe(true);\n    });\n\n\n    it('allows standard to hash across pages', function() {\n      var current = window.location.href;\n      var urlWithTargetHash = '/page#target';\n      var urlWithEmptyHash = '/page#';\n      var crossDomainUrlWithTargetHash = 'https://www.google.com/page#target';\n      var crossDomainUrlWithEmptyHash = 'https://www.google.com/page#';\n      expect(spf.nav.isNavigable_(urlWithTargetHash, current)).toBe(true);\n      expect(spf.nav.isNavigable_(urlWithEmptyHash, current)).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 crossDomainUrlWithTargetHash, current)).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 crossDomainUrlWithEmptyHash, current)).toBe(true);\n    });\n\n\n    it('allows hash to standard across pages', function() {\n      var current = window.location.href;\n      var currentWithTargetHash = spf.url.absolute(current) + '#target';\n      var currentWithEmptyHash = spf.url.absolute(current) + '#';\n      var url = '/page';\n      var crossDomainUrl = 'https://www.google.com/page';\n      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(true);\n      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 crossDomainUrl, currentWithTargetHash)).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 crossDomainUrl, currentWithEmptyHash)).toBe(true);\n    });\n\n\n    it('allows hash to hash across pages', function() {\n      var current = window.location.href;\n      var currentWithTargetHash = spf.url.absolute(current) + '#target';\n      var currentWithEmptyHash = spf.url.absolute(current) + '#';\n      var urlWithTargetHash = '/page#target';\n      var urlWithEmptyHash = '/page#';\n      var crossDomainUrlWithTargetHash = 'https://www.google.com/page#target';\n      var crossDomainUrlWithEmptyHash = 'https://www.google.com/page#';\n      expect(spf.nav.isNavigable_(\n                 urlWithTargetHash, currentWithTargetHash)).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 urlWithTargetHash, currentWithEmptyHash)).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 urlWithEmptyHash, currentWithTargetHash)).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 urlWithEmptyHash, currentWithEmptyHash)).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 crossDomainUrlWithTargetHash, currentWithTargetHash)\n             ).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 crossDomainUrlWithTargetHash, currentWithEmptyHash)\n             ).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 crossDomainUrlWithEmptyHash, currentWithTargetHash)\n             ).toBe(true);\n      expect(spf.nav.isNavigable_(\n                 crossDomainUrlWithEmptyHash, currentWithEmptyHash)\n             ).toBe(true);\n    });\n\n\n    it('allows standard to standard for same page', function() {\n      var current = window.location.href;\n      var url = current;\n      expect(spf.nav.isNavigable_(url, current)).toBe(true);\n    });\n\n\n    it('denies standard to hash for same page', function() {\n      var current = window.location.href;\n      var urlWithTargetHash = spf.url.absolute(current) + '#target';\n      var urlWithEmptyHash = spf.url.absolute(current) + '#';\n      expect(spf.nav.isNavigable_(urlWithTargetHash, current)).toBe(false);\n      expect(spf.nav.isNavigable_(urlWithEmptyHash, current)).toBe(false);\n    });\n\n\n    it('denies hash to standard for same page', function() {\n      var current = window.location.href;\n      var currentWithTargetHash = spf.url.absolute(current) + '#target';\n      var currentWithEmptyHash = spf.url.absolute(current) + '#';\n      var url = current;\n      expect(spf.nav.isNavigable_(url, currentWithTargetHash)).toBe(false);\n      expect(spf.nav.isNavigable_(url, currentWithEmptyHash)).toBe(false);\n    });\n\n\n    it('denies hash to hash for same page', function() {\n      var current = window.location.href;\n      var currentWithTargetHash = spf.url.absolute(current) + '#target';\n      var currentWithEmptyHash = spf.url.absolute(current) + '#';\n      var urlWithTargetHash = currentWithTargetHash;\n      var urlWithEmptyHash = currentWithEmptyHash;\n      expect(spf.nav.isNavigable_(\n                 urlWithTargetHash, currentWithTargetHash)).toBe(false);\n      expect(spf.nav.isNavigable_(\n                 urlWithTargetHash, currentWithEmptyHash)).toBe(false);\n      expect(spf.nav.isNavigable_(\n                 urlWithEmptyHash, currentWithTargetHash)).toBe(false);\n      expect(spf.nav.isNavigable_(\n                 urlWithEmptyHash, currentWithEmptyHash)).toBe(false);\n    });\n\n\n  });\n\n\n  describe('navigate', function() {\n\n\n    it('handles singlepart redirect response', function() {\n      // Skip history tests for browsers which don't support the history apis.\n      // TODO(dcphillips): Figure out a way to control this in the BUILD files.\n      if (!window.history.pushState || !window.history.replaceState) {\n        return;\n      }\n\n      spyOn(spf.nav, 'navigate_').and.callThrough();\n\n      var url = '/page';\n      var url2 = '/page2';\n      var fake = createFakeRequest({'redirect': url2});\n      spyOn(spf.nav.request, 'send').and.callFake(fake);\n\n      var absoluteUrl = spf.url.absolute(url);\n      spf.nav.navigate(url);\n\n      jasmine.clock().tick(MOCK_DELAY + 1);\n      expect(spf.nav.navigate_.calls.argsFor(1)[0]).toEqual(url2);\n      expect(spf.nav.navigate_.calls.count()).toEqual(2);\n    });\n\n\n    it('handles multipart redirect response', function() {\n      // Skip history tests for browsers which don't support the history apis.\n      // TODO(dcphillips): Figure out a way to control this in the BUILD files.\n      if (!window.history.pushState || !window.history.replaceState) {\n        return;\n      }\n\n      spyOn(spf.nav, 'navigate_').and.callThrough();\n\n      var url = '/page';\n      var url2 = '/page2';\n      var fake = createFakeRequest({'redirect': url2}, {'foobar': true});\n      spyOn(spf.nav.request, 'send').and.callFake(fake);\n\n      var absoluteUrl = spf.url.absolute(url);\n      spf.nav.navigate(url);\n\n      jasmine.clock().tick(2 * MOCK_DELAY + 1);\n      expect(spf.nav.navigate_.calls.argsFor(1)[0]).toEqual(url2);\n      expect(spf.nav.navigate_.calls.count()).toEqual(2);\n    });\n\n\n  });\n\n\n  describe('dispatchError', function() {\n\n    var url = '/page';\n    var err = new Error();\n\n\n    it('dispatches an event', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var proceed = spf.nav.dispatchError_(url, err);\n      var evtName = spf.dispatch.calls.argsFor(0)[0];\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(evtName).toEqual(spf.EventName.ERROR);\n      expect(proceed).toBe(true);\n    });\n\n\n    it('dispatches an event and propagates cancellation', function() {\n      spyOn(spf, 'dispatch').and.returnValue(false);\n      var proceed = spf.nav.dispatchError_(url, err);\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('executes a callback', function() {\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchError_(url, err,\n                                           {'onError': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('executes a callback and propagates cancellation', function() {\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchError_(url, err,\n                                           {'onError': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('does not dispatch an event if events are skipped', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchError_(url, err,\n                                           {'onError': fn}, true);\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('does not dispatch an event if a callback is canceled', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchError_(url, err,\n                                           {'onError': fn});\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('passes the same data to both events and callbacks', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchError_(url, err,\n                                           {'onError': fn});\n      var evtData = spf.dispatch.calls.argsFor(0)[1];\n      var fnData = fn.calls.argsFor(0)[0];\n      expect(evtData).toEqual(fnData);\n      expect(proceed).toBe(true);\n    });\n\n\n  });\n\n\n  describe('dispatchReload', function() {\n\n    var url = '/page';\n\n    it('dispatches an event', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      spf.nav.dispatchReload_(url);\n      var evtName = spf.dispatch.calls.argsFor(0)[0];\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(evtName).toEqual(spf.EventName.RELOAD);\n    });\n\n\n  });\n\n\n  describe('dispatchClick', function() {\n\n    var url = '/page';\n    var target = document.createElement('a');\n\n\n    it('dispatches an event', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var proceed = spf.nav.dispatchClick_(url, target);\n      var evtName = spf.dispatch.calls.argsFor(0)[0];\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(evtName).toEqual(spf.EventName.CLICK);\n      expect(proceed).toBe(true);\n    });\n\n\n    it('dispatches an event and propagates cancellation', function() {\n      spyOn(spf, 'dispatch').and.returnValue(false);\n      var proceed = spf.nav.dispatchClick_(url, target);\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n  });\n\n\n  describe('dispatchHistory', function() {\n\n    var url = '/page';\n\n\n    it('dispatches an event', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var proceed = spf.nav.dispatchHistory_(url);\n      var evtName = spf.dispatch.calls.argsFor(0)[0];\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(evtName).toEqual(spf.EventName.HISTORY);\n      expect(proceed).toBe(true);\n    });\n\n\n    it('dispatches an event and propagates cancellation', function() {\n      spyOn(spf, 'dispatch').and.returnValue(false);\n      var proceed = spf.nav.dispatchHistory_(url);\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n  });\n\n\n  describe('dispatchRequest', function() {\n\n    var url = '/page';\n    var referer = '/other';\n    var previous = '/previous';\n\n\n    it('dispatches an event', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var proceed = spf.nav.dispatchRequest_(url, referer, previous);\n      var evtName = spf.dispatch.calls.argsFor(0)[0];\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(evtName).toEqual(spf.EventName.REQUEST);\n      expect(proceed).toBe(true);\n    });\n\n\n    it('dispatches an event and propagates cancellation', function() {\n      spyOn(spf, 'dispatch').and.returnValue(false);\n      var proceed = spf.nav.dispatchRequest_(url, referer, previous);\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('executes a callback', function() {\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchRequest_(url, referer, previous,\n                                             {'onRequest': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('executes a callback and propagates cancellation', function() {\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchRequest_(url, referer, previous,\n                                             {'onRequest': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('does not dispatch an event if events are skipped', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchRequest_(url, referer, previous,\n                                             {'onRequest': fn}, true);\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('does not dispatch an event if a callback is canceled', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchRequest_(url, referer, previous,\n                                             {'onRequest': fn});\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('passes the same data to both events and callbacks', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchRequest_(url, referer, previous,\n                                             {'onRequest': fn});\n      var evtData = spf.dispatch.calls.argsFor(0)[1];\n      var fnData = fn.calls.argsFor(0)[0];\n      expect(evtData).toEqual(fnData);\n      expect(proceed).toBe(true);\n    });\n\n\n  });\n\n\n  describe('dispatchPartProcess', function() {\n\n    var url = '/page';\n    var partial = {};\n\n\n    it('dispatches an event', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var proceed = spf.nav.dispatchPartProcess_(url, partial);\n      var evtName = spf.dispatch.calls.argsFor(0)[0];\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(evtName).toEqual(spf.EventName.PART_PROCESS);\n      expect(proceed).toBe(true);\n    });\n\n\n    it('dispatches an event and propagates cancellation', function() {\n      spyOn(spf, 'dispatch').and.returnValue(false);\n      var proceed = spf.nav.dispatchPartProcess_(url, partial);\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('executes a callback', function() {\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchPartProcess_(url, partial,\n                                                 {'onPartProcess': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('executes a callback and propagates cancellation', function() {\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchPartProcess_(url, partial,\n                                                 {'onPartProcess': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('does not dispatch an event if events are skipped', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchPartProcess_(url, partial,\n                                                 {'onPartProcess': fn}, true);\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('does not dispatch an event if a callback is canceled', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchPartProcess_(url, partial,\n                                                 {'onPartProcess': fn});\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('passes the same data to both events and callbacks', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchPartProcess_(url, partial,\n                                                 {'onPartProcess': fn});\n      var evtData = spf.dispatch.calls.argsFor(0)[1];\n      var fnData = fn.calls.argsFor(0)[0];\n      expect(evtData).toEqual(fnData);\n      expect(proceed).toBe(true);\n    });\n\n\n  });\n\n\n  describe('dispatchPartDone', function() {\n\n    var url = '/page';\n    var partial = {};\n\n\n    it('dispatches an event', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var proceed = spf.nav.dispatchPartDone_(url, partial);\n      var evtName = spf.dispatch.calls.argsFor(0)[0];\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(evtName).toEqual(spf.EventName.PART_DONE);\n      expect(proceed).toBe(true);\n    });\n\n\n    it('dispatches an event and propagates cancellation', function() {\n      spyOn(spf, 'dispatch').and.returnValue(false);\n      var proceed = spf.nav.dispatchPartDone_(url, partial);\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('executes a callback', function() {\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchPartDone_(url, partial,\n                                              {'onPartDone': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('executes a callback and propagates cancellation', function() {\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchPartDone_(url, partial,\n                                              {'onPartDone': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('does not dispatch an event if events are skipped', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchPartDone_(url, partial,\n                                              {'onPartDone': fn}, true);\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('does not dispatch an event if a callback is canceled', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchPartDone_(url, partial,\n                                              {'onPartDone': fn});\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('passes the same data to both events and callbacks', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchPartDone_(url, partial,\n                                              {'onPartDone': fn});\n      var evtData = spf.dispatch.calls.argsFor(0)[1];\n      var fnData = fn.calls.argsFor(0)[0];\n      expect(evtData).toEqual(fnData);\n      expect(proceed).toBe(true);\n    });\n\n\n  });\n\n\n  describe('dispatchProcess', function() {\n\n    var url = '/page';\n    var response = {};\n\n\n    it('dispatches an event', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var proceed = spf.nav.dispatchProcess_(url, response);\n      var evtName = spf.dispatch.calls.argsFor(0)[0];\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(evtName).toEqual(spf.EventName.PROCESS);\n      expect(proceed).toBe(true);\n    });\n\n\n    it('dispatches an event and propagates cancellation', function() {\n      spyOn(spf, 'dispatch').and.returnValue(false);\n      var proceed = spf.nav.dispatchProcess_(url, response);\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('executes a callback', function() {\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchProcess_(url, response,\n                                             {'onProcess': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('executes a callback and propagates cancellation', function() {\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchProcess_(url, response,\n                                             {'onProcess': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('does not dispatch an event if events are skipped', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchProcess_(url, response,\n                                             {'onProcess': fn}, true);\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('does not dispatch an event if a callback is canceled', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchProcess_(url, response,\n                                             {'onProcess': fn});\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('passes the same data to both events and callbacks', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchProcess_(url, response,\n                                             {'onProcess': fn});\n      var evtData = spf.dispatch.calls.argsFor(0)[1];\n      var fnData = fn.calls.argsFor(0)[0];\n      expect(evtData).toEqual(fnData);\n      expect(proceed).toBe(true);\n    });\n\n\n  });\n\n\n  describe('dispatchDone', function() {\n\n    var url = '/page';\n    var response = {};\n\n\n    it('dispatches an event', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var proceed = spf.nav.dispatchDone_(url, response);\n      var evtName = spf.dispatch.calls.argsFor(0)[0];\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(evtName).toEqual(spf.EventName.DONE);\n      expect(proceed).toBe(true);\n    });\n\n\n    it('dispatches an event and propagates cancellation', function() {\n      spyOn(spf, 'dispatch').and.returnValue(false);\n      var proceed = spf.nav.dispatchDone_(url, response);\n      expect(spf.dispatch).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('executes a callback', function() {\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchDone_(url, response,\n                                          {'onDone': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('executes a callback and propagates cancellation', function() {\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchDone_(url, response,\n                                          {'onDone': fn});\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('does not dispatch an event if events are skipped', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchDone_(url, response,\n                                          {'onDone': fn}, true);\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(true);\n    });\n\n\n    it('does not dispatch an event if a callback is canceled', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn').and.returnValue(false);\n      var proceed = spf.nav.dispatchDone_(url, response,\n                                          {'onDone': fn});\n      expect(spf.dispatch).not.toHaveBeenCalled();\n      expect(fn).toHaveBeenCalled();\n      expect(proceed).toBe(false);\n    });\n\n\n    it('passes the same data to both events and callbacks', function() {\n      spyOn(spf, 'dispatch').and.callThrough();\n      var fn = jasmine.createSpy('fn');\n      var proceed = spf.nav.dispatchDone_(url, response,\n                                          {'onDone': fn});\n      var evtData = spf.dispatch.calls.argsFor(0)[1];\n      var fnData = fn.calls.argsFor(0)[0];\n      expect(evtData).toEqual(fnData);\n      expect(proceed).toBe(true);\n    });\n\n\n  });\n\n\n});\n", 'message': 'Merge pull request #381 from nicksay/fix-377-again\n\nFix reloads when the URL contains a hash', 'author_name': 'Alex Nicksay', 'lang': 'js'}