Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android-snaptesting/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies {
implementation(libs.androidx.test.monitor)
implementation(libs.androidx.test.runner)
implementation(libs.androidx.ui.test.junit4.android)
implementation(libs.espresso.core)
}

apply("${rootProject.projectDir}/mavencentral.gradle")
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@ import android.app.Activity
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Looper
import android.view.View
import android.view.View.INVISIBLE
import android.widget.EditText
import android.widget.HorizontalScrollView
import android.widget.ScrollView
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onRoot
import androidx.test.espresso.Espresso
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.screenshot.Screenshot
import com.dropbox.differ.ImageComparator
import com.dropbox.differ.Mask
Expand All @@ -34,6 +42,9 @@ public class ScreenshotsRule(

private val directories = Directories()

private val ignoredViews: List<Int>
get() = emptyList()

override fun apply(base: Statement, description: Description): Statement {
className = description.className
testName = description.methodName
Expand Down Expand Up @@ -62,15 +73,19 @@ public class ScreenshotsRule(
activity: Activity,
name: String? = null,
) {
val view = activity.findViewById<View>(android.R.id.content)

val bitmap = Screenshot.capture(activity).bitmap
compareScreenshot(bitmap, name)
compareScreenshot(bitmap, name, view)
}

@Suppress("MemberVisibilityCanBePrivate")
public fun compareScreenshot(
bitmap: Bitmap,
name: String? = null,
view: View? = null,
) {
disableFlakyComponentsAndWaitForIdle(view)
val resourceName = "${className}_${name ?: testName}.png"
val fileName = "$resourceName.${System.nanoTime()}"
saveScreenshot(fileName, bitmap)
Expand Down Expand Up @@ -141,4 +156,65 @@ public class ScreenshotsRule(
)
}
}

private fun disableFlakyComponentsAndWaitForIdle(view: View? = null) {
if (view != null) {
disableAnimatedComponents(view)
hideIgnoredViews(view)
}
if (notInAppMainThread()) {
waitForAnimationsToFinish()
}
}

private fun disableAnimatedComponents(view: View) {
runOnUi {
hideEditTextCursors(view)
hideScrollViewBars(view)
}
}

private fun hideEditTextCursors(view: View) {
view.childrenViews<EditText>().forEach {
it.isCursorVisible = false
}
}

private fun hideScrollViewBars(view: View) {
view.childrenViews<ScrollView>().forEach {
hideViewBars(it)
}

view.childrenViews<HorizontalScrollView>().forEach {
hideViewBars(it)
}
}

private fun hideViewBars(it: View) {
it.isHorizontalScrollBarEnabled = false
it.isVerticalScrollBarEnabled = false
it.overScrollMode = View.OVER_SCROLL_NEVER
}

private fun hideIgnoredViews(view: View) = runOnUi {
view.filterChildrenViews { children -> children.id in ignoredViews }.forEach { viewToIgnore ->
viewToIgnore.visibility = INVISIBLE
}
}

public fun waitForAnimationsToFinish() {
getInstrumentation().waitForIdleSync()
Espresso.onIdle()
}

public fun runOnUi(block: () -> Unit) {
if (notInAppMainThread()) {
getInstrumentation().runOnMainSync { block() }
} else {
block()
}
}

private fun notInAppMainThread() = Looper.myLooper() != Looper.getMainLooper()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.telefonica.androidsnaptesting.screenshots

import android.view.View
import android.view.ViewGroup

@Suppress("UNCHECKED_CAST")
public inline fun <reified T : View> View.childrenViews(): List<T> = filterChildrenViews {
it is T
} as List<T>

public fun View.filterChildrenViews(filter: (View) -> Boolean): List<View> {
val children = mutableSetOf<View>()
val view = this
if (view !is ViewGroup) {
if (filter.invoke(view)) {
children.add(view)
}
} else {
for (i in 0 until view.childCount) {
view.getChildAt(i).let {
children.addAll(it.filterChildrenViews(filter))
if (filter.invoke(it)) {
children.add(it)
}
}
}
}

return children.toList()
}