Home | History | Annotate | Download | only in cts
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package android.widget.cts;
     18 
     19 import static org.junit.Assert.assertEquals;
     20 import static org.junit.Assert.assertNotNull;
     21 import static org.junit.Assert.assertNull;
     22 import static org.mockito.Mockito.any;
     23 import static org.mockito.Mockito.mock;
     24 import static org.mockito.Mockito.never;
     25 import static org.mockito.Mockito.times;
     26 import static org.mockito.Mockito.verify;
     27 import static org.mockito.Mockito.verifyNoMoreInteractions;
     28 
     29 import android.app.Activity;
     30 import android.app.Instrumentation;
     31 import android.support.test.InstrumentationRegistry;
     32 import android.support.test.annotation.UiThreadTest;
     33 import android.support.test.filters.MediumTest;
     34 import android.support.test.rule.ActivityTestRule;
     35 import android.support.test.runner.AndroidJUnit4;
     36 import android.view.Gravity;
     37 import android.view.Menu;
     38 import android.view.MenuInflater;
     39 import android.view.MenuItem;
     40 import android.view.SubMenu;
     41 import android.view.View;
     42 import android.widget.EditText;
     43 import android.widget.ImageView;
     44 import android.widget.ListView;
     45 import android.widget.PopupMenu;
     46 
     47 import com.android.compatibility.common.util.CtsTouchUtils;
     48 import com.android.compatibility.common.util.WidgetTestUtils;
     49 
     50 import org.junit.After;
     51 import org.junit.Before;
     52 import org.junit.Rule;
     53 import org.junit.Test;
     54 import org.junit.runner.RunWith;
     55 
     56 @MediumTest
     57 @RunWith(AndroidJUnit4.class)
     58 public class PopupMenuTest {
     59     private Instrumentation mInstrumentation;
     60     private Activity mActivity;
     61 
     62     private Builder mBuilder;
     63     private PopupMenu mPopupMenu;
     64 
     65     @Rule
     66     public ActivityTestRule<PopupMenuCtsActivity> mActivityRule =
     67             new ActivityTestRule<>(PopupMenuCtsActivity.class);
     68 
     69 
     70     @UiThreadTest
     71     @Before
     72     public void setup() {
     73         mInstrumentation = InstrumentationRegistry.getInstrumentation();
     74         mActivity = mActivityRule.getActivity();
     75 
     76         // Disable and remove focusability on the first child of our activity so that
     77         // it doesn't bring in the soft keyboard that can mess up with some of the tests
     78         // (such as menu dismissal when we emulate a tap outside the menu bounds).
     79         final EditText editText = (EditText) mActivity.findViewById(R.id.anchor_upper_left);
     80         editText.setEnabled(false);
     81         editText.setFocusable(false);
     82     }
     83 
     84     @After
     85     public void teardown() throws Throwable {
     86         if (mPopupMenu != null) {
     87             mActivityRule.runOnUiThread(mPopupMenu::dismiss);
     88         }
     89     }
     90 
     91     private void verifyMenuContent() {
     92         final Menu menu = mPopupMenu.getMenu();
     93         assertEquals(6, menu.size());
     94         assertEquals(R.id.action_highlight, menu.getItem(0).getItemId());
     95         assertEquals(R.id.action_edit, menu.getItem(1).getItemId());
     96         assertEquals(R.id.action_delete, menu.getItem(2).getItemId());
     97         assertEquals(R.id.action_ignore, menu.getItem(3).getItemId());
     98         assertEquals(R.id.action_share, menu.getItem(4).getItemId());
     99         assertEquals(R.id.action_print, menu.getItem(5).getItemId());
    100 
    101         final SubMenu shareSubMenu = menu.getItem(4).getSubMenu();
    102         assertNotNull(shareSubMenu);
    103         assertEquals(2, shareSubMenu.size());
    104         assertEquals(R.id.action_share_email, shareSubMenu.getItem(0).getItemId());
    105         assertEquals(R.id.action_share_circles, shareSubMenu.getItem(1).getItemId());
    106     }
    107 
    108     @Test
    109     public void testPopulateViaInflater() throws Throwable {
    110         mBuilder = new Builder().inflateWithInflater(true);
    111         mActivityRule.runOnUiThread(mBuilder::show);
    112         mInstrumentation.waitForIdleSync();
    113 
    114         verifyMenuContent();
    115     }
    116 
    117     @Test
    118     public void testDirectPopulate() throws Throwable {
    119         mBuilder = new Builder().inflateWithInflater(false);
    120         mActivityRule.runOnUiThread(mBuilder::show);
    121         mInstrumentation.waitForIdleSync();
    122 
    123         verifyMenuContent();
    124     }
    125 
    126     @Test
    127     public void testAccessGravity() throws Throwable {
    128         mBuilder = new Builder();
    129         mActivityRule.runOnUiThread(mBuilder::show);
    130 
    131         assertEquals(Gravity.NO_GRAVITY, mPopupMenu.getGravity());
    132         mPopupMenu.setGravity(Gravity.TOP);
    133         assertEquals(Gravity.TOP, mPopupMenu.getGravity());
    134     }
    135 
    136     @Test
    137     public void testConstructorWithGravity() throws Throwable {
    138         mBuilder = new Builder().withGravity(Gravity.TOP);
    139         mActivityRule.runOnUiThread(mBuilder::show);
    140 
    141         assertEquals(Gravity.TOP, mPopupMenu.getGravity());
    142     }
    143 
    144     @Test
    145     public void testDismissalViaAPI() throws Throwable {
    146         mBuilder = new Builder().withDismissListener();
    147         mActivityRule.runOnUiThread(mBuilder::show);
    148 
    149         mInstrumentation.waitForIdleSync();
    150         verify(mBuilder.mOnDismissListener, never()).onDismiss(mPopupMenu);
    151 
    152         mActivityRule.runOnUiThread(mPopupMenu::dismiss);
    153         mInstrumentation.waitForIdleSync();
    154         verify(mBuilder.mOnDismissListener, times(1)).onDismiss(mPopupMenu);
    155 
    156         mActivityRule.runOnUiThread(mPopupMenu::dismiss);
    157         mInstrumentation.waitForIdleSync();
    158         // Shouldn't have any more interactions with our dismiss listener since the menu was
    159         // already dismissed when we called dismiss()
    160         verifyNoMoreInteractions(mBuilder.mOnDismissListener);
    161     }
    162 
    163     @Test
    164     public void testNestedDismissalViaAPI() throws Throwable {
    165         // Use empty popup style to remove all transitions from the popup. That way we don't
    166         // need to synchronize with the popup window enter transition before proceeding to
    167         // "click" a submenu item.
    168         mBuilder = new Builder().withDismissListener()
    169                 .withPopupStyleResource(R.style.PopupWindow_NullTransitions);
    170         mActivityRule.runOnUiThread(mBuilder::show);
    171         mInstrumentation.waitForIdleSync();
    172         verify(mBuilder.mOnDismissListener, never()).onDismiss(mPopupMenu);
    173 
    174         mActivityRule.runOnUiThread(
    175                 () -> mPopupMenu.getMenu().performIdentifierAction(R.id.action_share, 0));
    176         mInstrumentation.waitForIdleSync();
    177 
    178         mActivityRule.runOnUiThread(
    179                 () -> mPopupMenu.getMenu().findItem(R.id.action_share).getSubMenu().
    180                         performIdentifierAction(R.id.action_share_email, 0));
    181         mInstrumentation.waitForIdleSync();
    182 
    183         mActivityRule.runOnUiThread(mPopupMenu::dismiss);
    184         mInstrumentation.waitForIdleSync();
    185         verify(mBuilder.mOnDismissListener, times(1)).onDismiss(mPopupMenu);
    186 
    187         mActivityRule.runOnUiThread(mPopupMenu::dismiss);
    188         mInstrumentation.waitForIdleSync();
    189         // Shouldn't have any more interactions with our dismiss listener since the menu was
    190         // already dismissed when we called dismiss()
    191         verifyNoMoreInteractions(mBuilder.mOnDismissListener);
    192     }
    193 
    194     @Test
    195     public void testDismissalViaTouch() throws Throwable {
    196         // Use empty popup style to remove all transitions from the popup. That way we don't
    197         // need to synchronize with the popup window enter transition before proceeding to
    198         // emulate a click outside the popup window bounds.
    199         mBuilder = new Builder().withDismissListener()
    200                 .withPopupMenuContent(R.menu.popup_menu_single)
    201                 .withPopupStyleResource(R.style.PopupWindow_NullTransitions);
    202         mActivityRule.runOnUiThread(mBuilder::show);
    203         mInstrumentation.waitForIdleSync();
    204 
    205         // The call below uses Instrumentation to emulate a tap outside the bounds of the
    206         // displayed popup menu. This tap is then treated by the framework to be "split" as
    207         // the ACTION_OUTSIDE for the popup itself, as well as DOWN / MOVE / UP for the underlying
    208         // view root if the popup is not modal.
    209         // It is not correct to emulate these two sequences separately in the test, as it
    210         // wouldn't emulate the user-facing interaction for this test. Also, we don't want to use
    211         // View.dispatchTouchEvent directly as that would require emulation of two separate
    212         // sequences as well.
    213         CtsTouchUtils.emulateTapOnView(mInstrumentation, mBuilder.mAnchor, 10, -20);
    214 
    215         // At this point our popup should have notified its dismiss listener
    216         verify(mBuilder.mOnDismissListener, times(1)).onDismiss(mPopupMenu);
    217     }
    218 
    219     @Test
    220     public void testSimpleMenuItemClickViaAPI() throws Throwable {
    221         mBuilder = new Builder().withMenuItemClickListener().withDismissListener();
    222         mActivityRule.runOnUiThread(mBuilder::show);
    223 
    224         // Verify that our menu item click listener hasn't been called yet
    225         verify(mBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class));
    226 
    227         mActivityRule.runOnUiThread(
    228                 () -> mPopupMenu.getMenu().performIdentifierAction(R.id.action_highlight, 0));
    229 
    230         // Verify that our menu item click listener has been called with the expected menu item
    231         verify(mBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick(
    232                 mPopupMenu.getMenu().findItem(R.id.action_highlight));
    233 
    234         // Popup menu should be automatically dismissed on selecting an item
    235         verify(mBuilder.mOnDismissListener, times(1)).onDismiss(mPopupMenu);
    236         verifyNoMoreInteractions(mBuilder.mOnDismissListener);
    237     }
    238 
    239     @Test
    240     public void testSubMenuClickViaAPI() throws Throwable {
    241         // Use empty popup style to remove all transitions from the popup. That way we don't
    242         // need to synchronize with the popup window enter transition before proceeding to
    243         // "click" a submenu item.
    244         mBuilder = new Builder().withDismissListener().withMenuItemClickListener()
    245                 .withPopupStyleResource(R.style.PopupWindow_NullTransitions);
    246         mActivityRule.runOnUiThread(mBuilder::show);
    247         mInstrumentation.waitForIdleSync();
    248 
    249         // Verify that our menu item click listener hasn't been called yet
    250         verify(mBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class));
    251 
    252         mActivityRule.runOnUiThread(
    253                 () -> mPopupMenu.getMenu().performIdentifierAction(R.id.action_share, 0));
    254         // Verify that our menu item click listener has been called on "share" action
    255         // and that the dismiss listener hasn't been called just as a result of opening the submenu.
    256         verify(mBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick(
    257                 mPopupMenu.getMenu().findItem(R.id.action_share));
    258         verify(mBuilder.mOnDismissListener, never()).onDismiss(mPopupMenu);
    259 
    260         mActivityRule.runOnUiThread(
    261                 () -> mPopupMenu.getMenu().findItem(R.id.action_share).getSubMenu().
    262                         performIdentifierAction(R.id.action_share_email, 0));
    263 
    264         // Verify that out menu item click listener has been called with the expected menu item
    265         verify(mBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick(
    266                 mPopupMenu.getMenu().findItem(R.id.action_share).getSubMenu()
    267                         .findItem(R.id.action_share_email));
    268         verifyNoMoreInteractions(mBuilder.mOnMenuItemClickListener);
    269 
    270         // Popup menu should be automatically dismissed on selecting an item
    271         verify(mBuilder.mOnDismissListener, times(1)).onDismiss(mPopupMenu);
    272         verifyNoMoreInteractions(mBuilder.mOnDismissListener);
    273     }
    274 
    275     @Test
    276     public void testItemViewAttributes() throws Throwable {
    277         mBuilder = new Builder().withDismissListener().withAnchorId(R.id.anchor_upper_left);
    278         WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, mBuilder::show, true);
    279 
    280         Menu menu = mPopupMenu.getMenu();
    281         ListView menuItemList = mPopupMenu.getMenuListView();
    282 
    283         for (int i = 0; i != menu.size(); i++) {
    284             MenuItem item = menu.getItem(i);
    285             View itemView = null;
    286             // On smaller screens, not all menu items will be visible.
    287             if (i < menuItemList.getChildCount()) {
    288                 itemView = menuItemList.getChildAt(i);
    289                 assertNotNull(itemView);
    290             }
    291 
    292             if (i < 2) {
    293                 assertNotNull(item.getContentDescription());
    294                 assertNotNull(item.getTooltipText());
    295             } else {
    296                 assertNull(item.getContentDescription());
    297                 assertNull(item.getTooltipText());
    298             }
    299             if (itemView != null) {
    300                 // Tooltips are not set on list-based menus.
    301                 assertNull(itemView.getTooltipText());
    302                 assertEquals(item.getContentDescription(), itemView.getContentDescription());
    303             }
    304         }
    305     }
    306 
    307     @Test
    308     public void testGroupDividerEnabledAPI() throws Throwable {
    309         testGroupDivider(false);
    310         testGroupDivider(true);
    311     }
    312 
    313     private void testGroupDivider(boolean groupDividerEnabled) throws Throwable {
    314         mBuilder = new Builder().withGroupDivider(groupDividerEnabled)
    315             .withAnchorId(R.id.anchor_upper_left);
    316         WidgetTestUtils.runOnMainAndLayoutSync(mActivityRule, mBuilder::show, true);
    317 
    318         Menu menu = mPopupMenu.getMenu();
    319         ListView menuItemList = mPopupMenu.getMenuListView();
    320 
    321         for (int i = 0; i < menuItemList.getChildCount(); i++) {
    322             final int currGroupId = menu.getItem(i).getGroupId();
    323             final int prevGroupId =
    324                     i - 1 >= 0 ? menu.getItem(i - 1).getGroupId() : currGroupId;
    325             View itemView = menuItemList.getChildAt(i);
    326             ImageView groupDivider = itemView.findViewById(com.android.internal.R.id.group_divider);
    327 
    328             assertNotNull(groupDivider);
    329             if (!groupDividerEnabled || currGroupId == prevGroupId) {
    330                 assertEquals(groupDivider.getVisibility(), View.GONE);
    331             } else {
    332                 assertEquals(groupDivider.getVisibility(), View.VISIBLE);
    333             }
    334         }
    335 
    336         teardown();
    337     }
    338 
    339     /**
    340      * Inner helper class to configure an instance of {@link PopupMenu} for the specific test.
    341      * The main reason for its existence is that once a popup menu is shown with the show() method,
    342      * most of its configuration APIs are no-ops. This means that we can't add logic that is
    343      * specific to a certain test once it's shown and we have a reference to a displayed
    344      * {@link PopupMenu}.
    345      */
    346     public class Builder {
    347         private boolean mHasDismissListener;
    348         private boolean mHasMenuItemClickListener;
    349         private boolean mInflateWithInflater;
    350 
    351         private int mAnchorId = R.id.anchor_middle_left;
    352         private int mPopupMenuContent = R.menu.popup_menu;
    353 
    354         private boolean mUseCustomPopupResource;
    355         private int mPopupStyleResource = 0;
    356 
    357         private boolean mUseCustomGravity;
    358         private int mGravity = Gravity.NO_GRAVITY;
    359 
    360         private PopupMenu.OnMenuItemClickListener mOnMenuItemClickListener;
    361         private PopupMenu.OnDismissListener mOnDismissListener;
    362 
    363         private View mAnchor;
    364 
    365         private boolean mGroupDividerEnabled = false;
    366 
    367         public Builder withMenuItemClickListener() {
    368             mHasMenuItemClickListener = true;
    369             return this;
    370         }
    371 
    372         public Builder withDismissListener() {
    373             mHasDismissListener = true;
    374             return this;
    375         }
    376 
    377         public Builder inflateWithInflater(boolean inflateWithInflater) {
    378             mInflateWithInflater = inflateWithInflater;
    379             return this;
    380         }
    381 
    382         public Builder withPopupStyleResource(int popupStyleResource) {
    383             mUseCustomPopupResource = true;
    384             mPopupStyleResource = popupStyleResource;
    385             return this;
    386         }
    387 
    388         public Builder withPopupMenuContent(int popupMenuContent) {
    389             mPopupMenuContent = popupMenuContent;
    390             return this;
    391         }
    392 
    393         public Builder withGravity(int gravity) {
    394             mUseCustomGravity = true;
    395             mGravity = gravity;
    396             return this;
    397         }
    398 
    399         public Builder withGroupDivider(boolean groupDividerEnabled) {
    400             mGroupDividerEnabled = groupDividerEnabled;
    401             return this;
    402         }
    403 
    404         public Builder withAnchorId(int anchorId) {
    405             mAnchorId = anchorId;
    406             return this;
    407         }
    408 
    409         private void configure() {
    410             mAnchor = mActivity.findViewById(mAnchorId);
    411             if (!mUseCustomGravity && !mUseCustomPopupResource) {
    412                 mPopupMenu = new PopupMenu(mActivity, mAnchor);
    413             } else if (!mUseCustomPopupResource) {
    414                 mPopupMenu = new PopupMenu(mActivity, mAnchor, mGravity);
    415             } else {
    416                 mPopupMenu = new PopupMenu(mActivity, mAnchor, Gravity.NO_GRAVITY,
    417                         0, mPopupStyleResource);
    418             }
    419 
    420             if (mInflateWithInflater) {
    421                 final MenuInflater menuInflater = mPopupMenu.getMenuInflater();
    422                 menuInflater.inflate(mPopupMenuContent, mPopupMenu.getMenu());
    423             } else {
    424                 mPopupMenu.inflate(mPopupMenuContent);
    425             }
    426 
    427             if (mHasMenuItemClickListener) {
    428                 // Register a mock listener to be notified when a menu item in our popup menu has
    429                 // been clicked.
    430                 mOnMenuItemClickListener = mock(PopupMenu.OnMenuItemClickListener.class);
    431                 mPopupMenu.setOnMenuItemClickListener(mOnMenuItemClickListener);
    432             }
    433 
    434             if (mHasDismissListener) {
    435                 // Register a mock listener to be notified when our popup menu is dismissed.
    436                 mOnDismissListener = mock(PopupMenu.OnDismissListener.class);
    437                 mPopupMenu.setOnDismissListener(mOnDismissListener);
    438             }
    439 
    440             if (mGroupDividerEnabled) {
    441                 mPopupMenu.getMenu().setGroupDividerEnabled(true);
    442             }
    443         }
    444 
    445         public void show() {
    446             configure();
    447             // Show the popup menu
    448             mPopupMenu.show();
    449         }
    450     }
    451 }
    452