blob: 13ef9f54b28df1a64571ff905cccb70d13d8949b [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.appcompat.widget;
import static android.support.test.espresso.Espresso.onData;
import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.RootMatchers.isPlatformPopup;
import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.app.Instrumentation;
import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.SystemClock;
import android.support.test.InstrumentationRegistry;
import android.support.test.espresso.Root;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.filters.FlakyTest;
import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.text.TextUtils;
import android.view.InputDevice;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewParent;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ListView;
import androidx.appcompat.test.R;
import androidx.core.view.MenuItemCompat;
import androidx.testutils.PollingCheck;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
import org.hamcrest.TypeSafeMatcher;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
public class PopupMenuTest {
@Rule
public final ActivityTestRule<PopupTestActivity> mActivityTestRule =
new ActivityTestRule<>(PopupTestActivity.class);
// Since PopupMenu doesn't expose any access to the underlying
// implementation (like ListPopupWindow.getListView), we're relying on the
// class name of the list view from MenuPopupWindow that is being used
// in PopupMenu. This is not the cleanest, but it's not making any assumptions
// on the platform-specific details of the popup windows.
private static final String DROP_DOWN_CLASS_NAME =
"androidx.appcompat.widget.MenuPopupWindow$MenuDropDownListView";
private FrameLayout mContainer;
private Button mButton;
private PopupMenu mPopupMenu;
private Resources mResources;
private View mMainDecorView;
@Before
public void setUp() throws Exception {
final PopupTestActivity activity = mActivityTestRule.getActivity();
mContainer = activity.findViewById(R.id.container);
mButton = mContainer.findViewById(R.id.test_button);
mResources = mActivityTestRule.getActivity().getResources();
mMainDecorView = mActivityTestRule.getActivity().getWindow().getDecorView();
}
@After
public void tearDown() {
if (mPopupMenu != null) {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
@Override
public void run() {
mPopupMenu.dismiss();
}
});
}
}
@Test
@MediumTest
public void testBasicContent() throws Throwable {
final Builder menuBuilder = new Builder();
menuBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(click());
assertNotNull("Popup menu created", mPopupMenu);
final MenuItem hightlightItem = mPopupMenu.getMenu().findItem(R.id.action_highlight);
assertEquals(mResources.getString(R.string.popup_menu_highlight_description),
MenuItemCompat.getContentDescription(hightlightItem));
assertEquals(mResources.getString(R.string.popup_menu_highlight_tooltip),
MenuItemCompat.getTooltipText(hightlightItem));
final MenuItem editItem = mPopupMenu.getMenu().findItem(R.id.action_edit);
assertNotNull(MenuItemCompat.getContentDescription(hightlightItem));
assertNotNull(MenuItemCompat.getTooltipText(hightlightItem));
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
MenuItemCompat.setContentDescription(editItem,
mResources.getString(R.string.popup_menu_edit_description));
MenuItemCompat.setTooltipText(editItem,
mResources.getString(R.string.popup_menu_edit_tooltip));
}
});
// Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing.
// Use a custom matcher to check the visibility of the drop down list view instead.
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
.inRoot(isPlatformPopup()).check(matches(isDisplayed()));
// Note that MenuItem.isVisible() refers to the current "data" visibility state
// and not the "on screen" visibility state. This is why we're testing the display
// visibility of our main and sub menu items.
// onData(anything()).atPosition(x) operates on the content of the popup. It is used to
// scroll the popup if necessary to make sure the menu item views are visible.
onData(anything()).atPosition(0).check(matches(isDisplayed()));
onView(withText(mResources.getString(R.string.popup_menu_highlight)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.check(matches(isDisplayed()))
.check(matches(selfOrParentWithContentDescription(
mResources.getString(R.string.popup_menu_highlight_description))));
onData(anything()).atPosition(1).check(matches(isDisplayed()));
onView(withText(mResources.getString(R.string.popup_menu_edit)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.check(matches(isDisplayed()))
.check(matches(selfOrParentWithContentDescription(
mResources.getString(R.string.popup_menu_edit_description))));
onData(anything()).atPosition(2).check(matches(isDisplayed()));
onView(withText(mResources.getString(R.string.popup_menu_delete)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.check(matches(isDisplayed()));
onData(anything()).atPosition(3).check(matches(isDisplayed()));
onView(withText(mResources.getString(R.string.popup_menu_ignore)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.check(matches(isDisplayed()));
onData(anything()).atPosition(4).check(matches(isDisplayed()));
onView(withText(mResources.getString(R.string.popup_menu_share)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.check(matches(isDisplayed()));
onData(anything()).atPosition(5).check(matches(isDisplayed()));
onView(withText(mResources.getString(R.string.popup_menu_print)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.check(matches(isDisplayed()));
// Share submenu items should not be visible
onView(withText(mResources.getString(R.string.popup_menu_share_email)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.check(doesNotExist());
onView(withText(mResources.getString(R.string.popup_menu_share_circles)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.check(doesNotExist());
}
/**
* Returns the location of our popup menu in its window.
*/
private int[] getPopupLocationInWindow() {
final int[] location = new int[2];
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
.inRoot(isPlatformPopup()).perform(new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public String getDescription() {
return "Popup matcher";
}
@Override
public void perform(UiController uiController, View view) {
view.getLocationInWindow(location);
}
});
return location;
}
/**
* Returns the location of our popup menu on the screen.
*/
private int[] getPopupLocationOnScreen() {
final int[] location = new int[2];
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
.inRoot(isPlatformPopup()).perform(new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public String getDescription() {
return "Popup matcher";
}
@Override
public void perform(UiController uiController, View view) {
view.getLocationOnScreen(location);
}
});
return location;
}
/**
* Returns the combined padding around the content of our popup menu.
*/
private Rect getPopupPadding() {
final Rect result = new Rect();
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
.inRoot(isPlatformPopup()).perform(new ViewAction() {
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
}
@Override
public String getDescription() {
return "Popup matcher";
}
@Override
public void perform(UiController uiController, View view) {
// Traverse the parent hierarchy and combine all their paddings
result.setEmpty();
final Rect current = new Rect();
while (true) {
ViewParent parent = view.getParent();
if (parent == null || !(parent instanceof View)) {
return;
}
view = (View) parent;
Drawable currentBackground = view.getBackground();
if (currentBackground != null) {
currentBackground.getPadding(current);
result.left += current.left;
result.right += current.right;
result.top += current.top;
result.bottom += current.bottom;
}
}
}
});
return result;
}
/**
* Returns a root matcher that matches roots that have window focus on their decor view.
*/
private static Matcher<Root> hasWindowFocus() {
return new TypeSafeMatcher<Root>() {
@Override
public void describeTo(Description description) {
description.appendText("has window focus");
}
@Override
public boolean matchesSafely(Root root) {
View rootView = root.getDecorView();
return rootView.hasWindowFocus();
}
};
}
@FlakyTest(bugId = 33669575)
@Test
@LargeTest
public void testAnchoring() {
Builder menuBuilder = new Builder();
menuBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(click());
final int[] anchorOnScreenXY = new int[2];
final int[] popupOnScreenXY = getPopupLocationOnScreen();
final int[] popupInWindowXY = getPopupLocationInWindow();
final Rect popupPadding = getPopupPadding();
mButton.getLocationOnScreen(anchorOnScreenXY);
// Allow for off-by-one mismatch in anchoring
// See if Anchoring Y gets animated to the expected value.
PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
@Override
public boolean canProceed() {
final int y = anchorOnScreenXY[1] + popupInWindowXY[1] + mButton.getHeight()
- popupPadding.top;
return Math.abs(y - popupOnScreenXY[1]) <= 1;
}
});
assertEquals("Anchoring X", anchorOnScreenXY[0] + popupInWindowXY[0],
popupOnScreenXY[0], 1);
assertEquals("Anchoring Y",
anchorOnScreenXY[1] + popupInWindowXY[1] + mButton.getHeight() - popupPadding.top,
popupOnScreenXY[1], 1);
}
@Test
@MediumTest
public void testDismissalViaAPI() throws Throwable {
Builder menuBuilder = new Builder().withDismissListener();
menuBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(click());
// Since PopupMenu is not a View, we can't use Espresso's view actions to invoke
// the dismiss() API
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mPopupMenu.dismiss();
}
});
verify(menuBuilder.mOnDismissListener, times(1)).onDismiss(mPopupMenu);
// Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing.
// Use a custom matcher to check the visibility of the drop down list view instead.
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
}
@Test
@MediumTest
public void testDismissalViaTouch() throws Throwable {
Builder menuBuilder = new Builder().withDismissListener();
menuBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(click());
// Determine the location of the popup on the screen so that we can emulate
// a tap outside of its bounds to dismiss it
final int[] popupOnScreenXY = getPopupLocationOnScreen();
final Rect popupPadding = getPopupPadding();
int emulatedTapX = popupOnScreenXY[0] - popupPadding.left - 20;
int emulatedTapY = popupOnScreenXY[1] + popupPadding.top + 20;
// The logic below uses Instrumentation to emulate a tap outside the bounds of the
// displayed popup menu. This tap is then treated by the framework to be "split" as
// the ACTION_OUTSIDE for the popup itself, as well as DOWN / MOVE / UP for the underlying
// view root if the popup is not modal.
// It is not correct to emulate these two sequences separately in the test, as it
// wouldn't emulate the user-facing interaction for this test. Note that usage
// of Instrumentation is necessary here since Espresso's actions operate at the level
// of view or data. Also, we don't want to use View.dispatchTouchEvent directly as
// that would require emulation of two separate sequences as well.
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
// Inject DOWN event
long downTime = SystemClock.uptimeMillis();
MotionEvent eventDown = MotionEvent.obtain(
downTime, downTime, MotionEvent.ACTION_DOWN, emulatedTapX, emulatedTapY, 1);
instrumentation.sendPointerSync(eventDown);
// Inject MOVE event
long moveTime = SystemClock.uptimeMillis();
MotionEvent eventMove = MotionEvent.obtain(
moveTime, moveTime, MotionEvent.ACTION_MOVE, emulatedTapX, emulatedTapY, 1);
instrumentation.sendPointerSync(eventMove);
// Inject UP event
long upTime = SystemClock.uptimeMillis();
MotionEvent eventUp = MotionEvent.obtain(
upTime, upTime, MotionEvent.ACTION_UP, emulatedTapX, emulatedTapY, 1);
instrumentation.sendPointerSync(eventUp);
// Wait for the system to process all events in the queue
instrumentation.waitForIdleSync();
// At this point our popup should not be showing and should have notified its
// dismiss listener
verify(menuBuilder.mOnDismissListener, times(1)).onDismiss(mPopupMenu);
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
}
@Test
@MediumTest
public void testSimpleMenuItemClickViaEvent() {
Builder menuBuilder = new Builder().withMenuItemClickListener();
menuBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(click());
// Verify that our menu item click listener hasn't been called yet
verify(menuBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class));
onView(withText(mResources.getString(R.string.popup_menu_delete)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.perform(click());
// Verify that out menu item click listener has been called with the expected menu item
verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick(
mPopupMenu.getMenu().findItem(R.id.action_delete));
// Popup menu should be automatically dismissed on selecting an item
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
}
@Test
@MediumTest
public void testSimpleMenuItemClickViaAPI() throws Throwable {
Builder menuBuilder = new Builder().withMenuItemClickListener();
menuBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(click());
// Verify that our menu item click listener hasn't been called yet
verify(menuBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class));
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mPopupMenu.getMenu().performIdentifierAction(R.id.action_highlight, 0);
}
});
// Verify that out menu item click listener has been called with the expected menu item
verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick(
mPopupMenu.getMenu().findItem(R.id.action_highlight));
// Popup menu should be automatically dismissed on selecting an item
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
}
@Test
@MediumTest
public void testSubMenuClicksViaEvent() throws Throwable {
Builder menuBuilder = new Builder().withMenuItemClickListener();
menuBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(click());
// Verify that our menu item click listener hasn't been called yet
verify(menuBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class));
onView(withText(mResources.getString(R.string.popup_menu_share)))
.inRoot(withDecorView(not(is(mMainDecorView))))
.perform(click());
// Verify that out menu item click listener has been called with the expected menu item
verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick(
mPopupMenu.getMenu().findItem(R.id.action_share));
// Sleep for a bit to allow the menu -> submenu transition to complete
Thread.sleep(1000);
// At this point we should now have our sub-menu displayed. At this point on newer
// platform versions (L+) we have two view roots on the screen - one for the main popup
// menu and one for the submenu that has just been activated. If we only use the
// logic based on decor view, Espresso will time out on waiting for the first root
// to acquire window focus. This is why from this point on in this test we are using
// two root conditions to detect the submenu - one with decor view not being the same
// as the decor view of our main activity window, and the other that checks for window
// focus.
// Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing.
// Use a custom matcher to check the visibility of the drop down list view instead.
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
.inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
.check(matches(isDisplayed()));
// Note that MenuItem.isVisible() refers to the current "data" visibility state
// and not the "on screen" visibility state. This is why we're testing the display
// visibility of our main and sub menu items.
// Share submenu items should now be visible
onView(withText(mResources.getString(R.string.popup_menu_share_email)))
.inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
.check(matches(isDisplayed()));
onView(withText(mResources.getString(R.string.popup_menu_share_circles)))
.inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
.check(matches(isDisplayed()));
// Now click an item in the sub-menu
onView(withText(mResources.getString(R.string.popup_menu_share_circles)))
.inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
.perform(click());
// Verify that out menu item click listener has been called with the expected menu item
verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick(
mPopupMenu.getMenu().findItem(R.id.action_share_circles));
// Popup menu should be automatically dismissed on selecting an item in the submenu
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
}
@Test
@MediumTest
public void testSubMenuClicksViaAPI() throws Throwable {
Builder menuBuilder = new Builder().withMenuItemClickListener();
menuBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(click());
// Verify that our menu item click listener hasn't been called yet
verify(menuBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class));
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mPopupMenu.getMenu().performIdentifierAction(R.id.action_share, 0);
}
});
// Verify that out menu item click listener has been called with the expected menu item
verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick(
mPopupMenu.getMenu().findItem(R.id.action_share));
// Sleep for a bit to allow the menu -> submenu transition to complete
Thread.sleep(1000);
// At this point we should now have our sub-menu displayed. At this point on newer
// platform versions (L+) we have two view roots on the screen - one for the main popup
// menu and one for the submenu that has just been activated. If we only use the
// logic based on decor view, Espresso will time out on waiting for the first root
// to acquire window focus. This is why from this point on in this test we are using
// two root conditions to detect the submenu - one with decor view not being the same
// as the decor view of our main activity window, and the other that checks for window
// focus.
// Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing.
// Use a custom matcher to check the visibility of the drop down list view instead.
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
.inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
.check(matches(isDisplayed()));
// Note that MenuItem.isVisible() refers to the current "data" visibility state
// and not the "on screen" visibility state. This is why we're testing the display
// visibility of our main and sub menu items.
// Share submenu items should now be visible
onView(withText(mResources.getString(R.string.popup_menu_share_email)))
.inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
.check(matches(isDisplayed()));
onView(withText(mResources.getString(R.string.popup_menu_share_circles)))
.inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus()))
.check(matches(isDisplayed()));
// Now ask the share submenu to perform an action on its specific menu item
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
mPopupMenu.getMenu().findItem(R.id.action_share).getSubMenu().
performIdentifierAction(R.id.action_share_email, 0);
}
});
// Verify that out menu item click listener has been called with the expected menu item
verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick(
mPopupMenu.getMenu().findItem(R.id.action_share_email));
// Popup menu should be automatically dismissed on selecting an item in the submenu
onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
}
@Test
@MediumTest
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
public void testHoverSelectsMenuItem() throws Throwable {
Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
// Make menu long enough to be scrollable.
final Builder menuBuilder = new Builder().withExtraItems(100);
menuBuilder.wireToActionButton();
onView(withId(R.id.test_button)).perform(
click(InputDevice.SOURCE_MOUSE, MotionEvent.BUTTON_PRIMARY));
final ListView menuListView = mPopupMenu.getMenuListView();
assertEquals(0, menuListView.getFirstVisiblePosition());
emulateHoverOverVisibleItems(instrumentation, menuListView);
// Select the last item to force menu scrolling and emulate hover again.
mActivityTestRule.runOnUiThread(new Runnable() {
@Override
public void run() {
menuListView.setSelectionFromTop(mPopupMenu.getMenu().size() - 1, 0);
}
});
instrumentation.waitForIdleSync();
assertNotEquals("Too few menu items to test for scrolling",
0, menuListView.getFirstVisiblePosition());
emulateHoverOverVisibleItems(instrumentation, menuListView);
}
private void emulateHoverOverVisibleItems(Instrumentation instrumentation, ListView listView) {
final int childCount = listView.getChildCount();
for (int i = 0; i < childCount; i++) {
View itemView = listView.getChildAt(i);
injectMouseEvent(instrumentation, itemView, MotionEvent.ACTION_HOVER_MOVE);
// Wait for the system to process all events in the queue.
instrumentation.waitForIdleSync();
// Hovered menu item should be selected.
assertEquals(listView.getFirstVisiblePosition() + i,
listView.getSelectedItemPosition());
}
}
public static void injectMouseEvent(Instrumentation instrumentation, View view, int action) {
final int[] xy = new int[2];
view.getLocationOnScreen(xy);
long eventTime = SystemClock.uptimeMillis();
MotionEvent event = MotionEvent.obtain(eventTime, eventTime, action, xy[0], xy[1], 0);
event.setSource(InputDevice.SOURCE_MOUSE);
instrumentation.sendPointerSync(event);
event.recycle();
}
/**
* Inner helper class to configure an instance of <code>PopupMenu</code> for the
* specific test. The main reason for its existence is that once a popup menu is shown
* with the show() method, most of its configuration APIs are no-ops. This means that
* we can't add logic that is specific to a certain test once it's shown and we have a
* reference to a displayed PopupMenu.
*/
public class Builder {
private boolean mHasDismissListener;
private boolean mHasMenuItemClickListener;
private int mExtraItemCount;
private PopupMenu.OnMenuItemClickListener mOnMenuItemClickListener;
private PopupMenu.OnDismissListener mOnDismissListener;
public Builder withMenuItemClickListener() {
mHasMenuItemClickListener = true;
return this;
}
public Builder withDismissListener() {
mHasDismissListener = true;
return this;
}
public Builder withExtraItems(int count) {
mExtraItemCount = count;
return this;
}
private void show() {
mPopupMenu = new PopupMenu(mContainer.getContext(), mButton);
final MenuInflater menuInflater = mPopupMenu.getMenuInflater();
menuInflater.inflate(R.menu.popup_menu, mPopupMenu.getMenu());
if (mHasMenuItemClickListener) {
// Register a mock listener to be notified when a menu item in our popup menu has
// been clicked.
mOnMenuItemClickListener = mock(PopupMenu.OnMenuItemClickListener.class);
mPopupMenu.setOnMenuItemClickListener(mOnMenuItemClickListener);
}
if (mHasDismissListener) {
// Register a mock listener to be notified when our popup menu is dismissed.
mOnDismissListener = mock(PopupMenu.OnDismissListener.class);
mPopupMenu.setOnDismissListener(mOnDismissListener);
}
// Add extra items.
for (int i = 0; i < mExtraItemCount; i++) {
mPopupMenu.getMenu().add("Extra item " + i);
}
// Show the popup menu
mPopupMenu.show();
}
public void wireToActionButton() {
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
show();
}
});
}
}
private static Matcher<View> selfOrParentWithContentDescription(final CharSequence expected) {
return new TypeSafeMatcher<View>() {
@Override
public void describeTo(Description description) {
description.appendText("self of parent has content description: " + expected);
}
@Override
public boolean matchesSafely(View view) {
return TextUtils.equals(expected, getSelfOrParentContentDescription(view));
}
private CharSequence getSelfOrParentContentDescription(View view) {
while (view != null) {
final CharSequence contentDescription = view.getContentDescription();
if (contentDescription != null) {
return contentDescription;
}
final ViewParent parent = view.getParent();
if (!(parent instanceof View)) {
break;
}
view = (View) parent;
}
return null;
}
};
}
}