<!DOCTYPE html>
<html>
<head>
  <title>focus_test.html</title>
  <script src="test_bootstrap.js"></script>
  <script type="text/javascript">
    goog.require('bot.action');
    goog.require('bot.test');
    goog.require('goog.Promise');
    goog.require('goog.dom');
    goog.require('goog.events');
    goog.require('goog.events.EventType');
    goog.require('goog.testing.jsunit');
    goog.require('goog.userAgent');
    goog.require('goog.userAgent.product');
  </script>
  <script type="text/javascript">
    var events = goog.events;
    var eventType = goog.events.EventType;

    var text1, text2, actualEvents, expectedEvents;

    function setUp() {
      actualEvents = [];
      expectedEvents = [];

      text1 = goog.dom.$('text1');
      text2 = goog.dom.$('text2');

      // Make sure neither text1 nor text2 have focus. Since this could
      // cause events to dispatch, we need to wait for them to do so
      // before continuing. In IE, this means continuing in a timeout.
      bot.action.focusOnElement(goog.dom.$('dummyFocusHolder'));
      return timeout();
    }


    function tearDown() {
      events.removeAll(text1);
      events.removeAll(text2);
    }

    function buildRecord(id, type) {
      return id + ': ' + type;
    }

    function recordBlurAndFocus(el) {
      events.listen(el, [eventType.FOCUS, eventType.BLUR], function(e) {
        actualEvents.push(buildRecord(e.target.id, e.type));
      });
    }

    function expect(eventType, onElement) {
      expectedEvents.push(buildRecord(onElement.id, eventType));
    }

    function assertHasExpectedEventOrder() {
      assertArrayEquals(expectedEvents, actualEvents);
    }

    function assertElementIsActiveElement(expected) {
      var actual = goog.dom.getActiveElement(document);
      if (actual) {
        // NOTE(jleyba): Using assertEquals(expected, document.activeElement)
        // causes an error in IE when the assertion fails (from trying to
        // convert the DOM elements into strings for the error message):
        //     "'constructor' is null or not an object".
        // The following assertions give us the same check without this error.
        var msg = 'Expected "' + expected.id + '" to be the activeElement, ' +
                  'but was "' + actual.id + '".';
        assertTrue(msg, expected == actual);
      }
    }

    /**
     * Pauses the test momentarily so IE can dispatch the onfocus event, which
     * is scheduled for the next event loop after element.focus() is called.
     */
    function timeout() {
      return new goog.Promise(function(done) {
        setTimeout(done, 5);
      });
    }

    function testShouldBeAbleToFocusOnAnElement() {
      // NOTE(jleyba): Test does not work on IE 8 with the new IE Driver,
      // I suspect due to something similar to
      // http://support.microsoft.com/kb/973528
      if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher('8')) {
        return;
      }
      if (!bot.test.isWindowFocused()) {
        return;
      }
      recordBlurAndFocus(text1);
      recordBlurAndFocus(text2);

      expect(eventType.FOCUS, text1);
      bot.action.focusOnElement(text1);
      return timeout().then(function() {
        assertHasExpectedEventOrder();
        assertElementIsActiveElement(text1);

        expect(eventType.BLUR, text1);
        expect(eventType.FOCUS, text2);
        bot.action.focusOnElement(text2);
        return timeout();
      }).then(function() {
        assertHasExpectedEventOrder();
        assertElementIsActiveElement(text2);

        expect(eventType.BLUR, text2);
        expect(eventType.FOCUS, text1);
        bot.action.focusOnElement(text1);
        return timeout();
      }).then(function() {
        assertHasExpectedEventOrder();
        assertElementIsActiveElement(text1);
      });
    }

    function testShouldRetainFocusIfElementIsAlreadyActive() {
      if (!bot.test.isWindowFocused()) {
        return;
      }

      recordBlurAndFocus(text1);
      recordBlurAndFocus(text2);

      expect(eventType.FOCUS, text1);
      bot.action.focusOnElement(text1);
      return timeout().then(function() {
        assertHasExpectedEventOrder();
        assertElementIsActiveElement(text1);

        bot.action.focusOnElement(text1);
        return timeout();
      }).then(function() {
        assertHasExpectedEventOrder();
        assertElementIsActiveElement(text1);
      });
    }

    function testShouldRetainFocusIfFirstFocusableAncestorIsAlreadyActive() {
      if (!bot.test.isWindowFocused()) {
        return;
      }

      var parent = document.getElementById('hideOnBlur');
      var child = document.getElementById('hideOnBlurChild');
      recordBlurAndFocus(parent);

      expect(eventType.FOCUS, parent);
      bot.action.focusOnElement(parent);
      return timeout().then(function() {
        assertHasExpectedEventOrder();
        assertElementIsActiveElement(parent);

        bot.action.focusOnElement(child);
        // No additional events expected. The parent should retain focus.
        return timeout();
      }).then(function() {
        assertHasExpectedEventOrder();
        assertElementIsActiveElement(parent);

        bot.action.focusOnElement(text1);
        expect(eventType.BLUR, parent);
        return timeout();
      }).then(function() {
        assertHasExpectedEventOrder();
      });
    }

    function testFocusOnHiddenElementThrows() {
      var element = document.getElementById('invisibleText');
      assertThrows(goog.partial(bot.action.focusOnElement, element));
    }

    function testFocusOnDisabledElementThrows() {
      var element = document.getElementById('disabledText');
      assertThrows(goog.partial(bot.action.focusOnElement, element));
    }

    function testIsFocusable() {
      if (goog.userAgent.IE) {
        // TODO: Don't skip this.
        return;
      }
      function assertIsFocusable(element, opt_reason) {
        assertTrue(opt_reason || '', bot.dom.isFocusable(element));
      }

      function assertNotFocusable(element, opt_reason) {
        assertFalse(opt_reason || '', bot.dom.isFocusable(element));
      }

      goog.array.forEach(bot.dom.FOCUSABLE_FORM_FIELDS_, function(tagName) {
        assertIsFocusable(goog.dom.createDom(tagName),
            tagName + ' is a focusable form field');
      });

      var div = goog.dom.createDom('DIV');
      assertNotFocusable(div);

      div.setAttribute('tabindex', '0');
      assertIsFocusable(div, 'has a tab index of 0');

      div.tabIndex = -1;
      assertEquals('-1', bot.dom.getAttribute(div, 'tabindex'));
      assertNotFocusable(div, 'has a negative tab index');

      div.tabIndex = 2;
      assertEquals('2', bot.dom.getAttribute(div, 'tabindex'));
      assertIsFocusable(div, 'has a tab index of 2');

      div.removeAttribute('tabindex');
      assertNull(bot.dom.getAttribute(div, 'tabindex'));
      assertNotFocusable(div, 'tab index was removed');
    }
  </script>
</head>
<body>
<form action="javascript:void(0)">
  <input id="text1" type="text"/>
  <input id="text2" type="text"/>
  <input id="dummyFocusHolder" type="text"/>
  <div>
    <div>Input fields for manual testing. Each will report when they receive or
      lose focus.</div>
    Test 1: <input id="test1" type="text"/>
    Test 2: <input id="test2" type="text"/>
    <div>
      <button id="focusDance">Click to simulate focus change</button>
      <button id="clearTestLog">Click to clear log</button>
      <button id="opennewwindow" onclick="window.open(window.location)">
        Re-run test in a new window</button>
    </div>
    <div style="border:1px solid black; margin: 5px; padding: 5px; width:50%">
      <div style="border-bottom: 1px solid black">Test Log</div>
      <div id="testLog"></div>
    </div>
    <div>
      <div><strong>Invisible textbox</strong></div>
      <input id="invisibleText" type="text" style="visibility:hidden"/>
    </div>
    <div>
      <div><strong>Disabled textbox</strong></div>
      <input id="disabledText" type="text" value="I'm disabled" disabled/>
    </div>
    <div id="hideOnBlur" tabindex="0"
         style="width: 100px; height: 100px; border: 1px solid red"
         onblur="this.style.display = 'none';">
      <div id="hideOnBlurChild"
           style="width: 30px; height: 30px; border: 1px solid blue">
        x
      </div>
      Focusable. Will hide when focus is lost.
    </div>
    <script type="text/javascript">
      function log(msg) {
        goog.dom.$('testLog').appendChild(
            goog.dom.createDom('div', null,
              goog.dom.createTextNode(msg)));
      }

      function reportBlurFocus(e) {
        var et = eventType;
        goog.events.listen(e, [et.FOCUS, et.BLUR], function(e) {
          log(e.type + ' on ' + e.target.id);
        });
      }
      reportBlurFocus(goog.dom.$('test1'));
      reportBlurFocus(goog.dom.$('test2'));

      goog.events.listen(goog.dom.$('focusDance'), eventType.CLICK,
        function(e) {
          log('starting focus dance in .5s');
          log('  will focus on 1, then 2, then 1 again');
          window.setTimeout(function() {
            bot.action.focusOnElement(goog.dom.$('test1'));
            bot.action.focusOnElement(goog.dom.$('test2'));
            bot.action.focusOnElement(goog.dom.$('test1'));
            log('if you see this before event logs, then events are ' +
                'fired in a separate event loop');
          }, 500);
        });
      goog.events.listen(goog.dom.$('clearTestLog'), eventType.CLICK,
        function(e) {
          goog.dom.removeChildren(goog.dom.$('testLog'));
        });
    </script>
  </div>
</form>
</body>
</html>
