• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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.Espresso.onView
22 import androidx.test.espresso.assertion.ViewAssertions.matches
23 import androidx.test.espresso.matcher.BoundedMatcher
24 import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
25 import androidx.test.espresso.matcher.ViewMatchers.hasSibling
26 import androidx.test.espresso.matcher.ViewMatchers.isChecked
27 import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
28 import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
29 import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
30 import androidx.test.espresso.matcher.ViewMatchers.withId
31 import androidx.test.espresso.matcher.ViewMatchers.withTagValue
32 import androidx.test.espresso.matcher.ViewMatchers.withText
33 import org.hamcrest.Description
34 import org.hamcrest.Matcher
35 import org.hamcrest.Matchers.allOf
36 import org.hamcrest.Matchers.`is`
37 import org.hamcrest.Matchers.not
38 import org.hamcrest.TypeSafeMatcher
39 
40 /**
41  * A custom matcher used when there are more than one view with the same resourceId/text/contentDesc
42  * etc.
43  *
44  * @param matcher a view matcher for UI element which may have potentially more than one matched.
45  *   Typical example is the element that is repeated in ListView or RecyclerView
46  * @param index the index to select matcher if there are more than one matcher. it's started at
47  *   zero.
48  * @return the view matcher selected from given matcher and index.
49  */
withIndexnull50 fun withIndex(matcher: Matcher<View?>, index: Int): Matcher<View?> {
51     return object : TypeSafeMatcher<View?>() {
52         var currentIndex = 0
53 
54         override fun describeTo(description: Description) {
55             description.appendText("with index: ")
56             description.appendValue(index)
57             matcher.describeTo(description)
58         }
59 
60         override fun matchesSafely(view: View?): Boolean {
61             return matcher.matches(view) && currentIndex++ == index
62         }
63     }
64 }
65 
66 /**
67  * A custom matcher to find a CheckBox based on its sibling TextView's text.
68  *
69  * It is specifically useful for matching a CheckBox in a `SelectorWithWidgetPreference` layout,
70  * where the CheckBox and the associated TextView are side by side within separate LinearLayouts.
71  *
72  * @param keyText The text of the associated TextView to locate the CheckBox.
73  * @return A Matcher for the CheckBox that matches the given text of the sibling TextView.
74  */
checkBoxOfnull75 fun checkBoxOf(keyText: String): Matcher<View> {
76     return allOf(
77         withId(android.R.id.checkbox),
78         isDescendantOfA(
79             allOf(withId(android.R.id.widget_frame), hasSibling(hasDescendant(withText(keyText))))
80         ),
81     )
82 }
83 
84 /**
85  * Matches a view that is an indirect sibling of a given target view. An indirect sibling means the
86  * view is a descendant of a view that has a sibling which contains the target view somewhere in its
87  * hierarchy.
88  *
89  * Example: If `View A` and `View B` are inside `Parent 1`, and `Parent 1` has a sibling `Parent 2`
90  * that contains `View C`, then `View A` has an indirect sibling relationship with `View C`.
91  *
92  * @param targetMatcher The matcher for the target view that should be an indirect sibling.
93  * @return A matcher that verifies the indirect sibling relationship.
94  */
hasIndirectSiblingnull95 fun hasIndirectSibling(targetMatcher: Matcher<View>): Matcher<View> {
96     return isDescendantOfA(hasSibling(hasDescendant(targetMatcher)))
97 }
98 
99 /** A custom matcher to find a [Preference] with the given title and summary. */
withTitleAndSummarynull100 fun withTitleAndSummary(titleText: String, summaryText: String): Matcher<View> {
101     return allOf(
102         withId(android.R.id.title),
103         withText(titleText),
104         hasSibling(hasDescendant(withText(summaryText))),
105     )
106 }
107 
108 /** A custom matcher to find a [Preference] with the given title and no visible summary. */
withTitleNoSummarynull109 fun withTitleNoSummary(titleText: String): Matcher<View> {
110     return allOf(
111         withId(android.R.id.title),
112         withText(titleText),
113         hasSibling(hasDescendant(allOf(withId(android.R.id.summary), not(isDisplayed())))),
114     )
115 }
116 
atPositionnull117 fun atPosition(position: Int, itemMatcher: Matcher<View?>): Matcher<View?> {
118     return object : BoundedMatcher<View?, RecyclerView>(RecyclerView::class.java) {
119         override fun describeTo(description: Description) {
120             description.appendText("has item at position $position: ")
121             itemMatcher.describeTo(description)
122         }
123 
124         override fun matchesSafely(view: RecyclerView): Boolean {
125             val viewHolder: RecyclerView.ViewHolder =
126                 view.findViewHolderForAdapterPosition(position)
127                     ?: // has no item on such position
128                     return false
129             return itemMatcher.matches(viewHolder.itemView)
130         }
131     }
132 }
133 
isAbovenull134 fun isAbove(otherViewMatcher: Matcher<View>): TypeSafeMatcher<View> {
135     return object : TypeSafeMatcher<View>() {
136         private var otherView: View? = null
137 
138         override fun describeTo(description: Description) {
139             description.appendText("is above view: ")
140             otherViewMatcher.describeTo(description)
141         }
142 
143         override fun matchesSafely(view: View): Boolean {
144             if (otherView == null) {
145                 otherView = view.rootView.findViewByMatcher(otherViewMatcher)
146                 if (otherView == null) return false // Other view not found
147             }
148 
149             val location1 = IntArray(2)
150             val location2 = IntArray(2)
151             view.getLocationOnScreen(location1)
152             otherView!!.getLocationOnScreen(location2) // Safe call as otherView might be null
153 
154             return location1[1] < location2[1] // Compare y-coordinates
155         }
156     }
157 }
158 
159 // Extension function to find a view by matcher
findViewByMatchernull160 private fun View.findViewByMatcher(matcher: Matcher<View>): View? {
161     if (matcher.matches(this)) return this // Check if this view itself matches
162     if (this is ViewGroup) {
163         for (i in 0 until childCount) {
164             val child = getChildAt(i)
165             val foundView = child.findViewByMatcher(matcher)
166             if (foundView != null) return foundView
167         }
168     }
169     return null // No match found
170 }
171 
172 // Checkbox assertions
assertCheckboxNotCheckednull173 fun assertCheckboxNotChecked(
174     recyclerViewId: Int,
175     title: String,
176     position: Int,
177     tag: String = "checkbox",
178 ) {
179     onView(withId(recyclerViewId))
180         .check(
181             matches(
182                 atPosition(
183                     position,
184                     allOf(
185                         hasDescendant(withText(title)),
186                         hasDescendant(withTagValue(`is`(tag))),
187                         hasDescendant(isNotChecked()),
188                     ),
189                 )
190             )
191         )
192 }
193 
assertCheckboxNotShownnull194 fun assertCheckboxNotShown(
195     recyclerViewId: Int,
196     title: String,
197     position: Int,
198     tag: String = "checkbox",
199 ) {
200     onView(withId(recyclerViewId))
201         .check(
202             matches(
203                 atPosition(
204                     position,
205                     allOf(
206                         hasDescendant(withText(title)),
207                         not(hasDescendant(withTagValue(`is`(tag)))),
208                     ),
209                 )
210             )
211         )
212 }
213 
assertCheckboxCheckednull214 fun assertCheckboxChecked(
215     recyclerViewId: Int,
216     title: String,
217     position: Int,
218     tag: String = "checkbox",
219 ) {
220     onView(withId(recyclerViewId))
221         .check(
222             matches(
223                 atPosition(
224                     position,
225                     allOf(
226                         hasDescendant(withText(title)),
227                         hasDescendant(withTagValue(`is`(tag))),
228                         hasDescendant(isChecked()),
229                     ),
230                 )
231             )
232         )
233 }
234