androidtestingandroid-espressomatchermobile-app-testing

Android Espresso: How do I find the last (Non-ListView) table entry and extract View values?


I am trying to get into android espresso and have been given an .apk to try things out. Setting aside the legwork how I managed to get the test to run, I am now trying to figure out how to find the number of entries in a table, and how to extract values from Views and save them for later use.

Layout inspector snippet for the table and the first few rows

The table itself has a random number of rows and random integer entries. For the test I want to click the last entry and extract its text, since the next steps are slightly different depending on the number. Adressing the randomInts table is possible, though I need a workaround due to R.id.randomInts from the apk not being accessible during the build

int tableId = context.getResources()
                .getIdentifier("randomInts","id",context.getPackageName());
ViewInteraction table = onView(withId(tableId));
table.check(matches(isDisplayed()));

If I knew the rowcount, I could simply use the following statement with the correct number in withParenIndex.

onView(allOf(
                withParent(withId(itemRepo.id(itemRepo.idTableRandomInt))),
                withClassName(containsString("TableRow")),
                withParentIndex(40)
        )).perform(scrollTo(), click());

A lot of the questions about the topic use matchers to compare the child count with a known expected size, but I cannot use that due to the random rowcount. Other solutions like for this question need a ListView. I have tried converting the ViewInteraction into a ListView, but that causes "Inconvertible types; cannot cast 'androidx.test.espresso.ViewInteraction' to 'android.widget.ListView'".

ListView tableView = (ListView) table;
int tablecount = tableView.getAdapter().getCount();

Likewise, trying to enter my Id for the TableLayout into the matcher in this question results in java.lang.ClassCastException: android.widget.TableLayout cannot be cast to android.widget.ListView onData() also doesn't seem to work Inconvertible types; cannot cast 'androidx.test.espresso.DataInteraction' to 'android.widget.ListView'

DataInteraction tablerows = onData(withId(tableId))
                .inAdapterView(withClassName(containsString("TableRow")));
ListView tableList = (ListView) tablerows;

I tried imitating the approach here, but Android Studio complained when I placed it within the test method and when I placed it in the same class as the test method. It ended up within its own class, but the altered approach using TableRow.class returns 0 and would be inflexible even if it worked.

public class Matchers {
    private static int counter = 0;
    public void resetCounter() {
        counter = 0;
    }

    public static int getCounter() {
        return counter;
    }
    public static Matcher<View> countChildrenForParent(final Class classname) {
        checkNotNull(classname);
        return new TypeSafeMatcher<View>() {
            @Override
            public void describeTo(Description description) {
                description.appendText("with item id: " + classname);
            }

            @Override
            public boolean matchesSafely(View view) {
                if ((view.getClass() == classname)){
                    counter++;
                    return true;
                }
                return false;
            }
        };
    }
}
onView(Matchers.countChildrenForParent(TableRow.class));
        Log.d("tag", "Matcher for table entries returned " + Matchers.getCounter());

So as far as I can tell, I also won't be able to use a custom matcher to extract the random number from the last entry until I have figured this out. I also noticed that the already identified randomInts has a "child-count=111" property, but I haven't seen a way to access these values so far. TableLayout{id=2131231112, res-name=randomInts, visibility=VISIBLE, width=360, height=8655, has-focus=false, has-focusable=true, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, layout-params=android.widget.LinearLayout$LayoutParams@YYYYYY, tag=null, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0, child-count=111}

Edit: I managed to get the text of the first textview using the answer to this question here, and I hopefully should be able to adapt this solution later to get and compare background color for another part of the exercise.


Solution

  • Feels a bit weird to find an answer after going through the trouble to document this question, but maybe someone can improve upon it. After finding out "Matchers" is already part of the Hamcrest library, I renamed it from my last post to "MatcherMethods". I found this question about child elements and stitched its answer together with the getText matcher from the answer here. The resulting renamed helper class looks like this:

    public class MatcherMethods {
    
        public static String getText(final Matcher<View> matcher) {
            final String[] stringHolder = { null };
            onView(matcher).perform(new ViewAction() {
                @Override
                public Matcher<View> getConstraints() {
                    return isAssignableFrom(TextView.class);
                }
    
                @Override
                public String getDescription() {
                    return "getting text from a TextView";
                }
    
                @Override
                public void perform(UiController uiController, View view) {
                    TextView tv = (TextView)view; //Save, because of check in getConstraints()
                    stringHolder[0] = tv.getText().toString();
                }
            });
            return stringHolder[0];
        }
    
        public static int getChildCount(final Matcher<View> matcher, String className) {
            final int[] intHolder = { -1 };
            onView(matcher).perform(new ViewAction() {
                @Override
                public Matcher<View> getConstraints() {
                    return isAssignableFrom(View.class);
                }
    
                @Override
                public String getDescription() {
                    return "Getting count of child elements from a View for type " + className;
                }
    
                @Override
                public void perform(UiController uiController, View view) {
                    ViewGroup group = (ViewGroup)view;
                    int counter = 0;
                    final int totalChildCount = group.getChildCount();
                    for(int i = 0; i < totalChildCount; i++) {
                        //Count only if child element matches the desired type
                        if(group.getChildAt(i).getClass().getSimpleName().equals(className)) counter ++;
                    }
                    intHolder[0] = counter;
                }
            });
            return intHolder[0];
        }
    }
    

    The second matcher counts the child elements with the fitting className. Those matchers are used like this:

    //Table interactions
            int tableId = context.getResources()
                    .getIdentifier("randomInts","id",context.getPackageName());
            ViewInteraction table = onView(withId(tableId));
    
            table.check(matches(isDisplayed()));
            table.check(matches(hasMinimumChildCount(1)));
            Log.d("tag","Table is: " + table);
    
            int rowCount = MatcherMethods.getChildCount(withId(tableId),
                    TableRow.class.getSimpleName());
    
            Log.d("tag","Rowcount is: " + rowCount);
            onView(allOf(
                    withParent(withId(tableId)),
                    withClassName(containsString("TableRow")),
                    withParentIndex(rowCount - 1)
            )).perform(scrollTo(), click());
    
            //Extract table entry string
            String tableText = MatcherMethods.getText(allOf(
                    withParent( allOf(
                            withParent(withId(tableId)),
                            withClassName(containsString("TableRow")),
                            withParentIndex(rowCount - 1)
                    )),
                    withClassName(containsString("TextView"))
            ));
            Log.d("tag", "Table entry is " + tableText);
    

    My code has two issues now: First, if the table has children, but none of type TableRow. In that case, the withParentIndex(...) statements look for -1 and causes an error.Does Espresso have a matcher for that out of the box? Otherwise, I could use standard asserts. Second, I need to know the class name of the children. Looking simply for View.class finds 0 children. That can be resolved by creating a getChildCount method with only the first parameter and simply returning Viewgroup.getChildcount.