blob: 85334b81d2938ac17b54744002c2b37456a75fab [file] [log] [blame]
// 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();
}