67

Having gridView which has some images. The gridView's cell comes out from same predefined layout, which has same id and desc.

R.id.item_image == 2131493330

onView(withId(is(R.id.item_image))).perform(click());

Since all cells in the grid have same id, it got AmbiguousViewMatcherException. How to just pick up first one or any of one of them? Thanks!

android.support.test.espresso.AmbiguousViewMatcherException: 'with id: is <2131493330>' matches multiple views in the hierarchy. Problem views are marked with '****MATCHES****' below.

+------------->ImageView{id=2131493330, res-name=item_image, desc=Image, visibility=VISIBLE, width=262, height=262, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0} ****MATCHES****

+------------->ImageView{id=2131493330, res-name=item_image, desc=Image, visibility=VISIBLE, width=262, height=262, has-focus=false, has-focusable=false, has-window-focus=true, is-clickable=false, is-enabled=true, is-focused=false, is-focusable=false, is-layout-requested=false, is-selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0} ****MATCHES**** |

Thomas
  • 5,810
  • 7
  • 40
  • 48
lannyf
  • 9,865
  • 12
  • 70
  • 152

10 Answers10

120

EDIT: Someone mentioned in the comments that withParentIndex is now available, give that a try first before using the custom solution below.

I was amazed that I couldn't find a solution by simply providing an index along with a matcher (i.e. withText, withId). The accepted answer only solves the problem when you're dealing with onData and ListViews.

If you have more than one view on the screen with the same resId/text/contentDesc, you can choose which one you want without causing an AmbiguousViewMatcherException by using this custom matcher:

public static Matcher<View> withIndex(final Matcher<View> matcher, final int index) {
    return new TypeSafeMatcher<View>() {
        int currentIndex = 0;

        @Override
        public void describeTo(Description description) {
            description.appendText("with index: ");
            description.appendValue(index);
            matcher.describeTo(description);
        }

        @Override
        public boolean matchesSafely(View view) {
            return matcher.matches(view) && currentIndex++ == index;
        }
    };
}

For example:

onView(withIndex(withId(R.id.my_view), 2)).perform(click());

will perform a click action on the third instance of R.id.my_view.

0xMatthewGroves
  • 3,181
  • 3
  • 26
  • 43
31

Not completely related to grid view situation but you can use hamcrest allOf matchers to combine multiple conditions:

import static org.hamcrest.CoreMatchers.allOf;

onView(allOf(withId(R.id.login_password), 
             withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
        .check(matches(isCompletelyDisplayed()))
        .check(matches(withHint(R.string.password_placeholder)));
JJD
  • 50,076
  • 60
  • 203
  • 339
Shubham Chaudhary
  • 47,722
  • 9
  • 78
  • 80
28

You should use onData() to operate on GridView:

onData(withId(R.id.item_image))
        .inAdapterView(withId(R.id.grid_adapter_id))
        .atPosition(0)
        .perform(click());

This code will click on the image inside first item in GridView

denys
  • 6,834
  • 3
  • 37
  • 36
17

Tried @FrostRocket answer as was looking most promissing but needed to add some customisations:

public static Matcher<View> withIndex(final Matcher<View> matcher, final int index) {
    return new TypeSafeMatcher<View>() {
        int currentIndex;
        int viewObjHash;

        @SuppressLint("DefaultLocale") @Override
        public void describeTo(Description description) {
            description.appendText(String.format("with index: %d ", index));
            matcher.describeTo(description);
        }

        @Override
        public boolean matchesSafely(View view) {
            if (matcher.matches(view) && currentIndex++ == index) {
                viewObjHash = view.hashCode();
            }
            return view.hashCode() == viewObjHash;
        }
    };
}
fada21
  • 3,188
  • 1
  • 22
  • 21
  • hi, why did you have to add the hash code check? This version works for me, but I wasn't sure why it needs to do this. thanks! – reutsey Aug 14 '17 at 19:06
  • Hi, I have to say I don't remember exactly. I think it was something to do with view being mutated while under test but not sure now. – fada21 Aug 15 '17 at 15:33
  • I think the reason is that the matcher might be called multiple times on the same component. Keeping a reference to the view (instead of the hash code) should work in this case, too. – Stefan Haustein Dec 12 '17 at 13:53
12

I created a ViewMatcher which matches the first view it finds. Maybe it is helpful for somebody. E.g. when you don't have an AdapterView to use onData() on.

/**
 * Created by stost on 15.05.14.
 * Matches any view. But only on first match()-call.
 */
public class FirstViewMatcher extends BaseMatcher<View> {


   public static boolean matchedBefore = false;

   public FirstViewMatcher() {
       matchedBefore = false;
   }

   @Override
   public boolean matches(Object o) {
       if (matchedBefore) {
           return false;
       } else {
           matchedBefore = true;
           return true;
       }
   }

   @Override
   public void describeTo(Description description) {
       description.appendText(" is the first view that comes along ");
   }

   @Factory
   public static <T> Matcher<View> firstView() {
       return new FirstViewMatcher();
   }
}

Use it like this:

 onView(FirstViewMatcher.firstView()).perform(click());
StefanTo
  • 971
  • 1
  • 10
  • 28
  • Been trying to change this to return the first of a particular view type. Thoughts? – wapples May 12 '16 at 19:12
  • 2
    @wapples Depends... if you want to match a view type and all types inheriting from it you could use something like `if(o instanceof ClassToMatch.class)` in the matches()-methode. If you want to match only one specific class you could do something like `if("ClassToMatchAsString".equals(o.getClass().getName()))` or `if("ClassToMatchAsString".equals(o.getClass().getSimpleName()))`. Or if you don't want to edit the code at all this should also work: `onView(allOf(withClassName(endsWith("ClassToMatchAsString")),FirstViewMatcher.firstView())).perform(click());` – StefanTo May 25 '16 at 07:04
  • The `Matcher` contract says `Matcher`s should be stateless. This `Matcher` will break in subtle ways. – Heath Borders May 21 '20 at 20:10
10

Cases:

onView( withId( R.id.songListView ) ).perform( RealmRecyclerViewActions.scrollTo( Matchers.first(Matchers.withTextLabeled( "Love Song"))) );
onView( Matchers.first(withText( "Love Song")) ).perform( click() );

inside my Matchers.class

public static Matcher<View> first(Matcher<View> expected ){

    return new TypeSafeMatcher<View>() {
        private boolean first = false;

        @Override
        protected boolean matchesSafely(View item) {

            if( expected.matches(item) && !first ){
                return first = true;
            }

            return false;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("Matcher.first( " + expected.toString() + " )" );
        }
    };
}
Juan Mendez
  • 2,658
  • 1
  • 27
  • 23
  • How do we know that the first view we get is the one we want? – AliAvci Sep 24 '18 at 16:48
  • A string contained in a textview is not the same a unique id, so that's why for matching the first instance I used it. If there were more views matching the string, then they would be returning true as well, unless I didn't apply the flag. Feel free to use your own logic. – Juan Mendez Sep 27 '18 at 18:02
10

Not specifically related to grid view but I had a similar problem where both an element on my RecyclerView and on my root layout had the same id and were both displayed to the screen. What helped me to solve it was to check descendancy such as:

 onView(allOf(withId(R.id.my_view), not(isDescendantOfA(withId(R.id.recyclerView))))).check(matches(withText("My Text")));

enter image description here

Francislainy Campos
  • 3,462
  • 4
  • 33
  • 81
2

At latest * Run -> Record Espresso Test

By Click on the Same ID View with different position generate different Code for them so try it.

Its Actually Resolve these problem.

Pinak Gauswami
  • 789
  • 6
  • 10
1

You can simply make NthMatcher like:

   class NthMatcher internal constructor(private val id: Int, private val n: Int) : TypeSafeMatcher<View>(View::class.java) {
        companion object {
            var matchCount: Int = 0
        }
        init {
            var matchCount = 0
        }
        private var resources: Resources? = null
        override fun describeTo(description: Description) {
            var idDescription = Integer.toString(id)
            if (resources != null) {
                try {
                    idDescription = resources!!.getResourceName(id)
                } catch (e: Resources.NotFoundException) {
                    // No big deal, will just use the int value.
                    idDescription = String.format("%s (resource name not found)", id)
                }

            }
            description.appendText("with id: $idDescription")
        }

        public override fun matchesSafely(view: View): Boolean {
            resources = view.resources
            if (id == view.id) {
                matchCount++
                if(matchCount == n) {
                    return true
                }
            }
            return false
        }
    }

Declare like this:

fun withNthId(resId: Int, n: Int) = CustomMatchers.NthMatcher(resId, n)

And use like this:

onView(withNthId(R.id.textview, 1)).perform(click())
John
  • 1,139
  • 3
  • 16
  • 33
0

In Kakao see https://github.com/agoda-com/Kakao/issues/90:

class InputScreen : Screen<InputScreen>() {
    fun inputLayout(lambda: KEditText.() -> Unit) = 
        KEditText { withIndex(0, { withId(R.id.input_layout) }) }.invoke(lambda)
}
CoolMind
  • 26,736
  • 15
  • 188
  • 224