| // 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.SubMenuTest'); |
| goog.setTestOnly('goog.ui.SubMenuTest'); |
| |
| 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.functions'); |
| goog.require('goog.positioning'); |
| goog.require('goog.positioning.Overflow'); |
| goog.require('goog.style'); |
| goog.require('goog.testing.MockClock'); |
| goog.require('goog.testing.events'); |
| goog.require('goog.testing.jsunit'); |
| goog.require('goog.ui.Component'); |
| goog.require('goog.ui.Menu'); |
| goog.require('goog.ui.MenuItem'); |
| goog.require('goog.ui.SubMenu'); |
| goog.require('goog.ui.SubMenuRenderer'); |
| |
| var menu; |
| var clonedMenuDom; |
| |
| var mockClock; |
| |
| // mock out goog.positioning.positionAtCoordinate so that |
| // the menu always fits. (we don't care about testing the |
| // dynamic menu positioning if the menu doesn't fit in the window.) |
| var oldPositionFn = goog.positioning.positionAtCoordinate; |
| goog.positioning.positionAtCoordinate = function(absolutePos, movableElement, |
| movableElementCorner, |
| opt_margin, opt_overflow) { |
| return oldPositionFn.call(null, absolutePos, movableElement, |
| movableElementCorner, opt_margin, goog.positioning.Overflow.IGNORE); |
| }; |
| |
| function setUp() { |
| clonedMenuDom = goog.dom.getElement('demoMenu').cloneNode(true); |
| |
| menu = new goog.ui.Menu(); |
| } |
| |
| function tearDown() { |
| document.body.style.direction = 'ltr'; |
| menu.dispose(); |
| |
| var element = goog.dom.getElement('demoMenu'); |
| element.parentNode.replaceChild(clonedMenuDom, element); |
| |
| goog.dom.getElement('sandbox').innerHTML = ''; |
| |
| if (mockClock) { |
| mockClock.uninstall(); |
| mockClock = null; |
| } |
| } |
| |
| function assertKeyHandlingIsCorrect(keyToOpenSubMenu, keyToCloseSubMenu) { |
| menu.setFocusable(true); |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| |
| var KeyCodes = goog.events.KeyCodes; |
| var plainItem = menu.getChildAt(0); |
| plainItem.setMnemonic(KeyCodes.F); |
| |
| var subMenuItem1 = menu.getChildAt(1); |
| subMenuItem1.setMnemonic(KeyCodes.S); |
| var subMenuItem1Menu = subMenuItem1.getMenu(); |
| menu.setHighlighted(plainItem); |
| |
| var fireKeySequence = goog.testing.events.fireKeySequence; |
| |
| assertTrue( |
| 'Expected OpenSubMenu key to not be handled', |
| fireKeySequence(plainItem.getElement(), keyToOpenSubMenu)); |
| assertFalse(subMenuItem1Menu.isVisible()); |
| |
| assertFalse( |
| 'Expected F key to be handled', |
| fireKeySequence(plainItem.getElement(), KeyCodes.F)); |
| |
| assertFalse( |
| 'Expected DOWN key to be handled', |
| fireKeySequence(plainItem.getElement(), KeyCodes.DOWN)); |
| assertEquals(subMenuItem1, menu.getChildAt(1)); |
| |
| assertFalse( |
| 'Expected OpenSubMenu key to be handled', |
| fireKeySequence(subMenuItem1.getElement(), keyToOpenSubMenu)); |
| assertTrue(subMenuItem1Menu.isVisible()); |
| |
| assertFalse( |
| 'Expected CloseSubMenu key to be handled', |
| fireKeySequence(subMenuItem1.getElement(), keyToCloseSubMenu)); |
| assertFalse(subMenuItem1Menu.isVisible()); |
| |
| assertFalse( |
| 'Expected UP key to be handled', |
| fireKeySequence(subMenuItem1.getElement(), KeyCodes.UP)); |
| |
| assertFalse( |
| 'Expected S key to be handled', |
| fireKeySequence(plainItem.getElement(), KeyCodes.S)); |
| assertTrue(subMenuItem1Menu.isVisible()); |
| } |
| |
| function testKeyHandling_ltr() { |
| assertKeyHandlingIsCorrect(goog.events.KeyCodes.RIGHT, |
| goog.events.KeyCodes.LEFT); |
| } |
| |
| function testKeyHandling_rtl() { |
| document.body.style.direction = 'rtl'; |
| assertKeyHandlingIsCorrect(goog.events.KeyCodes.LEFT, |
| goog.events.KeyCodes.RIGHT); |
| } |
| |
| function testNormalLtrSubMenu() { |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| assertArrowDirection(subMenu, false); |
| assertRenderDirection(subMenu, false); |
| assertArrowPosition(subMenu, false); |
| } |
| |
| function testNormalRtlSubMenu() { |
| document.body.style.direction = 'rtl'; |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| assertArrowDirection(subMenu, true); |
| assertRenderDirection(subMenu, true); |
| assertArrowPosition(subMenu, true); |
| } |
| |
| function testLtrSubMenuAlignedToStart() { |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| subMenu.setAlignToEnd(false); |
| assertArrowDirection(subMenu, true); |
| assertRenderDirection(subMenu, true); |
| assertArrowPosition(subMenu, false); |
| } |
| |
| function testNullContentElement() { |
| var subMenu = new goog.ui.SubMenu(); |
| subMenu.setContent('demo'); |
| } |
| |
| function testRtlSubMenuAlignedToStart() { |
| document.body.style.direction = 'rtl'; |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| subMenu.setAlignToEnd(false); |
| assertArrowDirection(subMenu, false); |
| assertRenderDirection(subMenu, false); |
| assertArrowPosition(subMenu, true); |
| } |
| |
| function testSetContentKeepsArrow_ltr() { |
| document.body.style.direction = 'ltr'; |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| subMenu.setAlignToEnd(false); |
| subMenu.setContent('test'); |
| assertArrowDirection(subMenu, true); |
| assertRenderDirection(subMenu, true); |
| assertArrowPosition(subMenu, false); |
| } |
| |
| function testSetContentKeepsArrow_rtl() { |
| document.body.style.direction = 'rtl'; |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| subMenu.setAlignToEnd(false); |
| subMenu.setContent('test'); |
| assertArrowDirection(subMenu, false); |
| assertRenderDirection(subMenu, false); |
| assertArrowPosition(subMenu, true); |
| } |
| |
| function testExitDocument() { |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| var innerMenu = subMenu.getMenu(); |
| |
| assertTrue('Top-level menu was not in document', menu.isInDocument()); |
| assertTrue('Submenu was not in document', subMenu.isInDocument()); |
| assertTrue('Inner menu was not in document', innerMenu.isInDocument()); |
| |
| menu.exitDocument(); |
| |
| assertFalse('Top-level menu was in document', menu.isInDocument()); |
| assertFalse('Submenu was in document', subMenu.isInDocument()); |
| assertFalse('Inner menu was in document', innerMenu.isInDocument()); |
| } |
| |
| function testDisposal() { |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| var innerMenu = subMenu.getMenu(); |
| menu.dispose(); |
| |
| assert('Top-level menu was not disposed', menu.getDisposed()); |
| assert('Submenu was not disposed', subMenu.getDisposed()); |
| assert('Inner menu was not disposed', innerMenu.getDisposed()); |
| } |
| |
| function testShowAndDismissSubMenu() { |
| var openEventDispatched = false; |
| var closeEventDispatched = false; |
| |
| function handleEvent(e) { |
| switch (e.type) { |
| case goog.ui.Component.EventType.OPEN: |
| openEventDispatched = true; |
| break; |
| case goog.ui.Component.EventType.CLOSE: |
| closeEventDispatched = true; |
| break; |
| default: |
| fail('Invalid event type: ' + e.type); |
| } |
| } |
| |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| subMenu.setHighlighted(true); |
| |
| goog.events.listen(subMenu, [ |
| goog.ui.Component.EventType.OPEN, |
| goog.ui.Component.EventType.CLOSE |
| ], handleEvent); |
| |
| assertFalse('Submenu must not have "-open" CSS class', |
| goog.dom.classlist.contains(subMenu.getElement(), 'goog-submenu-open')); |
| assertFalse('Popup menu must not be visible', |
| subMenu.getMenu().isVisible()); |
| assertFalse('No OPEN event must have been dispatched', openEventDispatched); |
| assertFalse('No CLOSE event must have been dispatched', closeEventDispatched); |
| |
| subMenu.showSubMenu(); |
| assertTrue('Submenu must have "-open" CSS class', |
| goog.dom.classlist.contains(subMenu.getElement(), 'goog-submenu-open')); |
| assertTrue('Popup menu must be visible', |
| subMenu.getMenu().isVisible()); |
| assertTrue('OPEN event must have been dispatched', openEventDispatched); |
| assertFalse('No CLOSE event must have been dispatched', closeEventDispatched); |
| |
| subMenu.dismissSubMenu(); |
| assertFalse('Submenu must not have "-open" CSS class', |
| goog.dom.classlist.contains(subMenu.getElement(), 'goog-submenu-open')); |
| assertFalse('Popup menu must not be visible', |
| subMenu.getMenu().isVisible()); |
| assertTrue('CLOSE event must have been dispatched', closeEventDispatched); |
| |
| goog.events.unlisten(subMenu, [ |
| goog.ui.Component.EventType.OPEN, |
| goog.ui.Component.EventType.CLOSE |
| ], handleEvent); |
| } |
| |
| function testDismissWhenSubMenuNotVisible() { |
| var openEventDispatched = false; |
| var closeEventDispatched = false; |
| |
| function handleEvent(e) { |
| switch (e.type) { |
| case goog.ui.Component.EventType.OPEN: |
| openEventDispatched = true; |
| break; |
| case goog.ui.Component.EventType.CLOSE: |
| closeEventDispatched = true; |
| break; |
| default: |
| fail('Invalid event type: ' + e.type); |
| } |
| } |
| |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| subMenu.setHighlighted(true); |
| |
| goog.events.listen(subMenu, [ |
| goog.ui.Component.EventType.OPEN, |
| goog.ui.Component.EventType.CLOSE |
| ], handleEvent); |
| |
| assertFalse('Submenu must not have "-open" CSS class', |
| goog.dom.classlist.contains(subMenu.getElement(), 'goog-submenu-open')); |
| assertFalse('Popup menu must not be visible', |
| subMenu.getMenu().isVisible()); |
| assertFalse('No OPEN event must have been dispatched', openEventDispatched); |
| assertFalse('No CLOSE event must have been dispatched', closeEventDispatched); |
| |
| subMenu.showSubMenu(); |
| subMenu.getMenu().setVisible(false); |
| |
| subMenu.dismissSubMenu(); |
| assertFalse('Submenu must not have "-open" CSS class', |
| goog.dom.classlist.contains(subMenu.getElement(), 'goog-submenu-open')); |
| assertFalse(subMenu.menuIsVisible_); |
| assertFalse('Popup menu must not be visible', |
| subMenu.getMenu().isVisible()); |
| assertTrue('CLOSE event must have been dispatched', closeEventDispatched); |
| |
| goog.events.unlisten(subMenu, [ |
| goog.ui.Component.EventType.OPEN, |
| goog.ui.Component.EventType.CLOSE |
| ], handleEvent); |
| } |
| |
| function testLazyInstantiateSubMenu() { |
| menu.decorate(goog.dom.getElement('demoMenu')); |
| var subMenu = menu.getChildAt(1); |
| subMenu.setHighlighted(true); |
| |
| var lazyMenu; |
| |
| var key = goog.events.listen(subMenu, goog.ui.Component.EventType.OPEN, |
| function(e) { |
| lazyMenu = new goog.ui.Menu(); |
| lazyMenu.addItem(new goog.ui.MenuItem('foo')); |
| lazyMenu.addItem(new goog.ui.MenuItem('bar')); |
| subMenu.setMenu(lazyMenu, /* opt_internal */ false); |
| }); |
| |
| subMenu.showSubMenu(); |
| assertNotNull('Popup menu must have been created', lazyMenu); |
| assertEquals('Popup menu must be a child of the submenu', subMenu, |
| lazyMenu.getParent()); |
| assertTrue('Popup menu must have been rendered', lazyMenu.isInDocument()); |
| assertTrue('Popup menu must be visible', lazyMenu.isVisible()); |
| |
| menu.dispose(); |
| assertTrue('Submenu must have been disposed of', subMenu.isDisposed()); |
| assertFalse('Popup menu must not have been disposed of', |
| lazyMenu.isDisposed()); |
| |
| lazyMenu.dispose(); |
| |
| goog.events.unlistenByKey(key); |
| } |
| |
| function testReusableMenu() { |
| var subMenuOne = new goog.ui.SubMenu('SubMenu One'); |
| var subMenuTwo = new goog.ui.SubMenu('SubMenu Two'); |
| menu.addItem(subMenuOne); |
| menu.addItem(subMenuTwo); |
| menu.render(goog.dom.getElement('sandbox')); |
| |
| // It is possible for the same popup menu to be shared between different |
| // submenus. |
| var sharedMenu = new goog.ui.Menu(); |
| sharedMenu.addItem(new goog.ui.MenuItem('Hello')); |
| sharedMenu.addItem(new goog.ui.MenuItem('World')); |
| |
| assertNull('Shared menu must not have a parent', sharedMenu.getParent()); |
| |
| subMenuOne.setMenu(sharedMenu); |
| assertEquals('SubMenuOne must point to the shared menu', sharedMenu, |
| subMenuOne.getMenu()); |
| assertEquals('SubMenuOne must be the shared menu\'s parent', subMenuOne, |
| sharedMenu.getParent()); |
| |
| subMenuTwo.setMenu(sharedMenu); |
| assertEquals('SubMenuTwo must point to the shared menu', sharedMenu, |
| subMenuTwo.getMenu()); |
| assertEquals('SubMenuTwo must be the shared menu\'s parent', subMenuTwo, |
| sharedMenu.getParent()); |
| assertEquals('SubMenuOne must still point to the shared menu', sharedMenu, |
| subMenuOne.getMenu()); |
| |
| menu.setHighlighted(subMenuOne); |
| subMenuOne.showSubMenu(); |
| assertEquals('SubMenuOne must point to the shared menu', sharedMenu, |
| subMenuOne.getMenu()); |
| assertEquals('SubMenuOne must be the shared menu\'s parent', subMenuOne, |
| sharedMenu.getParent()); |
| assertEquals('SubMenuTwo must still point to the shared menu', sharedMenu, |
| subMenuTwo.getMenu()); |
| assertTrue('Shared menu must be visible', sharedMenu.isVisible()); |
| |
| menu.setHighlighted(subMenuTwo); |
| subMenuTwo.showSubMenu(); |
| assertEquals('SubMenuTwo must point to the shared menu', sharedMenu, |
| subMenuTwo.getMenu()); |
| assertEquals('SubMenuTwo must be the shared menu\'s parent', subMenuTwo, |
| sharedMenu.getParent()); |
| assertEquals('SubMenuOne must still point to the shared menu', sharedMenu, |
| subMenuOne.getMenu()); |
| assertTrue('Shared menu must be visible', sharedMenu.isVisible()); |
| } |
| |
| |
| /** |
| * If you remove a submenu in the interval between when a mouseover event |
| * is fired on it, and showSubMenu() is called, showSubMenu causes a null |
| * value to be dereferenced. This test validates that the fix for this works. |
| * (See bug 1823144). |
| */ |
| function testDeleteItemDuringSubmenuDisplayInterval() { |
| mockClock = new goog.testing.MockClock(true); |
| |
| var submenu = new goog.ui.SubMenu('submenu'); |
| submenu.addItem(new goog.ui.MenuItem('submenu item 1')); |
| menu.addItem(submenu); |
| |
| // Trigger mouseover, and remove item before showSubMenu can be called. |
| var e = new goog.events.Event(); |
| submenu.handleMouseOver(e); |
| menu.removeItem(submenu); |
| mockClock.tick(goog.ui.SubMenu.MENU_DELAY_MS); |
| // (No JS error should occur.) |
| } |
| |
| function testShowSubMenuAfterRemoval() { |
| var submenu = new goog.ui.SubMenu('submenu'); |
| menu.addItem(submenu); |
| menu.removeItem(submenu); |
| submenu.showSubMenu(); |
| // (No JS error should occur.) |
| } |
| |
| function testSubmenuSelectable() { |
| var submenu = new goog.ui.SubMenu('submenu'); |
| submenu.addItem(new goog.ui.MenuItem('submenu item 1')); |
| menu.addItem(submenu); |
| submenu.setSelectable(true); |
| |
| var numClicks = 0; |
| var menuClickedFn = function(e) { |
| numClicks++; |
| }; |
| |
| goog.events.listen(submenu, goog.ui.Component.EventType.ACTION, |
| menuClickedFn); |
| submenu.performActionInternal(null); |
| submenu.performActionInternal(null); |
| |
| assertEquals('The submenu should have fired an event', 2, numClicks); |
| |
| submenu.setSelectable(false); |
| submenu.performActionInternal(null); |
| |
| assertEquals('The submenu should not have fired any further events', 2, |
| numClicks); |
| } |
| |
| |
| /** |
| * Tests that entering a child menu cancels the dismiss timer for the submenu. |
| */ |
| function testEnteringChildCancelsDismiss() { |
| var submenu = new goog.ui.SubMenu('submenu'); |
| submenu.isInDocument = goog.functions.TRUE; |
| submenu.addItem(new goog.ui.MenuItem('submenu item 1')); |
| menu.addItem(submenu); |
| |
| mockClock = new goog.testing.MockClock(true); |
| submenu.getMenu().setVisible(true); |
| |
| // This starts the dismiss timer. |
| submenu.setHighlighted(false); |
| |
| // This should cancel the dismiss timer. |
| submenu.getMenu().dispatchEvent(goog.ui.Component.EventType.ENTER); |
| |
| // Tick the length of the dismiss timer. |
| mockClock.tick(goog.ui.SubMenu.MENU_DELAY_MS); |
| |
| // Check that the menu is now highlighted and still visible. |
| assertTrue(submenu.getMenu().isVisible()); |
| assertTrue(submenu.isHighlighted()); |
| } |
| |
| |
| /** |
| * Asserts that this sub menu renders in the right direction relative to |
| * the parent menu. |
| * @param {goog.ui.SubMenu} subMenu The sub menu. |
| * @param {boolean} left True for left-pointing, false for right-pointing. |
| */ |
| function assertRenderDirection(subMenu, left) { |
| subMenu.getParent().setHighlighted(subMenu); |
| subMenu.showSubMenu(); |
| var menuItemPosition = goog.style.getPageOffset(subMenu.getElement()); |
| var menuPosition = goog.style.getPageOffset(subMenu.getMenu().getElement()); |
| assert(Math.abs(menuItemPosition.y - menuPosition.y) < 5); |
| assertEquals( |
| 'Menu at: ' + menuPosition.x + |
| ', submenu item at: ' + menuItemPosition.x, |
| left, menuPosition.x < menuItemPosition.x); |
| } |
| |
| |
| /** |
| * Asserts that this sub menu has a properly-oriented arrow. |
| * @param {goog.ui.SubMenu} subMenu The sub menu. |
| * @param {boolean} left True for left-pointing, false for right-pointing. |
| */ |
| function assertArrowDirection(subMenu, left) { |
| assertEquals( |
| left ? goog.ui.SubMenuRenderer.LEFT_ARROW_ : |
| goog.ui.SubMenuRenderer.RIGHT_ARROW_, |
| getArrowElement(subMenu).innerHTML); |
| } |
| |
| |
| /** |
| * Asserts that the arrow position is correct. |
| * @param {goog.ui.SubMenu} subMenu The sub menu. |
| * @param {boolean} leftAlign True for left-aligned, false for right-aligned. |
| */ |
| function assertArrowPosition(subMenu, left) { |
| var arrow = getArrowElement(subMenu); |
| var expectedLeft = |
| left ? 0 : arrow.offsetParent.offsetWidth - arrow.offsetWidth; |
| var actualLeft = arrow.offsetLeft; |
| assertTrue('Expected left offset: ' + expectedLeft + '\n' + |
| 'Actual left offset: ' + actualLeft + '\n', |
| Math.abs(expectedLeft - actualLeft) < 5); |
| } |
| |
| |
| /** |
| * Gets the arrow element of a sub menu. |
| * @param {goog.ui.SubMenu} subMenu The sub menu. |
| * @return {Element} The arrow. |
| */ |
| function getArrowElement(subMenu) { |
| return subMenu.getContentElement().lastChild; |
| } |