// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.ui.ContainerTest');
goog.setTestOnly('goog.ui.ContainerTest');

goog.require('goog.a11y.aria');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.KeyEvent');
goog.require('goog.testing.events');
goog.require('goog.testing.jsunit');
goog.require('goog.ui.Component');
goog.require('goog.ui.Container');
goog.require('goog.ui.Control');

var sandbox;
var containerElement;
var container;
var keyContainer;
var listContainer;

function setUpPage() {
  sandbox = goog.dom.getElement('sandbox');
}

function setUp() {
  container = new goog.ui.Container();
  keyContainer = null;
  listContainer = null;

  sandbox.innerHTML =
      '<div id="containerElement" class="goog-container">\n' +
      '  <div class="goog-control" id="hello">Hello</div>\n' +
      '  <div class="goog-control" id="world">World</div>\n' +
      '</div>';
  containerElement = goog.dom.getElement('containerElement');
}

function tearDown() {
  goog.dom.removeChildren(sandbox);
  container.dispose();
  goog.dispose(keyContainer);
  goog.dispose(listContainer);
}

function testDecorateHidden() {
  containerElement.style.display = 'none';

  assertTrue('Container must be visible', container.isVisible());
  container.decorate(containerElement);
  assertFalse('Container must be hidden', container.isVisible());
  container.forEachChild(function(control) {
    assertTrue('Child control ' + control.getId() + ' must report being ' +
        'visible, even if in a hidden container', control.isVisible());
  });
}

function testDecorateDisabled() {
  goog.dom.classlist.add(containerElement, 'goog-container-disabled');

  assertTrue('Container must be enabled', container.isEnabled());
  container.decorate(containerElement);
  assertFalse('Container must be disabled', container.isEnabled());
  container.forEachChild(function(control) {
    assertFalse('Child control ' + control.getId() + ' must be disabled, ' +
        'because the host container is disabled', control.isEnabled());
  });
}

function testDecorateFocusableContainer() {
  container.decorate(containerElement);
  assertTrue('Container must be focusable', container.isFocusable());
  container.forEachChild(function(control) {
    assertFalse('Child control ' + control.getId() + ' must not be ' +
        'focusable',
        control.isSupportedState(goog.ui.Component.State.FOCUSED));
  });
}

function testDecorateFocusableChildrenContainer() {
  container.setFocusable(false);
  container.setFocusableChildrenAllowed(true);
  container.decorate(containerElement);
  assertFalse('Container must not be focusable', container.isFocusable());
  container.forEachChild(function(control) {
    assertTrue('Child control ' + control.getId() + ' must be ' +
        'focusable',
        control.isSupportedState(goog.ui.Component.State.FOCUSED));
  });
}

function testHighlightOnEnter() {
  // This interaction test ensures that containers enforce that children
  // get highlighted on mouseover, and that one and only one child may
  // be highlighted at a time.  Although integration tests aren't the
  // best, it's difficult to test these event-based interactions due to
  // their disposition toward the "misunderstood contract" problem.

  container.decorate(containerElement);
  assertFalse('Child 0 should initially not be highlighted',
      container.getChildAt(0).isHighlighted());

  goog.testing.events.fireMouseOverEvent(
      container.getChildAt(0).getElement(), sandbox);
  assertTrue('Child 0 should become highlighted after a mouse over',
      container.getChildAt(0).isHighlighted());
  assertEquals('Child 0 should be the active descendant',
      container.getChildAt(0).getElement(),
      goog.a11y.aria.getActiveDescendant(container.getElement()));

  goog.testing.events.fireMouseOverEvent(
      container.getChildAt(1).getElement(),
      container.getChildAt(0).getElement());
  assertFalse('Child 0 should lose highlight when child 1 is moused ' +
      'over, even if no mouseout occurs.',
      container.getChildAt(0).isHighlighted());
  assertTrue('Child 1 should now be highlighted.',
      container.getChildAt(1).isHighlighted());
  assertEquals('Child 1 should be the active descendant',
      container.getChildAt(1).getElement(),
      goog.a11y.aria.getActiveDescendant(container.getElement()));
}

function testHighlightOnEnterPreventable() {
  container.decorate(containerElement);
  goog.events.listen(container, goog.ui.Component.EventType.ENTER,
      function(event) {
        event.preventDefault();
      });
  goog.testing.events.fireMouseOverEvent(
      container.getChildAt(0).getElement(), sandbox);
  assertFalse('Child 0 should not be highlighted if preventDefault called',
      container.getChildAt(0).isHighlighted());
}

function testHighlightDisabled() {
  // Another interaction test.  Already tested in control_test.
  container.decorate(containerElement);
  container.getChildAt(0).setEnabled(false);
  goog.testing.events.fireMouseOverEvent(
      container.getChildAt(0).getElement(), sandbox);
  assertFalse('Disabled children should not be highlighted',
      container.getChildAt(0).isHighlighted());
}

function testGetOwnerControl() {
  container.decorate(containerElement);

  assertEquals('Must return appropriate control given an element in the ' +
      'control.',
      container.getChildAt(1),
      container.getOwnerControl(container.getChildAt(1).getElement()));

  assertNull('Must return null for element not associated with control.',
      container.getOwnerControl(document.body));
  assertNull('Must return null if given null node',
      container.getOwnerControl(null));
}

function testShowEvent() {
  container.decorate(containerElement);
  container.setVisible(false);
  var eventFired = false;
  goog.events.listen(container, goog.ui.Component.EventType.SHOW,
      function() {
        assertFalse('Container must not be visible when SHOW event is ' +
                    'fired',
                    container.isVisible());
        eventFired = true;
      });
  container.setVisible(true);
  assertTrue('SHOW event expected', eventFired);
}

function testAfterShowEvent() {
  container.decorate(containerElement);
  container.setVisible(false);
  var eventFired = false;
  goog.events.listen(container, goog.ui.Container.EventType.AFTER_SHOW,
      function() {
        assertTrue('Container must be visible when AFTER_SHOW event is ' +
                   'fired',
                   container.isVisible());
        eventFired = true;
      });
  container.setVisible(true);
  assertTrue('AFTER_SHOW event expected', eventFired);
}

function testHideEvents() {
  var events = [];
  container.decorate(containerElement);
  container.setVisible(true);
  var eventFired = false;
  goog.events.listen(container, goog.ui.Component.EventType.HIDE,
      function(e) {
        assertTrue(
            'Container must be visible when HIDE event is fired',
            container.isVisible());
        events.push(e.type);
      });
  goog.events.listen(container, goog.ui.Container.EventType.AFTER_HIDE,
      function(e) {
        assertFalse(
            'Container must not be visible when AFTER_HIDE event is fired',
            container.isVisible());
        events.push(e.type);
      });
  container.setVisible(false);
  assertArrayEquals('HIDE event followed by AFTER_HIDE expected', [
    goog.ui.Component.EventType.HIDE,
    goog.ui.Container.EventType.AFTER_HIDE
  ], events);
}



/**
 * Test container to which the elements have to be added with
 * {@code container.addChild(element, false)}
 * @constructor
 * @extends {goog.ui.Container}
 */
function ListContainer() {
  goog.ui.Container.call(this);
}
goog.inherits(ListContainer, goog.ui.Container);


/** @override */
ListContainer.prototype.createDom = function() {
  ListContainer.superClass_.createDom.call(this);
  var ul = this.getDomHelper().createDom('ul');
  this.forEachChild(function(child) {
    child.createDom();
    var childEl = child.getElement();
    ul.appendChild(this.getDomHelper().createDom('li', {}, childEl));
  }, this);
  this.getContentElement().appendChild(ul);
};

function testGetOwnerControlWithNoRenderingInAddChild() {
  listContainer = new ListContainer();
  var control = new goog.ui.Control('item');
  listContainer.addChild(control);
  listContainer.render();
  var ownerControl = listContainer.getOwnerControl(control.getElement());

  assertEquals('Control was added with addChild(control, false)',
      control, ownerControl);
}



/**
 * Test container for tracking key events being handled.
 * @constructor
 * @extends {goog.ui.Container}
 */
function KeyHandlingContainer() {
  goog.ui.Container.call(this);
  this.keyEventsHandled = 0;
}
goog.inherits(KeyHandlingContainer, goog.ui.Container);


/** @override */
KeyHandlingContainer.prototype.handleKeyEventInternal = function() {
  this.keyEventsHandled++;
  return false;
};

function testHandleKeyEvent_onlyHandlesWhenVisible() {
  keyContainer = new KeyHandlingContainer();
  keyContainer.decorate(containerElement);

  keyContainer.setVisible(false);
  keyContainer.handleKeyEvent(new goog.events.Event());
  assertEquals('No key events should be handled',
      0, keyContainer.keyEventsHandled);

  keyContainer.setVisible(true);
  keyContainer.handleKeyEvent(new goog.events.Event());
  assertEquals('One key event should be handled',
      1, keyContainer.keyEventsHandled);
}

function testHandleKeyEvent_onlyHandlesWhenEnabled() {
  keyContainer = new KeyHandlingContainer();
  keyContainer.decorate(containerElement);
  keyContainer.setVisible(true);

  keyContainer.setEnabled(false);
  keyContainer.handleKeyEvent(new goog.events.Event());
  assertEquals('No key events should be handled',
      0, keyContainer.keyEventsHandled);

  keyContainer.setEnabled(true);
  keyContainer.handleKeyEvent(new goog.events.Event());
  assertEquals('One key event should be handled',
      1, keyContainer.keyEventsHandled);
}

function testHandleKeyEvent_childlessContainersIgnoreKeyEvents() {
  keyContainer = new KeyHandlingContainer();
  keyContainer.render();
  keyContainer.setVisible(true);

  keyContainer.handleKeyEvent(new goog.events.Event());
  assertEquals('No key events should be handled',
      0, keyContainer.keyEventsHandled);

  keyContainer.addChild(new goog.ui.Control());
  keyContainer.handleKeyEvent(new goog.events.Event());
  assertEquals('One key event should be handled',
      1, keyContainer.keyEventsHandled);
}

function testHandleKeyEvent_alwaysHandlesWithKeyEventTarget() {
  keyContainer = new KeyHandlingContainer();
  keyContainer.render();
  keyContainer.setKeyEventTarget(goog.dom.createDom('div'));
  keyContainer.setVisible(true);

  keyContainer.handleKeyEvent(new goog.events.Event());
  assertEquals('One key events should be handled',
      1, keyContainer.keyEventsHandled);
}

function testHandleKeyEventInternal_onlyHandlesUnmodified() {
  container.setKeyEventTarget(sandbox);
  var event = new goog.events.KeyEvent(
      goog.events.KeyCodes.ESC, 0, false, null);

  var propertyNames = [
    'shiftKey',
    'altKey',
    'ctrlKey',
    'metaKey'
  ];

  // Verify that the event is not handled whenever a modifier key is true.
  for (var i = 0, propertyName; propertyName = propertyNames[i]; i++) {
    assertTrue('Event should be handled when modifer key is not pressed.',
        container.handleKeyEventInternal(event));
    event[propertyName] = true;
    assertFalse('Event should not be handled when modifer key is pressed.',
        container.handleKeyEventInternal(event));
    event[propertyName] = false;
  }
}

function testOpenFollowsHighlight() {
  container.decorate(containerElement);
  container.setOpenFollowsHighlight(true);
  assertTrue('isOpenFollowsHighlight should return true',
      container.isOpenFollowsHighlight());

  // Make the children openable.
  container.forEachChild(function(child) {
    child.setSupportedState(goog.ui.Component.State.OPENED, true);
  });
  // Open child 1 initially.
  container.getChildAt(1).setOpen(true);

  assertFalse('Child 0 should initially not be highlighted',
      container.getChildAt(0).isHighlighted());
  goog.testing.events.fireMouseOverEvent(
      container.getChildAt(0).getElement(), sandbox);
  assertTrue('Child 0 should become highlighted after a mouse over',
      container.getChildAt(0).isHighlighted());
  assertTrue('Child 0 should become open after higlighted',
      container.getChildAt(0).isOpen());
  assertFalse('Child 1 should become closed once 0 is open',
      container.getChildAt(1).isOpen());
  assertEquals('OpenItem should be child 0',
      container.getChildAt(0), container.getOpenItem());
}

function testOpenNotFollowsHighlight() {
  container.decorate(containerElement);
  container.setOpenFollowsHighlight(false);
  assertFalse('isOpenFollowsHighlight should return false',
      container.isOpenFollowsHighlight());

  // Make the children openable.
  container.forEachChild(function(child) {
    child.setSupportedState(goog.ui.Component.State.OPENED, true);
  });
  // Open child 1 initially.
  container.getChildAt(1).setOpen(true);

  assertFalse('Child 0 should initially not be highlighted',
      container.getChildAt(0).isHighlighted());
  goog.testing.events.fireMouseOverEvent(
      container.getChildAt(0).getElement(), sandbox);
  assertTrue('Child 0 should become highlighted after a mouse over',
      container.getChildAt(0).isHighlighted());
  assertFalse('Child 0 should remain closed after higlighted',
      container.getChildAt(0).isOpen());
  assertTrue('Child 1 should remain open',
      container.getChildAt(1).isOpen());
  assertEquals('OpenItem should be child 1',
      container.getChildAt(1), container.getOpenItem());
}

function testRemoveChild() {
  goog.dom.removeChildren(containerElement);
  container.decorate(containerElement);

  var a = new goog.ui.Control('A');
  var b = new goog.ui.Control('B');
  var c = new goog.ui.Control('C');

  a.setId('a');
  b.setId('b');
  c.setId('c');

  container.addChild(a, true);
  container.addChild(b, true);
  container.addChild(c, true);

  container.setHighlightedIndex(2);

  assertEquals('Parent must remove and return child by ID', b,
      container.removeChild('b'));
  assertNull('Parent must no longer contain this child',
      container.getChild('b'));
  assertEquals('Highlighted index must be decreased', 1,
      container.getHighlightedIndex());
  assertTrue('The removed control must handle its own mouse events',
      b.isHandleMouseEvents());

  assertEquals('Parent must remove and return child', c,
      container.removeChild(c));
  assertNull('Parent must no longer contain this child',
      container.getChild('c'));
  assertFalse('This child must no longer be highlighted',
      c.isHighlighted());
  assertTrue('The removed control must handle its own mouse events',
      c.isHandleMouseEvents());

  assertEquals('Parent must remove and return child by index', a,
      container.removeChildAt(0));
  assertNull('Parent must no longer contain this child',
      container.getChild('a'));
  assertTrue('The removed control must handle its own mouse events',
      a.isHandleMouseEvents());
}

function testRemoveHighlightedDisposedChild() {
  goog.dom.removeChildren(containerElement);
  container.decorate(containerElement);

  var a = new goog.ui.Control('A');
  container.addChild(a, true);

  container.setHighlightedIndex(0);
  a.dispose();
  container.removeChild(a);
  container.dispose();
}


/**
 * Checks that getHighlighted() returns the expected value and checks
 * that the child at this index is highlighted and other children are not.
 * @param {string} explanation Message indicating what is expected.
 * @param {number} index Expected return value of getHighlightedIndex().
 */
function assertHighlightedIndex(explanation, index) {
  assertEquals(explanation, index, container.getHighlightedIndex());
  for (var i = 0; i < container.getChildCount(); i++) {
    if (i == index) {
      assertTrue('Child at highlighted index should be highlighted',
          container.getChildAt(i).isHighlighted());
    } else {
      assertFalse('Only child at highlighted index should be highlighted',
          container.getChildAt(i).isHighlighted());
    }
  }
}

function testUpdateHighlightedIndex_updatesWhenChildrenAreAdded() {
  goog.dom.removeChildren(containerElement);
  container.decorate(containerElement);

  var a = new goog.ui.Control('A');
  var b = new goog.ui.Control('B');
  var c = new goog.ui.Control('C');

  container.addChild(a);
  container.setHighlightedIndex(0);
  assertHighlightedIndex('Highlighted index should match set value', 0);

  // Add child before the highlighted one.
  container.addChildAt(b, 0);
  assertHighlightedIndex('Highlighted index should be increased', 1);

  // Add child after the highlighted one.
  container.addChildAt(c, 2);
  assertHighlightedIndex('Highlighted index should not change', 1);

  container.dispose();
}

function testUpdateHighlightedIndex_updatesWhenChildrenAreMoved() {
  goog.dom.removeChildren(containerElement);
  container.decorate(containerElement);

  var a = new goog.ui.Control('A');
  var b = new goog.ui.Control('B');
  var c = new goog.ui.Control('C');

  container.addChild(a);
  container.addChild(b);
  container.addChild(c);

  // Highlight 'c' and swap 'a' and 'b'
  // [a, b, c] -> [a, b, *c] -> [b, a, *c] (* indicates the highlighted child)
  container.setHighlightedIndex(2);
  container.addChildAt(a, 1, false);
  assertHighlightedIndex('Highlighted index should not change', 2);

  // Move the highlighted child 'c' from index 2 to index 1.
  // [b, a, *c] -> [b, *c, a]
  container.addChildAt(c, 1, false);
  assertHighlightedIndex('Highlighted index must follow the moved child', 1);

  // Take the element in front of the highlighted index and move it behind it.
  // [b, *c, a] -> [*c, a, b]
  container.addChildAt(b, 2, false);
  assertHighlightedIndex('Highlighted index must be decreased', 0);

  // And move the element back to the front.
  // [*c, a, b] -> [b, *c, a]
  container.addChildAt(b, 0, false);
  assertHighlightedIndex('Highlighted index must be increased', 1);

  container.dispose();
}

function testUpdateHighlightedIndex_notChangedOnNoOp() {
  goog.dom.removeChildren(containerElement);
  container.decorate(containerElement);

  container.addChild(new goog.ui.Control('A'));
  container.addChild(new goog.ui.Control('B'));
  container.setHighlightedIndex(1);

  // Re-add a child to its current position.
  container.addChildAt(container.getChildAt(0), 0, false);
  assertHighlightedIndex('Highlighted index must not change', 1);

  container.dispose();
}

function testUpdateHighlightedIndex_notChangedWhenNoChildSelected() {
  goog.dom.removeChildren(containerElement);
  container.decorate(containerElement);

  var a = new goog.ui.Control('A');
  var b = new goog.ui.Control('B');
  var c = new goog.ui.Control('C');
  container.addChild(a);
  container.addChild(b);
  container.addChild(c);

  // Move children around.
  container.addChildAt(a, 2, false);
  container.addChildAt(b, 1, false);
  container.addChildAt(c, 2, false);

  assertHighlightedIndex('Highlighted index must not change', -1);

  container.dispose();
}

function testUpdateHighlightedIndex_indexStaysInBoundsWhenMovedToMaxIndex() {
  goog.dom.removeChildren(containerElement);
  container.decorate(containerElement);

  var a = new goog.ui.Control('A');
  var b = new goog.ui.Control('B');
  container.addChild(a);
  container.addChild(b);

  // Move higlighted child to an index one behind last child.
  container.setHighlightedIndex(0);
  container.addChildAt(a, 2);

  assertEquals('Child should be moved to index 1', a, container.getChildAt(1));
  assertEquals('Child count should not change', 2, container.getChildCount());
  assertHighlightedIndex('Highlighted index must point to new index', 1);

  container.dispose();
}
