diff --git a/blueprint-testing-robot/api/blueprint-testing-robot.api b/blueprint-testing-robot/api/blueprint-testing-robot.api index 3f2b54ad..650185ac 100644 --- a/blueprint-testing-robot/api/blueprint-testing-robot.api +++ b/blueprint-testing-robot/api/blueprint-testing-robot.api @@ -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 (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; } diff --git a/blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/ViewRobotAssertions.kt b/blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/ViewRobotAssertions.kt index 0af16460..116b3778 100644 --- a/blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/ViewRobotAssertions.kt +++ b/blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/assertion/ViewRobotAssertions.kt @@ -1,11 +1,12 @@ 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 /** @@ -13,7 +14,7 @@ import reactivecircus.blueprint.testing.RobotAssertions */ 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())) } } @@ -23,8 +24,17 @@ 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(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()) } } @@ -32,7 +42,7 @@ public fun RobotAssertions.viewNotDisplayed(@IdRes vararg viewIds: Int) { * 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())) } @@ -40,22 +50,22 @@ public fun RobotAssertions.viewEnabled(@IdRes viewId: Int) { * 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(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(ViewMatchers.isClickable()))) + onView(ViewMatchers.withId(viewId)) + .check(ViewAssertions.matches(not(isClickable()))) } diff --git a/blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/matcher/RecyclerViewMatcher.kt b/blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/matcher/RecyclerViewMatcher.kt index 1f1e2619..e81d9409 100644 --- a/blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/matcher/RecyclerViewMatcher.kt +++ b/blueprint-testing-robot/src/main/kotlin/reactivecircus/blueprint/testing/matcher/RecyclerViewMatcher.kt @@ -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 @@ -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 { return atPositionOnView(position, -1) @@ -67,14 +68,73 @@ public class RecyclerViewMatcher(private val recyclerViewId: Int) { view === targetView } } + } + } + + public fun withItemViewType( + @IdRes recyclerViewId: Int, + itemViewType: Int, + ): Matcher { + return withItemViewType(recyclerViewId, itemViewType, -1) + } + + public fun withItemViewType( + @IdRes recyclerViewId: Int, + itemViewType: Int, + targetViewId: Int, + ): Matcher { + return object : TypeSafeMatcher() { + 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(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) +}