blob: d2948ac2ef98562a43825d93e1a21f00337b2d25 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/** @type {cr.ui.MultiMenuButton} */
let menubutton;
/** @type {cr.ui.Menu} */
let topMenu;
/** @type {cr.ui.Menu} */
let subMenu;
/** @type {cr.ui.Menu} */
let secondSubMenu;
// Set up test components.
function setUp() {
// Internals of WebUI reference this property when processing
// keyboard events, so we need to prepare it to stop asserts.
loadTimeData.data = {'SHORTCUT_ENTER': 'Enter'};
// Install cr.ui <command> elements and <cr-menu>s on the page.
document.body.innerHTML = [
'<style>',
' cr-menu {',
' position: fixed;',
' padding: 8px;',
' }',
' cr-menu-item {',
' width: 10px;',
' height: 10px;',
' display: block;',
' background-color: blue;',
' }',
'</style>',
'<command id="default-task">',
'<command id="more-actions">',
'<command id="show-submenu" shortcut="Enter">',
'<button id="test-menu-button" menu="#menu"></button>',
'<cr-menu id="menu" hidden>',
' <cr-menu-item id="default" command="#default-task"></cr-menu-item>',
' <cr-menu-item id="more" command="#more-actions"></cr-menu-item>',
' <cr-menu-item id="host-sub-menu" command="#show-submenu"',
'visibleif="full-page" class="hide-on-toolbar"',
'sub-menu="#sub-menu" hidden></cr-menu-item>',
' <cr-menu-item id="host-second-sub-menu" command="#show-submenu"',
'visibleif="full-page" class="hide-on-toolbar"',
'sub-menu="#second-sub-menu" hidden></cr-menu-item>',
'</cr-menu>',
'<cr-menu id="sub-menu" hidden>',
' <cr-menu-item id="first" class="custom-appearance"></cr-menu-item>',
' <cr-menu-item id="second" class="custom-appearance"></cr-menu-item>',
'</cr-menu>',
'<cr-menu id="second-sub-menu" hidden>',
' <cr-menu-item id="secondone" class="custom-appearance"></cr-menu-item>',
'</cr-menu>',
'<div id="focus-div" tabindex="1"/>',
'<button id="focus-button" tabindex="2"/>',
'<cr-input id="focus-input" tabindex="3">',
'</cr-input>',
].join('');
// Initialize cr.ui.Command with the <command>s.
cr.ui.decorate('command', cr.ui.Command);
menubutton =
util.queryDecoratedElement('#test-menu-button', cr.ui.MultiMenuButton);
topMenu = util.queryDecoratedElement('#menu', cr.ui.Menu);
subMenu = util.queryDecoratedElement('#sub-menu', cr.ui.Menu);
secondSubMenu = util.queryDecoratedElement('#second-sub-menu', cr.ui.Menu);
}
/**
* Send a 'mouseover' event to the element target of a query.
* @param {string} targetQuery Query to specify the element.
*/
function sendMouseOver(targetQuery) {
const event = new MouseEvent('mouseover', {
bubbles: true,
composed: true, // Allow the event to bubble past shadow DOM root.
});
const target = document.querySelector(targetQuery);
assertTrue(!!target);
return target.dispatchEvent(event);
}
/**
* Send a 'mousedown' event to the element target of a query.
* @param {string} targetQuery Query to specify the element.
*/
function sendMouseDown(targetQuery) {
const event = new MouseEvent('mousedown', {
bubbles: true,
composed: true, // Allow the event to bubble past shadow DOM root.
});
const target = document.querySelector(targetQuery);
assertTrue(!!target);
return target.dispatchEvent(event);
}
/**
* Send a 'mouseover' event to the element target of a query.
* @param {string} targetQuery Query to specify the element.
* @param {number} x Position of the event in 'X'.
* @param {number} y Position of the event in 'X'.
*/
function sendMouseOut(targetQuery, x, y) {
const event = new MouseEvent('mouseout', {
bubbles: true,
composed: true, // Allow the event to bubble past shadow DOM root.
clientX: x,
clientY: y,
});
const target = document.querySelector(targetQuery);
assertTrue(!!target);
return target.dispatchEvent(event);
}
/**
* Send a 'keydown' event to the element target of a query.
* @param {string} targetQuery Query to specify the element.
* @param {string} key property value for the key.
*/
function sendKeyDown(targetQuery, key) {
const event = new KeyboardEvent('keydown', {
key: key,
bubbles: true,
composed: true, // Allow the event to bubble past shadow DOM root.
});
const target = document.querySelector(targetQuery);
assertTrue(!!target);
return target.dispatchEvent(event);
}
/**
* Tests that making the top level menu visible doesn't
* cause the sub-menu to become visible.
*/
function testShowMenuDoesntShowSubMenu() {
menubutton.showMenu(true);
// Check the top level menu is not hidden.
assertFalse(topMenu.hasAttribute('hidden'));
// Check the sub-menu is hidden
assertTrue(subMenu.hasAttribute('hidden'));
}
/**
* Tests that a 'mouseover' event on top of normal menu-items
* doesn't cause the sub-menu to become visible.
*/
function testMouseOverNormalItemsDoesntShowSubMenu() {
menubutton.showMenu(true);
sendMouseOver('#default-task');
assertTrue(subMenu.hasAttribute('hidden'));
sendMouseOver('#more-actions');
assertTrue(subMenu.hasAttribute('hidden'));
}
/**
* Tests that 'mouseover' on a menu-item with 'show-submenu' command
* causes the sub-menu to become visible.
*/
function testMouseOverHostMenuShowsSubMenu() {
menubutton.showMenu(true);
sendMouseOver('#host-sub-menu');
assertFalse(subMenu.hasAttribute('hidden'));
}
/**
* Tests that 'mouseout' with the mouse over the top level
* menu causes the sub-menu to hide.
*/
function testMouseoutFromHostMenuItemToHostMenu() {
menubutton.showMenu(true);
sendMouseOver('#host-sub-menu');
assertFalse(subMenu.hasAttribute('hidden'));
// Get the location of one of our menu-items to send with the event.
const item = document.querySelector('#default-task');
const loc = item.getBoundingClientRect();
sendMouseOut('#host-sub-menu', loc.left, loc.top);
assertTrue(subMenu.hasAttribute('hidden'));
}
/**
* Tests that 'mouseout' with the mouse over the sub-menu
* doesn't hide the sub-menu.
*/
function testMouseoutFromHostMenuToSubMenu() {
menubutton.showMenu(true);
sendMouseOver('#host-sub-menu');
assertFalse(subMenu.hasAttribute('hidden'));
// Get the location of our sub-menu to send with the event.
const loc = subMenu.getBoundingClientRect();
sendMouseOut('#host-sub-menu', loc.left, loc.top);
assertFalse(subMenu.hasAttribute('hidden'));
}
/**
* Tests that selecting a menu-item with a 'show-submenu' command
* doesn't cause the sub-menu to become visible.
*/
function testSelectHostMenuItem() {
menubutton.showMenu(true);
topMenu.selectedIndex = 2;
const hostItem = document.querySelector('#host-sub-menu');
assert(hostItem.hasAttribute('selected'));
assertEquals(hostItem.getAttribute('selected'), 'selected');
assertTrue(subMenu.hasAttribute('hidden'));
}
/**
* Tests that selecting a menu-item with a 'show-submenu' command
* followed by calling the showSubMenu() method causes the
* sub-menu to become visible.
* (Note: in an application, this would happen from a command
* being executed rather than a direct showSubMenu() call.)
*/
function testSelectHostMenuItemAndCallShowSubMenu() {
testSelectHostMenuItem();
menubutton.menu.showSubMenu();
assertFalse(subMenu.hasAttribute('hidden'));
}
/**
* Tests that a mouse click outside of a menu and sub-menu causes
* both menus to hide.
*/
function testClickOutsideVisibleMenuAndSubMenu() {
testSelectHostMenuItemAndCallShowSubMenu();
const event = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
view: window,
composed: true, // Allow the event to bubble past shadow DOM root.
clientX: 0, // 0, 0 is in the padding area of the viewport
clientY: 0,
});
menubutton.dispatchEvent(event);
assertTrue(topMenu.hasAttribute('hidden'));
assertTrue(subMenu.hasAttribute('hidden'));
}
/**
* Tests that shrinking the window height will limit
* the height of the sub-menu.
*/
function testShrinkWindowSizesSubMenu() {
testSelectHostMenuItemAndCallShowSubMenu();
const subMenuPosition = subMenu.getBoundingClientRect();
// Reduce window innerHeight so sub-menu won't fit.
window.innerHeight = subMenuPosition.bottom - 10;
// Navigate from sub-menu to the parent menu.
sendKeyDown('#test-menu-button', 'ArrowLeft');
// Call the internal hide method, then re-show it
// to force the resizing behavior.
menubutton.menu.hideSubMenu_();
menubutton.menu.showSubMenu();
const shrunkPosition = subMenu.getBoundingClientRect();
assertTrue(shrunkPosition.bottom < window.innerHeight);
}
/**
* Tests that growing the window height will increase
* the height of the sub-menu.
*/
function testGrowWindowSizesSubMenu() {
// Remember the full size of the sub-menu
testSelectHostMenuItemAndCallShowSubMenu();
const subMenuPosition = subMenu.getBoundingClientRect();
// Make sure the sub-menu has been reduced in height.
testShrinkWindowSizesSubMenu();
// Make the window taller than the sub-menu plus padding.
window.innerHeight = subMenuPosition.bottom + 20;
// Navigate from sub-menu to the parent menu.
sendKeyDown('#test-menu-button', 'ArrowLeft');
// Call the internal hide method, then re-show it
// to force the resizing behavior.
menubutton.menu.hideSubMenu_();
menubutton.menu.showSubMenu();
const grownPosition = subMenu.getBoundingClientRect();
// Test that the height of the sub-menu is the same as
// the height at the start of this test (before we
// deliberately shrank it).
assertTrue(grownPosition.height === subMenuPosition.height);
}
/**
* Utility function to prepare the menu and sub-menu for keyboard tests.
*/
function prepareForKeyboardNavigation() {
// Make sure the both of the menus are active.
testSelectHostMenuItemAndCallShowSubMenu();
// Re-enable the menu-items, since showMenu() disables
// all of them due to the canExecute() tests all returning
// false since we're just a unit test harness. This is
// needed since the arrow key handlers skip over disabled items.
document.querySelector('#default').removeAttribute('disabled');
document.querySelector('#more').removeAttribute('disabled');
document.querySelector('#host-sub-menu').removeAttribute('disabled');
}
/**
* Tests that arrow navigates from main menu to sub-menu.
*/
function testNavigateFromMenuToSubMenu() {
prepareForKeyboardNavigation();
// Check that the hosting menu-item is not selected.
const hostItem = document.querySelector('#host-sub-menu');
assertFalse(hostItem.hasAttribute('selected'));
// Check that the sub-menu has taken selection.
const subItem = document.querySelector('#first');
assertTrue(subItem.hasAttribute('selected'));
}
/**
* Tests that arrow left moves back to the top level menu
* only when the selected sub-menu item is the first one.
*/
function testNavigateFromSubMenuToParentMenu() {
testNavigateFromMenuToSubMenu();
// Use the arrow key to go to the next sub-menu item.
sendKeyDown('#test-menu-button', 'ArrowDown');
const secondItem = document.querySelector('#second');
assertTrue(secondItem.hasAttribute('selected'));
// Try to navigate from sub-menu to the parent menu.
sendKeyDown('#test-menu-button', 'ArrowLeft');
// Check that parent menu hosting item didn't get selected.
const hostItem = document.querySelector('#host-sub-menu');
assertFalse(hostItem.hasAttribute('selected'));
// Check that selection is still on the sub-menu item.
assertTrue(secondItem.hasAttribute('selected'));
// Navigate up to the first sub-menu item.
sendKeyDown('#test-menu-button', 'ArrowUp');
// Check that the first sub-menu item is selected.
const firstItem = document.querySelector('#first');
assertTrue(firstItem.hasAttribute('selected'));
// Navigate back to the parent menu.
sendKeyDown('#test-menu-button', 'ArrowLeft');
// Check selection has moved back to the parent menu.
assertTrue(hostItem.hasAttribute('selected'));
assertFalse(firstItem.hasAttribute('selected'));
}
/**
* Tests that arrow up on the top level menu hides the
* sub menu when the sub-menu is visible.
*/
function testTopMenuArrowUpDismissesSubMenu() {
prepareForKeyboardNavigation();
// Check that the hosting menu-item is not selected.
const hostItem = document.querySelector('#host-sub-menu');
assertFalse(hostItem.hasAttribute('selected'));
// Navigate from sub-menu to the parent menu.
sendKeyDown('#test-menu-button', 'ArrowLeft');
// Check that the hosting menu-item is not selected.
assertTrue(hostItem.hasAttribute('selected'));
// Navigate up the main menu.
sendKeyDown('#test-menu-button', 'ArrowUp');
// Check that the sub-menu has been hidden.
assertTrue(subMenu.hasAttribute('hidden'));
}
/**
* Tests that the top level menu is resized when the parent
* window is too small to fit in without clipping.
*/
function testShrinkWindowSizesTopMenu() {
menubutton.showMenu(true);
const menuPosition = topMenu.getBoundingClientRect();
// Reduce window innerHeight so the menu won't fit.
window.innerHeight = menuPosition.height - 10;
// Call showMenu() which will first hide it, then re-open
// it to force the resizing behavior.
menubutton.showMenu(true);
const shrunkPosition = topMenu.getBoundingClientRect();
assertTrue(shrunkPosition.height == (window.innerHeight - 2));
}
/**
* Tests that mousedown the menu button grabs focus.
*/
function testFocusMenuButtonWithMouse() {
// Set focus on a div element.
//* @type {HTMLElement} */
const divElement = document.querySelector('#focus-div');
divElement.focus();
// Send mousedown event to the menu button.
sendMouseDown('#test-menu-button');
// Verify that the previously focused element still has focus.
assertTrue(document.hasFocus() && document.activeElement === divElement);
// Set focus on a button element.
//* @type {HTMLElement} */
const buttonElement = document.querySelector('#focus-button');
buttonElement.focus();
// Send mousedown event to the menu button.
sendMouseDown('#test-menu-button');
// Verify that the previously focused button has lost focus.
assertFalse(document.hasFocus() && document.activeElement === buttonElement);
// Verify the menu button has taken focus.
assertTrue(document.hasFocus() && document.activeElement === menubutton);
// Set focus on a cr-input element.
//* @type {HTMLElement} */
const inputElement = document.querySelector('#focus-input');
inputElement.focus();
// Send mousedown event to the menu button.
sendMouseDown('#test-menu-button');
// Verify the cr-input element has lost focus.
assertFalse(document.hasFocus() && document.activeElement === inputElement);
// Verify the menu button has taken focus.
assertTrue(document.hasFocus() && document.activeElement === menubutton);
}
/**
* Tests that opening a sub menu hides any showing sub menu.
*/
function testShowSubMenuHidesExisting() {
testMouseOverHostMenuShowsSubMenu();
sendMouseOver('#host-second-sub-menu');
// Check the previously shown sub menu is hidden.
assertTrue(subMenu.hasAttribute('hidden'));
// Check the second sub menu is visible.
assertFalse(secondSubMenu.hasAttribute('hidden'));
}