// Copyright 2010 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.SelectTest');
goog.setTestOnly('goog.ui.SelectTest');

goog.require('goog.a11y.aria');
goog.require('goog.a11y.aria.Role');
goog.require('goog.a11y.aria.State');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
goog.require('goog.ui.Component');
goog.require('goog.ui.CustomButtonRenderer');
goog.require('goog.ui.Menu');
goog.require('goog.ui.MenuItem');
goog.require('goog.ui.Select');
goog.require('goog.ui.Separator');

var defaultCaption = 'initial caption';
var sandboxEl;
var select;

function setUp() {
  sandboxEl = goog.dom.getElement('sandbox');
  select = new goog.ui.Select(defaultCaption);
}

function tearDown() {
  select.dispose();
  goog.dom.removeChildren(sandboxEl);
}


/**
 * Checks that the default caption passed in the constructor and in the setter
 * is returned by getDefaultCaption, and acts as a default caption, i.e. is
 * shown as a caption when no items are selected.
 */
function testDefaultCaption() {
  select.render(sandboxEl);
  var item1 = new goog.ui.MenuItem('item 1');
  select.addItem(item1);
  select.addItem(new goog.ui.MenuItem('item 2'));
  assertEquals(defaultCaption, select.getDefaultCaption());
  assertEquals(defaultCaption, select.getCaption());

  var newCaption = 'new caption';
  select.setDefaultCaption(newCaption);
  assertEquals(newCaption, select.getDefaultCaption());
  assertEquals(newCaption, select.getCaption());

  select.setSelectedItem(item1);
  assertNotEquals(newCaption, select.getCaption());

  select.setSelectedItem(null);
  assertEquals(newCaption, select.getCaption());
}

function testNoDefaultCaption() {
  assertNull(new goog.ui.Select().getDefaultCaption());
  assertEquals('', new goog.ui.Select('').getDefaultCaption());
}

// Confirms that aria roles for select conform to spec:
// http://www.w3.org/TR/wai-aria/roles#listbox
// Basically the select should have a role of LISTBOX and all the items should
// have a role of OPTION.
function testAriaRoles() {
  select.render(sandboxEl);
  var item1 = new goog.ui.MenuItem('item 1');
  select.addItem(item1);
  // Added a separator to make sure that the SETSIZE ignores the separator
  // items.
  var separator = new goog.ui.Separator();
  select.addItem(separator);
  var item2 = new goog.ui.MenuItem('item 2');
  select.addItem(item2);
  assertNotNull(select.getElement());
  assertNotNull(item1.getElement());
  assertNotNull(item2.getElement());
  assertEquals(goog.a11y.aria.Role.LISTBOX,
      goog.a11y.aria.getRole(select.getElement()));
  assertEquals(goog.a11y.aria.Role.OPTION,
      goog.a11y.aria.getRole(item1.getElement()));
  assertEquals(goog.a11y.aria.Role.OPTION,
      goog.a11y.aria.getRole(item2.getElement()));
  assertNotNull(goog.a11y.aria.getState(select.getElement(),
      goog.a11y.aria.State.ACTIVEDESCENDANT));
  var contentElement = select.getRenderer().
      getContentElement(select.getElement());
  assertEquals('2', goog.a11y.aria.getState(contentElement,
      goog.a11y.aria.State.SETSIZE));
  assertEquals('0', goog.a11y.aria.getState(contentElement,
      goog.a11y.aria.State.POSINSET));
}


/**
 * Checks that the select control handles ACTION events from its items.
 */
function testHandlesItemActions() {
  select.render(sandboxEl);
  var item1 = new goog.ui.MenuItem('item 1');
  var item2 = new goog.ui.MenuItem('item 2');
  select.addItem(item1);
  select.addItem(item2);

  item1.dispatchEvent(goog.ui.Component.EventType.ACTION);
  assertEquals(item1, select.getSelectedItem());
  assertEquals(item1.getCaption(), select.getCaption());

  item2.dispatchEvent(goog.ui.Component.EventType.ACTION);
  assertEquals(item2, select.getSelectedItem());
  assertEquals(item2.getCaption(), select.getCaption());
}


/**
 * Tests goog.ui.Select.prototype.setValue.
 */
function testSetValue() {
  select.render(sandboxEl);
  var item1 = new goog.ui.MenuItem('item 1', 1);
  var item2 = new goog.ui.MenuItem('item 2', 2);
  select.addItem(item1);
  select.addItem(item2);

  select.setValue(1);
  assertEquals(item1, select.getSelectedItem());

  select.setValue(2);
  assertEquals(item2, select.getSelectedItem());

  select.setValue(3);
  assertNull(select.getSelectedItem());
}


/**
 * Checks that the current selection is cleared when the selected item is
 * removed.
 */
function testSelectionIsClearedWhenSelectedItemIsRemoved() {
  select.render(sandboxEl);
  var item1 = new goog.ui.MenuItem('item 1');
  select.addItem(item1);
  select.addItem(new goog.ui.MenuItem('item 2'));

  select.setSelectedItem(item1);
  select.removeItem(item1);
  assertNull(select.getSelectedItem());
}


/**
 * Check that the select control is subscribed to its selection model events
 * after being added, removed and added back again into the document.
 */
function testExitAndEnterDocument() {
  var component = new goog.ui.Component();
  component.render(sandboxEl);

  var item1 = new goog.ui.MenuItem('item 1');
  var item2 = new goog.ui.MenuItem('item 2');
  var item3 = new goog.ui.MenuItem('item 3');

  select.addItem(item1);
  select.addItem(item2);
  select.addItem(item3);

  component.addChild(select, true);
  item2.dispatchEvent(goog.ui.Component.EventType.ACTION);
  assertEquals(item2.getCaption(), select.getCaption());

  component.removeChild(select, true);
  item1.dispatchEvent(goog.ui.Component.EventType.ACTION);
  assertEquals(item2.getCaption(), select.getCaption());

  component.addChild(select, true);
  item3.dispatchEvent(goog.ui.Component.EventType.ACTION);
  assertEquals(item3.getCaption(), select.getCaption());
}

function testSelectEventFiresForProgrammaticChange() {
  select.render();
  var item1 = new goog.ui.MenuItem('item 1');
  var item2 = new goog.ui.MenuItem('item 2');
  select.addItem(item1);
  select.addItem(item2);

  var recordingHandler = new goog.testing.recordFunction();
  goog.events.listen(
      select, goog.ui.Component.EventType.CHANGE, recordingHandler);

  select.setSelectedItem(item2);
  assertEquals('Selecting new item should fire CHANGE event.',
      1, recordingHandler.getCallCount());

  select.setSelectedItem(item2);
  assertEquals('Selecting the same item should not fire CHANGE event.',
      1, recordingHandler.getCallCount());

  select.setSelectedIndex(0);
  assertEquals('Selecting new item should fire CHANGE event.',
      2, recordingHandler.getCallCount());

  select.setSelectedIndex(0);
  assertEquals('Selecting the same item should not fire CHANGE event.',
      2, recordingHandler.getCallCount());
}

function testSelectEventFiresForUserInitiatedAction() {
  select.render();
  var item1 = new goog.ui.MenuItem('item 1');
  var item2 = new goog.ui.MenuItem('item 2');
  select.addItem(item1);
  select.addItem(item2);

  var recordingHandler = new goog.testing.recordFunction();
  goog.events.listen(
      select, goog.ui.Component.EventType.CHANGE, recordingHandler);

  select.setOpen(true);

  item2.dispatchEvent(goog.ui.Component.EventType.ACTION);
  assertEquals('Selecting new item should fire CHANGE event.',
      1, recordingHandler.getCallCount());
  assertFalse(select.isOpen());

  select.setOpen(true);

  item2.dispatchEvent(goog.ui.Component.EventType.ACTION);
  assertEquals('Selecting the same item should not fire CHANGE event.',
      1, recordingHandler.getCallCount());
  assertFalse(select.isOpen());
}


/**
 * Checks that if an item is selected before decorate is called, the selection
 * is preserved after decorate.
 */
function testSetSelectedItemBeforeRender() {
  select.addItem(new goog.ui.MenuItem('item 1'));
  select.addItem(new goog.ui.MenuItem('item 2'));
  var item3 = new goog.ui.MenuItem('item 3');
  select.addItem(item3);
  select.setSelectedItem(item3);
  assertEquals(2, select.getSelectedIndex());

  select.decorate(sandboxEl);
  assertEquals(2, select.getSelectedIndex());
}


/**
 * Checks that if a value is set before decorate is called, the value is
 * preserved after decorate.
 */
function testSetValueBeforeRender() {
  select.addItem(new goog.ui.MenuItem('item 1', 1));
  select.addItem(new goog.ui.MenuItem('item 2', 2));
  select.setValue(2);
  assertEquals(2, select.getValue());

  select.decorate(sandboxEl);
  assertEquals(2, select.getValue());
}


function testUpdateCaption_aria() {
  select.render(sandboxEl);

  // Verify default state.
  assertEquals(defaultCaption, select.getCaption());
  assertFalse(
      !!goog.a11y.aria.getLabel(
          select.getRenderer().getContentElement(select.getElement())));

  // Add and select an item with aria-label.
  var item1 = new goog.ui.MenuItem();
  select.addItem(item1);
  item1.getElement().setAttribute('aria-label', 'item1');
  select.setSelectedIndex(0);
  assertEquals(
      'item1',
      goog.a11y.aria.getLabel(
          select.getRenderer().getContentElement(select.getElement())));

  // Add and select an item without a label.
  var item2 = new goog.ui.MenuItem();
  select.addItem(item2);
  select.setSelectedIndex(1);
  assertFalse(
      !!goog.a11y.aria.getLabel(
          select.getRenderer().getContentElement(select.getElement())));
}

function testDisposeWhenInnerHTMLHasBeenClearedInIE10() {
  assertNotThrows(function() {
    var customSelect = new goog.ui.Select(null /* label */, new goog.ui.Menu(),
        new goog.ui.CustomButtonRenderer());
    customSelect.render(sandboxEl);

    // In IE10 setting the innerHTML of a node invalidates the parent child
    // relation of all its child nodes (unlike removeNode).
    sandboxEl.innerHTML = '';

    // goog.ui.Select's disposeInternal trigger's goog.ui.Component's
    // disposeInternal, which triggers goog.ui.MenuButton's exitDocument,
    // which closes the associated menu and updates the activeDescendent.
    // In the case of a CustomMenuButton the contentElement is referenced by
    // element.firstChild.firstChild, an invalid relation in IE 10.
    customSelect.dispose();
  });
}
