text: /**
   * @license
   * Copyright The Closure Library Authors.
   * SPDX-License-Identifier: Apache-2.0
   */
  
  goog.module('goog.ui.SubMenuTest');
  goog.setTestOnly();
  
  const Component = goog.require('goog.ui.Component');
  const GoogEvent = goog.require('goog.events.Event');
  const KeyCodes = goog.require('goog.events.KeyCodes');
  const KeyHandler = goog.require('goog.events.KeyHandler');
  const Menu = goog.require('goog.ui.Menu');
  const MenuItem = goog.require('goog.ui.MenuItem');
  const MockClock = goog.require('goog.testing.MockClock');
  const Overflow = goog.require('goog.positioning.Overflow');
  const State = goog.require('goog.a11y.aria.State');
  const SubMenu = goog.require('goog.ui.SubMenu');
  const SubMenuRenderer = goog.require('goog.ui.SubMenuRenderer');
  const aria = goog.require('goog.a11y.aria');
  const classlist = goog.require('goog.dom.classlist');
  const dom = goog.require('goog.dom');
  const events = goog.require('goog.events');
  const functions = goog.require('goog.functions');
  const positioning = goog.require('goog.positioning');
  const style = goog.require('goog.style');
  const testSuite = goog.require('goog.testing.testSuite');
  const testingEvents = goog.require('goog.testing.events');
  
  let menu;
  let clonedMenuDom;
  
  let 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.)
  const oldPositionFn = positioning.positionAtCoordinate;
  /** @suppress {checkTypes} suppression added to enable type checking */
  positioning.positionAtCoordinate =
      (absolutePos, movableElement, movableElementCorner, margin = undefined,
       overflow = undefined) =>
          oldPositionFn.call(
              null, absolutePos, movableElement, movableElementCorner, margin,
              Overflow.IGNORE);
  
  function assertKeyHandlingIsCorrect(keyToOpenSubMenu, keyToCloseSubMenu) {
    menu.setFocusable(true);
    menu.decorate(dom.getElement('demoMenu'));
  
    const plainItem = menu.getChildAt(0);
    plainItem.setMnemonic(KeyCodes.F);
  
    const subMenuItem1 = menu.getChildAt(1);
    subMenuItem1.setMnemonic(KeyCodes.S);
    const subMenuItem1Menu = subMenuItem1.getMenu();
    menu.setHighlighted(plainItem);
  
    const fireKeySequence = testingEvents.fireKeySequence;
  
    const keySequenceReturn =
        fireKeySequence(plainItem.getElement(), keyToOpenSubMenu);
  
    // Expect keyToOpenSubMenu to only be handled when ENTER because menuItem
    // handles ENTER, while subMenu handles LEFT, RIGHT and ENTER.
    if (keyToOpenSubMenu == KeyCodes.ENTER) {
      assertFalse(
          'Expected OpenSubMenu key to be handled when KeyCode is ENTER',
          keySequenceReturn);
    } else {
      assertTrue('Expected OpenSubMenu key to not be handled', keySequenceReturn);
    }
    assertFalse(subMenuItem1Menu.isVisible());
  
    // Expect ENTER to be handled when subMenu is visible.
    subMenuItem1Menu.setVisible(true);
    assertFalse(fireKeySequence(subMenuItem1.getElement(), KeyCodes.ENTER));
  
    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));
  
    // Expect keyToOpenSubMenu to be handled when subMenu is not visible.
    subMenuItem1Menu.setVisible(false);
    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());
  }
  
  /**
   * Asserts that this sub menu renders in the right direction relative to
   * the parent menu.
   * @param {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();
    const menuItemPosition = style.getPageOffset(subMenu.getElement());
    const menuPosition = 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 {SubMenu} subMenu The sub menu.
   * @param {boolean} left True for left-pointing, false for right-pointing.
   * @suppress {visibility} suppression added to enable type checking
   */
  function assertArrowDirection(subMenu, left) {
    assertEquals(
        left ? SubMenuRenderer.LEFT_ARROW_ : SubMenuRenderer.RIGHT_ARROW_,
        getArrowElement(subMenu).innerHTML);
  }
  
  /**
   * Asserts that the arrow position is correct.
   * @param {SubMenu} subMenu The sub menu.
   * @param {boolean} leftAlign True for left-aligned, false for right-aligned.
   * @suppress {checkTypes} suppression added to enable type checking
   */
  function assertArrowPosition(subMenu, left) {
    const arrow = getArrowElement(subMenu);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    const expectedLeft =
        left ? 0 : arrow.offsetParent.offsetWidth - arrow.offsetWidth;
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    const actualLeft = arrow.offsetLeft;
    assertTrue(
        `Expected left offset: ${expectedLeft}
  ` +
            'Actual left offset: ' + actualLeft + '\n',
        Math.abs(expectedLeft - actualLeft) < 5);
  }
  
  /**
   * Gets the arrow element of a sub menu.
   * @param {SubMenu} subMenu The sub menu.
   * @return {Element} The arrow.
   * @suppress {checkTypes} suppression added to enable type checking
   */
  function getArrowElement(subMenu) {
    return subMenu.getContentElement().lastChild;
  }
  testSuite({
    setUp() {
      clonedMenuDom = dom.getElement('demoMenu').cloneNode(true);
  
      menu = new Menu();
    },
  
    tearDown() {
      document.body.style.direction = 'ltr';
      menu.dispose();
  
      const element = dom.getElement('demoMenu');
      element.parentNode.replaceChild(clonedMenuDom, element);
  
      dom.removeChildren(dom.getElement('sandbox'));
  
      if (mockClock) {
        mockClock.uninstall();
        mockClock = null;
      }
    },
  
    testEnterOpensSubmenu() {
      assertKeyHandlingIsCorrect(KeyCodes.ENTER, KeyCodes.LEFT);
    },
  
    testKeyHandling_ltr() {
      assertKeyHandlingIsCorrect(KeyCodes.RIGHT, KeyCodes.LEFT);
    },
  
    testKeyHandling_rtl() {
      document.body.style.direction = 'rtl';
      assertKeyHandlingIsCorrect(KeyCodes.LEFT, KeyCodes.RIGHT);
    },
  
    testNormalLtrSubMenu() {
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      assertArrowDirection(subMenu, false);
      assertRenderDirection(subMenu, false);
      assertArrowPosition(subMenu, false);
    },
  
    testNormalRtlSubMenu() {
      document.body.style.direction = 'rtl';
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      assertArrowDirection(subMenu, true);
      assertRenderDirection(subMenu, true);
      assertArrowPosition(subMenu, true);
    },
  
    testLtrSubMenuAlignedToStart() {
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      subMenu.setAlignToEnd(false);
      assertArrowDirection(subMenu, true);
      assertRenderDirection(subMenu, true);
      assertArrowPosition(subMenu, false);
    },
  
    testNullContentElement() {
      /** @suppress {checkTypes} suppression added to enable type checking */
      const subMenu = new SubMenu();
      subMenu.setContent('demo');
    },
  
    testRtlSubMenuAlignedToStart() {
      document.body.style.direction = 'rtl';
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      subMenu.setAlignToEnd(false);
      assertArrowDirection(subMenu, false);
      assertRenderDirection(subMenu, false);
      assertArrowPosition(subMenu, true);
    },
  
    testSetContentKeepsArrow_ltr() {
      document.body.style.direction = 'ltr';
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      subMenu.setAlignToEnd(false);
      subMenu.setContent('test');
      assertArrowDirection(subMenu, true);
      assertRenderDirection(subMenu, true);
      assertArrowPosition(subMenu, false);
    },
  
    testSetContentKeepsArrow_rtl() {
      document.body.style.direction = 'rtl';
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      subMenu.setAlignToEnd(false);
      subMenu.setContent('test');
      assertArrowDirection(subMenu, false);
      assertRenderDirection(subMenu, false);
      assertArrowPosition(subMenu, true);
    },
  
    testExitDocument() {
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      const 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());
    },
  
    testDisposal() {
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      const 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());
    },
  
    testShowAndDismissSubMenu() {
      let openEventDispatched = false;
      let closeEventDispatched = false;
  
      function handleEvent(e) {
        switch (e.type) {
          case Component.EventType.OPEN:
            openEventDispatched = true;
            break;
          case Component.EventType.CLOSE:
            closeEventDispatched = true;
            break;
          default:
            fail('Invalid event type: ' + e.type);
        }
      }
  
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      subMenu.setHighlighted(true);
  
      events.listen(
          subMenu, [Component.EventType.OPEN, Component.EventType.CLOSE],
          handleEvent);
  
      assertFalse(
          'Submenu must not have "-open" CSS class',
          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',
          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',
          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);
  
      events.unlisten(
          subMenu, [Component.EventType.OPEN, Component.EventType.CLOSE],
          handleEvent);
    },
  
    testDismissWhenSubMenuNotVisible() {
      let openEventDispatched = false;
      let closeEventDispatched = false;
  
      function handleEvent(e) {
        switch (e.type) {
          case Component.EventType.OPEN:
            openEventDispatched = true;
            break;
          case Component.EventType.CLOSE:
            closeEventDispatched = true;
            break;
          default:
            fail('Invalid event type: ' + e.type);
        }
      }
  
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      subMenu.setHighlighted(true);
  
      events.listen(
          subMenu, [Component.EventType.OPEN, Component.EventType.CLOSE],
          handleEvent);
  
      assertFalse(
          'Submenu must not have "-open" CSS class',
          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',
          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);
  
      events.unlisten(
          subMenu, [Component.EventType.OPEN, Component.EventType.CLOSE],
          handleEvent);
    },
  
    /** @suppress {checkTypes} suppression added to enable type checking */
    testCloseSubMenuBehavior() {
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      subMenu.getElement().id = 'subMenu';
  
      const innerMenu = subMenu.getMenu();
      innerMenu.getChildAt(0).getElement().id = 'child1';
  
      subMenu.setHighlighted(true);
      subMenu.showSubMenu();
  
      function MyFakeEvent(keyCode, opt_eventType) {
        this.type = opt_eventType || 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(KeyCodes.DOWN));
      assertEquals(
          'First item in submenu must be the aria-activedescendant', 'child1',
          aria.getState(menu.getElement(), State.ACTIVEDESCENDANT));
  
      // Dismiss the submenu and verify the activedescendant is updated correctly.
      subMenu.handleKeyEvent(new MyFakeEvent(KeyCodes.LEFT));
      assertEquals(
          'Submenu must be the aria-activedescendant', 'subMenu',
          aria.getState(menu.getElement(), State.ACTIVEDESCENDANT));
    },
  
    testLazyInstantiateSubMenu() {
      menu.decorate(dom.getElement('demoMenu'));
      const subMenu = menu.getChildAt(1);
      subMenu.setHighlighted(true);
  
      let lazyMenu;
  
      const key = events.listen(subMenu, Component.EventType.OPEN, (e) => {
        lazyMenu = new Menu();
        lazyMenu.addItem(new MenuItem('foo'));
        lazyMenu.addItem(new 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();
  
      events.unlistenByKey(key);
    },
  
    testReusableMenu() {
      const subMenuOne = new SubMenu('SubMenu One');
      const subMenuTwo = new SubMenu('SubMenu Two');
      menu.addItem(subMenuOne);
      menu.addItem(subMenuTwo);
      menu.render(dom.getElement('sandbox'));
  
      // It is possible for the same popup menu to be shared between different
      // submenus.
      const sharedMenu = new Menu();
      sharedMenu.addItem(new MenuItem('Hello'));
      sharedMenu.addItem(new 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).
     * @suppress {checkTypes} suppression added to enable type checking
     */
    testDeleteItemDuringSubmenuDisplayInterval() {
      mockClock = new MockClock(true);
  
      const submenu = new SubMenu('submenu');
      submenu.addItem(new MenuItem('submenu item 1'));
      menu.addItem(submenu);
  
      // Trigger mouseover, and remove item before showSubMenu can be called.
      /** @suppress {checkTypes} suppression added to enable type checking */
      const e = new GoogEvent();
      submenu.handleMouseOver(e);
      menu.removeItem(submenu);
      mockClock.tick(SubMenu.MENU_DELAY_MS);
      // (No JS error should occur.)
    },
  
    testShowSubMenuAfterRemoval() {
      const submenu = new 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.
       @suppress {visibility} suppression added to enable type checking
     */
    testSubmenuSelectable() {
      const submenu = new SubMenu('submenu');
      submenu.addItem(new MenuItem('submenu item 1'));
      menu.addItem(submenu);
      submenu.setSelectable(true);
  
      let numClicks = 0;
      const menuClickedFn = (e) => {
        numClicks++;
      };
  
      events.listen(submenu, 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.
       @suppress {visibility} suppression added to enable type checking
     */
    testSubmenuCheckable() {
      const submenu = new SubMenu('submenu');
      submenu.addItem(new MenuItem('submenu item 1'));
      menu.addItem(submenu);
      submenu.setCheckable(true);
  
      let numClicks = 0;
      const menuClickedFn = (e) => {
        numClicks++;
      };
  
      events.listen(submenu, 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.
     */
    testEnteringChildCancelsDismiss() {
      const submenu = new SubMenu('submenu');
      submenu.isInDocument = functions.TRUE;
      submenu.addItem(new MenuItem('submenu item 1'));
      menu.addItem(submenu);
  
      mockClock = new MockClock(true);
      submenu.getMenu().setVisible(true);
  
      // This starts the dismiss timer.
      submenu.setHighlighted(false);
  
      // This should cancel the dismiss timer.
      submenu.getMenu().dispatchEvent(Component.EventType.ENTER);
  
      // Tick the length of the dismiss timer.
      mockClock.tick(SubMenu.MENU_DELAY_MS);
  
      // Check that the menu is now highlighted and still visible.
      assertTrue(submenu.getMenu().isVisible());
      assertTrue(submenu.isHighlighted());
    },
  });
  