123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627 |
- // 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.a11y.aria');
- goog.require('goog.a11y.aria.State');
- 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.KeyHandler');
- 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.removeChildren(goog.dom.getElement('sandbox'));
- 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 testCloseSubMenuBehavior() {
- menu.decorate(goog.dom.getElement('demoMenu'));
- var subMenu = menu.getChildAt(1);
- subMenu.getElement().id = 'subMenu';
- var innerMenu = subMenu.getMenu();
- innerMenu.getChildAt(0).getElement().id = 'child1';
- subMenu.setHighlighted(true);
- subMenu.showSubMenu();
- function MyFakeEvent(keyCode, opt_eventType) {
- this.type = opt_eventType || goog.events.KeyHandler.EventType.KEY;
- this.keyCode = keyCode;
- this.propagationStopped = false;
- this.preventDefault = goog.nullFunction;
- this.stopPropagation = function() { this.propagationStopped = true; };
- }
- // Focus on the first item in the submenu and verify the activedescendant is
- // set correctly.
- subMenu.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.DOWN));
- assertEquals(
- 'First item in submenu must be the aria-activedescendant', 'child1',
- goog.a11y.aria.getState(
- menu.getElement(), goog.a11y.aria.State.ACTIVEDESCENDANT));
- // Dismiss the submenu and verify the activedescendant is updated correctly.
- subMenu.handleKeyEvent(new MyFakeEvent(goog.events.KeyCodes.LEFT));
- assertEquals(
- 'Submenu must be the aria-activedescendant', 'subMenu',
- goog.a11y.aria.getState(
- menu.getElement(), goog.a11y.aria.State.ACTIVEDESCENDANT));
- }
- 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.)
- }
- /**
- * Tests that if a sub menu is selectable, then it can handle actions.
- */
- 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 if a sub menu is checkable, then it can handle actions.
- */
- function testSubmenuCheckable() {
- var submenu = new goog.ui.SubMenu('submenu');
- submenu.addItem(new goog.ui.MenuItem('submenu item 1'));
- menu.addItem(submenu);
- submenu.setCheckable(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.setCheckable(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;
- }
|