Tests for PopupMenu

Change-Id: Ibaa30e2a5fd01318959c2539313d0cb47363b255
diff --git a/v7/appcompat/tests/AndroidManifest.xml b/v7/appcompat/tests/AndroidManifest.xml
index 6f6fefb..418d72b 100644
--- a/v7/appcompat/tests/AndroidManifest.xml
+++ b/v7/appcompat/tests/AndroidManifest.xml
@@ -53,7 +53,7 @@
 
         <activity
                 android:name="android.support.v7.widget.PopupTestActivity"
-                android:label="@string/list_popup_window_activity"
+                android:label="@string/popup_activity"
                 android:theme="@style/Theme.AppCompat.Light" />
 
         <activity
diff --git a/v7/appcompat/tests/res/layout/popup_test_activity.xml b/v7/appcompat/tests/res/layout/popup_test_activity.xml
index 152ddaf..9ea488f 100644
--- a/v7/appcompat/tests/res/layout/popup_test_activity.xml
+++ b/v7/appcompat/tests/res/layout/popup_test_activity.xml
@@ -24,6 +24,6 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_gravity="center_horizontal"
-        android:text="@string/list_popup_window_show" />
+        android:text="@string/popup_show" />
 </FrameLayout>
 
diff --git a/v7/appcompat/tests/res/menu/popup_menu.xml b/v7/appcompat/tests/res/menu/popup_menu.xml
new file mode 100644
index 0000000..f50efc5
--- /dev/null
+++ b/v7/appcompat/tests/res/menu/popup_menu.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 Google Inc.
+
+     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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/action_highlight"
+          android:title="@string/popup_menu_highlight" />
+    <item android:id="@+id/action_edit"
+          android:title="@string/popup_menu_edit" />
+    <item android:id="@+id/action_delete"
+          android:title="@string/popup_menu_delete" />
+    <item android:id="@+id/action_ignore"
+          android:title="@string/popup_menu_ignore" />
+    <item android:id="@+id/action_share"
+          android:title="@string/popup_menu_share">
+        <menu>
+            <item android:id="@+id/action_share_email"
+                  android:title="@string/popup_menu_share_email" />
+            <item android:id="@+id/action_share_circles"
+                  android:title="@string/popup_menu_share_circles" />
+        </menu>
+    </item>
+    <item android:id="@+id/action_print"
+          android:title="@string/popup_menu_print" />
+</menu>
diff --git a/v7/appcompat/tests/res/values/strings.xml b/v7/appcompat/tests/res/values/strings.xml
index b7347a2..e094078 100644
--- a/v7/appcompat/tests/res/values/strings.xml
+++ b/v7/appcompat/tests/res/values/strings.xml
@@ -21,8 +21,16 @@
     <string name="drawer_open">Open navigation drawer</string>
     <string name="drawer_close">Close navigation drawer</string>
 
-    <string name="list_popup_window_activity">List popup window</string>
-    <string name="list_popup_window_show">Show popup</string>
+    <string name="popup_activity">Popup activity</string>
+    <string name="popup_show">Show popup</string>
+    <string name="popup_menu_highlight">Highlight</string>
+    <string name="popup_menu_edit">Edit</string>
+    <string name="popup_menu_delete">Delete</string>
+    <string name="popup_menu_ignore">Ignore</string>
+    <string name="popup_menu_share">Share</string>
+    <string name="popup_menu_share_email">Via email</string>
+    <string name="popup_menu_share_circles">To my circles</string>
+    <string name="popup_menu_print">Print</string>
 
     <string name="alert_dialog_activity">Alert dialog</string>
     <string name="alert_dialog_show">Show alert dialog</string>
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
index 14e1bfe..0c78623 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/ListPopupWindowTest.java
@@ -18,8 +18,8 @@
 import android.app.Instrumentation;
 import android.graphics.Rect;
 import android.os.SystemClock;
+import android.support.v7.app.BaseInstrumentationTestCase;
 import android.support.v7.appcompat.test.R;
-import android.test.ActivityInstrumentationTestCase2;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -31,18 +31,17 @@
 import android.widget.FrameLayout;
 import android.widget.PopupWindow;
 import android.widget.TextView;
+import org.junit.Test;
 
 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.matches;
 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.withId;
-import static android.support.test.espresso.matcher.ViewMatchers.withText;
+import static android.support.test.espresso.matcher.ViewMatchers.*;
 import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsNot.not;
 
-public class ListPopupWindowTest extends ActivityInstrumentationTestCase2<PopupTestActivity> {
+public class ListPopupWindowTest extends BaseInstrumentationTestCase<PopupTestActivity> {
     private FrameLayout mContainer;
 
     private Button mButton;
@@ -62,7 +61,7 @@
     }
 
     @Override
-    protected void setUp() throws Exception {
+    public void setUp() throws Exception {
         super.setUp();
 
         final PopupTestActivity activity = getActivity();
@@ -70,6 +69,7 @@
         mButton = (Button) mContainer.findViewById(R.id.test_button);
     }
 
+    @Test
     @SmallTest
     public void testBasicContent() {
         Builder popupBuilder = new Builder();
@@ -97,6 +97,7 @@
                 .check(matches(isDisplayed()));
     }
 
+    @Test
     @SmallTest
     public void testAnchoring() {
         Builder popupBuilder = new Builder();
@@ -121,6 +122,7 @@
                 popupOnScreenXY[1] + rect.top);
     }
 
+    @Test
     @SmallTest
     public void testDismissalViaAPI() throws Throwable {
         Builder popupBuilder = new Builder().withDismissListener();
@@ -211,16 +213,19 @@
         assertEquals("Click on underlying container", !setupAsModal, mIsContainerClicked);
     }
 
+    @Test
     @SmallTest
     public void testDismissalOutsideNonModal() throws Throwable {
         testDismissalViaTouch(false);
     }
 
+    @Test
     @SmallTest
     public void testDismissalOutsideModal() throws Throwable {
         testDismissalViaTouch(true);
     }
 
+    @Test
     @SmallTest
     public void testItemClickViaEvent() {
         Builder popupBuilder = new Builder().withItemClickListener();
@@ -239,6 +244,7 @@
         assertFalse("Popup window not showing after click", mListPopupWindow.isShowing());
     }
 
+    @Test
     @SmallTest
     public void testItemClickViaAPI() throws Throwable {
         Builder popupBuilder = new Builder().withItemClickListener();
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
new file mode 100644
index 0000000..fac9b3d
--- /dev/null
+++ b/v7/appcompat/tests/src/android/support/v7/widget/PopupMenuTest.java
@@ -0,0 +1,573 @@
+/*
+ * 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 android.support.v7.widget;
+
+import android.app.Instrumentation;
+import android.content.res.Resources;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
+import android.support.test.espresso.Root;
+import android.support.test.espresso.UiController;
+import android.support.test.espresso.ViewAction;
+import android.support.v7.app.BaseInstrumentationTestCase;
+import android.support.v7.appcompat.test.R;
+import android.test.suitebuilder.annotation.SmallTest;
+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 org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Test;
+
+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.*;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsNot.not;
+
+public class PopupMenuTest extends BaseInstrumentationTestCase<PopupTestActivity> {
+    // 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 ListPopupWindow 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 =
+            "android.support.v7.widget.ListPopupWindow$DropDownListView";
+    private FrameLayout mContainer;
+
+    private Button mButton;
+
+    private PopupMenu mPopupMenu;
+
+    private int mPopupClickedMenuItemId = -1;
+
+    private boolean mIsDismissedCalled = false;
+
+    public PopupMenuTest() {
+        super(PopupTestActivity.class);
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        final PopupTestActivity activity = getActivity();
+        mContainer = (FrameLayout) activity.findViewById(R.id.container);
+        mButton = (Button) mContainer.findViewById(R.id.test_button);
+    }
+
+    @Test
+    @SmallTest
+    public void testBasicContent() {
+        final Builder menuBuilder = new Builder();
+        menuBuilder.wireToActionButton();
+
+        onView(withId(R.id.test_button)).perform(click());
+        assertNotNull("Popup menu created", 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)))
+                .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.
+
+        final Resources res = getActivity().getResources();
+        final View mainDecorView = getActivity().getWindow().getDecorView();
+        onView(withText(res.getString(R.string.popup_menu_highlight)))
+                .inRoot(withDecorView(not(is(mainDecorView))))
+                .check(matches(isDisplayed()));
+        onView(withText(res.getString(R.string.popup_menu_edit)))
+                .inRoot(withDecorView(not(is(mainDecorView))))
+                .check(matches(isDisplayed()));
+        onView(withText(res.getString(R.string.popup_menu_delete)))
+                .inRoot(withDecorView(not(is(mainDecorView))))
+                .check(matches(isDisplayed()));
+        onView(withText(res.getString(R.string.popup_menu_ignore)))
+                .inRoot(withDecorView(not(is(mainDecorView))))
+                .check(matches(isDisplayed()));
+        onView(withText(res.getString(R.string.popup_menu_share)))
+                .inRoot(withDecorView(not(is(mainDecorView))))
+                .check(matches(isDisplayed()));
+        onView(withText(res.getString(R.string.popup_menu_print)))
+                .inRoot(withDecorView(not(is(mainDecorView))))
+                .check(matches(isDisplayed()));
+
+        // Share submenu items should not be visible
+        onView(withText(res.getString(R.string.popup_menu_share_email)))
+                .inRoot(withDecorView(not(is(mainDecorView))))
+                .check(doesNotExist());
+        onView(withText(res.getString(R.string.popup_menu_share_circles)))
+                .inRoot(withDecorView(not(is(mainDecorView))))
+                .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();
+            }
+        };
+    }
+
+    @Test
+    @SmallTest
+    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
+        assertEquals("Anchoring X", anchorOnScreenXY[0] + popupInWindowXY[0],
+                popupOnScreenXY[0], 1);
+        assertEquals("Anchoring Y", anchorOnScreenXY[1] + popupInWindowXY[1] + mButton.getHeight(),
+                popupOnScreenXY[1] + popupPadding.top, 1);
+    }
+
+    @Test
+    @SmallTest
+    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
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mPopupMenu.dismiss();
+            }
+        });
+
+        assertTrue("Dismiss listener called", mIsDismissedCalled);
+        // 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
+    @SmallTest
+    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 = 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
+        assertTrue("Dismiss listener called", mIsDismissedCalled);
+        onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+    }
+
+    @Test
+    @SmallTest
+    public void testSimpleMenuItemClickViaEvent() {
+        Builder menuBuilder = new Builder().withMenuItemClickListener();
+        menuBuilder.wireToActionButton();
+
+        onView(withId(R.id.test_button)).perform(click());
+
+        assertEquals("Clicked item before click", -1, mPopupClickedMenuItemId);
+
+        final Resources res = getActivity().getResources();
+        onView(withText(res.getString(R.string.popup_menu_delete)))
+                .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
+                .perform(click());
+        assertEquals("Clicked item after click", R.id.action_delete, mPopupClickedMenuItemId);
+
+        // Popup menu should be automatically dismissed on selecting an item
+        onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+    }
+
+    @Test
+    @SmallTest
+    public void testSimpleMenuItemClickViaAPI() throws Throwable {
+        Builder menuBuilder = new Builder().withMenuItemClickListener();
+        menuBuilder.wireToActionButton();
+
+        onView(withId(R.id.test_button)).perform(click());
+
+        assertEquals("Clicked item before click", -1, mPopupClickedMenuItemId);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mPopupMenu.getMenu().performIdentifierAction(R.id.action_highlight, 0);
+            }
+        });
+
+        assertEquals("Clicked item after click", R.id.action_highlight, mPopupClickedMenuItemId);
+
+        // Popup menu should be automatically dismissed on selecting an item
+        onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+    }
+
+    @Test
+    @SmallTest
+    public void testSubMenuClicksViaEvent() throws Throwable {
+        Builder menuBuilder = new Builder().withMenuItemClickListener();
+        menuBuilder.wireToActionButton();
+
+        onView(withId(R.id.test_button)).perform(click());
+
+        assertEquals("Clicked item before click", -1, mPopupClickedMenuItemId);
+
+        final Resources res = getActivity().getResources();
+        onView(withText(res.getString(R.string.popup_menu_share)))
+                .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
+                .perform(click());
+        assertEquals("Clicked item after click", R.id.action_share, mPopupClickedMenuItemId);
+
+        // 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.
+        final View mainDecorView = getActivity().getWindow().getDecorView();
+        onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
+                .inRoot(allOf(withDecorView(not(is(mainDecorView))), 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(res.getString(R.string.popup_menu_share_email)))
+                .inRoot(allOf(withDecorView(not(is(mainDecorView))), hasWindowFocus()))
+                .check(matches(isDisplayed()));
+        onView(withText(res.getString(R.string.popup_menu_share_circles)))
+                .inRoot(allOf(withDecorView(not(is(mainDecorView))), hasWindowFocus()))
+                .check(matches(isDisplayed()));
+
+        // Now click an item in the sub-menu
+        onView(withText(res.getString(R.string.popup_menu_share_circles)))
+                .inRoot(allOf(withDecorView(not(is(mainDecorView))), hasWindowFocus()))
+                .perform(click());
+        assertEquals("Clicked submenu item after click", R.id.action_share_circles,
+                mPopupClickedMenuItemId);
+
+        // Popup menu should be automatically dismissed on selecting an item in the submenu
+        onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+    }
+
+    @Test
+    @SmallTest
+    public void testSubMenuClicksViaAPI() throws Throwable {
+        Builder menuBuilder = new Builder().withMenuItemClickListener();
+        menuBuilder.wireToActionButton();
+
+        onView(withId(R.id.test_button)).perform(click());
+
+        assertEquals("Clicked item before click", -1, mPopupClickedMenuItemId);
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mPopupMenu.getMenu().performIdentifierAction(R.id.action_share, 0);
+            }
+        });
+
+        assertEquals("Clicked item after click", R.id.action_share, mPopupClickedMenuItemId);
+
+        // 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.
+        final View mainDecorView = getActivity().getWindow().getDecorView();
+        onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME)))
+                .inRoot(allOf(withDecorView(not(is(mainDecorView))), 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.
+
+        final Resources res = getActivity().getResources();
+
+        // Share submenu items should now be visible
+        onView(withText(res.getString(R.string.popup_menu_share_email)))
+                .inRoot(allOf(withDecorView(not(is(mainDecorView))), hasWindowFocus()))
+                .check(matches(isDisplayed()));
+        onView(withText(res.getString(R.string.popup_menu_share_circles)))
+                .inRoot(allOf(withDecorView(not(is(mainDecorView))), hasWindowFocus()))
+                .check(matches(isDisplayed()));
+
+        // Now ask the share submenu to perform an action on its specific menu item
+        runTestOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mPopupMenu.getMenu().findItem(R.id.action_share).getSubMenu().
+                        performIdentifierAction(R.id.action_share_email, 0);
+            }
+        });
+        assertEquals("Clicked submenu item after click", R.id.action_share_email,
+                mPopupClickedMenuItemId);
+
+        // Popup menu should be automatically dismissed on selecting an item in the submenu
+        onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist());
+    }
+
+    /**
+     * 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.
+     */
+    private class Builder {
+        private boolean mHasDismissListener;
+        private boolean mHasMenuItemClickListener;
+
+        public Builder() {
+        }
+
+        public Builder withMenuItemClickListener() {
+            mHasMenuItemClickListener = true;
+            return this;
+        }
+
+        public Builder withDismissListener() {
+            mHasDismissListener = true;
+            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 listener to be notified when a menu item in our popup menu has
+                // been clicked.
+                mPopupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
+                    @Override
+                    public boolean onMenuItemClick(MenuItem item) {
+                        mPopupClickedMenuItemId = item.getItemId();
+                        return true;
+                    }
+                });
+            }
+
+            if (mHasDismissListener) {
+                // Register a listener to be notified when our popup menu is dismissed.
+                mPopupMenu.setOnDismissListener(new PopupMenu.OnDismissListener() {
+                    @Override
+                    public void onDismiss(PopupMenu menu) {
+                        mIsDismissedCalled = true;
+                    }
+                });
+            }
+
+            // Show the popup menu
+            mPopupMenu.show();
+        }
+
+        public void wireToActionButton() {
+            mButton.setOnClickListener(new View.OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    show();
+                }
+            });
+        }
+    }
+}
diff --git a/v7/appcompat/tests/src/android/support/v7/widget/PopupTestActivity.java b/v7/appcompat/tests/src/android/support/v7/widget/PopupTestActivity.java
index f7ddba5..9a3736a 100644
--- a/v7/appcompat/tests/src/android/support/v7/widget/PopupTestActivity.java
+++ b/v7/appcompat/tests/src/android/support/v7/widget/PopupTestActivity.java
@@ -18,6 +18,10 @@
 import android.support.v7.appcompat.test.R;
 import android.support.v7.testutils.BaseTestActivity;
 
+/**
+ * This activity is used to test both {@link ListPopupWindow} and {@link PopupMenu} classes.
+ *
+ */
 public class PopupTestActivity extends BaseTestActivity {
     @Override
     protected int getContentViewLayoutResId() {