@@ -24,19 +24,29 @@ import android.app.Activity
2424import android.content.res.Configuration
2525import android.graphics.PointF
2626import android.os.Build
27+ import android.os.SystemClock
2728import android.util.TypedValue
2829import android.view.View
30+ import androidx.core.view.isVisible
2931import androidx.test.espresso.Espresso
32+ import androidx.test.espresso.Espresso.onView
3033import androidx.test.espresso.NoMatchingViewException
34+ import androidx.test.espresso.UiController
35+ import androidx.test.espresso.ViewAction
36+ import androidx.test.espresso.ViewAssertion
3137import androidx.test.espresso.assertion.ViewAssertions
32- import androidx.test.espresso.matcher.ViewMatchers
38+ import androidx.test.espresso.assertion.ViewAssertions.matches
3339import androidx.test.platform.app.InstrumentationRegistry
3440import androidx.test.rule.GrantPermissionRule
3541import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
3642import androidx.test.runner.lifecycle.Stage
3743import org.catrobat.paintroid.MainActivity
3844import org.hamcrest.Matcher
3945import org.junit.Assert
46+ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
47+ import androidx.test.espresso.matcher.ViewMatchers.isRoot
48+ import androidx.test.espresso.util.TreeIterables
49+ import org.hamcrest.Matchers.not
4050
4151object EspressoUtils {
4252 const val DEFAULT_STROKE_WIDTH = 25
@@ -87,19 +97,55 @@ object EspressoUtils {
8797 val viewInteraction = Espresso .onView(viewMatcher).inRoot(UiMatcher .isToast)
8898 while (System .currentTimeMillis() < waitTime) {
8999 try {
90- viewInteraction.check(ViewAssertions .matches(ViewMatchers . isDisplayed()))
100+ viewInteraction.check(ViewAssertions .matches(isDisplayed()))
91101 return
92102 } catch (e: NoMatchingViewException ) {
93- Espresso .onView(ViewMatchers .isRoot()).perform(UiInteractions .waitFor(250 ))
103+ Espresso .onView(isRoot()).perform(UiInteractions .waitFor(250 ))
104+ }
105+ }
106+ viewInteraction.check(ViewAssertions .matches(isDisplayed()))
107+ }
108+
109+ @SuppressWarnings(" SwallowedException" )
110+ fun waitForViewToDisappear (
111+ viewMatcher : org.hamcrest.Matcher <android.view.View >,
112+ timeoutMillis : Long = 10_000, // Default timeout 10 seconds
113+ pollIntervalMillis : Long = 100 // Default poll interval 100 ms
114+ ) {
115+ val endTime = System .currentTimeMillis() + timeoutMillis
116+ var viewFound = true // Assume view is initially present
117+
118+ while (System .currentTimeMillis() < endTime && viewFound) {
119+ try {
120+ // Check if the view is still displayed
121+ onView(viewMatcher).check(matches(isDisplayed()))
122+ // If the check passes, the view is still displayed.
123+ // Wait for a short interval before trying again.
124+ onView(isRoot()).perform(UiInteractions .waitFor(pollIntervalMillis))
125+ } catch (e: NoMatchingViewException ) {
126+ // View is not found in the hierarchy, so it has "disappeared" in this sense.
127+ viewFound = false
128+ } catch (e: AssertionError ) {
129+ // View is in the hierarchy but not displayed (e.g., GONE, INVISIBLE).
130+ // This also means it has "disappeared" for the user.
131+ viewFound = false
132+ }
133+ }
134+
135+ if (viewFound) {
136+ try {
137+ onView(viewMatcher).check(matches(not (isDisplayed())))
138+ } catch (e: NoMatchingViewException ) {
139+ // This is acceptable, means it's not in hierarchy
94140 }
95141 }
96- viewInteraction.check(ViewAssertions .matches(ViewMatchers .isDisplayed()))
97142 }
98143
99144 val configuration: Configuration
100145 get() = InstrumentationRegistry .getInstrumentation().targetContext.resources.configuration
101146
102147 fun grantPermissionRulesVersionCheck (): GrantPermissionRule {
148+
103149 return if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .Q ) {
104150 GrantPermissionRule .grant(Manifest .permission.READ_EXTERNAL_STORAGE )
105151 } else {
@@ -109,4 +155,45 @@ object EspressoUtils {
109155 )
110156 }
111157 }
158+ fun searchFor (matcher : Matcher <View >): ViewAction {
159+ return object : ViewAction {
160+
161+ override fun getConstraints (): Matcher <View > {
162+ return isRoot()
163+ }
164+
165+ override fun getDescription (): String {
166+ return " searching for view $matcher in the root view"
167+ }
168+
169+ override fun perform (uiController : UiController , view : View ) {
170+ val childViews: Iterable <View > = TreeIterables .breadthFirstViewTraversal(view)
171+ childViews.forEach {
172+ if (matcher.matches(it).and (it.isVisible)) {
173+ return
174+ }
175+ }
176+ throw NoMatchingViewException .Builder ()
177+ .withRootView(view)
178+ .withViewMatcher(matcher)
179+ .build()
180+ }
181+ }
182+ }
183+
184+ @SuppressWarnings(" SwallowedException" )
185+ fun assertOnView (viewMatcher : Matcher <View ?>? , assertion : ViewAssertion , waitMillis : Int = 10_000, waitMillisPerTry : Long = 50) {
186+ val endTime = System .currentTimeMillis() + waitMillis
187+ while (System .currentTimeMillis() < endTime) {
188+ try {
189+ onView(viewMatcher).check(assertion)
190+ return
191+ } catch (e: NoMatchingViewException ) {
192+ SystemClock .sleep(waitMillisPerTry)
193+ } catch (e: AssertionError ) {
194+ SystemClock .sleep(waitMillisPerTry)
195+ }
196+ }
197+ onView(viewMatcher).check(assertion)
198+ }
112199}
0 commit comments