1 /** 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * ``` 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * ``` 10 * 11 * Unless required by applicable law or agreed to in writing, software distributed under the License 12 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 * or implied. See the License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 package com.android.healthconnect.controller.tests.utils 17 18 import android.view.View 19 import android.view.ViewGroup 20 import androidx.recyclerview.widget.RecyclerView 21 import androidx.test.espresso.matcher.BoundedMatcher 22 import org.hamcrest.Description 23 import org.hamcrest.Matcher 24 import org.hamcrest.TypeSafeMatcher 25 26 /** 27 * A custom matcher used when there are more than one view with the same resourceId/text/contentDesc 28 * etc. 29 * 30 * @param matcher a view matcher for UI element which may have potentially more than one matched. 31 * Typical example is the element that is repeated in ListView or RecyclerView 32 * @param index the index to select matcher if there are more than one matcher. it's started at 33 * zero. 34 * @return the view matcher selected from given matcher and index. 35 */ withIndexnull36fun withIndex(matcher: Matcher<View?>, index: Int): Matcher<View?> { 37 return object : TypeSafeMatcher<View?>() { 38 var currentIndex = 0 39 40 override fun describeTo(description: Description) { 41 description.appendText("with index: ") 42 description.appendValue(index) 43 matcher.describeTo(description) 44 } 45 46 override fun matchesSafely(view: View?): Boolean { 47 return matcher.matches(view) && currentIndex++ == index 48 } 49 } 50 } 51 atPositionnull52fun atPosition(position: Int, itemMatcher: Matcher<View?>): Matcher<View?> { 53 return object : BoundedMatcher<View?, RecyclerView>(RecyclerView::class.java) { 54 override fun describeTo(description: Description) { 55 description.appendText("has item at position $position: ") 56 itemMatcher.describeTo(description) 57 } 58 59 override fun matchesSafely(view: RecyclerView): Boolean { 60 val viewHolder: RecyclerView.ViewHolder = 61 view.findViewHolderForAdapterPosition(position) 62 ?: // has no item on such position 63 return false 64 return itemMatcher.matches(viewHolder.itemView) 65 } 66 } 67 } 68 isAbovenull69fun isAbove(otherViewMatcher: Matcher<View>): TypeSafeMatcher<View> { 70 return object : TypeSafeMatcher<View>() { 71 private var otherView: View? = null 72 73 override fun describeTo(description: Description) { 74 description.appendText("is above view: ") 75 otherViewMatcher.describeTo(description) 76 } 77 78 override fun matchesSafely(view: View): Boolean { 79 if (otherView == null) { 80 otherView = view.rootView.findViewByMatcher(otherViewMatcher) 81 if (otherView == null) return false // Other view not found 82 } 83 84 val location1 = IntArray(2) 85 val location2 = IntArray(2) 86 view.getLocationOnScreen(location1) 87 otherView!!.getLocationOnScreen(location2) // Safe call as otherView might be null 88 89 return location1[1] < location2[1] // Compare y-coordinates 90 } 91 } 92 } 93 94 // Extension function to find a view by matcher findViewByMatchernull95private fun View.findViewByMatcher(matcher: Matcher<View>): View? { 96 if (matcher.matches(this)) return this // Check if this view itself matches 97 if (this is ViewGroup) { 98 for (i in 0 until childCount) { 99 val child = getChildAt(i) 100 val foundView = child.findViewByMatcher(matcher) 101 if (foundView != null) return foundView 102 } 103 } 104 return null // No match found 105 } 106