1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.chrome.browser.appmenu; 6 7import android.app.Activity; 8import android.content.pm.ActivityInfo; 9import android.test.suitebuilder.annotation.SmallTest; 10import android.view.KeyEvent; 11import android.view.MenuItem; 12import android.view.View; 13import android.widget.ListPopupWindow; 14import android.widget.ListView; 15 16import org.chromium.base.ThreadUtils; 17import org.chromium.base.test.util.Feature; 18import org.chromium.chrome.shell.ChromeShellActivity; 19import org.chromium.chrome.shell.ChromeShellActivity.AppMenuHandlerFactory; 20import org.chromium.chrome.shell.ChromeShellTestBase; 21import org.chromium.chrome.shell.R; 22import org.chromium.content.browser.test.util.Criteria; 23import org.chromium.content.browser.test.util.CriteriaHelper; 24 25/** 26 * Tests AppMenu popup 27 */ 28public class AppMenuTest extends ChromeShellTestBase { 29 private AppMenu mAppMenu; 30 private AppMenuHandlerForTest mAppMenuHandler; 31 32 /** 33 * AppMenuHandler that will be used to intercept item selections for testing. 34 */ 35 public static class AppMenuHandlerForTest extends AppMenuHandler { 36 int mLastSelectedItemId = -1; 37 38 /** 39 * AppMenuHandler for intercepting options item selections. 40 */ 41 public AppMenuHandlerForTest(Activity activity, AppMenuPropertiesDelegate delegate, 42 int menuResourceId) { 43 super(activity, delegate, menuResourceId); 44 } 45 46 @Override 47 void onOptionsItemSelected(MenuItem item) { 48 mLastSelectedItemId = item.getItemId(); 49 } 50 51 } 52 53 @Override 54 protected void setUp() throws Exception { 55 super.setUp(); 56 ChromeShellActivity.setAppMenuHandlerFactory(new AppMenuHandlerFactory() { 57 @Override 58 public AppMenuHandler getAppMenuHandler(Activity activity, 59 AppMenuPropertiesDelegate delegate, int menuResourceId) { 60 mAppMenuHandler = new AppMenuHandlerForTest(activity, delegate, menuResourceId); 61 return mAppMenuHandler; 62 } 63 }); 64 launchChromeShellWithBlankPage(); 65 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading()); 66 67 showAppMenuAndAssertMenuShown(); 68 mAppMenu = getActivity().getAppMenuHandler().getAppMenu(); 69 ThreadUtils.runOnUiThread(new Runnable() { 70 @Override 71 public void run() { 72 mAppMenu.getPopup().getListView().setSelection(0); 73 } 74 }); 75 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 76 @Override 77 public boolean isSatisfied() { 78 return getCurrentFocusedRow() == 0; 79 } 80 })); 81 getInstrumentation().waitForIdleSync(); 82 } 83 84 /** 85 * Test bounds when accessing the menu through the keyboard. 86 * Make sure that the menu stays open when trying to move past the first and last items. 87 */ 88 @SmallTest 89 @Feature({"Browser", "Main"}) 90 public void testKeyboardMenuBoundaries() throws InterruptedException { 91 moveToBoundary(false, true); 92 assertEquals(getCount() - 1, getCurrentFocusedRow()); 93 moveToBoundary(true, true); 94 assertEquals(0, getCurrentFocusedRow()); 95 moveToBoundary(false, true); 96 assertEquals(getCount() - 1, getCurrentFocusedRow()); 97 } 98 99 /** 100 * Test that typing ENTER immediately opening the menu works. 101 */ 102 @SmallTest 103 @Feature({"Browser", "Main"}) 104 public void testKeyboardMenuEnterOnOpen() throws InterruptedException { 105 hitEnterAndAssertAppMenuDismissed(); 106 } 107 108 /** 109 * Test that hitting ENTER past the top item doesn't crash Chrome. 110 */ 111 @SmallTest 112 @Feature({"Browser", "Main"}) 113 public void testKeyboardEnterAfterMovePastTopItem() throws InterruptedException { 114 moveToBoundary(true, true); 115 assertEquals(0, getCurrentFocusedRow()); 116 hitEnterAndAssertAppMenuDismissed(); 117 } 118 119 /** 120 * Test that hitting ENTER past the bottom item doesn't crash Chrome. 121 * Catches regressions for http://crbug.com/181067 122 */ 123 @SmallTest 124 @Feature({"Browser", "Main"}) 125 public void testKeyboardEnterAfterMovePastBottomItem() throws InterruptedException { 126 moveToBoundary(false, true); 127 assertEquals(getCount() - 1, getCurrentFocusedRow()); 128 hitEnterAndAssertAppMenuDismissed(); 129 } 130 131 /** 132 * Test that hitting ENTER on the top item actually triggers the top item. 133 * Catches regressions for https://crbug.com/191239 for shrunken menus. 134 */ 135 @SmallTest 136 @Feature({"Browser", "Main"}) 137 public void testKeyboardMenuEnterOnTopItemLandscape() throws InterruptedException { 138 getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 139 showAppMenuAndAssertMenuShown(); 140 moveToBoundary(true, false); 141 assertEquals(0, getCurrentFocusedRow()); 142 hitEnterAndAssertAppMenuDismissed(); 143 } 144 145 /** 146 * Test that hitting ENTER on the top item doesn't crash Chrome. 147 */ 148 @SmallTest 149 @Feature({"Browser", "Main"}) 150 public void testKeyboardMenuEnterOnTopItemPortrait() throws InterruptedException { 151 getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 152 showAppMenuAndAssertMenuShown(); 153 moveToBoundary(true, false); 154 assertEquals(0, getCurrentFocusedRow()); 155 hitEnterAndAssertAppMenuDismissed(); 156 } 157 158 /** 159 * Test that changing orientation hides the menu. 160 */ 161 @SmallTest 162 @Feature({"Browser", "Main"}) 163 public void testChangingOrientationHidesMenu() throws InterruptedException { 164 getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); 165 showAppMenuAndAssertMenuShown(); 166 getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); 167 assertTrue("AppMenu did not dismiss", 168 CriteriaHelper.pollForCriteria(new Criteria() { 169 @Override 170 public boolean isSatisfied() { 171 return !mAppMenuHandler.isAppMenuShowing(); 172 } 173 })); 174 } 175 176 private void showAppMenuAndAssertMenuShown() throws InterruptedException { 177 final View menuButton = getActivity().findViewById(R.id.menu_button); 178 ThreadUtils.runOnUiThread(new Runnable() { 179 @Override 180 public void run() { 181 menuButton.performClick(); 182 } 183 }); 184 assertTrue("AppMenu did not show", 185 CriteriaHelper.pollForCriteria(new Criteria() { 186 @Override 187 public boolean isSatisfied() { 188 return mAppMenuHandler.isAppMenuShowing(); 189 } 190 })); 191 } 192 193 private void hitEnterAndAssertAppMenuDismissed() throws InterruptedException { 194 getInstrumentation().waitForIdleSync(); 195 pressKey(KeyEvent.KEYCODE_ENTER); 196 assertTrue("AppMenu did not dismiss", 197 CriteriaHelper.pollForCriteria(new Criteria() { 198 @Override 199 public boolean isSatisfied() { 200 return !mAppMenuHandler.isAppMenuShowing(); 201 } 202 })); 203 } 204 205 private void moveToBoundary(boolean towardsTop, boolean movePast) throws InterruptedException { 206 // Move to the boundary. 207 final int end = towardsTop ? 0 : getCount() - 1; 208 int increment = towardsTop ? -1 : 1; 209 for (int index = getCurrentFocusedRow(); index != end; index += increment) { 210 pressKey(towardsTop ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN); 211 final int expectedPosition = index + increment; 212 assertTrue("Focus did not move to the next menu item", 213 CriteriaHelper.pollForCriteria(new Criteria() { 214 @Override 215 public boolean isSatisfied() { 216 return getCurrentFocusedRow() == expectedPosition; 217 } 218 })); 219 } 220 221 // Try moving past it by one. 222 if (movePast) { 223 pressKey(towardsTop ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN); 224 assertTrue("Focus moved past the edge menu item", 225 CriteriaHelper.pollForCriteria(new Criteria() { 226 @Override 227 public boolean isSatisfied() { 228 return getCurrentFocusedRow() == end; 229 } 230 })); 231 } 232 233 // The menu should stay open. 234 assertTrue(mAppMenu.isShowing()); 235 } 236 237 private void pressKey(final int keycode) { 238 final View view = mAppMenu.getPopup().getListView(); 239 ThreadUtils.runOnUiThread(new Runnable() { 240 @Override 241 public void run() { 242 view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keycode)); 243 view.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keycode)); 244 } 245 }); 246 getInstrumentation().waitForIdleSync(); 247 } 248 249 private int getCurrentFocusedRow() { 250 ListPopupWindow popup = mAppMenu.getPopup(); 251 if (popup == null || popup.getListView() == null) return ListView.INVALID_POSITION; 252 ListView listView = popup.getListView(); 253 return listView.getSelectedItemPosition(); 254 } 255 256 private int getCount() { 257 ListPopupWindow popup = mAppMenu.getPopup(); 258 if (popup == null || popup.getListView() == null) return 0; 259 return popup.getListView().getCount(); 260 } 261} 262