Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Custom failure handle for espresso
import android.os.Environment.DIRECTORY_DOCUMENTS
import android.os.Environment.getExternalStoragePublicDirectory
import android.util.Log
import android.view.View
import androidx.test.espresso.AmbiguousViewMatcherException
import androidx.test.espresso.EspressoException
import androidx.test.espresso.FailureHandler
import androidx.test.espresso.InjectEventSecurityException
import androidx.test.espresso.PerformException
import androidx.test.espresso.core.internal.deps.guava.base.Throwables.throwIfUnchecked
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import junit.framework.AssertionFailedError
import org.hamcrest.Matcher
import org.junit.runner.Description
import java.io.File
import java.io.IOException
class CustomFailureHandler(private val description: Description?) : FailureHandler {
private val device: UiDevice
by lazy { UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) }
override fun handle(error: Throwable, viewMatcher: Matcher<View>) {
if (error is EspressoException
|| error is AssertionFailedError
|| error is AssertionError) {
throwIfUnchecked(getUserFriendlyError(error, viewMatcher, description))
throw RuntimeException(getUserFriendlyError(error, viewMatcher, description))
} else {
throwIfUnchecked(error)
throw RuntimeException(error)
}
}
private fun getUserFriendlyError(cause: Throwable, viewMatcher: Matcher<View>, description: Description?): Throwable {
dumpViewHierarchyToXml(description)
val error = when (cause) {
is PerformException -> handlePerformException(cause, viewMatcher)
is AssertionError -> handleAssertionException(cause)
is AmbiguousViewMatcherException -> handleAmbiguousViewMatcherException(cause)
is InjectEventSecurityException -> handleInjectEventSecurityException(cause, viewMatcher)
else -> cause
}
error.stackTrace = Thread.currentThread().stackTrace
return error
}
private fun handlePerformException(cause: PerformException, viewMatcher: Matcher<View>): Throwable {
val sb = StringBuilder()
sb.append(viewMatcher.toString())
// Re-throw the exception with the viewMatcher (used to locate the view) as the view
// description (makes the error more readable). The reason we do this here: not all creators
// of PerformException have access to the viewMatcher.
return PerformException.Builder()
.from(cause)
.withViewDescription(sb.toString())
.withActionDescription(sb.toString())
.build()
}
private fun handleAmbiguousViewMatcherException(cause: AmbiguousViewMatcherException): Throwable {
return AmbiguousViewMatcherException.Builder().from(cause)
.includeViewHierarchy(true).build()
}
private fun handleAssertionException(cause: Throwable): Throwable {
// reports Failure instead of Error.
// assertThat(...) throws an AssertionFailedError.
return AssertionFailedWithCauseError(cause.message, cause)
}
private fun handleInjectEventSecurityException(cause: InjectEventSecurityException, viewMatcher: Matcher<View>): Throwable {
val sb = StringBuilder()
sb.append("Verify your keyboard -" +
"It seems it was in front of your layout during an interaction, but it shouldn't")
sb.append(viewMatcher.toString())
return InjectEventSecurityException(sb.toString(), cause)
}
private class AssertionFailedWithCauseError(message: String?, cause: Throwable) : AssertionFailedError(message) {
init {
initCause(cause)
}
}
private fun dumpViewHierarchyToXml(description: Description?) {
val dump = File(
getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS),
"${description?.methodName}.xml")
if (dump.exists()) {
dump.delete()
}
dump.createNewFile()
try {
Log.e(TAG, "Saving view hierarchy into XML")
device.dumpWindowHierarchy(dump)
Log.e(TAG, dump.absolutePath)
} catch (exception: IOException) {
Log.e(TAG, "Couldn't save view hierarchy dump", exception)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment