container_test.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644
  1. // Copyright 2008 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. goog.provide('goog.ui.ContainerTest');
  15. goog.setTestOnly('goog.ui.ContainerTest');
  16. goog.require('goog.a11y.aria');
  17. goog.require('goog.dom');
  18. goog.require('goog.dom.TagName');
  19. goog.require('goog.dom.classlist');
  20. goog.require('goog.events');
  21. goog.require('goog.events.Event');
  22. goog.require('goog.events.KeyCodes');
  23. goog.require('goog.events.KeyEvent');
  24. goog.require('goog.testing.events');
  25. goog.require('goog.testing.jsunit');
  26. goog.require('goog.ui.Component');
  27. goog.require('goog.ui.Container');
  28. goog.require('goog.ui.Control');
  29. var sandbox;
  30. var containerElement;
  31. var container;
  32. var keyContainer;
  33. var listContainer;
  34. function setUpPage() {
  35. sandbox = goog.dom.getElement('sandbox');
  36. }
  37. function setUp() {
  38. container = new goog.ui.Container();
  39. keyContainer = null;
  40. listContainer = null;
  41. sandbox.innerHTML = '<div id="containerElement" class="goog-container">\n' +
  42. ' <div class="goog-control" id="hello">Hello</div>\n' +
  43. ' <div class="goog-control" id="world">World</div>\n' +
  44. '</div>';
  45. containerElement = goog.dom.getElement('containerElement');
  46. }
  47. function tearDown() {
  48. goog.dom.removeChildren(sandbox);
  49. container.dispose();
  50. goog.dispose(keyContainer);
  51. goog.dispose(listContainer);
  52. }
  53. function testDecorateHidden() {
  54. containerElement.style.display = 'none';
  55. assertTrue('Container must be visible', container.isVisible());
  56. container.decorate(containerElement);
  57. assertFalse('Container must be hidden', container.isVisible());
  58. container.forEachChild(function(control) {
  59. assertTrue(
  60. 'Child control ' + control.getId() + ' must report being ' +
  61. 'visible, even if in a hidden container',
  62. control.isVisible());
  63. });
  64. }
  65. function testDecorateDisabled() {
  66. goog.dom.classlist.add(containerElement, 'goog-container-disabled');
  67. assertTrue('Container must be enabled', container.isEnabled());
  68. container.decorate(containerElement);
  69. assertFalse('Container must be disabled', container.isEnabled());
  70. container.forEachChild(function(control) {
  71. assertFalse(
  72. 'Child control ' + control.getId() + ' must be disabled, ' +
  73. 'because the host container is disabled',
  74. control.isEnabled());
  75. });
  76. }
  77. function testDecorateFocusableContainer() {
  78. container.decorate(containerElement);
  79. assertTrue('Container must be focusable', container.isFocusable());
  80. container.forEachChild(function(control) {
  81. assertFalse(
  82. 'Child control ' + control.getId() + ' must not be ' +
  83. 'focusable',
  84. control.isSupportedState(goog.ui.Component.State.FOCUSED));
  85. });
  86. }
  87. function testDecorateFocusableChildrenContainer() {
  88. container.setFocusable(false);
  89. container.setFocusableChildrenAllowed(true);
  90. container.decorate(containerElement);
  91. assertFalse('Container must not be focusable', container.isFocusable());
  92. container.forEachChild(function(control) {
  93. assertTrue(
  94. 'Child control ' + control.getId() + ' must be ' +
  95. 'focusable',
  96. control.isSupportedState(goog.ui.Component.State.FOCUSED));
  97. });
  98. }
  99. function testHighlightOnEnter() {
  100. // This interaction test ensures that containers enforce that children
  101. // get highlighted on mouseover, and that one and only one child may
  102. // be highlighted at a time. Although integration tests aren't the
  103. // best, it's difficult to test these event-based interactions due to
  104. // their disposition toward the "misunderstood contract" problem.
  105. container.decorate(containerElement);
  106. assertFalse(
  107. 'Child 0 should initially not be highlighted',
  108. container.getChildAt(0).isHighlighted());
  109. goog.testing.events.fireMouseOverEvent(
  110. container.getChildAt(0).getElement(), sandbox);
  111. assertTrue(
  112. 'Child 0 should become highlighted after a mouse over',
  113. container.getChildAt(0).isHighlighted());
  114. assertEquals(
  115. 'Child 0 should be the active descendant',
  116. container.getChildAt(0).getElement(),
  117. goog.a11y.aria.getActiveDescendant(container.getElement()));
  118. goog.testing.events.fireMouseOverEvent(
  119. container.getChildAt(1).getElement(),
  120. container.getChildAt(0).getElement());
  121. assertFalse(
  122. 'Child 0 should lose highlight when child 1 is moused ' +
  123. 'over, even if no mouseout occurs.',
  124. container.getChildAt(0).isHighlighted());
  125. assertTrue(
  126. 'Child 1 should now be highlighted.',
  127. container.getChildAt(1).isHighlighted());
  128. assertEquals(
  129. 'Child 1 should be the active descendant',
  130. container.getChildAt(1).getElement(),
  131. goog.a11y.aria.getActiveDescendant(container.getElement()));
  132. }
  133. function testHighlightOnEnterPreventable() {
  134. container.decorate(containerElement);
  135. goog.events.listen(
  136. container, goog.ui.Component.EventType.ENTER,
  137. function(event) { event.preventDefault(); });
  138. goog.testing.events.fireMouseOverEvent(
  139. container.getChildAt(0).getElement(), sandbox);
  140. assertFalse(
  141. 'Child 0 should not be highlighted if preventDefault called',
  142. container.getChildAt(0).isHighlighted());
  143. }
  144. function testHighlightDisabled() {
  145. // Another interaction test. Already tested in control_test.
  146. container.decorate(containerElement);
  147. container.getChildAt(0).setEnabled(false);
  148. goog.testing.events.fireMouseOverEvent(
  149. container.getChildAt(0).getElement(), sandbox);
  150. assertFalse(
  151. 'Disabled children should not be highlighted',
  152. container.getChildAt(0).isHighlighted());
  153. }
  154. function testGetOwnerControl() {
  155. container.decorate(containerElement);
  156. assertEquals(
  157. 'Must return appropriate control given an element in the ' +
  158. 'control.',
  159. container.getChildAt(1),
  160. container.getOwnerControl(container.getChildAt(1).getElement()));
  161. assertNull(
  162. 'Must return null for element not associated with control.',
  163. container.getOwnerControl(document.body));
  164. assertNull(
  165. 'Must return null if given null node', container.getOwnerControl(null));
  166. }
  167. function testShowEvent() {
  168. container.decorate(containerElement);
  169. container.setVisible(false);
  170. var eventFired = false;
  171. goog.events.listen(container, goog.ui.Component.EventType.SHOW, function() {
  172. assertFalse(
  173. 'Container must not be visible when SHOW event is ' +
  174. 'fired',
  175. container.isVisible());
  176. eventFired = true;
  177. });
  178. container.setVisible(true);
  179. assertTrue('SHOW event expected', eventFired);
  180. }
  181. function testAfterShowEvent() {
  182. container.decorate(containerElement);
  183. container.setVisible(false);
  184. var eventFired = false;
  185. goog.events.listen(
  186. container, goog.ui.Container.EventType.AFTER_SHOW, function() {
  187. assertTrue(
  188. 'Container must be visible when AFTER_SHOW event is ' +
  189. 'fired',
  190. container.isVisible());
  191. eventFired = true;
  192. });
  193. container.setVisible(true);
  194. assertTrue('AFTER_SHOW event expected', eventFired);
  195. }
  196. function testHideEvents() {
  197. var events = [];
  198. container.decorate(containerElement);
  199. container.setVisible(true);
  200. var eventFired = false;
  201. goog.events.listen(container, goog.ui.Component.EventType.HIDE, function(e) {
  202. assertTrue(
  203. 'Container must be visible when HIDE event is fired',
  204. container.isVisible());
  205. events.push(e.type);
  206. });
  207. goog.events.listen(
  208. container, goog.ui.Container.EventType.AFTER_HIDE, function(e) {
  209. assertFalse(
  210. 'Container must not be visible when AFTER_HIDE event is fired',
  211. container.isVisible());
  212. events.push(e.type);
  213. });
  214. container.setVisible(false);
  215. assertArrayEquals(
  216. 'HIDE event followed by AFTER_HIDE expected',
  217. [
  218. goog.ui.Component.EventType.HIDE, goog.ui.Container.EventType.AFTER_HIDE
  219. ],
  220. events);
  221. }
  222. /**
  223. * Test container to which the elements have to be added with
  224. * {@code container.addChild(element, false)}
  225. * @constructor
  226. * @extends {goog.ui.Container}
  227. */
  228. function ListContainer() {
  229. goog.ui.Container.call(this);
  230. }
  231. goog.inherits(ListContainer, goog.ui.Container);
  232. /** @override */
  233. ListContainer.prototype.createDom = function() {
  234. ListContainer.superClass_.createDom.call(this);
  235. var ul = this.getDomHelper().createDom(goog.dom.TagName.UL);
  236. this.forEachChild(function(child) {
  237. child.createDom();
  238. var childEl = child.getElement();
  239. ul.appendChild(
  240. this.getDomHelper().createDom(goog.dom.TagName.LI, {}, childEl));
  241. }, this);
  242. this.getContentElement().appendChild(ul);
  243. };
  244. function testGetOwnerControlWithNoRenderingInAddChild() {
  245. listContainer = new ListContainer();
  246. var control = new goog.ui.Control('item');
  247. listContainer.addChild(control);
  248. listContainer.render();
  249. var ownerControl = listContainer.getOwnerControl(control.getElement());
  250. assertEquals(
  251. 'Control was added with addChild(control, false)', control, ownerControl);
  252. }
  253. /**
  254. * Test container for tracking key events being handled.
  255. * @constructor
  256. * @extends {goog.ui.Container}
  257. */
  258. function KeyHandlingContainer() {
  259. goog.ui.Container.call(this);
  260. this.keyEventsHandled = 0;
  261. }
  262. goog.inherits(KeyHandlingContainer, goog.ui.Container);
  263. /** @override */
  264. KeyHandlingContainer.prototype.handleKeyEventInternal = function() {
  265. this.keyEventsHandled++;
  266. return false;
  267. };
  268. function testHandleKeyEvent_onlyHandlesWhenVisible() {
  269. keyContainer = new KeyHandlingContainer();
  270. keyContainer.decorate(containerElement);
  271. keyContainer.setVisible(false);
  272. keyContainer.handleKeyEvent(new goog.events.Event());
  273. assertEquals(
  274. 'No key events should be handled', 0, keyContainer.keyEventsHandled);
  275. keyContainer.setVisible(true);
  276. keyContainer.handleKeyEvent(new goog.events.Event());
  277. assertEquals(
  278. 'One key event should be handled', 1, keyContainer.keyEventsHandled);
  279. }
  280. function testHandleKeyEvent_onlyHandlesWhenEnabled() {
  281. keyContainer = new KeyHandlingContainer();
  282. keyContainer.decorate(containerElement);
  283. keyContainer.setVisible(true);
  284. keyContainer.setEnabled(false);
  285. keyContainer.handleKeyEvent(new goog.events.Event());
  286. assertEquals(
  287. 'No key events should be handled', 0, keyContainer.keyEventsHandled);
  288. keyContainer.setEnabled(true);
  289. keyContainer.handleKeyEvent(new goog.events.Event());
  290. assertEquals(
  291. 'One key event should be handled', 1, keyContainer.keyEventsHandled);
  292. }
  293. function testHandleKeyEvent_childlessContainersIgnoreKeyEvents() {
  294. keyContainer = new KeyHandlingContainer();
  295. keyContainer.render();
  296. keyContainer.setVisible(true);
  297. keyContainer.handleKeyEvent(new goog.events.Event());
  298. assertEquals(
  299. 'No key events should be handled', 0, keyContainer.keyEventsHandled);
  300. keyContainer.addChild(new goog.ui.Control());
  301. keyContainer.handleKeyEvent(new goog.events.Event());
  302. assertEquals(
  303. 'One key event should be handled', 1, keyContainer.keyEventsHandled);
  304. }
  305. function testHandleKeyEvent_alwaysHandlesWithKeyEventTarget() {
  306. keyContainer = new KeyHandlingContainer();
  307. keyContainer.render();
  308. keyContainer.setKeyEventTarget(goog.dom.createDom(goog.dom.TagName.DIV));
  309. keyContainer.setVisible(true);
  310. keyContainer.handleKeyEvent(new goog.events.Event());
  311. assertEquals(
  312. 'One key events should be handled', 1, keyContainer.keyEventsHandled);
  313. }
  314. function testHandleKeyEventInternal_onlyHandlesUnmodified() {
  315. container.setKeyEventTarget(sandbox);
  316. var event =
  317. new goog.events.KeyEvent(goog.events.KeyCodes.ESC, 0, false, null);
  318. var propertyNames = ['shiftKey', 'altKey', 'ctrlKey', 'metaKey'];
  319. // Verify that the event is not handled whenever a modifier key is true.
  320. for (var i = 0, propertyName; propertyName = propertyNames[i]; i++) {
  321. assertTrue(
  322. 'Event should be handled when modifer key is not pressed.',
  323. container.handleKeyEventInternal(event));
  324. event[propertyName] = true;
  325. assertFalse(
  326. 'Event should not be handled when modifer key is pressed.',
  327. container.handleKeyEventInternal(event));
  328. event[propertyName] = false;
  329. }
  330. }
  331. function testOpenFollowsHighlight() {
  332. container.decorate(containerElement);
  333. container.setOpenFollowsHighlight(true);
  334. assertTrue(
  335. 'isOpenFollowsHighlight should return true',
  336. container.isOpenFollowsHighlight());
  337. // Make the children openable.
  338. container.forEachChild(function(child) {
  339. child.setSupportedState(goog.ui.Component.State.OPENED, true);
  340. });
  341. // Open child 1 initially.
  342. container.getChildAt(1).setOpen(true);
  343. assertFalse(
  344. 'Child 0 should initially not be highlighted',
  345. container.getChildAt(0).isHighlighted());
  346. goog.testing.events.fireMouseOverEvent(
  347. container.getChildAt(0).getElement(), sandbox);
  348. assertTrue(
  349. 'Child 0 should become highlighted after a mouse over',
  350. container.getChildAt(0).isHighlighted());
  351. assertTrue(
  352. 'Child 0 should become open after higlighted',
  353. container.getChildAt(0).isOpen());
  354. assertFalse(
  355. 'Child 1 should become closed once 0 is open',
  356. container.getChildAt(1).isOpen());
  357. assertEquals(
  358. 'OpenItem should be child 0', container.getChildAt(0),
  359. container.getOpenItem());
  360. }
  361. function testOpenNotFollowsHighlight() {
  362. container.decorate(containerElement);
  363. container.setOpenFollowsHighlight(false);
  364. assertFalse(
  365. 'isOpenFollowsHighlight should return false',
  366. container.isOpenFollowsHighlight());
  367. // Make the children openable.
  368. container.forEachChild(function(child) {
  369. child.setSupportedState(goog.ui.Component.State.OPENED, true);
  370. });
  371. // Open child 1 initially.
  372. container.getChildAt(1).setOpen(true);
  373. assertFalse(
  374. 'Child 0 should initially not be highlighted',
  375. container.getChildAt(0).isHighlighted());
  376. goog.testing.events.fireMouseOverEvent(
  377. container.getChildAt(0).getElement(), sandbox);
  378. assertTrue(
  379. 'Child 0 should become highlighted after a mouse over',
  380. container.getChildAt(0).isHighlighted());
  381. assertFalse(
  382. 'Child 0 should remain closed after higlighted',
  383. container.getChildAt(0).isOpen());
  384. assertTrue('Child 1 should remain open', container.getChildAt(1).isOpen());
  385. assertEquals(
  386. 'OpenItem should be child 1', container.getChildAt(1),
  387. container.getOpenItem());
  388. }
  389. function testRemoveChild() {
  390. goog.dom.removeChildren(containerElement);
  391. container.decorate(containerElement);
  392. var a = new goog.ui.Control('A');
  393. var b = new goog.ui.Control('B');
  394. var c = new goog.ui.Control('C');
  395. a.setId('a');
  396. b.setId('b');
  397. c.setId('c');
  398. container.addChild(a, true);
  399. container.addChild(b, true);
  400. container.addChild(c, true);
  401. container.setHighlightedIndex(2);
  402. assertEquals(
  403. 'Parent must remove and return child by ID', b,
  404. container.removeChild('b'));
  405. assertNull(
  406. 'Parent must no longer contain this child', container.getChild('b'));
  407. assertEquals(
  408. 'Highlighted index must be decreased', 1,
  409. container.getHighlightedIndex());
  410. assertTrue(
  411. 'The removed control must handle its own mouse events',
  412. b.isHandleMouseEvents());
  413. assertEquals(
  414. 'Parent must remove and return child', c, container.removeChild(c));
  415. assertNull(
  416. 'Parent must no longer contain this child', container.getChild('c'));
  417. assertFalse('This child must no longer be highlighted', c.isHighlighted());
  418. assertTrue(
  419. 'The removed control must handle its own mouse events',
  420. c.isHandleMouseEvents());
  421. assertEquals(
  422. 'Parent must remove and return child by index', a,
  423. container.removeChildAt(0));
  424. assertNull(
  425. 'Parent must no longer contain this child', container.getChild('a'));
  426. assertTrue(
  427. 'The removed control must handle its own mouse events',
  428. a.isHandleMouseEvents());
  429. }
  430. function testRemoveHighlightedDisposedChild() {
  431. goog.dom.removeChildren(containerElement);
  432. container.decorate(containerElement);
  433. var a = new goog.ui.Control('A');
  434. container.addChild(a, true);
  435. container.setHighlightedIndex(0);
  436. a.dispose();
  437. container.removeChild(a);
  438. container.dispose();
  439. }
  440. /**
  441. * Checks that getHighlighted() returns the expected value and checks
  442. * that the child at this index is highlighted and other children are not.
  443. * @param {string} explanation Message indicating what is expected.
  444. * @param {number} index Expected return value of getHighlightedIndex().
  445. */
  446. function assertHighlightedIndex(explanation, index) {
  447. assertEquals(explanation, index, container.getHighlightedIndex());
  448. for (var i = 0; i < container.getChildCount(); i++) {
  449. if (i == index) {
  450. assertTrue(
  451. 'Child at highlighted index should be highlighted',
  452. container.getChildAt(i).isHighlighted());
  453. } else {
  454. assertFalse(
  455. 'Only child at highlighted index should be highlighted',
  456. container.getChildAt(i).isHighlighted());
  457. }
  458. }
  459. }
  460. function testUpdateHighlightedIndex_updatesWhenChildrenAreAdded() {
  461. goog.dom.removeChildren(containerElement);
  462. container.decorate(containerElement);
  463. var a = new goog.ui.Control('A');
  464. var b = new goog.ui.Control('B');
  465. var c = new goog.ui.Control('C');
  466. container.addChild(a);
  467. container.setHighlightedIndex(0);
  468. assertHighlightedIndex('Highlighted index should match set value', 0);
  469. // Add child before the highlighted one.
  470. container.addChildAt(b, 0);
  471. assertHighlightedIndex('Highlighted index should be increased', 1);
  472. // Add child after the highlighted one.
  473. container.addChildAt(c, 2);
  474. assertHighlightedIndex('Highlighted index should not change', 1);
  475. container.dispose();
  476. }
  477. function testUpdateHighlightedIndex_updatesWhenChildrenAreMoved() {
  478. goog.dom.removeChildren(containerElement);
  479. container.decorate(containerElement);
  480. var a = new goog.ui.Control('A');
  481. var b = new goog.ui.Control('B');
  482. var c = new goog.ui.Control('C');
  483. container.addChild(a);
  484. container.addChild(b);
  485. container.addChild(c);
  486. // Highlight 'c' and swap 'a' and 'b'
  487. // [a, b, c] -> [a, b, *c] -> [b, a, *c] (* indicates the highlighted child)
  488. container.setHighlightedIndex(2);
  489. container.addChildAt(a, 1, false);
  490. assertHighlightedIndex('Highlighted index should not change', 2);
  491. // Move the highlighted child 'c' from index 2 to index 1.
  492. // [b, a, *c] -> [b, *c, a]
  493. container.addChildAt(c, 1, false);
  494. assertHighlightedIndex('Highlighted index must follow the moved child', 1);
  495. // Take the element in front of the highlighted index and move it behind it.
  496. // [b, *c, a] -> [*c, a, b]
  497. container.addChildAt(b, 2, false);
  498. assertHighlightedIndex('Highlighted index must be decreased', 0);
  499. // And move the element back to the front.
  500. // [*c, a, b] -> [b, *c, a]
  501. container.addChildAt(b, 0, false);
  502. assertHighlightedIndex('Highlighted index must be increased', 1);
  503. container.dispose();
  504. }
  505. function testUpdateHighlightedIndex_notChangedOnNoOp() {
  506. goog.dom.removeChildren(containerElement);
  507. container.decorate(containerElement);
  508. container.addChild(new goog.ui.Control('A'));
  509. container.addChild(new goog.ui.Control('B'));
  510. container.setHighlightedIndex(1);
  511. // Re-add a child to its current position.
  512. container.addChildAt(container.getChildAt(0), 0, false);
  513. assertHighlightedIndex('Highlighted index must not change', 1);
  514. container.dispose();
  515. }
  516. function testUpdateHighlightedIndex_notChangedWhenNoChildSelected() {
  517. goog.dom.removeChildren(containerElement);
  518. container.decorate(containerElement);
  519. var a = new goog.ui.Control('A');
  520. var b = new goog.ui.Control('B');
  521. var c = new goog.ui.Control('C');
  522. container.addChild(a);
  523. container.addChild(b);
  524. container.addChild(c);
  525. // Move children around.
  526. container.addChildAt(a, 2, false);
  527. container.addChildAt(b, 1, false);
  528. container.addChildAt(c, 2, false);
  529. assertHighlightedIndex('Highlighted index must not change', -1);
  530. container.dispose();
  531. }
  532. function testUpdateHighlightedIndex_indexStaysInBoundsWhenMovedToMaxIndex() {
  533. goog.dom.removeChildren(containerElement);
  534. container.decorate(containerElement);
  535. var a = new goog.ui.Control('A');
  536. var b = new goog.ui.Control('B');
  537. container.addChild(a);
  538. container.addChild(b);
  539. // Move higlighted child to an index one behind last child.
  540. container.setHighlightedIndex(0);
  541. container.addChildAt(a, 2);
  542. assertEquals('Child should be moved to index 1', a, container.getChildAt(1));
  543. assertEquals('Child count should not change', 2, container.getChildCount());
  544. assertHighlightedIndex('Highlighted index must point to new index', 1);
  545. container.dispose();
  546. }