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