spincycle
Version: 
A reactive message router and object manager that lets clients subscribe to object property changes on the server
500 lines (448 loc) • 18 kB
HTML
<!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<html>
<head>
  <title>iron-location</title>
  <script src="../../webcomponentsjs/webcomponents-lite.js"></script>
  <script src="../../web-component-tester/browser.js"></script>
  <link rel="import" href="../../polymer/polymer.html">
  <link rel="import" href="../../promise-polyfill/promise-polyfill.html">
  <link rel="import" href="../iron-location.html">
  <link rel="import" href="./redirection.html">
  <style>
    #safari-cooldown {
      font-size: 18px;
      max-width: 300px;
    }
    #safari-cooldown div {
      margin-bottom: 20px;
    }
  </style>
</head>
<body>
  <div id='safari-cooldown' hidden>
    <div>Danger: URL overheating.</div>
    <div>
      Safari requires a cooldown period before we call
      pushState/replaceState any more.
    </div>
    <div>Testing will resume after 30 seconds.</div>
    <div>
      <a href="https://forums.developer.apple.com/thread/36650">More info</a>
    </div>
  </div>
<test-fixture id='Solo'>
  <template>
    <iron-location></iron-location>
  </template>
</test-fixture>
<test-fixture id='Together'>
  <template>
    <div>
      <iron-location id='one'></iron-location>
      <iron-location id='two'></iron-location>
    </div>
  </template>
</test-fixture>
<test-fixture id='RedirectHash'>
  <template>
    <redirect-hash></redirect-hash>
  </template>
</test-fixture>
<test-fixture id='RedirectPath'>
  <template>
    <redirect-path></redirect-path>
  </template>
</test-fixture>
<test-fixture id='RedirectQuery'>
  <template>
    <redirect-query></redirect-query>
  </template>
</test-fixture>
<script>;
  function timePasses(ms) {
    return new Promise(function(resolve) {
      window.setTimeout(function() {
        resolve();
      }, ms);
    });
  }
  function makeAbsoluteUrl(path) {
    return window.location.protocol + '//' + window.location.host + path;
  }
  // A window.history.replaceState wrapper that's smart about baseURI.
  function replaceState(path) {
    window.history.replaceState({}, '', makeAbsoluteUrl(path));
  }
  /**
   * We run the iron location tests with a couple different page configurations
   * (e.g. with and withoug a base URI), so we define the test set as a function
   * that we call in each of these configurations.
   */
  function ironLocationTests() {
    suite('when used solo', function() {
      var urlElem;
      var toRemove;
      setup(function() {
        replaceState('/');
        urlElem = fixture('Solo');
        toRemove = [];
      });
      teardown(function() {
        for (var i = 0; i < toRemove.length; i++) {
          document.body.removeChild(toRemove[i]);
        }
      });
      test('basic functionality with #hash urls', function() {
        // Initialized to the window location's hash.
        expect(window.location.hash).to.be.equal(urlElem.hash);
        // Changing the urlElem's hash changes the URL
        urlElem.hash = '/lol/ok';
        expect(window.location.hash).to.be.equal('#/lol/ok');
        // Changing the hash via normal means changes the urlElem.
        var anchor = document.createElement('a');
        var base =
            window.location.protocol + '//' + window.location.host +
            window.location.pathname;
        anchor.href = base + '#/wat';
        document.body.appendChild(anchor);
        anchor.click();
        // In IE10 it appears that the hashchange event is asynchronous.
        return timePasses(10).then(function() {
          expect(urlElem.hash).to.be.equal('/wat');
        });
      });
      test('basic functionality with paths', function() {
        // Initialized to the window location's path.
        expect(window.location.pathname).to.be.equal(urlElem.path);
        // Changing the urlElem's path changes the URL
        urlElem.path = '/foo/bar';
        expect(window.location.pathname).to.be.equal('/foo/bar');
        // Changing the path and sending a custom event on the window changes
        // the urlElem.
        replaceState('/baz');
        window.dispatchEvent(new CustomEvent('location-changed'));
        expect(urlElem.path).to.be.equal('/baz');
      });
      function makeTemporaryIronLocation() {
        var ironLocation = document.createElement('iron-location');
        document.body.appendChild(ironLocation);
        toRemove.push(ironLocation);
        return ironLocation
      }
      test('dealing with paths with unusual characters', function() {
        var pathEncodingExamples = {
          '/foo': '/foo',
          '/': '/',
          '/foo bar': '/foo%20bar',
          '/foo#bar': '/foo%23bar',
          '/foo?xyz': '/foo%3Fxyz',
          '/foo\'bar\'baz': '/foo\'bar\'baz',
        };
        for (var plainTextPath in pathEncodingExamples) {
          var encodedPath = pathEncodingExamples[plainTextPath];
          urlElem.path = plainTextPath;
          expect(window.location.pathname).to.be.equal(encodedPath);
          expect(urlElem.path).to.be.equal(plainTextPath);
          var temporaryIronLocation = makeTemporaryIronLocation();
          expect(temporaryIronLocation.path).to.be.equal(plainTextPath);
        }
      });
      test('dealing with hashes with unusual characters', function() {
        var hashEncodingExamples = {
          'foo': '#foo',
          '': '',
          'foo bar': ['#foo%20bar', '#foo bar'],
          'foo#bar': '#foo#bar',
          'foo?bar': '#foo?bar',
          '/foo\'bar\'baz': ['#/foo%27bar%27baz', '#/foo\'bar\'baz'],
        };
        for (var plainTextHash in hashEncodingExamples) {
          var encodedHashes = hashEncodingExamples[plainTextHash];
          if (typeof encodedHashes === 'string') {
            encodedHashes = [encodedHashes];
          }
          urlElem.hash = plainTextHash;
          expect(encodedHashes).to.contain(window.location.hash);
          expect(urlElem.hash).to.be.equal(plainTextHash);
          expect(makeTemporaryIronLocation().hash).to.be.equal(plainTextHash);
        }
      });
      test('dealing with queries with unusual characters', function() {
        var queryEncodingExamples = {
          'foo': '?foo',
          '': '',
          'foo bar': '?foo%20bar',
          'foo#bar': '?foo%23bar',
          'foo?bar': '?foo?bar',
          '/foo\'bar\'baz': ['?/foo%27bar%27baz', '?/foo\'bar\'baz'],
          'foo/bar/baz': '?foo/bar/baz'
        };
        for (var plainTextQuery in queryEncodingExamples) {
          var encodedQueries = queryEncodingExamples[plainTextQuery];
          if (typeof encodedQueries === 'string') {
            encodedQueries = [encodedQueries];
          }
          
          var ironLocationQuery = encodedQueries.map(function(value) {
            return value.substring(1);
          });
          expect(urlElem._initialized).to.be.eq(true);
          urlElem.query = plainTextQuery;
          expect(encodedQueries).to.contain(window.location.search);
          expect(ironLocationQuery).to.contain(urlElem.query);
          expect(ironLocationQuery).to.contain(makeTemporaryIronLocation().query);
          urlElem.query = 'dummyValue';
          urlElem.query = ironLocationQuery[0];
          expect(encodedQueries).to.contain(window.location.search);
          expect(ironLocationQuery).to.contain(urlElem.query);
          expect(ironLocationQuery).to.contain(makeTemporaryIronLocation().query);
        }
      });
      test('assigning to a relative path URL', function() {
        urlElem.path = '/foo/bar';
        expect(window.location.pathname).to.be.equal('/foo/bar');
        // A relative path is treated as an absolute one, just with a
        // missing leading slash.
        urlElem.path = 'baz';
        expect(window.location.pathname).to.be.equal('/baz');
      });
      test('basic functionality with ?key=value params', function() {
        // Initialized to the window location's params.
        expect(urlElem.query).to.be.eq('');
        // Changing location.search and sending a custom event on the window
        // changes the urlElem.
        replaceState('/?greeting=hello&target=world');
        window.dispatchEvent(new CustomEvent('location-changed'));
        expect(urlElem.query).to.be.equal('greeting=hello&target=world');
        // Changing the urlElem's query changes the URL.
        urlElem.query = 'greeting=hello&target=world&another=key';
        expect(window.location.search).to.be.equal(
            '?greeting=hello&target=world&another=key');
      });
    });
    suite('does not spam the user\'s history', function() {
      var replaceStateCalls, pushStateCalls;
      var nativeReplaceState, nativePushState;
      setup(function() {
        replaceStateCalls = pushStateCalls = 0;
        nativeReplaceState = window.history.replaceState;
        nativePushState = window.history.pushState;
        window.history.replaceState = function() {
          replaceStateCalls++;
        };
        window.history.pushState = function() {
          pushStateCalls++;
        };
      });
      teardown(function() {
        window.history.replaceState = nativeReplaceState;
        window.history.pushState = nativePushState;
      });
      test('when a change happens immediately after ' +
           'the iron-location is attached', function() {
        var ironLocation = fixture('Solo');
        expect(pushStateCalls).to.be.equal(0);
        expect(replaceStateCalls).to.be.equal(0);
        ironLocation.path = '/foo';
        expect(replaceStateCalls).to.be.equal(1);
        expect(pushStateCalls).to.be.equal(0);
    });
      suite('when intercepting links', function() {
      /**
       * Clicks the given link while an iron-location element with the given
       * urlSpaceRegex is in the same document. Returns whether the
       * iron-location prevented the default behavior of the click.
       *
       * No matter what, it prevents the default behavior of the click itself
       * to ensure that no navigation occurs (as that would interrupt
       * running and reporting these tests!)
       */
      function isClickCaptured(anchor, urlSpaceRegex) {
        var defaultWasPrevented;
        function handler(event) {
          expect(event.target).to.be.eq(anchor);
          defaultWasPrevented = event.defaultPrevented;
          event.preventDefault();
          expect(event.defaultPrevented).to.be.eq(true);
        }
        window.addEventListener('click', handler);
        var ironLocation = fixture('Solo');
        if (urlSpaceRegex != null) {
          ironLocation.urlSpaceRegex = urlSpaceRegex;
        }
        document.body.appendChild(anchor);
        anchor.click();
        document.body.removeChild(anchor);
        window.removeEventListener('click', handler);
        return defaultWasPrevented;
      }
      test('simple link to / is intercepted', function() {
        var anchor = document.createElement('a');
        if (document.baseURI !== window.location.href) {
          anchor.href = makeAbsoluteUrl('/');
        } else {
          anchor.href = '/';
        }
        expect(isClickCaptured(anchor)).to.be.eq(true);
          expect(pushStateCalls).to.be.equal(1);
      });
      test('link that matches url space is intercepted', function() {
        var anchor = document.createElement('a');
        anchor.href = makeAbsoluteUrl('/foo');
        expect(isClickCaptured(anchor, '/fo+')).to.be.eq(true);
          expect(pushStateCalls).to.be.equal(1);
      });
      test('link that doesn\'t match url space isn\'t intercepted', function() {
        var anchor = document.createElement('a');
        anchor.href = makeAbsoluteUrl('/bar');
        expect(isClickCaptured(anchor, '/fo+')).to.be.eq(false);
          expect(pushStateCalls).to.be.equal(0);
      });
      test('link to another domain isn\'t intercepted', function() {
        var anchor = document.createElement('a');
        anchor.href = 'http://example.com/';
        expect(isClickCaptured(anchor)).to.be.eq(false);
          expect(pushStateCalls).to.be.equal(0);
      });
      test('a link with target=_blank isn\'t intercepted', function() {
        var anchor = document.createElement('a');
        anchor.href = makeAbsoluteUrl('/');
        anchor.target = '_blank';
        expect(isClickCaptured(anchor)).to.be.eq(false);
          expect(pushStateCalls).to.be.equal(0);
        });
        test('a link with an href to the ' +
             'current page shouldn\'t add to history.', function() {
          var anchor = document.createElement('a');
          anchor.href = window.location.href;
          // The click is captured, but it doesn't add to history.
          expect(isClickCaptured(anchor)).to.be.equal(true);
          expect(pushStateCalls).to.be.equal(0);
        });
        test('a click that has already been defaultPrevented ' +
             'shouldn\'t result in a navigation', function() {
           fixture('Solo');
           var anchor = document.createElement('a');
           anchor.href = makeAbsoluteUrl('/');
           anchor.addEventListener('click', function(event) {
             event.preventDefault();
           });
           document.body.appendChild(anchor);
           var originalPushState = window.history.pushState;
           var count = 0;
           window.history.pushState = function() {
             count++;
           }
           anchor.click();
           window.history.pushState = originalPushState;
           expect(count).to.be.equal(0);
      })
    });
    });
    suite('when used with other iron-location elements', function() {
      var otherUrlElem;
      var urlElem;
      setup(function() {
        var elems = fixture('Together');
        urlElem = elems.querySelector('#one');
        otherUrlElem = elems.querySelector('#two');
      });
      function assertHaveSameUrls(urlElemOne, urlElemTwo) {
        expect(urlElemOne.path).to.be.equal(urlElemTwo.path);
        expect(urlElemOne.hash).to.be.equal(urlElemTwo.hash);
        expect(urlElemOne.query).to.be.equal(urlElemTwo.query);
      }
      test('coordinate their changes (by firing events on window)', function() {
        assertHaveSameUrls(urlElem, otherUrlElem);
        urlElem.path = '/a/b/c';
        assertHaveSameUrls(urlElem, otherUrlElem);
        otherUrlElem.query = 'alsdkjflaksjfd=alksjfdlkajsdfl';
        assertHaveSameUrls(urlElem, otherUrlElem);
        urlElem.hash = 'lkjljifosjkldfjlksjfldsjf';
        assertHaveSameUrls(urlElem, otherUrlElem);
      });
    });
    suite('supports doing synchronous redirection', function() {
      test('of the hash portion of the URL', function() {
        expect(window.location.hash).to.be.equal('');
        var redirector = fixture('RedirectHash');
        expect(window.location.hash).to.be.equal('#redirectedTo');
        expect(redirector.hash).to.be.equal('redirectedTo');
        redirector.hash = 'newHash';
        expect(window.location.hash).to.be.equal('#redirectedTo');
        expect(redirector.hash).to.be.equal('redirectedTo');
      });
      test('of the path portion of the URL', function() {
        expect(window.location.pathname).to.not.be.equal('/redirectedTo');
        var redirector = fixture('RedirectPath');
        expect(window.location.pathname).to.be.equal('/redirectedTo');
        expect(redirector.path).to.be.equal('/redirectedTo');
        redirector.path = '/newPath';
        expect(window.location.pathname).to.be.equal('/redirectedTo');
        expect(redirector.path).to.be.equal('/redirectedTo');
      });
      test('of the query portion of the URL', function() {
        expect(window.location.search).to.be.equal('');
        var redirector = fixture('RedirectQuery');
        expect(window.location.search).to.be.equal('?redirectedTo');
        expect(redirector.query).to.be.equal('redirectedTo');
        redirector.query = 'newQuery';
        expect(window.location.search).to.be.equal('?redirectedTo');
        expect(redirector.query).to.be.equal('redirectedTo');
      });
    });
  }
  suite('<iron-location>', function () {
    var initialUrl;
    setup(function() {
      initialUrl = window.location.href;
    });
    teardown(function(){
      window.history.replaceState({}, '', initialUrl);
    });
    // This is as dumb as it looks. See #safari-cooldown in the dom above.
    var cooldownFunction = function() {};
    if (/^Apple/.test(navigator.vendor)) {
      cooldownFunction = function(done) {
        var cooldownPeriod = 30 * 1000;
        this.timeout(cooldownPeriod + 5000);
        var cooldownMessage = document.querySelector('#safari-cooldown');
        cooldownMessage.removeAttribute('hidden');
        setTimeout(function() {
          done();
          cooldownMessage.setAttribute('hidden', 'hidden');
        }, cooldownPeriod);
      };
    }
    suite('without a base URI', function() {
      ironLocationTests();
      suiteTeardown(cooldownFunction);
    });
    suite('with a base URI', function() {
      var baseElem;
      setup(function() {
        expect(document.baseURI).to.be.equal(window.location.href);
        baseElem = document.createElement('base');
        var href = 'https://example.com/i/dont/exist/obviously'
        baseElem.href = href;
        document.head.appendChild(baseElem);
        expect(document.baseURI).to.be.equal(href);
      });
      teardown(function() {
        document.head.removeChild(baseElem);
      });
      suiteTeardown(cooldownFunction);
      ironLocationTests();
    });
  });
</script>
</body>