在 Espresso 中,当多个视图匹配时如何避免 AmbiguousViewMatcherException
In Espresso, how to avoid AmbiguousViewMatcherException when multiple views match
有带有一些图像的 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 <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****
|
你应该使用onData()
来操作GridView
:
onData(withId(R.id.item_image))
.inAdapterView(withId(R.id.grid_adapter_id))
.atPosition(0)
.perform(click());
此代码将点击 GridView
中第一项内的图像
我创建了一个 ViewMatcher,它匹配它找到的第一个视图。
也许这对某人有帮助。
例如。当你没有 AdapterView 来使用 onData() 时。
/**
* 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());
与网格视图情况不完全相关,但您可以使用 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)));
编辑:有人在评论中提到 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 的第三个实例执行点击操作。
案例:
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() + " )" );
}
};
}
尝试了@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;
}
};
}
您可以简单地使 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())
与网格视图没有特别相关,但我有一个类似的问题,我的 RecyclerView 和 root layout 上的元素都有相同的 ID 并且两者 显示 到屏幕上。帮助我解决它的是检查 descendancy
例如:
onView(allOf(withId(R.id.my_view), not(isDescendantOfA(withId(R.id.recyclerView))))).check(matches(withText("My Text")));
最迟 * 运行 -> 记录 Espresso 测试
通过点击不同位置的相同 ID 视图为他们生成不同的代码,所以试试吧。
它实际上解决了这些问题。
有带有一些图像的 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 <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**** |
你应该使用onData()
来操作GridView
:
onData(withId(R.id.item_image))
.inAdapterView(withId(R.id.grid_adapter_id))
.atPosition(0)
.perform(click());
此代码将点击 GridView
我创建了一个 ViewMatcher,它匹配它找到的第一个视图。 也许这对某人有帮助。 例如。当你没有 AdapterView 来使用 onData() 时。
/**
* 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());
与网格视图情况不完全相关,但您可以使用 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)));
编辑:有人在评论中提到 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 的第三个实例执行点击操作。
案例:
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() + " )" );
}
};
}
尝试了@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;
}
};
}
您可以简单地使 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())
与网格视图没有特别相关,但我有一个类似的问题,我的 RecyclerView 和 root layout 上的元素都有相同的 ID 并且两者 显示 到屏幕上。帮助我解决它的是检查 descendancy
例如:
onView(allOf(withId(R.id.my_view), not(isDescendantOfA(withId(R.id.recyclerView))))).check(matches(withText("My Text")));
最迟 * 运行 -> 记录 Espresso 测试
通过点击不同位置的相同 ID 视图为他们生成不同的代码,所以试试吧。
它实际上解决了这些问题。