Matcher<? super View> 타입을 구현해 View 객체를 판별하는 규칙을 제공하는 인터페이스ViewMatchers(withId, withText, isDisplayed 등)를 제공하며 모두 Matcher<? super View> 형태로 onView()에 전달onView(Matcher<? super View> viewMatcher)는 현재 activity의 view 계층을 탐색하여 전달한 matcher와 일치하는 view를 찾고 ViewInteraction 객체를 반환ViewInteraction을 통해 perform() 또는 check() 수행Matcher<T> 인터페이스 사용. 즉, ViewMatchers나 RootMatchers는 Hamcrest의 Matcher 구현체ClassCastException 발생.TypeSafeMatcher<T> 추상 클래스는 제네릭 타입을 명시하여 매칭 시 타입 안정성을 보장TypeSafeMatcher<View> 또는 TypeSafeMatcher<Root>를 상속해 커스텀 matcher를 만듦matches(Object item)가 있고 내부에서 instanceof 검사 후 matchesSafely(T item) 호출T) 으로 매칭 로직만 작성하면 됨matchesSafely(T item)를 오버라이드하게 함instanceof 검사 진행. 해당 조건들이 모두 통과되면 matchesSafely(T item)을 호출. 즉, 타입 검사를 자동으로 처리해 주며 개발자는 안전하게 matchesSafely()만 구현하면 됨@Override
public final boolean matches(Object item) {
return item != null
&& expectedType.isInstance(item)
&& matchesSafely((T) item);
}
override fun matchesSafely(view: View): Boolean {
return (view as TextView).text == "OK"
}
override fun describeTo(description: Description) {
description.appendText("has text 'OK'")
}
override fun describeMismatch(item: Any?, mismatchDescription: Description) {
val view = item as? TextView
mismatchDescription.appendText("was '${view?.text}'")
}
TypeSafeMatcher(): 리플렉션으로 제네릭 타입 추론TypeSafeMatcher(Class<?> expectedType): 직접 검사할 타입을 명시TypeSafeMatcher<View>처럼 명시적으로 사용하면 보통 기본 생성자를 사용onView(matcher) 호출 시 모든 view에 대해 matcher.matches(view) 호출TypeSafeMatcher는 내부에서 instanceof 검사 후, matchesSafely(view)만 호출androidx.test.espresso.Root 클래스onView()는 현재 포커스를 가진 Root(activity 윈도우) 에서만 view를 탐색..inRoot(matcher)를 사용해 Root를 지정 필수onView(...)만으로는 확인이 어려움TypeSafeMatcher<Root>를 상속해 matchesSafely(root)를 구현하면 해당 Root(윈도우)가 Toast인지 판별 가능!root.decorView.hasWindowFocus() 처럼 포커스가 없는 윈도우를 Toast로 간주root.windowLayoutParams.get().type == WindowManager.LayoutParams.TYPE_TOAST처럼 윈도우 타입 검사도 가능class ToastMatcher : TypeSafeMatcher<Root?>() {
override fun describeTo(description: Description?) {
description?.appendText("is toast")
}
override fun matchesSafely(root: Root?): Boolean {
if (root == null) return false
val type = root.windowLayoutParams?.get()?.type ?: return false
// TYPE_TOAST를 직접 확인(환경에 따라 다르게 동작할 수 있음)
if (type == WindowManager.LayoutParams.TYPE_TOAST) {
val windowToken = root.decorView.windowToken
val appToken = root.decorView.applicationWindowToken
// 같은 토큰이면 애플리케이션 윈도우 (true = 일반 윈도우 아님)
return windowToken === appToken.not()
}
return false
}
}
withText나 withId로는 판별 불가능class WithTextColor(private val expectedColor: Int) : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("with text color: $expectedColor")
}
override fun matchesSafely(view: View): Boolean {
val textView = view as? TextView ?: return false
return textView.currentTextColor == expectedColor
}
}
onView(withId(R.id.statusLabel))
.check(matches(WithTextColor(Color.RED)))
fun hasChildWithText(text: String): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("has child with text: $text")
}
override fun matchesSafely(view: View): Boolean {
if (view !is ViewGroup) return false
return (0 until view.childCount).any {
val child = view.getChildAt(it)
(child as? TextView)?.text == text
}
}
}
}
onView(withText(...))로는 찾기 어려움fun atPosition(position: Int, itemMatcher: Matcher<View>): Matcher<View> {
return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) {
description.appendText("has item at position $position: ")
itemMatcher.describeTo(description)
}
override fun matchesSafely(view: View): Boolean {
val recycler = view as? RecyclerView ?: return false
val holder = recycler.findViewHolderForAdapterPosition(position)
?: return false
return itemMatcher.matches(holder.itemView)
}
}
}
ChatGPT의 도움을 받아 작성된 포스팅입니다