@@ -4,12 +4,20 @@ import android.app.Activity
44import android.graphics.Bitmap
55import android.graphics.BitmapFactory
66import android.os.Build
7+ import android.os.Looper
8+ import android.view.View
9+ import android.view.View.INVISIBLE
10+ import android.widget.EditText
11+ import android.widget.HorizontalScrollView
12+ import android.widget.ScrollView
713import androidx.annotation.RequiresApi
814import androidx.compose.ui.graphics.asAndroidBitmap
915import androidx.compose.ui.test.captureToImage
1016import androidx.compose.ui.test.junit4.ComposeTestRule
1117import androidx.compose.ui.test.onRoot
18+ import androidx.test.espresso.Espresso
1219import androidx.test.platform.app.InstrumentationRegistry
20+ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
1321import androidx.test.runner.screenshot.Screenshot
1422import com.dropbox.differ.ImageComparator
1523import com.dropbox.differ.Mask
@@ -34,6 +42,9 @@ public class ScreenshotsRule(
3442
3543 private val directories = Directories ()
3644
45+ private val ignoredViews: List <Int >
46+ get() = emptyList()
47+
3748 override fun apply (base : Statement , description : Description ): Statement {
3849 className = description.className
3950 testName = description.methodName
@@ -62,15 +73,19 @@ public class ScreenshotsRule(
6273 activity : Activity ,
6374 name : String? = null,
6475 ) {
76+ val view = activity.findViewById<View >(android.R .id.content)
77+
6578 val bitmap = Screenshot .capture(activity).bitmap
66- compareScreenshot(bitmap, name)
79+ compareScreenshot(bitmap, name, view )
6780 }
6881
6982 @Suppress(" MemberVisibilityCanBePrivate" )
7083 public fun compareScreenshot (
7184 bitmap : Bitmap ,
7285 name : String? = null,
86+ view : View ? = null,
7387 ) {
88+ disableFlakyComponentsAndWaitForIdle(view)
7489 val resourceName = " ${className} _${name ? : testName} .png"
7590 val fileName = " $resourceName .${System .nanoTime()} "
7691 saveScreenshot(fileName, bitmap)
@@ -141,4 +156,65 @@ public class ScreenshotsRule(
141156 )
142157 }
143158 }
159+
160+ private fun disableFlakyComponentsAndWaitForIdle (view : View ? = null) {
161+ if (view != null ) {
162+ disableAnimatedComponents(view)
163+ hideIgnoredViews(view)
164+ }
165+ if (notInAppMainThread()) {
166+ waitForAnimationsToFinish()
167+ }
168+ }
169+
170+ private fun disableAnimatedComponents (view : View ) {
171+ runOnUi {
172+ hideEditTextCursors(view)
173+ hideScrollViewBars(view)
174+ }
175+ }
176+
177+ private fun hideEditTextCursors (view : View ) {
178+ view.childrenViews<EditText >().forEach {
179+ it.isCursorVisible = false
180+ }
181+ }
182+
183+ private fun hideScrollViewBars (view : View ) {
184+ view.childrenViews<ScrollView >().forEach {
185+ hideViewBars(it)
186+ }
187+
188+ view.childrenViews<HorizontalScrollView >().forEach {
189+ hideViewBars(it)
190+ }
191+ }
192+
193+ private fun hideViewBars (it : View ) {
194+ it.isHorizontalScrollBarEnabled = false
195+ it.isVerticalScrollBarEnabled = false
196+ it.overScrollMode = View .OVER_SCROLL_NEVER
197+ }
198+
199+ private fun hideIgnoredViews (view : View ) = runOnUi {
200+ view.filterChildrenViews { children -> children.id in ignoredViews }.forEach { viewToIgnore ->
201+ viewToIgnore.visibility = INVISIBLE
202+ }
203+ }
204+
205+ public fun waitForAnimationsToFinish () {
206+ getInstrumentation().waitForIdleSync()
207+ Espresso .onIdle()
208+ }
209+
210+ public fun runOnUi (block : () -> Unit ) {
211+ if (notInAppMainThread()) {
212+ getInstrumentation().runOnMainSync { block() }
213+ } else {
214+ block()
215+ }
216+ }
217+
218+ private fun notInAppMainThread () = Looper .myLooper() != Looper .getMainLooper()
219+
144220}
0 commit comments