Skip to content

Instantly share code, notes, and snippets.

@akueisara
Last active November 10, 2022 06:53
Show Gist options
  • Save akueisara/d7329d79630c709346ffceafd211ded2 to your computer and use it in GitHub Desktop.
Save akueisara/d7329d79630c709346ffceafd211ded2 to your computer and use it in GitHub Desktop.
import android.view.View
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.testing.FragmentScenario
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.IdlingResource
import java.util.*
// A custom idling resource for data binding
class DataBindingIdlingResource : IdlingResource {
// List of registered callbacks
private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()
// Give it a unique id to work around an Espresso bug where you cannot register/unregister
// an idling resource with the same name.
private val id = UUID.randomUUID().toString()
// Holds whether isIdle was called and the result was false. We track this to avoid calling
// onTransitionToIdle callbacks if Espresso never thought we were idle in the first place.
private var wasNotIdle = false
lateinit var activity: FragmentActivity
override fun getName() = "DataBinding $id"
override fun isIdleNow(): Boolean {
val idle = !getBindings().any { it.hasPendingBindings() }
@Suppress("LiftReturnOrAssignment")
if (idle) {
if (wasNotIdle) {
// Notify observers to avoid Espresso race detector.
idlingCallbacks.forEach { it.onTransitionToIdle() }
}
wasNotIdle = false
} else {
wasNotIdle = true
// Check next frame.
activity.findViewById<View>(android.R.id.content).postDelayed({
isIdleNow
}, 16)
}
return idle
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) {
idlingCallbacks.add(callback)
}
/**
* Find all binding classes in all currently available fragments.
*/
private fun getBindings(): List<ViewDataBinding> {
val fragments = (activity as? FragmentActivity)
?.supportFragmentManager
?.fragments
val bindings =
fragments?.mapNotNull {
it.view?.getBinding()
} ?: emptyList()
val childrenBindings = fragments?.flatMap { it.childFragmentManager.fragments }
?.mapNotNull { it.view?.getBinding() } ?: emptyList()
return bindings + childrenBindings
}
}
private fun View.getBinding(): ViewDataBinding? = DataBindingUtil.getBinding(this)
/**
* Sets the activity from an [ActivityScenario] to be used from [DataBindingIdlingResource].
*/
fun DataBindingIdlingResource.monitorActivity(
activityScenario: ActivityScenario<out FragmentActivity>
) {
activityScenario.onActivity {
this.activity = it
}
}
/**
* Sets the fragment from a [FragmentScenario] to be used from [DataBindingIdlingResource].
*/
fun DataBindingIdlingResource.monitorFragment(fragmentScenario: FragmentScenario<out Fragment>) {
fragmentScenario.onFragment {
this.activity = it.requireActivity()
}
}
import androidx.test.espresso.idling.CountingIdlingResource
// Espresso idling resources are a synchronization mechanism for Espresso and your long running operations.
object EspressoIdlingResource {
private const val RESOURCE = "GLOBAL"
// allows you to increment or decrement the counter
// counter >= 0, the app is working
// counter < 0, the app is idle
@JvmField
val countingIdlingResource = CountingIdlingResource(RESOURCE)
fun increment() {
countingIdlingResource.increment()
}
fun decrement() {
if (!countingIdlingResource.isIdleNow) {
countingIdlingResource.decrement()
}
}
}
inline fun <T> wrapEspressoIdlingResource(function: () -> T): T {
// Espresso does not work well with coroutines yet. See
// https://github.com/Kotlin/kotlinx.coroutines/issues/982
EspressoIdlingResource.increment() // Set app as busy.
return try {
function()
} finally {
EspressoIdlingResource.decrement() // Set app as idle.
}
}
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.*
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import com.example.android.architecture.blueprints.todoapp.util.DataBindingIdlingResource
import com.example.android.architecture.blueprints.todoapp.util.EspressoIdlingResource
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers.not
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
@LargeTest
class MyActivityTest {
// An idling resource that waits for Data Binding to have no pending bindings.
private val dataBindingIdlingResource = DataBindingIdlingResource()
/**
* Idling resources tell Espresso that the app is idle or busy. This is needed when operations
* are not scheduled in the main Looper (for example when executed on a different thread).
* In other words, Expresso will wait when the app is busy (the counter is greater than 0)
*/
@Before
fun registerIdlingResource() {
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
IdlingRegistry.getInstance().register(dataBindingIdlingResource)
}
/**
* Unregister your Idling Resource so it can be garbage collected and does not leak any memory.
*/
@After
fun unregisterIdlingResource() {
IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
IdlingRegistry.getInstance().unregister(dataBindingIdlingResource)
}
@Test
fun Test1() {
// Set initial state.
// repository code ...
// Start up Tasks screen.
val activityScenario = ActivityScenario.launch(MyActivity::class.java)
dataBindingIdlingResource.monitorActivity(activityScenario) // LOOK HERE
// Espresso code will go here.
// Verify the UI result
// ....
// Make sure the activity is closed before resetting the db:
activityScenario.close()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment