0

I have a list where every row contains a name and a button that invokes a Context Menu of options. I would like to write a test that verifies the following things

  1. the context menu contains the correct NUMBER of items
  2. the context menu contains the correct VALUES
  3. the context menu does not contain any unwarranted options (the checks 1and 2 above will test this case)

I would also like to test the contents of the actionBar and actionBar overflow menu when the item is long selected.

For both tests, I can write a check that ensures there is a view element with the correct "label" displayed (i.e finding the view using onView(withText(this.elementText)). However I have 2 actions which have the same label but different IDs and I need to ensure the correct action is in the context menu/long click menu.

I can not use the ID I specified in the XML for my context menu's menu because Android's Context Menu views do not have those IDs, instead they contain an internal Android ID (see the screenshot below).enter image description here

When I wrote tests using Robotium, i had to get all current views of a certain type and the parse through them checking if they are the actionBar items, see sample code below.

public static List<MenuItemImpl> getLongClickMenuItems(String itemName) {
    List<MenuItemImpl> menuItems = new ArrayList<>();

    // long select the item
    solo.clickLongOnText(itemName);

    // get the children of the of the long click action bar
    ArrayList<ActionMenuView> outViews = solo.getCurrentViews(ActionMenuView.class, solo.getView(R.id.action_mode_bar));

    if (!outViews.isEmpty()) {
        // get the first child which contains the action bar actions
        ActionMenuView actionMenuView = outViews.get(0);
        // loop over the children of the ActionMenuView which is the individual ActionMenuItemViews
        // only a few fit will fit on the actionBar, others will be in the overflow menu
        int count = actionMenuView.getChildCount();
        for (int i = 0; i < count; i++) {
            View child = actionMenuView.getChildAt(i);

            if (child instanceof ActionMenuItemView) {
                menuItems.add(((ActionMenuItemView) child).getItemData());
            } else {
                // this is the more button, click on it and wait for the popup window
                // which will contain a list of ListMenuItemView
                // As we are using the AppCompat the actionBar's menu items are the
                // the AppCompat's ListMenuItemView (android.support.v7.view.menu.ListMenuItemView)
                // In the context menu, the menu items are Android's native ListMenuItemView
                // (com.android.internal.view.menu.ListMenuItemView)
                solo.clickOnView(child);
                solo.waitForView(ListMenuItemView.class);
                ArrayList<ListMenuItemView> popupItems = solo.getCurrentViews(ListMenuItemView.class);
                for (ListMenuItemView lvItem : popupItems) {
                    menuItems.add(lvItem.getItemData());
                }

                // close the more button actions menu
                solo.goBack();
            }
        }
    }

    // get out of long click mode
    solo.goBack();

    return menuItems;
}

Does anyone know how I can get the list of Context Row menu items using Expresso.

Does anyone know how I can get the actionBar items (including all items in the overflow menu) using Expresso?

se22as
  • 2,282
  • 5
  • 32
  • 54

2 Answers2

0

If I understand your question correctly, you should be able to use on the onData() method to interact with the context menus as they are just AdapterViews (notice from your screenshot the popup is a ListPopupWindow$DropDownListView).

So you should be able to do something like this:

onView(withText("Item Label")).perform(longClick());
final int expectedItemCount = 10;

// Now the floating popup should be showing,
// first assert it has the expected item count

onView(isAssignableFrom(AdapterView.class))
.check(matches(withItemCount(expectedItemCount)));

// Now assert each entry (which should just be a string) is shown correctly
for (int i = 0; i < expectedItemCount; i++) {
    String expectedItemText = getExpectedItemTextForIndex(i);
    onData(instanceOf(String.class)).atPosition(i)
        .check(matches(withText(expectedItemText)));
}

Where the withItemCount matcher above is a simple matcher that asserts against the given adapter view's adapter count:

public static Matcher<View> withNumberOfItems(final int itemsCount) {
    return new BoundedMatcher<View, AdapterView>(AdapterView.class) {
        @Override
        public void describeTo(Description description) {
            description.appendText("with number of items: " + itemsCount);
        }

        @Override
        protected boolean matchesSafely(AdapterView item) {
            return item.getAdapter().getCount() == itemsCount;
        }
    };
}

I think the same concept should work for an action bar overflow menu.

I'm not clear what you mean by:

For both tests, I can write a check that ensures there is a view element with the correct "label" displayed (i.e finding the view using onView(withText(this.elementText)). However I have 2 actions which have the same label but different IDs and I need to ensure the correct action is in the context menu/long click menu.

But the atPosition should get you exactly the view you're interested in the list, and you can add onChildView if you need to target as sub-view within a given item in the list.

Hope that helps!

dominicoder
  • 9,338
  • 1
  • 26
  • 32
  • Thank you for your very quick answer. I have been playing with your suggested code and I am having a problem. The onView(..) returns an "android.widget.PopupWindow$PopupDecorView" and therefore the "onView(isAssignableFrom(AdapterView.class))" fails as I do not appear to get a view which is assignable from an Adapter view – se22as Jun 18 '17 at 14:17
  • To clarify the section you said you were not clear about. Menu items have a title and an ID in the xml file layouts. I can check the menu item has the text the same as the title in the xml layout file (your example code does that). But i need to test using the ID from the xml layout file. I can not see how to get this ID from the menu items as the "ListMenuItemView" contains child elements whose IDs are not the ID from the xml layout file – se22as Jun 18 '17 at 14:19
  • The suggestions to use `onView(isAssignableFrom(AdapterView.class))` was untested, I just assumed the class would match that. What I would do is look at the log that the failure spits out (the long dump of "Expect such and such view but got such and such view instead, here's the hierarchy" - find the view you're trying to match against (the popup). You could try `onView(both(isRoot()).and((not(is(getActivity().getWindow().getDecorView())))));` (i.e., the root view that is not the main activity root view, i.e., the root view of the popup) – dominicoder Jun 18 '17 at 20:55
  • Regarding the menu IDs, I don't think you can do anything with them via Espresso, as you noticed. The docs around interacting with menus say to just use the text on the menu item. But why do you care about them? You should be able to click and assert on a given menu item by it's text and / or position in the list. – dominicoder Jun 18 '17 at 20:57
  • Menu Ids: I have 2 actions which have the string "Open", if the user owns the item then "Open" will open "screen1", if the user does not own the item, then "Open" will open "screen2". Therefore i have 2 actions with the same label but different IDs, for example "Open"/R.id.open_for_owner and "Open"/R.id.open_for_non_owner. I want to check that the actions for a file owned by a user has the "Open"/R.id.open_for_owner action and not "Open"/R.id.open_for_non_owner action. In my test i am purely wanting to test the contents of the action list, i do not want to click each action to verify them. – se22as Jun 19 '17 at 08:48
  • Ah, I see. Looks like you found a solution, but another option: instead of having two distinct menu items that are both "open" where the ID varies, have one menu item "Open" where the behavior on clicking it varies. So your test then becomes `clickingOpenAsFileOwnerShouldNavigateToScreenX()` and `clickingOpenNotAsFileOwnerShouldNavigateToScreenY()`, for which you can use the `Intents` helper classes. – dominicoder Jun 19 '17 at 17:51
  • Yes I could rewrite my product to do it that way also. – se22as Jun 19 '17 at 18:20
0

Thank you so much to dominicoder for pretty much giving me the answer to this question. working on their reply i have managed to get it working.

Instead of using "isAssignableFrom(AdapterView.class)" I use "isAssignableFrom(ListView.class)".

I then use the exact same matcher "dominicoder" provided to verify the context menu item count.

Using "dominicoder's" sample matchers I was able to code one myself that checks the MenuItem at a certain position in the ListView and I can compare the IDs to make sure its the ID that I am expecting.

public boolean verifyRowContextMenuContents(String name, MyActionObject[] actions){
    // invoke the row context menu
    clickRowActionButton(name);

    // The Context Menu Popup contains a ListView
    int expectedItemCount = actions.length;

    // first check the Popup's listView contains the correct number of items
    onView(isAssignableFrom(ListView.class))
            .check(matches(correctNumberOfItems(expectedItemCount)));

    // now check the order and the IDs of each action in the menu is the expected action
    for (int i = 0; i < expectedItemCount; i++) {
        onView(isAssignableFrom(ListView.class))
                .check(matches(correctMenuId(i, actions[i].getId())));
    }

    // close the context menu
    pressBack();

    return true;
}

private static Matcher<View> correctNumberOfItems(final int itemsCount) {
    return new BoundedMatcher<View, ListView>(ListView.class) {
        @Override
        public void describeTo(Description description) {
            description.appendText("with number of items: " + itemsCount);
        }

        @Override
        protected boolean matchesSafely(ListView listView) {
            ListAdapter adapter = listView.getAdapter();
            return adapter.getCount() == itemsCount;
        }
    };
}

private static Matcher<View> correctMenuId(final int position, final int expectedId) {
    return new BoundedMatcher<View, ListView>(ListView.class) {
        @Override
        public void describeTo(Description description) {
            description.appendText("with position : " + position + " expecting id: " + expectedId);
        }

        @Override
        protected boolean matchesSafely(ListView listView) {
            ListAdapter adapter = listView.getAdapter();
            Object obj = adapter.getItem(position);
            if (obj instanceof MenuItem){
                MenuItem menuItem = (MenuItem)obj;
                return menuItem.getItemId() == expectedId;
            }
            return false;
        }
    };
}   

With this code I can check the Context Menu contains the correct number of menu items, and that the items in the menu are the ones i am expecting (using ID verification) and the order I am expecting.

Huge thank you to "dominicoder". Its a shame you can not mark both answers as correct as you did actually give me virtually the correct answer.

se22as
  • 2,282
  • 5
  • 32
  • 54