Skip to content

Commit

Permalink
Add RobotAssertion for checking views are not present in view hierarc…
Browse files Browse the repository at this point in the history
…hy. Add recycler view item matcher using ViewHolder's viewItemType.
  • Loading branch information
ychescale9 committed Aug 29, 2020
1 parent 41dbde8 commit 3ab0579
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 19 deletions.
4 changes: 4 additions & 0 deletions blueprint-testing-robot/api/blueprint-testing-robot.api
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,19 @@ public final class reactivecircus/blueprint/testing/assertion/ViewRobotAssertion
public static final fun viewEnabled (Lreactivecircus/blueprint/testing/RobotAssertions;I)V
public static final fun viewNotClickable (Lreactivecircus/blueprint/testing/RobotAssertions;I)V
public static final fun viewNotDisplayed (Lreactivecircus/blueprint/testing/RobotAssertions;[I)V
public static final fun viewNotExists (Lreactivecircus/blueprint/testing/RobotAssertions;[I)V
}

public final class reactivecircus/blueprint/testing/matcher/RecyclerViewMatcher {
public fun <init> (I)V
public final fun atPosition (I)Lorg/hamcrest/Matcher;
public final fun atPositionOnView (II)Lorg/hamcrest/Matcher;
public final fun withItemViewType (II)Lorg/hamcrest/Matcher;
public final fun withItemViewType (III)Lorg/hamcrest/Matcher;
}

public final class reactivecircus/blueprint/testing/matcher/RecyclerViewMatcherKt {
public static final fun findParentRecursively (Landroid/view/View;I)Landroid/view/ViewParent;
public static final fun withRecyclerView (I)Lreactivecircus/blueprint/testing/matcher/RecyclerViewMatcher;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package reactivecircus.blueprint.testing.assertion

import android.view.View
import androidx.annotation.IdRes
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.matcher.ViewMatchers
import org.hamcrest.CoreMatchers
import androidx.test.espresso.matcher.ViewMatchers.isClickable
import org.hamcrest.CoreMatchers.not
import reactivecircus.blueprint.testing.RobotAssertions

/**
* Check if all views associated with [viewIds] are displayed.
*/
public fun RobotAssertions.viewDisplayed(@IdRes vararg viewIds: Int) {
viewIds.forEach { viewId ->
Espresso.onView(ViewMatchers.withId(viewId))
onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
}
}
Expand All @@ -23,39 +24,48 @@ public fun RobotAssertions.viewDisplayed(@IdRes vararg viewIds: Int) {
*/
public fun RobotAssertions.viewNotDisplayed(@IdRes vararg viewIds: Int) {
viewIds.forEach { viewId ->
Espresso.onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(CoreMatchers.not<View>(ViewMatchers.isDisplayed())))
onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(not(ViewMatchers.isDisplayed())))
}
}

/**
* Check if all views associated with [viewIds] are not present in the view hierarchy.
*/
public fun RobotAssertions.viewNotExists(@IdRes vararg viewIds: Int) {
viewIds.forEach { viewId ->
onView(ViewMatchers.withId(viewId)).check(doesNotExist())
}
}

/**
* Check if the view associated with [viewId] is enabled.
*/
public fun RobotAssertions.viewEnabled(@IdRes viewId: Int) {
Espresso.onView(ViewMatchers.withId(viewId))
onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(ViewMatchers.isEnabled()))
}

/**
* Check if the view associated with [viewId] is disabled.
*/
public fun RobotAssertions.viewDisabled(@IdRes viewId: Int) {
Espresso.onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(CoreMatchers.not<View>(ViewMatchers.isEnabled())))
onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(not(ViewMatchers.isEnabled())))
}

/**
* Check if the view associated with [viewId] is clickable.
*/
public fun RobotAssertions.viewClickable(@IdRes viewId: Int) {
Espresso.onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(ViewMatchers.isClickable()))
onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(isClickable()))
}

/**
* Check if the view associated with [viewId] is NOT clickable.
*/
public fun RobotAssertions.viewNotClickable(@IdRes viewId: Int) {
Espresso.onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(CoreMatchers.not<View>(ViewMatchers.isClickable())))
onView(ViewMatchers.withId(viewId))
.check(ViewAssertions.matches(not(isClickable())))
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.res.Resources
import android.view.View
import android.view.ViewParent
import androidx.annotation.IdRes
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
import androidx.test.espresso.matcher.ViewMatchers.withId
Expand All @@ -18,7 +19,7 @@ public fun withRecyclerView(@IdRes recyclerViewId: Int): RecyclerViewMatcher {
return RecyclerViewMatcher(recyclerViewId)
}

public class RecyclerViewMatcher(private val recyclerViewId: Int) {
public class RecyclerViewMatcher(@PublishedApi internal val recyclerViewId: Int) {

public fun atPosition(position: Int): Matcher<View> {
return atPositionOnView(position, -1)
Expand Down Expand Up @@ -67,14 +68,73 @@ public class RecyclerViewMatcher(private val recyclerViewId: Int) {
view === targetView
}
}
}
}

public fun withItemViewType(
@IdRes recyclerViewId: Int,
itemViewType: Int,
): Matcher<View> {
return withItemViewType(recyclerViewId, itemViewType, -1)
}

public fun withItemViewType(
@IdRes recyclerViewId: Int,
itemViewType: Int,
targetViewId: Int,
): Matcher<View> {
return object : TypeSafeMatcher<View>() {
var resources: Resources? = null
var itemView: View? = null

private fun findParentRecursively(view: View, targetId: Int): ViewParent? {
if (view.id == targetId) {
return view as ViewParent
override fun describeTo(description: Description) {
var idDescription = recyclerViewId.toString()
if (resources != null) {
idDescription = try {
requireNotNull(resources).getResourceName(recyclerViewId)
} catch (var4: Resources.NotFoundException) {
String.format("%s (resource name not found)", recyclerViewId)
}
}
description.appendText("with id: $idDescription")
}

@Suppress("ReturnCount")
override fun matchesSafely(view: View): Boolean {
resources = view.resources

// only try to match views which are within descendant of RecyclerView
if (!isDescendantOfA(withId(recyclerViewId)).matches(view)) {
return false
}

if (itemView == null) {
val recyclerView = findParentRecursively(view, recyclerViewId) as RecyclerView?
if (recyclerView != null && recyclerView.id == recyclerViewId) {
itemView = recyclerView.children.firstOrNull {
recyclerView.getChildViewHolder(it).itemViewType == itemViewType
}
} else {
return false
}
}

return if (targetViewId == -1) {
view === itemView
} else {
val targetView = itemView?.findViewById<View>(targetViewId)
view === targetView
}
val parent = view.parent as View
return findParentRecursively(parent, targetId)
}
}
}
}

@PublishedApi
internal fun findParentRecursively(view: View, targetId: Int): ViewParent? {
if (view.id == targetId) {
return view as ViewParent
}
val parent = view.parent as View
return findParentRecursively(parent, targetId)
}

0 comments on commit 3ab0579

Please sign in to comment.