【问题标题】:In Espresso, how to avoid AmbiguousViewMatcherException when multiple views match在 Espresso 中,当多个视图匹配时如何避免 AmbiguousViewMatcherException
【发布时间】:2015-06-05 09:03:43
【问题描述】:

拥有包含一些图像的 gridView。 gridView 的单元格来自相同的预定义布局,具有相同的 id 和 desc。

R.id.item_image == 2131493330

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

由于网格中的所有单元格都具有相同的 id,因此它得到了AmbiguousViewMatcherException。 如何只拿起第一个或其中任何一个? 谢谢!

android.support.test.espresso.AmbiguousViewMatcherException: 'with id: is ' 匹配层次结构中的多个视图。 问题视图在下方标有“**** MATCHES****”。

+------------->ImageView{id=2131493330, res-name=item_image, desc=Image, visibility=VISIBLE, width=262, height=262, has-focus=假,有焦点=假,有窗口焦点=真,是可点击=假,是启用=真,是焦点=假,是焦点=假,是布局请求=假,是- 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=假,有焦点=假,有窗口焦点=真,是可点击=假,是启用=真,是焦点=假,是焦点=假,是布局请求=假,是- selected=false, root-is-layout-requested=false, has-input-connection=false, x=0.0, y=0.0} ****匹配**** |

【问题讨论】:

    标签: android android-espresso


    【解决方案1】:

    编辑:有人在 cmets 中提到 withParentIndex 现在可用,请先尝试一下,然后再使用下面的自定义解决方案。

    我很惊讶我无法通过简单地提供索引和匹配器(即 withText、withId)来找到解决方案。接受的答案仅在您处理 onData 和 ListViews 时解决问题。

    如果屏幕上有多个视图具有相同的 resId/text/contentDesc,则可以使用此自定义匹配器选择所需的视图,而不会导致 AmbiguousViewMatcherException:

    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;
            }
        };
    }
    

    例如:

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

    将对 R.id.my_view 的第三个实例执行点击操作。

    【讨论】:

    • 这是一个很好的解决方案,我一直在寻找这个,因为我的视图不是适配器视图,我们使用的是粘性列表视图,它实现了没有列表视图的列表。
    • 太棒了。我还将这个解决方案包装成 first() 和 second() 等版本。
    • 如果我需要一个last() 匹配器,是否可以制作一个?
    • 太棒了!完全解决了我的这个问题。谢谢。
    • Matcher 合同规定Matchers 应该是无状态的。这个Matcher 会以微妙的方式中断。
    【解决方案2】:

    你应该使用onData()GridView进行操作:

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

    此代码将点击GridView中第一项内的图像

    【讨论】:

    • 嗨,R.id.grid_adapter_id 是实际的RecyclerView/ListView?谢谢!
    • @NeonWarge,是的,GridView 的 id 或者也可以是 ListView。但不是回收站视图。你不能使用onData()
    【解决方案3】:

    与网格视图情况不完全相关,但您可以使用 hamcrest allOf 匹配器来组合多个条件:

    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)));
    

    【讨论】:

      【解决方案4】:

      尝试了@FrostRocket 的回答,看起来最有希望,但需要添加一些自定义:

      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;
              }
          };
      }
      

      【讨论】:

      • 嗨,为什么要添加哈希码检查?这个版本对我有用,但我不确定它为什么需要这样做。谢谢!
      • 嗨,我得说我不记得了。我认为这与视图在测试时发生变异有关,但现在不确定。
      • 我认为原因是匹配器可能在同一个组件上被多次调用。在这种情况下,保持对视图的引用(而不是哈希码)也应该有效。
      【解决方案5】:

      我创建了一个与它找到的第一个视图匹配的 ViewMatcher。 也许它对某人有帮助。 例如。当您没有要使用 onData() 的 AdapterView 时。

      /**
       * 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();
         }
      }
      

      像这样使用它:

       onView(FirstViewMatcher.firstView()).perform(click());
      

      【讨论】:

      • 一直在尝试更改它以返回特定视图类型中的第一个。想法?
      • @wapples 取决于...如果你想匹配一个视图类型和从它继承的所有类型,你可以在 match() 方法中使用类似 if(o instanceof ClassToMatch.class) 的东西。如果您只想匹配一个特定的类,您可以执行if("ClassToMatchAsString".equals(o.getClass().getName()))if("ClassToMatchAsString".equals(o.getClass().getSimpleName())) 之类的操作。或者,如果您根本不想编辑代码,这也应该可以:onView(allOf(withClassName(endsWith("ClassToMatchAsString")),FirstViewMatcher.firstView())).perform(click());
      • Matcher 合同规定Matchers 应该是无状态的。这个Matcher 会以微妙的方式中断。
      【解决方案6】:

      案例:

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

      在我的 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() + " )" );
              }
          };
      }
      

      【讨论】:

      • 我们怎么知道我们得到的第一个视图是我们想要的?
      • 文本视图中包含的字符串与唯一 ID 不同,这就是我使用它匹配第一个实例的原因。如果有更多与字符串匹配的视图,那么它们也会返回 true,除非我没有应用该标志。随意使用你自己的逻辑。
      【解决方案7】:

      与网格视图无关,但我遇到了类似的问题,我的 RecyclerView根布局 上的元素都有 same id 并且 显示到屏幕上。帮助我解决它的是检查descendancy,例如:

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

      【讨论】:

        【解决方案8】:

        最迟 * 运行 -> 记录 Espresso 测试

        通过点击不同位置的相同 ID 视图为它们生成不同的代码,请尝试一下。

        它实际上解决了这些问题。

        【讨论】:

          【解决方案9】:

          您可以简单地将 NthMatcher 设为:

             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
                  }
              }
          

          这样声明:

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

          并像这样使用:

          onView(withNthId(R.id.textview, 1)).perform(click())
          

          【讨论】:

          • what id n in withNthId(resId: Int, n: Int),我们为什么需要这个?
          猜你喜欢
          • 1970-01-01
          • 2017-06-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-10-01
          • 1970-01-01
          • 2015-05-03
          • 1970-01-01
          相关资源
          最近更新 更多