menubutton_test.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  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.MenuButtonTest');
  15. goog.setTestOnly('goog.ui.MenuButtonTest');
  16. goog.require('goog.Timer');
  17. goog.require('goog.a11y.aria');
  18. goog.require('goog.a11y.aria.State');
  19. goog.require('goog.dom');
  20. goog.require('goog.dom.TagName');
  21. goog.require('goog.events');
  22. goog.require('goog.events.Event');
  23. goog.require('goog.events.EventType');
  24. goog.require('goog.events.KeyCodes');
  25. goog.require('goog.events.KeyHandler');
  26. goog.require('goog.positioning');
  27. goog.require('goog.positioning.Corner');
  28. goog.require('goog.positioning.MenuAnchoredPosition');
  29. goog.require('goog.positioning.Overflow');
  30. goog.require('goog.style');
  31. goog.require('goog.testing.ExpectedFailures');
  32. goog.require('goog.testing.PropertyReplacer');
  33. goog.require('goog.testing.events');
  34. goog.require('goog.testing.jsunit');
  35. goog.require('goog.testing.recordFunction');
  36. goog.require('goog.ui.Component');
  37. goog.require('goog.ui.Menu');
  38. goog.require('goog.ui.MenuButton');
  39. goog.require('goog.ui.MenuItem');
  40. goog.require('goog.ui.SubMenu');
  41. goog.require('goog.userAgent');
  42. goog.require('goog.userAgent.product');
  43. goog.require('goog.userAgent.product.isVersion');
  44. var menuButton;
  45. var clonedMenuButtonDom;
  46. var expectedFailures;
  47. function setUpPage() {
  48. expectedFailures = new goog.testing.ExpectedFailures();
  49. }
  50. // Mock out goog.positioning.positionAtCoordinate to always ignore failure when
  51. // the window is too small, since we don't care about the viewport size on
  52. // the selenium farm.
  53. // TODO(nicksantos): Move this into a common location if we ever have enough
  54. // code for a general goog.testing.ui library.
  55. var originalPositionAtCoordinate = goog.positioning.positionAtCoordinate;
  56. goog.positioning.positionAtCoordinate = function(
  57. absolutePos, movableElement, movableElementCorner, opt_margin, opt_viewport,
  58. opt_overflow, opt_preferredSize) {
  59. return originalPositionAtCoordinate.call(
  60. this, absolutePos, movableElement, movableElementCorner, opt_margin,
  61. opt_viewport, goog.positioning.Overflow.IGNORE, opt_preferredSize);
  62. };
  63. function MyFakeEvent(keyCode, opt_eventType) {
  64. this.type = opt_eventType || goog.events.KeyHandler.EventType.KEY;
  65. this.keyCode = keyCode;
  66. this.propagationStopped = false;
  67. this.preventDefault = goog.nullFunction;
  68. this.stopPropagation = function() { this.propagationStopped = true; };
  69. }
  70. function setUp() {
  71. window.scrollTo(0, 0);
  72. var viewportSize = goog.dom.getViewportSize();
  73. // Some tests need enough size viewport.
  74. if (viewportSize.width < 600 || viewportSize.height < 600) {
  75. window.moveTo(0, 0);
  76. window.resizeTo(640, 640);
  77. }
  78. clonedMenuButtonDom = goog.dom.getElement('demoMenuButton').cloneNode(true);
  79. menuButton = new goog.ui.MenuButton();
  80. }
  81. function tearDown() {
  82. expectedFailures.handleTearDown();
  83. menuButton.dispose();
  84. var element = goog.dom.getElement('demoMenuButton');
  85. element.parentNode.replaceChild(clonedMenuButtonDom, element);
  86. }
  87. /**
  88. * Check if the aria-haspopup property is set correctly.
  89. */
  90. function checkHasPopUp() {
  91. menuButton.enterDocument();
  92. assertFalse(
  93. 'Menu button must have aria-haspopup attribute set to false',
  94. goog.a11y.aria.getState(
  95. menuButton.getElement(), goog.a11y.aria.State.HASPOPUP));
  96. var menu = new goog.ui.Menu();
  97. menu.createDom();
  98. menuButton.setMenu(menu);
  99. assertTrue(
  100. 'Menu button must have aria-haspopup attribute set to true',
  101. goog.a11y.aria.getState(
  102. menuButton.getElement(), goog.a11y.aria.State.HASPOPUP));
  103. menuButton.setMenu(null);
  104. assertFale(
  105. 'Menu button must have aria-haspopup attribute set to false',
  106. goog.a11y.aria.getState(
  107. menuButton.getElement(), goog.a11y.aria.State.HASPOPUP));
  108. }
  109. /**
  110. * Open the menu and click on the menu item inside.
  111. * Check if the aria-haspopup property is set correctly.
  112. */
  113. function testBasicButtonBehavior() {
  114. var node = goog.dom.getElement('demoMenuButton');
  115. menuButton.decorate(node);
  116. assertEquals(
  117. 'Menu button must have aria-haspopup attribute set to true', 'true',
  118. goog.a11y.aria.getState(
  119. menuButton.getElement(), goog.a11y.aria.State.HASPOPUP));
  120. goog.testing.events.fireClickSequence(node);
  121. assertTrue('Menu must open after click', menuButton.isOpen());
  122. var menuItemClicked = 0;
  123. var lastMenuItemClicked = null;
  124. goog.events.listen(
  125. menuButton.getMenu(), goog.ui.Component.EventType.ACTION, function(e) {
  126. menuItemClicked++;
  127. lastMenuItemClicked = e.target;
  128. });
  129. var menuItem2 = goog.dom.getElement('menuItem2');
  130. goog.testing.events.fireClickSequence(menuItem2);
  131. assertFalse('Menu must close on clicking when open', menuButton.isOpen());
  132. assertEquals('Number of menu items clicked should be 1', 1, menuItemClicked);
  133. assertEquals(
  134. 'menuItem2 should be the last menuitem clicked', menuItem2,
  135. lastMenuItemClicked.getElement());
  136. }
  137. /**
  138. * Open the menu, highlight first menuitem and then the second.
  139. * Check if the aria-activedescendant property is set correctly.
  140. */
  141. function testHighlightItemBehavior() {
  142. var node = goog.dom.getElement('demoMenuButton');
  143. menuButton.decorate(node);
  144. goog.testing.events.fireClickSequence(node);
  145. assertTrue('Menu must open after click', menuButton.isOpen());
  146. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.DOWN));
  147. assertNotNull(menuButton.getElement());
  148. assertEquals(
  149. 'First menuitem must be the aria-activedescendant', 'menuItem1',
  150. goog.a11y.aria.getState(
  151. menuButton.getElement(), goog.a11y.aria.State.ACTIVEDESCENDANT));
  152. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.DOWN));
  153. assertEquals(
  154. 'Second menuitem must be the aria-activedescendant', 'menuItem2',
  155. goog.a11y.aria.getState(
  156. menuButton.getElement(), goog.a11y.aria.State.ACTIVEDESCENDANT));
  157. }
  158. /**
  159. * Check that the appropriate items are selected when menus are opened with the
  160. * keyboard and setSelectFirstOnEnterOrSpace is not set.
  161. */
  162. function testHighlightFirstOnOpen() {
  163. var node = goog.dom.getElement('demoMenuButton');
  164. menuButton.decorate(node);
  165. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.ENTER));
  166. assertEquals(
  167. 'By default no items should be highlighted when opened with enter.', null,
  168. menuButton.getMenu().getHighlighted());
  169. menuButton.setOpen(false);
  170. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.DOWN));
  171. assertTrue('Menu must open after down key', menuButton.isOpen());
  172. assertEquals(
  173. 'First menuitem must be highlighted', 'menuItem1',
  174. menuButton.getMenu().getHighlighted().getElement().id);
  175. }
  176. /**
  177. * Check that the appropriate items are selected when menus are opened with the
  178. * keyboard, setSelectFirstOnEnterOrSpace is not set, and the first menu item is
  179. * disabled.
  180. */
  181. function testHighlightFirstOnOpen_withFirstDisabled() {
  182. var node = goog.dom.getElement('demoMenuButton');
  183. menuButton.decorate(node);
  184. var menu = menuButton.getMenu();
  185. menu.getItemAt(0).setEnabled(false);
  186. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.ENTER));
  187. assertEquals(
  188. 'By default no items should be highlighted when opened with enter.', null,
  189. menuButton.getMenu().getHighlighted());
  190. menuButton.setOpen(false);
  191. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.DOWN));
  192. assertTrue('Menu must open after down key', menuButton.isOpen());
  193. assertEquals(
  194. 'First enabled menuitem must be highlighted', 'menuItem2',
  195. menuButton.getMenu().getHighlighted().getElement().id);
  196. }
  197. /**
  198. * Check that the appropriate items are selected when menus are opened with the
  199. * keyboard and setSelectFirstOnEnterOrSpace is set.
  200. */
  201. function testHighlightFirstOnOpen_withEnterOrSpaceSet() {
  202. var node = goog.dom.getElement('demoMenuButton');
  203. menuButton.decorate(node);
  204. menuButton.setSelectFirstOnEnterOrSpace(true);
  205. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.ENTER));
  206. assertEquals(
  207. 'The first item should be highlighted when opened with enter ' +
  208. 'after setting selectFirstOnEnterOrSpace',
  209. 'menuItem1', menuButton.getMenu().getHighlighted().getElement().id);
  210. }
  211. /**
  212. * Check that the appropriate item is selected when a menu is opened with the
  213. * keyboard, setSelectFirstOnEnterOrSpace is true, and the first menu item is
  214. * disabled.
  215. */
  216. function testHighlightFirstOnOpen_withEnterOrSpaceSetAndFirstDisabled() {
  217. var node = goog.dom.getElement('demoMenuButton');
  218. menuButton.decorate(node);
  219. menuButton.setSelectFirstOnEnterOrSpace(true);
  220. var menu = menuButton.getMenu();
  221. menu.getItemAt(0).setEnabled(false);
  222. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.ENTER));
  223. assertEquals(
  224. 'The first enabled item should be highlighted when opened ' +
  225. 'with enter after setting selectFirstOnEnterOrSpace',
  226. 'menuItem2', menuButton.getMenu().getHighlighted().getElement().id);
  227. }
  228. /**
  229. * Open the menu, enter a submenu and then back out of it.
  230. * Check if the aria-activedescendant property is set correctly.
  231. */
  232. function testCloseSubMenuBehavior() {
  233. var node = goog.dom.getElement('demoMenuButton');
  234. menuButton.decorate(node);
  235. var menu = menuButton.getMenu();
  236. var subMenu = new goog.ui.SubMenu('Submenu');
  237. menu.addItem(subMenu);
  238. subMenu.getElement().id = 'subMenu';
  239. var subMenuMenu = new goog.ui.Menu();
  240. subMenu.setMenu(subMenuMenu);
  241. var subMenuItem = new goog.ui.MenuItem('Submenu item 1');
  242. subMenuMenu.addItem(subMenuItem);
  243. subMenuItem.getElement().id = 'subMenuItem1';
  244. menuButton.setOpen(true);
  245. for (var i = 0; i < 4; i++) {
  246. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.DOWN));
  247. }
  248. assertEquals(
  249. 'Submenu must be the aria-activedescendant', 'subMenu',
  250. goog.a11y.aria.getState(
  251. menuButton.getElement(), goog.a11y.aria.State.ACTIVEDESCENDANT));
  252. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.RIGHT));
  253. assertEquals(
  254. 'Submenu item 1 must be the aria-activedescendant', 'subMenuItem1',
  255. goog.a11y.aria.getState(
  256. menuButton.getElement(), goog.a11y.aria.State.ACTIVEDESCENDANT));
  257. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.LEFT));
  258. assertEquals(
  259. 'Submenu must be the aria-activedescendant', 'subMenu',
  260. goog.a11y.aria.getState(
  261. menuButton.getElement(), goog.a11y.aria.State.ACTIVEDESCENDANT));
  262. }
  263. /**
  264. * Make sure the menu opens when enter is pressed.
  265. */
  266. function testEnterOpensMenu() {
  267. var node = goog.dom.getElement('demoMenuButton');
  268. menuButton.decorate(node);
  269. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.ENTER));
  270. assertTrue('Menu must open after enter', menuButton.isOpen());
  271. }
  272. /**
  273. * Tests the behavior of the enter and space keys when the menu is open.
  274. */
  275. function testSpaceOrEnterClosesMenu() {
  276. var node = goog.dom.getElement('demoMenuButton');
  277. menuButton.decorate(node);
  278. menuButton.setOpen(true);
  279. menuButton.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.ENTER));
  280. assertFalse('Menu should close after pressing Enter', menuButton.isOpen());
  281. menuButton.setOpen(true);
  282. menuButton.handleKeyEvent(
  283. new MyFakeEvent(goog.events.KeyCodes.SPACE, goog.events.EventType.KEYUP));
  284. assertFalse('Menu should close after pressing Space', menuButton.isOpen());
  285. }
  286. /**
  287. * Tests that a keydown event of the escape key propagates normally when the
  288. * menu is closed.
  289. */
  290. function testStopEscapePropagationMenuClosed() {
  291. var node = goog.dom.getElement('demoMenuButton');
  292. var fakeEvent = new MyFakeEvent(
  293. goog.events.KeyCodes.ESCAPE, goog.events.EventType.KEYDOWN);
  294. menuButton.decorate(node);
  295. menuButton.setOpen(false);
  296. menuButton.handleKeyDownEvent_(fakeEvent);
  297. assertFalse(
  298. 'Event propagation was erroneously stopped.',
  299. fakeEvent.propagationStopped);
  300. }
  301. /**
  302. * Tests that a keydown event of the escape key is prevented from propagating
  303. * when the menu is open.
  304. */
  305. function testStopEscapePropagationMenuOpen() {
  306. var node = goog.dom.getElement('demoMenuButton');
  307. var fakeEvent = new MyFakeEvent(
  308. goog.events.KeyCodes.ESCAPE, goog.events.EventType.KEYDOWN);
  309. menuButton.decorate(node);
  310. menuButton.setOpen(true);
  311. menuButton.handleKeyDownEvent_(fakeEvent);
  312. assertTrue(
  313. 'Event propagation was not stopped.', fakeEvent.propagationStopped);
  314. }
  315. /**
  316. * Open the menu and click on the menu item inside after exiting and entering
  317. * the document once, to test proper setup/teardown behavior of MenuButton.
  318. */
  319. function testButtonAfterEnterDocument() {
  320. var node = goog.dom.getElement('demoMenuButton');
  321. menuButton.decorate(node);
  322. menuButton.exitDocument();
  323. menuButton.enterDocument();
  324. goog.testing.events.fireClickSequence(node);
  325. assertTrue('Menu must open after click', menuButton.isOpen());
  326. var menuItem2 = goog.dom.getElement('menuItem2');
  327. goog.testing.events.fireClickSequence(menuItem2);
  328. assertFalse('Menu must close on clicking when open', menuButton.isOpen());
  329. }
  330. /**
  331. * Renders the menu button, moves its menu and then repositions to make sure the
  332. * position is more or less ok.
  333. */
  334. function testPositionMenu() {
  335. var node = goog.dom.getElement('demoMenuButton');
  336. menuButton.decorate(node);
  337. var menu = menuButton.getMenu();
  338. menu.setVisible(true, true);
  339. // Move to 500, 500
  340. menu.setPosition(500, 500);
  341. // Now reposition and make sure position is more or less ok.
  342. menuButton.positionMenu();
  343. var menuNode = goog.dom.getElement('demoMenu');
  344. assertRoughlyEquals(
  345. menuNode.offsetTop, node.offsetTop + node.offsetHeight, 20);
  346. assertRoughlyEquals(menuNode.offsetLeft, node.offsetLeft, 20);
  347. }
  348. /**
  349. * Tests that calling positionMenu when the menu is not in the document does not
  350. * throw an exception.
  351. */
  352. function testPositionMenuNotInDocument() {
  353. var menu = new goog.ui.Menu();
  354. menu.createDom();
  355. menuButton.setMenu(menu);
  356. menuButton.positionMenu();
  357. }
  358. /**
  359. * Shows the menu and moves the menu button, a timer correct the menu position.
  360. */
  361. function testOpenedMenuPositionCorrection() {
  362. var iframe = goog.dom.getElement('iframe1');
  363. var iframeDoc = goog.dom.getFrameContentDocument(iframe);
  364. var iframeDom = goog.dom.getDomHelper(iframeDoc);
  365. var iframeWindow = goog.dom.getWindow(iframeDoc);
  366. var button = new goog.ui.MenuButton();
  367. iframeWindow.scrollTo(0, 0);
  368. var node = iframeDom.getElement('demoMenuButton');
  369. button.decorate(node);
  370. var mockTimer = new goog.Timer();
  371. // Don't start the timer. We manually dispatch the Tick event.
  372. mockTimer.start = goog.nullFunction;
  373. button.timer_ = mockTimer;
  374. var replacer = new goog.testing.PropertyReplacer();
  375. var positionMenuCalled;
  376. var origPositionMenu = goog.bind(button.positionMenu, button);
  377. replacer.set(button, 'positionMenu', function() {
  378. positionMenuCalled = true;
  379. origPositionMenu();
  380. });
  381. // Show the menu.
  382. button.setOpen(true);
  383. // Confirm the menu position
  384. var menuNode = iframeDom.getElement('demoMenu');
  385. assertRoughlyEquals(
  386. menuNode.offsetTop, node.offsetTop + node.offsetHeight, 20);
  387. assertRoughlyEquals(menuNode.offsetLeft, node.offsetLeft, 20);
  388. positionMenuCalled = false;
  389. // A Tick event is dispatched.
  390. mockTimer.dispatchEvent(goog.Timer.TICK);
  391. assertFalse('positionMenu() shouldn\'t be called.', positionMenuCalled);
  392. // Move the menu button by DOM structure change
  393. var p1 = iframeDom.createDom(
  394. goog.dom.TagName.P, null, iframeDom.createTextNode('foo'));
  395. var p2 = iframeDom.createDom(
  396. goog.dom.TagName.P, null, iframeDom.createTextNode('foo'));
  397. var p3 = iframeDom.createDom(
  398. goog.dom.TagName.P, null, iframeDom.createTextNode('foo'));
  399. iframeDom.insertSiblingBefore(p1, node);
  400. iframeDom.insertSiblingBefore(p2, node);
  401. iframeDom.insertSiblingBefore(p3, node);
  402. // Confirm the menu is detached from the button.
  403. assertTrue(
  404. Math.abs(node.offsetTop + node.offsetHeight - menuNode.offsetTop) > 20);
  405. positionMenuCalled = false;
  406. // A Tick event is dispatched.
  407. mockTimer.dispatchEvent(goog.Timer.TICK);
  408. assertTrue('positionMenu() should be called.', positionMenuCalled);
  409. // The menu is moved to appropriate position again.
  410. assertRoughlyEquals(
  411. menuNode.offsetTop, node.offsetTop + node.offsetHeight, 20);
  412. // Make the frame page scrollable.
  413. var viewportHeight = iframeDom.getViewportSize().height;
  414. var footer = iframeDom.getElement('footer');
  415. goog.style.setSize(footer, 1, viewportHeight * 2);
  416. // Change the viewport offset.
  417. iframeWindow.scrollTo(0, viewportHeight);
  418. // A Tick event is dispatched and positionMenu() should be called.
  419. positionMenuCalled = false;
  420. mockTimer.dispatchEvent(goog.Timer.TICK);
  421. assertTrue('positionMenu() should be called.', positionMenuCalled);
  422. goog.style.setSize(footer, 1, 1);
  423. // Tear down.
  424. iframeDom.removeNode(p1);
  425. iframeDom.removeNode(p2);
  426. iframeDom.removeNode(p3);
  427. replacer.reset();
  428. button.dispose();
  429. }
  430. /**
  431. * Use a different button to position the menu and make sure it does so
  432. * correctly.
  433. */
  434. function testAlternatePositioningElement() {
  435. var node = goog.dom.getElement('demoMenuButton');
  436. menuButton.decorate(node);
  437. var posElement = goog.dom.getElement('positionElement');
  438. menuButton.setPositionElement(posElement);
  439. // Show the menu.
  440. menuButton.setOpen(true);
  441. // Confirm the menu position
  442. var menuNode = menuButton.getMenu().getElement();
  443. assertRoughlyEquals(
  444. menuNode.offsetTop, posElement.offsetTop + posElement.offsetHeight, 20);
  445. assertRoughlyEquals(menuNode.offsetLeft, posElement.offsetLeft, 20);
  446. }
  447. /**
  448. * Test forced positioning above the button.
  449. */
  450. function testPositioningAboveAnchor() {
  451. var node = goog.dom.getElement('demoMenuButton');
  452. menuButton.decorate(node);
  453. // Show the menu.
  454. menuButton.setAlignMenuToStart(true); // Should get overridden below
  455. menuButton.setScrollOnOverflow(true); // Should get overridden below
  456. var position = new goog.positioning.MenuAnchoredPosition(
  457. menuButton.getElement(), goog.positioning.Corner.TOP_START,
  458. /* opt_adjust */ false, /* opt_resize */ false);
  459. menuButton.setMenuPosition(position);
  460. menuButton.setOpen(true);
  461. // Confirm the menu position
  462. var buttonBounds = goog.style.getBounds(node);
  463. var menuNode = menuButton.getMenu().getElement();
  464. var menuBounds = goog.style.getBounds(menuNode);
  465. assertRoughlyEquals(menuBounds.top + menuBounds.height, buttonBounds.top, 3);
  466. assertRoughlyEquals(menuBounds.left, buttonBounds.left, 3);
  467. // For this test to be valid, the node must have non-trival height.
  468. assertRoughlyEquals(node.offsetHeight, 19, 3);
  469. }
  470. /**
  471. * Test forced positioning below the button.
  472. */
  473. function testPositioningBelowAnchor() {
  474. var node = goog.dom.getElement('demoMenuButton');
  475. menuButton.decorate(node);
  476. // Show the menu.
  477. menuButton.setAlignMenuToStart(true); // Should get overridden below
  478. menuButton.setScrollOnOverflow(true); // Should get overridden below
  479. var position = new goog.positioning.MenuAnchoredPosition(
  480. menuButton.getElement(), goog.positioning.Corner.BOTTOM_START,
  481. /* opt_adjust */ false, /* opt_resize */ false);
  482. menuButton.setMenuPosition(position);
  483. menuButton.setOpen(true);
  484. // Confirm the menu position
  485. var buttonBounds = goog.style.getBounds(node);
  486. var menuNode = menuButton.getMenu().getElement();
  487. var menuBounds = goog.style.getBounds(menuNode);
  488. expectedFailures.expectFailureFor(isWinSafariBefore5());
  489. try {
  490. assertRoughlyEquals(
  491. menuBounds.top, buttonBounds.top + buttonBounds.height, 3);
  492. assertRoughlyEquals(menuBounds.left, buttonBounds.left, 3);
  493. } catch (e) {
  494. expectedFailures.handleException(e);
  495. }
  496. // For this test to be valid, the node must have non-trival height.
  497. assertRoughlyEquals(node.offsetHeight, 19, 3);
  498. }
  499. function isWinSafariBefore5() {
  500. return goog.userAgent.WINDOWS && goog.userAgent.product.SAFARI &&
  501. goog.userAgent.product.isVersion(4) &&
  502. !goog.userAgent.product.isVersion(5);
  503. }
  504. /**
  505. * Tests that space, and only space, fire on key up.
  506. */
  507. function testSpaceFireOnKeyUp() {
  508. var node = goog.dom.getElement('demoMenuButton');
  509. menuButton.decorate(node);
  510. e = new goog.events.Event(goog.events.KeyHandler.EventType.KEY, menuButton);
  511. e.preventDefault = goog.testing.recordFunction();
  512. e.keyCode = goog.events.KeyCodes.SPACE;
  513. menuButton.handleKeyEvent(e);
  514. assertFalse(
  515. 'Menu must not have been triggered by Space keypress',
  516. menuButton.isOpen());
  517. assertNotNull('Page scrolling is prevented', e.preventDefault.getLastCall());
  518. e = new goog.events.Event(goog.events.EventType.KEYUP, menuButton);
  519. e.keyCode = goog.events.KeyCodes.SPACE;
  520. menuButton.handleKeyEvent(e);
  521. assertTrue(
  522. 'Menu must have been triggered by Space keyup', menuButton.isOpen());
  523. menuButton.getMenu().setHighlightedIndex(0);
  524. e = new goog.events.Event(goog.events.KeyHandler.EventType.KEY, menuButton);
  525. e.keyCode = goog.events.KeyCodes.DOWN;
  526. menuButton.handleKeyEvent(e);
  527. assertEquals(
  528. 'Highlighted menu item must have hanged by Down keypress', 1,
  529. menuButton.getMenu().getHighlightedIndex());
  530. menuButton.getMenu().setHighlightedIndex(0);
  531. e = new goog.events.Event(goog.events.EventType.KEYUP, menuButton);
  532. e.keyCode = goog.events.KeyCodes.DOWN;
  533. menuButton.handleKeyEvent(e);
  534. assertEquals(
  535. 'Highlighted menu item must not have changed by Down keyup', 0,
  536. menuButton.getMenu().getHighlightedIndex());
  537. }
  538. /**
  539. * Tests that preventing the button from closing also prevents the menu from
  540. * being hidden.
  541. */
  542. function testPreventHide() {
  543. var node = goog.dom.getElement('demoMenuButton');
  544. menuButton.decorate(node);
  545. menuButton.setDispatchTransitionEvents(goog.ui.Component.State.OPENED, true);
  546. // Show the menu.
  547. menuButton.setOpen(true);
  548. assertTrue('Menu button should be open.', menuButton.isOpen());
  549. assertTrue('Menu should be visible.', menuButton.getMenu().isVisible());
  550. var key = goog.events.listen(
  551. menuButton, goog.ui.Component.EventType.CLOSE,
  552. function(event) { event.preventDefault(); });
  553. // Try to hide the menu.
  554. menuButton.setOpen(false);
  555. assertTrue('Menu button should still be open.', menuButton.isOpen());
  556. assertTrue('Menu should still be visible.', menuButton.getMenu().isVisible());
  557. // Remove listener and try again.
  558. goog.events.unlistenByKey(key);
  559. menuButton.setOpen(false);
  560. assertFalse('Menu button should not be open.', menuButton.isOpen());
  561. assertFalse('Menu should not be visible.', menuButton.getMenu().isVisible());
  562. }
  563. /**
  564. * Tests that opening and closing the menu does not affect how adding or
  565. * removing menu items changes the size of the menu.
  566. */
  567. function testResizeOnItemAddOrRemove() {
  568. var node = goog.dom.getElement('demoMenuButton');
  569. menuButton.decorate(node);
  570. var menu = menuButton.getMenu();
  571. // Show the menu.
  572. menuButton.setOpen(true);
  573. var originalSize = goog.style.getSize(menu.getElement());
  574. // Check that removing an item while the menu is left open correctly changes
  575. // the size of the menu.
  576. // Remove an item using a method on Menu.
  577. var item = menu.removeChildAt(0, true);
  578. // Confirm size of menu changed.
  579. var afterRemoveSize = goog.style.getSize(menu.getElement());
  580. assertTrue(
  581. 'Height of menu must decrease after removing a menu item.',
  582. afterRemoveSize.height < originalSize.height);
  583. // Check that removing an item while the menu is closed, then opened
  584. // (so that reposition is called) correctly changes the size of the menu.
  585. // Hide menu.
  586. menuButton.setOpen(false);
  587. var item2 = menu.removeChildAt(0, true);
  588. menuButton.setOpen(true);
  589. // Confirm size of menu changed.
  590. var afterRemoveAgainSize = goog.style.getSize(menu.getElement());
  591. assertTrue(
  592. 'Height of menu must decrease after removing a second menu item.',
  593. afterRemoveAgainSize.height < afterRemoveSize.height);
  594. // Check that adding an item while the menu is opened, then closed, then
  595. // opened, correctly changes the size of the menu.
  596. // Add an item, this time using a MenuButton method.
  597. menuButton.setOpen(true);
  598. menuButton.addItem(item2);
  599. menuButton.setOpen(false);
  600. menuButton.setOpen(true);
  601. // Confirm size of menu changed.
  602. var afterAddSize = goog.style.getSize(menu.getElement());
  603. assertTrue(
  604. 'Height of menu must increase after adding a menu item.',
  605. afterRemoveAgainSize.height < afterAddSize.height);
  606. assertEquals(
  607. 'Removing and adding back items must not change the height of a menu.',
  608. afterRemoveSize.height, afterAddSize.height);
  609. // Add back the last item to keep state consistent.
  610. menuButton.addItem(item);
  611. }
  612. /**
  613. * Tests that adding and removing items from a menu with scrollOnOverflow is on
  614. * correctly resizes the menu.
  615. */
  616. function testResizeOnItemAddOrRemoveWithScrollOnOverflow() {
  617. var node = goog.dom.getElement('demoMenuButton');
  618. menuButton.decorate(node);
  619. var menu = menuButton.getMenu();
  620. // Show the menu.
  621. menuButton.setScrollOnOverflow(true);
  622. menuButton.setOpen(true);
  623. var originalSize = goog.style.getSize(menu.getElement());
  624. // Check that removing an item while the menu is left open correctly changes
  625. // the size of the menu.
  626. // Remove an item using a method on Menu.
  627. var item = menu.removeChildAt(0, true);
  628. menuButton.invalidateMenuSize();
  629. menuButton.positionMenu();
  630. // Confirm size of menu changed.
  631. var afterRemoveSize = goog.style.getSize(menu.getElement());
  632. assertTrue(
  633. 'Height of menu must decrease after removing a menu item.',
  634. afterRemoveSize.height < originalSize.height);
  635. var item2 = menu.removeChildAt(0, true);
  636. menuButton.invalidateMenuSize();
  637. menuButton.positionMenu();
  638. // Confirm size of menu changed.
  639. var afterRemoveAgainSize = goog.style.getSize(menu.getElement());
  640. assertTrue(
  641. 'Height of menu must decrease after removing a second menu item.',
  642. afterRemoveAgainSize.height < afterRemoveSize.height);
  643. // Check that adding an item while the menu is opened correctly changes the
  644. // size of the menu.
  645. menuButton.addItem(item2);
  646. menuButton.invalidateMenuSize();
  647. menuButton.positionMenu();
  648. // Confirm size of menu changed.
  649. var afterAddSize = goog.style.getSize(menu.getElement());
  650. assertTrue(
  651. 'Height of menu must increase after adding a menu item.',
  652. afterRemoveAgainSize.height < afterAddSize.height);
  653. assertEquals(
  654. 'Removing and adding back items must not change the height of a menu.',
  655. afterRemoveSize.height, afterAddSize.height);
  656. }
  657. /**
  658. * Try rendering the menu as a sibling rather than as a child of the dom. This
  659. * tests the case when the button is rendered, rather than decorated.
  660. */
  661. function testRenderMenuAsSibling() {
  662. menuButton.setRenderMenuAsSibling(true);
  663. menuButton.addItem(new goog.ui.MenuItem('Menu item 1'));
  664. menuButton.addItem(new goog.ui.MenuItem('Menu item 2'));
  665. // By default the menu is rendered into the top level dom and the button
  666. // is rendered into whatever parent we provide. If we don't provide a
  667. // parent then we aren't really testing anything, since both would be, by
  668. // default, rendered into the top level dom, and therefore siblings.
  669. menuButton.render(goog.dom.getElement('siblingTest'));
  670. menuButton.setOpen(true);
  671. assertEquals(
  672. menuButton.getElement().parentNode,
  673. menuButton.getMenu().getElement().parentNode);
  674. }
  675. /**
  676. * Check that we render the menu as a sibling of the menu button, immediately
  677. * after the menu button.
  678. */
  679. function testRenderMenuAsSiblingForDecoratedButton() {
  680. var menu = new goog.ui.Menu();
  681. menu.addChild(new goog.ui.MenuItem('Menu item 1'), true /* render */);
  682. menu.addChild(new goog.ui.MenuItem('Menu item 2'), true /* render */);
  683. menu.addChild(new goog.ui.MenuItem('Menu item 3'), true /* render */);
  684. var menuButton = new goog.ui.MenuButton();
  685. menuButton.setMenu(menu);
  686. menuButton.setRenderMenuAsSibling(true);
  687. var node = goog.dom.getElement('button1');
  688. menuButton.decorate(node);
  689. menuButton.setOpen(true);
  690. assertEquals(
  691. 'The menu should be rendered immediately after the menu button',
  692. goog.dom.getNextElementSibling(menuButton.getElement()),
  693. menu.getElement());
  694. assertEquals(
  695. 'The menu should be rendered immediately before the next button',
  696. goog.dom.getNextElementSibling(menu.getElement()),
  697. goog.dom.getElement('button2'));
  698. }
  699. function testAlignToStartSetter() {
  700. assertTrue(menuButton.isAlignMenuToStart());
  701. menuButton.setAlignMenuToStart(false);
  702. assertFalse(menuButton.isAlignMenuToStart());
  703. menuButton.setAlignMenuToStart(true);
  704. assertTrue(menuButton.isAlignMenuToStart());
  705. }
  706. function testScrollOnOverflowSetter() {
  707. assertFalse(menuButton.isScrollOnOverflow());
  708. menuButton.setScrollOnOverflow(true);
  709. assertTrue(menuButton.isScrollOnOverflow());
  710. menuButton.setScrollOnOverflow(false);
  711. assertFalse(menuButton.isScrollOnOverflow());
  712. }
  713. /**
  714. * Tests that the attached menu has been set to aria-hidden=false explicitly
  715. * when the menu is opened.
  716. */
  717. function testSetOpenUnsetsAriaHidden() {
  718. var node = goog.dom.getElement('demoMenuButton');
  719. menuButton.decorate(node);
  720. var menuElem = menuButton.getMenu().getElementStrict();
  721. goog.a11y.aria.setState(menuElem, goog.a11y.aria.State.HIDDEN, true);
  722. menuButton.setOpen(true);
  723. assertEquals(
  724. '', goog.a11y.aria.getState(menuElem, goog.a11y.aria.State.HIDDEN));
  725. }