UNPKG

spincycle

Version:

A reactive message router and object manager that lets clients subscribe to object property changes on the server

1,251 lines 50.5 kB
<!doctype 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-overlay-behavior tests</title>

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1, user-scalable=yes">

    <script src="../../webcomponentsjs/webcomponents-lite.js"></script>

    <script src="../../web-component-tester/browser.js"></script>
    <link rel="import" href="../../iron-test-helpers/iron-test-helpers.html">
    <link rel="import" href="test-overlay.html">
    <link rel="import" href="test-overlay2.html">
    <link rel="import" href="test-buttons.html">
    <link rel="import" href="test-menu-button.html">

    <style is="custom-style">
      iron-overlay-backdrop {
        /* For quicker tests */
        --iron-overlay-backdrop: {
          transition: none;
        }
      }
    </style>

  </head>

  <body>

    <test-fixture id="basic">
      <template>
        <test-overlay>
          Basic Overlay
        </test-overlay>
      </template>
    </test-fixture>

    <test-fixture id="opened">
      <template>
        <test-overlay opened>
          Basic Overlay
        </test-overlay>
      </template>
    </test-fixture>

    <test-fixture id="autofocus">
      <template>
        <test-overlay>
          Autofocus
          <button autofocus>button</button>
        </test-overlay>
      </template>
    </test-fixture>

    <test-fixture id="focusables">
      <template>
        <test-overlay tabindex="-1">
          <h2>Focusables (no tabindex)</h2>
          <div>
            <input class="focusable1" placeholder="1 (nested)">
          </div>
          <button class="focusable2">1</button>
          <button disabled> disabled button</button>
          <div tabindex="-1">not focusable</div>
          <button class="focusable3">2</button>
        </test-overlay>
        <test-overlay tabindex="-1">
          <h2>Focusables (with tabindex)</h2>
          <div tabindex="-1">not focusable</div>
          <div tabindex="3" class="focusable3">3</div>
          <div tabindex="4" class="focusable4">4</div>
          <div tabindex="5" class="focusable5">5</div>
          <div>
            <div tabindex="1" class="focusable1">1 (nested)</div>
            <div tabindex="6" class="focusable6">6 (nested)</div>
          </div>
          <div tabindex="2" class="focusable2">2</div>
        </test-overlay>
        <test-overlay2>
          Overlay with optimized focusableNodes getter
          <button class="focusable1">1</button>
        </test-overlay2>
      </template>
    </test-fixture>

    <test-fixture id="backdrop">
      <template>
        <test-overlay with-backdrop>
          Overlay with backdrop
        </test-overlay>
      </template>
    </test-fixture>

    <test-fixture id="multiple">
      <template>
        <test-overlay class="overlay-1">
          Test overlay 1
        </test-overlay>
        <test-overlay class="overlay-2">
          Test overlay 2
          <button>Click</button>
        </test-overlay>
        <test-overlay2 class="overlay-3">
          Other overlay 3
        </test-overlay2>
      </template>
    </test-fixture>

    <test-fixture id="composed">
      <template>
        <test-menu-button></test-menu-button>
      </template>
    </test-fixture>

    <test-buttons id="buttons"></test-buttons>
    <input id="focusInput" placeholder="focus input">

    <script>

      function runAfterOpen(overlay, callback) {
        overlay.addEventListener('iron-overlay-opened', callback);
        overlay.open();
      }

      function runAfterClose(overlay, callback) {
        overlay.addEventListener('iron-overlay-closed', callback);
        overlay.close();
      }

      suite('basic overlay', function() {
        var overlay;

        setup(function() {
          overlay = fixture('basic');
        });

        test('overlay starts hidden', function() {
          assert.isFalse(overlay.opened, 'overlay starts closed');
          assert.equal(getComputedStyle(overlay).display, 'none', 'overlay starts hidden');
        });

        test('_renderOpened called only after is attached', function(done) {
          var overlay = document.createElement('test-overlay');
          // The overlay is ready at this point, but not yet attached.
          var spy = sinon.spy(overlay, '_renderOpened');
          // This triggers _openedChanged.
          overlay.opened = true;
          // Wait long enough for requestAnimationFrame callback.
          overlay.async(function() {
            assert.isFalse(spy.called, '_renderOpened not called');
            // Because not attached yet, overlay should not be the current overlay!
            assert.isNotOk(overlay._manager.currentOverlay(), 'currentOverlay not set');
            done();
          }, 100);
        });

        test('overlay open/close events', function(done) {
          var nevents = 0;

          overlay.addEventListener('iron-overlay-opened', function() {
            nevents += 1;
            overlay.opened = false;
          });

          overlay.addEventListener('iron-overlay-closed', function() {
            nevents += 1;
            assert.equal(nevents, 2, 'opened and closed events fired');
            done();
          });

          overlay.opened = true;
        });

        test('open() refits overlay only once', function(done) {
          var spy = sinon.spy(overlay, 'refit');
          runAfterOpen(overlay, function() {
            assert.equal(spy.callCount, 1, 'overlay did refit only once');
            done();
          });
        });

        test('open overlay refits on iron-resize', function(done) {
          runAfterOpen(overlay, function() {
            var spy = sinon.spy(overlay, 'refit');
            overlay.fire('iron-resize');
            Polymer.dom.flush();
            requestAnimationFrame(function() {
              assert.isTrue(spy.called, 'overlay did refit');
              done();
            });
          });
        });

        test('closed overlay does not refit on iron-resize', function(done) {
          var spy = sinon.spy(overlay, 'refit');
          overlay.fire('iron-resize');
          Polymer.dom.flush();
          requestAnimationFrame(function() {
            assert.isFalse(spy.called, 'overlay should not refit');
            done();
          });
        });

        test('open() triggers iron-resize', function(done) {
          var callCount = 0;
          // Ignore iron-resize triggered by window resize.
          window.addEventListener('resize', function() { callCount--; }, true);
          overlay.addEventListener('iron-resize', function() { callCount++; });
          runAfterOpen(overlay, function() {
            assert.equal(callCount, 1, 'iron-resize called once before iron-overlay-opened');
            done();
          });
        });

        test('close() triggers iron-resize', function(done) {
          runAfterOpen(overlay, function() {
            var spy = sinon.stub();
            overlay.addEventListener('iron-resize', spy);
            runAfterClose(overlay, function() {
              assert.equal(spy.callCount, 1, 'iron-resize called once before iron-overlay-closed');
              done();
            });
          });
        });

        test('closed overlay does not trigger iron-resize when its content changes', function() {
          // Ignore iron-resize triggered by window resize.
          var callCount = 0;
          window.addEventListener('resize', function() { callCount--; }, true);
          overlay.addEventListener('iron-resize', function() { callCount++; });
          Polymer.dom(overlay).appendChild(document.createElement('div'));
          Polymer.dom.flush();
          assert.equal(callCount, 0, 'iron-resize should not be called');
        });

        test('open overlay triggers iron-resize when its content changes', function(done) {
          runAfterOpen(overlay, function() {
            var spy = sinon.stub();
            overlay.addEventListener('iron-resize', spy);
            Polymer.dom(overlay).appendChild(document.createElement('div'));
            Polymer.dom.flush();
            assert.equal(spy.callCount, 1, 'iron-resize should be called once');
            done();
          });
        });

        test('close an overlay quickly after open', function(done) {
          // first, open the overlay
          overlay.open();
          overlay.async(function() {
            // during the opening transition, close the overlay
            this.close();
            // wait for any exceptions to be thrown until the transition is done
            this.async(function() {
              done();
            }, 300);
          });
        });

        test('clicking an overlay does not close it', function(done) {
          runAfterOpen(overlay, function() {
            var spy = sinon.stub();
            overlay.addEventListener('iron-overlay-closed', spy);
            MockInteractions.tap(overlay);
            overlay.async(function() {
              assert.isFalse(spy.called, 'iron-overlay-closed should not fire');
              done();
            }, 10);
          });
        });

        test('open overlay on mousedown does not close it', function(done) {
          var btn = document.createElement('button');
          btn.addEventListener('mousedown', overlay.open.bind(overlay));
          document.body.appendChild(btn);
          // It triggers mousedown, mouseup, and click.
          MockInteractions.tap(btn);
          document.body.removeChild(btn);

          assert.isTrue(overlay.opened, 'overlay opened');
          overlay.async(function() {
            assert.isTrue(overlay.opened, 'overlay is still open');
            done();
          }, 10);
        });

        test('clicking outside fires iron-overlay-canceled', function(done) {
          runAfterOpen(overlay, function() {
            overlay.addEventListener('iron-overlay-canceled', function(event) {
              assert.equal(event.detail.target, document.body, 'detail contains original click event');
              done();
            });
            MockInteractions.tap(document.body);
          });
        });

        test('clicking outside closes the overlay', function(done) {
          runAfterOpen(overlay, function() {
            overlay.addEventListener('iron-overlay-closed', function(event) {
              assert.isTrue(event.detail.canceled, 'overlay is canceled');
              done();
            });
            MockInteractions.tap(document.body);
          });
        });

        test('iron-overlay-canceled event can be prevented', function(done) {
          runAfterOpen(overlay, function() {
            overlay.addEventListener('iron-overlay-canceled', function(event) {
              event.preventDefault();
            });
            var spy = sinon.stub();
            overlay.addEventListener('iron-overlay-closed', spy);
            MockInteractions.tap(document.body);
            Polymer.Base.async(function() {
              assert.isTrue(overlay.opened, 'overlay is still open');
              assert.isFalse(spy.called, 'iron-overlay-closed not fired');
              done();
            }, 10);
          });
        });

        test('cancel an overlay with esc key', function(done) {
          runAfterOpen(overlay, function() {
            overlay.addEventListener('iron-overlay-canceled', function(event) {
              assert.equal(event.detail.type, 'keydown');
              done();
            });
            MockInteractions.pressAndReleaseKeyOn(document, 27);
          });
        });

        test('close an overlay with esc key', function(done) {
          runAfterOpen(overlay, function() {
            overlay.addEventListener('iron-overlay-closed', function(event) {
              assert.isTrue(event.detail.canceled, 'overlay is canceled');
              done();
            });
            MockInteractions.pressAndReleaseKeyOn(document, 27);
          });
        });

        test('no-cancel-on-outside-click property', function(done) {
          overlay.noCancelOnOutsideClick = true;
          runAfterOpen(overlay, function() {
            var spy = sinon.stub();
            overlay.addEventListener('iron-overlay-closed', spy);
            MockInteractions.tap(document.body);
            Polymer.Base.async(function() {
              assert.isFalse(spy.called, 'iron-overlay-closed should not fire');
              done();
            }, 10);
          });
        });

        test('no-cancel-on-esc-key property', function(done) {
          overlay.noCancelOnEscKey = true;
          runAfterOpen(overlay, function() {
            var spy = sinon.stub();
            overlay.addEventListener('iron-overlay-closed', spy);
            MockInteractions.pressAndReleaseKeyOn(document, 27);
            Polymer.Base.async(function() {
              assert.isFalse(spy.called, 'iron-overlay-cancel should not fire');
              done();
            }, 10);
          });
        });

        test('with-backdrop sets tabindex=-1 and removes it', function() {
          overlay.withBackdrop = true;
          assert.equal(overlay.getAttribute('tabindex'), '-1', 'tabindex is -1');
          overlay.withBackdrop = false;
          assert.isFalse(overlay.hasAttribute('tabindex'), 'tabindex removed');
        });

        test('with-backdrop does not override tabindex if already set', function() {
          overlay.setAttribute('tabindex', '1');
          overlay.withBackdrop = true;
          assert.equal(overlay.getAttribute('tabindex'), '1', 'tabindex is 1');
          overlay.withBackdrop = false;
          assert.equal(overlay.getAttribute('tabindex'), '1', 'tabindex is still 1');
        });

      });

      suite('keyboard event listener', function() {
        var overlay;

        var preventKeyDown = function(event) {
          event.preventDefault();
          event.stopPropagation();
        }

        suiteSetup(function() {
          // Worst case scenario: listener with useCapture = true that prevents & stops propagation
          // added before the overlay is initialized.
          document.addEventListener('keydown', preventKeyDown, true);
        });

        setup(function() {
          overlay = fixture('basic');
        });

        suiteTeardown(function() {
          document.removeEventListener('keydown', preventKeyDown, true);
        });

        test('cancel an overlay with esc key even if event is prevented by other listeners', function(done) {
          runAfterOpen(overlay, function() {
            overlay.addEventListener('iron-overlay-canceled', function(event) {
              done();
            });
            MockInteractions.pressAndReleaseKeyOn(document, 27);
          });
        });
      });

      suite('opened overlay', function() {
        var overlay;

        setup(function() {
          overlay = fixture('opened');
        });

        test('overlay open by default', function(done) {
          overlay.addEventListener('iron-overlay-opened', function() {
            assert.isTrue(overlay.opened, 'overlay starts opened');
            assert.notEqual(getComputedStyle(overlay).display, 'none', 'overlay starts showing');
            done();
          });
        });

        test('overlay positioned & sized properly', function(done) {
          overlay.addEventListener('iron-overlay-opened', function() {
            var s = getComputedStyle(overlay);
            assert.closeTo(parseFloat(s.left), (window.innerWidth - overlay.offsetWidth) / 2, 1, 'centered horizontally');
            assert.closeTo(parseFloat(s.top), (window.innerHeight - overlay.offsetHeight) / 2, 1, 'centered vertically');
            done();
          });
        });
      });

      suite('focus handling', function() {
        var overlay;

        setup(function() {
          // Ensure focus is set to document.body
          document.body.focus();
          overlay = fixture('autofocus');
        });

        test('node with autofocus is focused', function(done) {
          runAfterOpen(overlay, function() {
            assert.equal(Polymer.dom(overlay).querySelector('[autofocus]'), document.activeElement, '<button autofocus> is focused');
            done();
          });
        });

        test('no-auto-focus will not focus node with autofocus', function(done) {
          overlay.noAutoFocus = true;
          runAfterOpen(overlay, function() {
            assert.notEqual(Polymer.dom(overlay).querySelector('[autofocus]'), document.activeElement, '<button autofocus> not focused after opened');
            done();
          });
          // In Safari the element with autofocus will immediately receive focus when displayed for the first time http://jsbin.com/woroci/2/
          // Ensure this is not the case for overlay.
          assert.notEqual(Polymer.dom(overlay).querySelector('[autofocus]'), document.activeElement, '<button autofocus> not immediately focused');
        });

        test('no-cancel-on-outside-click property; focus stays on overlay when click outside', function(done) {
          overlay.noCancelOnOutsideClick = true;
          runAfterOpen(overlay, function() {
            MockInteractions.tap(document.body);
            Polymer.Base.async(function() {
              assert.equal(Polymer.dom(overlay).querySelector('[autofocus]'), document.activeElement, '<button autofocus> is focused');
              done();
            }, 10);
          });
        });

        test('with-backdrop traps the focus within the overlay', function(done) {
          var focusSpy = sinon.stub();
          var button = document.createElement('button');
          document.body.appendChild(button);
          button.addEventListener('focus', focusSpy, true);

          overlay.withBackdrop = true;
          runAfterOpen(overlay, function() {
            // Try to steal the focus
            MockInteractions.focus(button);
            assert.equal(Polymer.dom(overlay).querySelector('[autofocus]'), document.activeElement, '<button autofocus> is focused');
            assert.equal(focusSpy.callCount, 0, 'button in body did not get the focus');
            document.body.removeChild(button);
            done();
          });
        });

        test('overlay with-backdrop and 1 focusable: prevent TAB and trap the focus', function(done) {
          overlay.withBackdrop = true;
          runAfterOpen(overlay, function() {
            // 1ms timeout needed by IE10 to have proper focus switching.
            Polymer.Base.async(function() {
              // Spy keydown.
              var tabSpy = sinon.spy();
              document.addEventListener('keydown', tabSpy);
              // Simulate TAB.
              MockInteractions.pressAndReleaseKeyOn(document, 9);
              assert.equal(Polymer.dom(overlay).querySelector('[autofocus]'), document.activeElement, 'focus stays on button');
              assert.isTrue(tabSpy.calledOnce, 'keydown spy called');
              assert.isTrue(tabSpy.getCall(0).args[0].defaultPrevented, 'keydown default prevented');
              // Cleanup.
              document.removeEventListener('keydown', tabSpy);
              done();
            }, 1);
          });
        });

        test('empty overlay with-backdrop: prevent TAB and trap the focus', function(done) {
          overlay = fixture('basic');
          overlay.withBackdrop = true;
          runAfterOpen(overlay, function() {
            // 1ms timeout needed by IE10 to have proper focus switching.
            Polymer.Base.async(function() {
              // Spy keydown.
              var tabSpy = sinon.spy();
              document.addEventListener('keydown', tabSpy);
              // Simulate TAB.
              MockInteractions.pressAndReleaseKeyOn(document, 9);
              assert.equal(overlay, document.activeElement, 'focus stays on overlay');
              assert.isTrue(tabSpy.calledOnce, 'keydown spy called');
              assert.isTrue(tabSpy.getCall(0).args[0].defaultPrevented, 'keydown default prevented');
              // Cleanup.
              document.removeEventListener('keydown', tabSpy);
              done();
            }, 1);
          });
        });

      });

      suite('focusable nodes', function() {
        var overlay, overlayWithTabIndex, overlayFocusableNodes;

        setup(function() {
          var f = fixture('focusables');
          overlay = f[0];
          overlayWithTabIndex = f[1];
          overlayFocusableNodes = f[2];
        });

        test('_focusableNodes returns nodes that are focusable', function(done) {
          runAfterOpen(overlay, function() {
            var focusableNodes = overlay._focusableNodes;
            assert.equal(focusableNodes.length, 3, '3 nodes are focusable');
            assert.equal(focusableNodes[0], Polymer.dom(overlay).querySelector('.focusable1'));
            assert.equal(focusableNodes[1], Polymer.dom(overlay).querySelector('.focusable2'));
            assert.equal(focusableNodes[2], Polymer.dom(overlay).querySelector('.focusable3'));
            done();
          });
        });

        test('_focusableNodes includes overlay if it has a valid tabindex', function(done) {
          runAfterOpen(overlay, function() {
            overlay.setAttribute('tabindex', '0');
            var focusableNodes = overlay._focusableNodes;
            assert.equal(focusableNodes.length, 4, '4 focusable nodes');
            assert.notEqual(focusableNodes.indexOf(overlay), -1, 'overlay is included');
            done();
          });
        });

        test('_focusableNodes respects the tabindex order', function(done) {
          runAfterOpen(overlayWithTabIndex, function() {
            var focusableNodes = overlayWithTabIndex._focusableNodes;
            assert.equal(focusableNodes.length, 6, '6 nodes are focusable');
            assert.equal(focusableNodes[0], Polymer.dom(overlayWithTabIndex).querySelector('.focusable1'));
            assert.equal(focusableNodes[1], Polymer.dom(overlayWithTabIndex).querySelector('.focusable2'));
            assert.equal(focusableNodes[2], Polymer.dom(overlayWithTabIndex).querySelector('.focusable3'));
            assert.equal(focusableNodes[3], Polymer.dom(overlayWithTabIndex).querySelector('.focusable4'));
            assert.equal(focusableNodes[4], Polymer.dom(overlayWithTabIndex).querySelector('.focusable5'));
            assert.equal(focusableNodes[5], Polymer.dom(overlayWithTabIndex).querySelector('.focusable6'));
            done();
          });
        });

        test('_focusableNodes can be overridden', function(done) {
          runAfterOpen(overlayFocusableNodes, function() {
            // It has 1 focusable in the light dom, and 2 in the shadow dom.
            var focusableNodes = overlayFocusableNodes._focusableNodes;
            assert.equal(focusableNodes.length, 2, 'length ok');
            assert.equal(focusableNodes[0], overlayFocusableNodes.$.first, 'first ok');
            assert.equal(focusableNodes[1], overlayFocusableNodes.$.last, 'last ok');
            done();
          });
        });

        test('with-backdrop: TAB & Shift+TAB wrap focus', function(done) {
          overlay.withBackdrop = true;
          runAfterOpen(overlay, function() {
            var focusableNodes = overlay._focusableNodes;
            // 1ms timeout needed by IE10 to have proper focus switching.
            Polymer.Base.async(function() {
              // Go to last element.
              focusableNodes[focusableNodes.length-1].focus();
              // Spy keydown.
              var tabSpy = sinon.spy();
              document.addEventListener('keydown', tabSpy);
              // Simulate TAB.
              MockInteractions.pressAndReleaseKeyOn(document, 9);
              assert.equal(focusableNodes[0], document.activeElement, 'focus wrapped to first focusable');
              assert.isTrue(tabSpy.calledOnce, 'keydown spy called');
              assert.isTrue(tabSpy.getCall(0).args[0].defaultPrevented, 'keydown default prevented');
              // Simulate Shift+TAB.
              MockInteractions.pressAndReleaseKeyOn(document, 9, ['shift']);
              assert.equal(focusableNodes[focusableNodes.length-1], document.activeElement, 'focus wrapped to last focusable');
              assert.isTrue(tabSpy.calledTwice, 'keydown spy called again');
              assert.isTrue(tabSpy.getCall(1).args[0].defaultPrevented, 'keydown default prevented again');
              // Cleanup.
              document.removeEventListener('keydown', tabSpy);
              done();
            }, 1);
          });
        });

        test('with-backdrop: TAB & Shift+TAB wrap focus respecting tabindex', function(done) {
          overlayWithTabIndex.withBackdrop = true;
          runAfterOpen(overlayWithTabIndex, function() {
            var focusableNodes = overlayWithTabIndex._focusableNodes;
            // 1ms timeout needed by IE10 to have proper focus switching.
            Polymer.Base.async(function() {
              // Go to last element.
              focusableNodes[focusableNodes.length-1].focus();
              // Simulate TAB.
              MockInteractions.pressAndReleaseKeyOn(document, 9);
              assert.equal(focusableNodes[0], document.activeElement, 'focus wrapped to first focusable');
              // Simulate Shift+TAB.
              MockInteractions.pressAndReleaseKeyOn(document, 9, ['shift']);
              assert.equal(focusableNodes[focusableNodes.length-1], document.activeElement, 'focus wrapped to last focusable');
              done();
            }, 1);
          });
        });

        test('with-backdrop: Shift+TAB after open wrap focus', function(done) {
          overlay.withBackdrop = true;
          runAfterOpen(overlay, function() {
            var focusableNodes = overlay._focusableNodes;
            // 1ms timeout needed by IE10 to have proper focus switching.
            Polymer.Base.async(function() {
              // Spy keydown.
              var tabSpy = sinon.spy();
              document.addEventListener('keydown', tabSpy);
              // Simulate Shift+TAB.
              MockInteractions.pressAndReleaseKeyOn(document, 9, ['shift']);
              assert.equal(focusableNodes[focusableNodes.length-1], document.activeElement, 'focus wrapped to last focusable');
              assert.isTrue(tabSpy.calledOnce, 'keydown spy called');
              assert.isTrue(tabSpy.getCall(0).args[0].defaultPrevented, 'keydown default prevented');
              // Cleanup.
              document.removeEventListener('keydown', tabSpy);
              done();
            }, 1);
          });
        });

        test('with-backdrop: after open, update last focusable node and then Shift+TAB', function(done) {
          overlay.withBackdrop = true;
          runAfterOpen(overlay, function() {
            var focusableNodes = overlay._focusableNodes;
            // 1ms timeout needed by IE10 to have proper focus switching.
            Polymer.Base.async(function() {
              // Before tabbing, make lastFocusable non-tabbable. This will make
              // the one before it (focusableNodes.length - 2), the new last focusable node.
              focusableNodes[focusableNodes.length-1].setAttribute('tabindex', '-1');
              overlay.invalidateTabbables();
              // Simulate Shift+TAB.
              MockInteractions.pressAndReleaseKeyOn(document, 9, ['shift']);
              assert.equal(focusableNodes[focusableNodes.length-2], document.activeElement, 'focus wrapped correctly');
              done();
            }, 1);
          });
        });

        test('with-backdrop: Shift+TAB wrap focus in shadowDOM', function(done) {
          overlayFocusableNodes.withBackdrop = true;
          runAfterOpen(overlayFocusableNodes, function() {
            // 1ms timeout needed by IE10 to have proper focus switching.
            Polymer.Base.async(function() {
              // Spy keydown.
              var tabSpy = sinon.spy();
              document.addEventListener('keydown', tabSpy);
              // Simulate Shift+TAB.
              MockInteractions.pressAndReleaseKeyOn(document, 9, ['shift']);
              assert.equal(overlayFocusableNodes.$.last, Polymer.IronOverlayManager.deepActiveElement, 'focus wrapped to last focusable in the shadowDOM');
              assert.isTrue(tabSpy.calledOnce, 'keydown spy called');
              assert.isTrue(tabSpy.getCall(0).args[0].defaultPrevented, 'keydown default prevented');
              // Cleanup.
              document.removeEventListener('keydown', tabSpy);
              done();
            }, 1);
          });
        });

      });

      suite('Polymer.IronOverlayManager.deepActiveElement', function() {

        test('handles document.body', function () {
          document.body.focus();
          assert.equal(Polymer.IronOverlayManager.deepActiveElement, document.body);
        });

        test('handles light dom', function () {
          var focusable = document.getElementById('focusInput');
          focusable.focus();
          assert.equal(Polymer.IronOverlayManager.deepActiveElement, focusable, 'input is handled');
          focusable.blur();
        });

        test('handles shadow dom', function () {
          var focusable = document.getElementById('buttons').$.button0;
          focusable.focus();
          assert.equal(Polymer.IronOverlayManager.deepActiveElement, focusable);
          focusable.blur();
        });

      });

      suite('restore-focus-on-close', function() {

        var overlay;
        setup(function () {
          overlay = fixture('autofocus');
          overlay.restoreFocusOnClose = true;
        });

        teardown(function () {
          // No matter what, return the focus to body!
          document.body.focus();
        });

        test('does not return focus on close by default (restore-focus-on-close=false)', function(done) {
          overlay.restoreFocusOnClose = false;
          var focusable = document.getElementById('focusInput');
          focusable.focus();
          runAfterOpen(overlay, function() {
            runAfterClose(overlay, function() {
              assert.notEqual(Polymer.IronOverlayManager.deepActiveElement, focusable, 'focus is not restored to focusable');
              done();
            });
          });
        });

        test('overlay returns focus on close', function(done) {
          var focusable = document.getElementById('focusInput');
          focusable.focus();
          runAfterOpen(overlay, function() {
            runAfterClose(overlay, function() {
              assert.equal(Polymer.IronOverlayManager.deepActiveElement, focusable, 'focus restored to focusable');
              done();
            });
          });
        });

        test('overlay returns focus on close (ShadowDOM)', function(done) {
          var focusable = document.getElementById('buttons').$.button0;
          focusable.focus();
          runAfterOpen(overlay, function() {
            runAfterClose(overlay, function() {
              assert.equal(Polymer.IronOverlayManager.deepActiveElement, focusable, 'focus restored to focusable');
              done();
            });
          });
        });

      });

      suite('overlay with backdrop', function() {
        var overlay;

        setup(function() {
          overlay = fixture('backdrop');
        });

        test('backdrop is opened when overlay is opened', function(done) {
          assert.isOk(overlay.backdropElement, 'backdrop is defined');
          runAfterOpen(overlay, function() {
            assert.isTrue(overlay.backdropElement.opened, 'backdrop is opened');
            assert.isOk(overlay.backdropElement.parentNode, 'backdrop is inserted in the DOM');
            done();
          });
        });

        test('backdrop appears behind the overlay', function(done) {
          runAfterOpen(overlay, function() {
            styleZ = parseInt(window.getComputedStyle(overlay).zIndex, 10);
            backdropStyleZ = parseInt(window.getComputedStyle(overlay.backdropElement).zIndex, 10);
            assert.isTrue(styleZ > backdropStyleZ, 'overlay has higher z-index than backdrop');
            done();
          });
        });

        test('backdrop is removed when overlay is closed', function(done) {
          runAfterOpen(overlay, function() {
            runAfterClose(overlay, function() {
              assert.isFalse(overlay.backdropElement.opened, 'backdrop is closed');
              assert.isNotOk(overlay.backdropElement.parentNode, 'backdrop is removed from the DOM');
              assert.lengthOf(document.querySelectorAll('iron-overlay-backdrop'), 0, 'no backdrop elements on body');
              done();
            });
          });
        });

        test('backdrop is removed when the element is removed from DOM', function(done) {
          runAfterOpen(overlay, function() {
            Polymer.dom(overlay).parentNode.removeChild(overlay);
            // Ensure detached is executed.
            Polymer.dom.flush();
            assert.isFalse(overlay.backdropElement.opened, 'backdrop is closed');
            assert.isNotOk(overlay.backdropElement.parentNode, 'backdrop is removed from the DOM');
            assert.lengthOf(document.querySelectorAll('iron-overlay-backdrop'), 0, 'no backdrop elements on body');
            assert.isNotOk(overlay._manager.currentOverlay(), 'currentOverlay ok');
            done();
          });
        });

        test('manager.getBackdrops() updated on opened changes', function(done) {
          runAfterOpen(overlay, function() {
            assert.equal(Polymer.IronOverlayManager.getBackdrops().length, 1, 'overlay added to manager backdrops');
            runAfterClose(overlay, function() {
              assert.equal(Polymer.IronOverlayManager.getBackdrops().length, 0, 'overlay removed from manager backdrops');
              done();
            });
          });
        });

        test('updating with-backdrop to false closes backdrop', function(done) {
          runAfterOpen(overlay, function() {
            overlay.withBackdrop = false;
            assert.isFalse(overlay.backdropElement.opened, 'backdrop is closed');
            assert.isNotObject(overlay.backdropElement.parentNode, 'backdrop is removed from document');
            done();
          });
        });

        test('backdrop is removed when toggling overlay opened', function(done) {
          overlay.open();
          runAfterClose(overlay, function() {
            assert.isFalse(overlay.backdropElement.opened, 'backdrop is closed');
            assert.isNotOk(overlay.backdropElement.parentNode, 'backdrop is removed from document');
            done();
          });
        });

        test('withBackdrop = false does not prevent click outside event', function(done) {
          overlay.withBackdrop = false;
          runAfterOpen(overlay, function() {
            overlay.addEventListener('iron-overlay-canceled', function(event) {
              assert.isFalse(event.detail.defaultPrevented, 'click event not prevented');
              done();
            });
            MockInteractions.tap(document.body);
          });
        });
      });

      suite('multiple overlays', function() {
        var overlay1, overlay2;

        setup(function() {
          var f = fixture('multiple');
          overlay1 = f[0];
          overlay2 = f[1];
        });

        test('new overlays appear on top', function(done) {
          runAfterOpen(overlay1, function() {
            runAfterOpen(overlay2, function() {
              var styleZ = parseInt(window.getComputedStyle(overlay1).zIndex, 10);
              var styleZ1 = parseInt(window.getComputedStyle(overlay2).zIndex, 10);
              assert.isTrue(styleZ1 > styleZ, 'overlay2 has higher z-index than overlay1');
              done();
            });
          });
        });

        test('ESC closes only the top overlay', function(done) {
          runAfterOpen(overlay1, function() {
            runAfterOpen(overlay2, function() {
              MockInteractions.pressAndReleaseKeyOn(document, 27);
              assert.isFalse(overlay2.opened, 'overlay2 was closed');
              assert.isTrue(overlay1.opened, 'overlay1 is still opened');
              done();
            });
          });
        });

        test('close an overlay in proximity to another overlay', function(done) {
          // Open and close a separate overlay.
          overlay1.open();
          overlay1.close();

          // Open the overlay we care about.
          overlay2.open();

          // Immediately close the first overlay.
          // Wait for infinite recursion, otherwise we win.
          runAfterClose(overlay2, function() {
            done();
          })
        });

      });

      suite('Manager overlays in sync', function() {
        var overlay1, overlay2;
        var overlays;

        setup(function() {
          var f = fixture('multiple');
          overlay1 = f[0];
          overlay2 = f[1];
          overlays = Polymer.IronOverlayManager._overlays;
        });

        test('no duplicates after attached', function(done) {
          overlay1 = document.createElement('test-overlay');
          runAfterOpen(overlay1, function() {
            assert.equal(overlays.length, 1, 'correct count after open and attached');
            document.body.removeChild(overlay1);
            done();
          });
          document.body.appendChild(overlay1);
        });

        test('call open multiple times handled', function(done) {
          overlay1.open();
          overlay1.open();
          runAfterOpen(overlay1, function() {
            assert.equal(overlays.length, 1, '1 overlay after open');
            done();
          })
        });

        test('close handled', function(done) {
          runAfterOpen(overlay1, function() {
            runAfterClose(overlay1, function() {
              assert.equal(overlays.length, 0, '0 overlays after close');
              done();
            });
          });
        });

        test('open/close brings overlay on top', function(done) {
          overlay1.open();
          runAfterOpen(overlay2, function() {
            assert.equal(overlays.indexOf(overlay1), 0, 'overlay1 at index 0');
            assert.equal(overlays.indexOf(overlay2), 1, 'overlay2 at index 1');
            overlay1.close();
            runAfterOpen(overlay1, function() {
              assert.equal(overlays.indexOf(overlay1), 1, 'overlay1 moved at index 1');
              assert.isAbove(parseInt(overlay1.style.zIndex), parseInt(overlay2.style.zIndex), 'overlay1 on top of overlay2');
              done();
            });
          });
        });
      });

      suite('z-ordering', function() {

        var originalMinimumZ;
        var overlay1, overlay2;

        setup(function() {
          var f = fixture('multiple');
          overlay1 = f[0];
          overlay2 = f[1];
          originalMinimumZ = Polymer.IronOverlayManager._minimumZ;
        });

        teardown(function() {
          Polymer.IronOverlayManager._minimumZ = originalMinimumZ;
        });

        // for iframes
        test('default z-index is greater than 100', function(done) {
          runAfterOpen(overlay1, function() {
            var styleZ = parseInt(window.getComputedStyle(overlay1).zIndex, 10);
            assert.isTrue(styleZ > 100, 'overlay1 z-index is <= 100');
            done();
          });
        });

        test('ensureMinimumZ() effects z-index', function(done) {
          Polymer.IronOverlayManager.ensureMinimumZ(1000);

          runAfterOpen(overlay1, function() {
            var styleZ = parseInt(window.getComputedStyle(overlay1).zIndex, 10);
            assert.isTrue(styleZ > 1000, 'overlay1 z-index is <= 1000');
            done();
          });
        });

        test('ensureMinimumZ() never decreases the minimum z-index', function(done) {
          Polymer.IronOverlayManager.ensureMinimumZ(1000);
          Polymer.IronOverlayManager.ensureMinimumZ(500);

          runAfterOpen(overlay1, function() {
            var styleZ = parseInt(window.getComputedStyle(overlay1).zIndex, 10);
            assert.isTrue(styleZ > 1000, 'overlay1 z-index is <= 1000');
            done();
          });
        });

      });

      suite('multiple overlays with backdrop', function() {
        var overlay1, overlay2, overlay3;

        setup(function() {
          var f = fixture('multiple');
          overlay1 = f[0];
          overlay2 = f[1];
          overlay3 = f[2];
          overlay1.withBackdrop = overlay2.withBackdrop = overlay3.withBackdrop = true;
        });

        test('multiple overlays share the same backdrop', function() {
          assert.isTrue(overlay1.backdropElement === overlay2.backdropElement, 'overlay1 and overlay2 have the same backdrop element');
          assert.isTrue(overlay1.backdropElement === overlay3.backdropElement, 'overlay1 and overlay3 have the same backdrop element');
        });

        test('only one iron-overlay-backdrop in the DOM', function(done) {
          // Open them all.
          overlay1.opened = true;
          overlay2.opened = true;
          runAfterOpen(overlay3, function() {
            assert.lengthOf(document.querySelectorAll('iron-overlay-backdrop'), 1, 'only one backdrop element in the DOM');
            done();
          });
        });

        test('iron-overlay-backdrop is removed from the DOM when all overlays with backdrop are closed', function(done) {
          // Open & close them all.
          overlay1.opened = true;
          overlay2.opened = true;
          runAfterOpen(overlay3, function() {
            overlay1.opened = overlay2.opened = false;
            runAfterClose(overlay3, function() {
              assert.lengthOf(document.querySelectorAll('iron-overlay-backdrop'), 0, 'backdrop element removed from the DOM');
              done();
            });
          });
        });

        test('newest overlay appear on top', function(done) {
          runAfterOpen(overlay1, function() {
            runAfterOpen(overlay2, function() {
              var styleZ = parseInt(window.getComputedStyle(overlay1).zIndex, 10);
              var style1Z = parseInt(window.getComputedStyle(overlay2).zIndex, 10);
              var bgStyleZ = parseInt(window.getComputedStyle(overlay1.backdropElement).zIndex, 10);
              assert.isTrue(style1Z > styleZ, 'overlay2 has higher z-index than overlay1');
              assert.isTrue(styleZ > bgStyleZ, 'overlay1 has higher z-index than backdrop');
              done();
            });
          });
        });

        var clickEvents = ['click', 'tap'];
        for (var i = 0; i < clickEvents.length; i++) {
          var eventName = clickEvents[i];
          test(eventName + ' event handled only by top overlay', function(done) {
            runAfterOpen(overlay1, function() {
              runAfterOpen(overlay2, function() {
                var btn = Polymer.dom(overlay2).querySelector('button');
                btn.addEventListener(eventName, overlay2.close.bind(overlay2));
                MockInteractions.tap(btn);
                assert.isFalse(overlay2.opened, 'overlay2 closed');
                assert.isTrue(overlay1.opened, 'overlay1 opened');
                overlay2.addEventListener('iron-overlay-closed', function() {
                  assert.isTrue(overlay1.opened, 'overlay1 still opened');
                  done();
                });
              });
            });
          });
        }

        test('updating with-backdrop updates z-index', function(done) {
          runAfterOpen(overlay1, function() {
            runAfterOpen(overlay2, function() {
              overlay1.withBackdrop = false;
              var styleZ = parseInt(window.getComputedStyle(overlay1).zIndex, 10);
              var style1Z = parseInt(window.getComputedStyle(overlay2).zIndex, 10);
              var bgStyleZ = parseInt(window.getComputedStyle(overlay1.backdropElement).zIndex, 10);
              assert.isTrue(style1Z > bgStyleZ, 'overlay2 has higher z-index than backdrop');
              assert.isTrue(styleZ < bgStyleZ, 'overlay1 has lower z-index than backdrop');
              done();
            });
          });
        });

      });

      suite('overlay in composed tree', function() {
        var composed, overlay, trigger;
        setup(function(done) {
          composed = fixture('composed');
          overlay = composed.$.overlay;
          trigger = composed.$.trigger;
          overlay.withBackdrop = true;
          overlay.addEventListener('iron-overlay-opened', function() {
            done();
          });
          // Opens the overlay.
          MockInteractions.tap(trigger);
        });

        test('click on overlay content does not close it', function(done) {
          // Tap on button inside overlay.
          MockInteractions.tap(Polymer.dom(overlay).querySelector('button'));
          Polymer.Base.async(function(){
            assert.isTrue(overlay.opened, 'overlay still opened');
            done();
          }, 1);
        });

        test('with-backdrop wraps the focus within the overlay', function(done) {
          // 1ms timeout needed by IE10 to have proper focus switching.
          Polymer.Base.async(function(){
            var buttons = Polymer.dom(overlay).querySelectorAll('button');
            // Go to last element.
            buttons[buttons.length-1].focus();
            // Spy keydown.
            var tabSpy = sinon.spy();
            document.addEventListener('keydown', tabSpy);
            // Simulate TAB.
            MockInteractions.pressAndReleaseKeyOn(document, 9);
            assert.equal(buttons[0], Polymer.IronOverlayManager.deepActiveElement, 'focus wrapped to first focusable');
            assert.isTrue(tabSpy.calledOnce, 'keydown spy called');
            assert.isTrue(tabSpy.getCall(0).args[0].defaultPrevented, 'keydown default prevented');
            // Simulate Shift+TAB.
            MockInteractions.pressAndReleaseKeyOn(document, 9, ['shift']);
            assert.equal(buttons[buttons.length-1], Polymer.IronOverlayManager.deepActiveElement, 'focus wrapped to last focusable');
            assert.isTrue(tabSpy.calledTwice, 'keydown spy called again');
            assert.isTrue(tabSpy.getCall(1).args[0].defaultPrevented, 'keydown default prevented again');
            // Cleanup.
            document.removeEventListener('keydown', tabSpy);
            done();
          }, 1);
        });

      });

      suite('always-on-top', function() {
        var overlay1, overlay2;

        setup(function() {
          var f = fixture('multiple');
          overlay1 = f[0];
          overlay2 = f[1];
          overlay1.alwaysOnTop = true;
        });

        test('stays on top', function(done) {
          runAfterOpen(overlay1, function() {
            runAfterOpen(overlay2, function() {
              var zIndex1 = parseInt(window.getComputedStyle(overlay1).zIndex, 10);
              var zIndex2 = parseInt(window.getComputedStyle(overlay2).zIndex, 10);
              assert.isAbove(zIndex1, zIndex2, 'overlay1 on top');
              assert.equal(Polymer.IronOverlayManager.currentOverlay(), overlay1, 'currentOverlay ok');
              done();
            });
          });
        });

        test('stays on top also if another overlay is with-backdrop', function(done) {
          overlay2.withBackdrop = true;
          runAfterOpen(overlay1, function() {
            runAfterOpen(overlay2, function() {
              var zIndex1 = parseInt(window.getComputedStyle(overlay1).zIndex, 10);
              var zIndex2 = parseInt(window.getComputedStyle(overlay2).zIndex, 10);
              assert.isAbove(zIndex1, zIndex2, 'overlay1 on top');
              assert.equal(Polymer.IronOverlayManager.currentOverlay(), overlay1, 'currentOverlay ok');
              done();
            });
          });
        });

        test('last overlay with always-on-top wins', function(done) {
          overlay2.alwaysOnTop = true;
          runAfterOpen(overlay1, function() {
            runAfterOpen(overlay2, function() {
              var zIndex1 = parseInt(window.getComputedStyle(overlay1).zIndex, 10);
              var zIndex2 = parseInt(window.getComputedStyle(overlay2).zIndex, 10);
              assert.isAbove(zIndex2, zIndex1, 'overlay2 on top');
              assert.equal(Polymer.IronOverlayManager.currentOverlay(), overlay2, 'currentOverlay ok');
              done();
            });
          });
        });

      });

      suite('animations', function() {

        test('overlay animations correctly triggered', function(done) {
          var overlay = fixture('basic');
          overlay.animated = true;
          overlay.open();
          overlay.addEventListener('simple-overlay-open-animation-start', function() {
            // Since animated overlay will transition center + 300px to center,
            // we should not find the element at the center when the open animation starts.
            var centerElement = document.elementFromPoint(window.innerWidth/2, window.innerHeight/2);
            assert.notEqual(centerElement, overlay, 'overlay should not be centered already');
            done();
          });
        });

      });

      suite('a11y', function() {

        test('overlay has aria-hidden=true when opened', function() {
          var overlay = fixture('basic');
          assert.equal(overlay.getAttribute('aria-hidden'), 'true', 'overlay has aria-hidden="true"');
          overlay.open();
          assert.isFalse(overlay.hasAttribute('aria-hidden'), 'overlay does not have aria-hidden attribute');
          overlay.close();
          assert.equal(overlay.getAttribute('aria-hidden'), 'true', 'overlay has aria-hidden="true"');
        });

      });
    </script>

  </body>

</html>