espresso how do you check if a dialog isn't visible - android

I have a test that checks to see if a dialog is present or not.
#Test
fun dismissedWhenClicked() {
//dimiss dialog
onView(withText(R.string.simple)).inRoot(isDialog()).perform(click())
//check dialog
onView(isRoot()).inRoot(isDialog()).check(matches(not(isDisplayed())))
}
above is my best guess, but fails because Matcher 'is dialog' did not match any of the following roots
i have found 3 questions on here that address it but none seem to solve it.
Espresso check if no dialog is displayed - the comment works but it also passes when there is a dialog
Check the dialog is visible - Espresso - this doesn't check, instead it will just fail gracefully, i think.
espresso: Assert a Dialog is not shown - seems to have no answer.

I have solved this with a custom matcher modified slightly from here
#Test
fun dismissedWhenClicked() {
onView(withText(R.string.simple)).inRoot(isDialog()).perform(click())
onView(withId(R.id.fragment_layout)).inRoot(Utils.ActivityMatcher()).check(matches(isDisplayed()))
}
class ActivityMatcher : TypeSafeMatcher<Root>() {
override fun describeTo(description: Description) {
description.appendText("is activity")
}
public override fun matchesSafely(root: Root): Boolean {
val type: Int = root.windowLayoutParams.get().type
if (type == WindowManager.LayoutParams.TYPE_BASE_APPLICATION) {
val windowToken: IBinder = root.decorView.windowToken
val appToken: IBinder = root.decorView.applicationWindowToken
if (windowToken === appToken) {
//means this window isn't contained by any other windows.
return true
}
}
return false
}
}

Related

Android Permissions Helper Functions

I have an activity that requires camera permission.
this activity can be called from several user configurable places in the app.
The rationale dialog and permission dialog themselves should be shown before the activity opens.
right now I am trying to handle these dialogs in some kind of extension function.
fun handlePermissions(context: Context, required_permissions: Array<String>, activity: FragmentActivity?, fragment: Fragment?): Boolean {
var isGranted = allPermissionsGranted(context, required_permissions)
if (!isGranted) {
//null here is where I used to pass my listener which was the calling fragment previously that implemented an interface
val dialog = DialogPermissionFragment(null, DialogPermissionFragment.PermissionType.QR)
activity?.supportFragmentManager?.let { dialog.show(it, "") }
//get result from dialog? how?
//if accepted launch actual permission request
fragment?.registerForActivityResult(ActivityResultContracts.RequestPermission()) { success ->
isGranted = success
}?.launch(android.Manifest.permission.CAMERA)
}
return isGranted
}
But I am having trouble to get the dialog results back from the rationale/explanation dialog.
My research until now brought me to maybe using a higher order function, to pass a function variable to the dialog fragment that returns a Boolean value to the helper function. But I am absolutely unsure if thats the right thing.
I thought using my own code would be cleaner and less overhead, could I achieve this easier when using a framework like eazy-permissions? (is Dexter still recommendable since its no longer under development)
is that even a viable thing I am trying to achieve here?
One approach that I've implemented and seems viable to use is this:
Class PermissionsHelper
class PermissionsHelper(private val activity: Activity) {
fun requestPermissionsFromDevice(
arrayPermissions: Array<String>, permissionsResultInterface: PermissionsResultInterface
) {
setPermissionResultInterface(permissionsResultInterface)
getMyPermissionRequestActivity().launch(arrayPermissions)
}
fun checkPermissionsFromDevice(permission: String): Boolean {
return ContextCompat.checkSelfPermission(activity, permission) ==
PackageManager.PERMISSION_GRANTED
}
}
Class PermissionsResultInterface to the helper class be able to communicate with the activity.
interface PermissionsResultInterface {
fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>)
}
Usage with this approach to remove all files from app directory:
private fun clearFiles(firstCall: Boolean = false) {
if (verifyStoragePermissions(firstCall)) {
val dir = File(getExternalFilesDir(null).toString())
removeFileOrDirectory(dir)
Toast.makeText(
applicationContext,
resources.getString(R.string.done),
Toast.LENGTH_SHORT
).show()
}
}
private fun verifyStoragePermissions(firstCall: Boolean = false): Boolean {
val arrayListPermissions = arrayOf(
android.Manifest.permission.READ_EXTERNAL_STORAGE,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
for (permission in arrayListPermissions) {
if (!PermissionsHelper(this).checkPermissionsFromDevice(permission)) {
if (firstCall) PermissionsHelper(this)
.requestPermissionsFromDevice(arrayListPermissions, this)
else PermissionsDialogs(this).showPermissionsErrorDialog()
return false
}
}
return true
}
override fun onPermissionFinishResult(permissions: MutableMap<String, Boolean>) {
clearFiles()
}
With this approach you are able to call the permissions helper and using the result interface, after each of the answers from user, decide wether you still need to make a call for permissions or show a dialog to him.
If you need any help don't hesitate to contact me.

Logging in with Room Library

So far in my android app I have been using Shared Preferences for storing local data. Now, I am working on replacing it with Room Persistance Library.
The thing that I am trying to accomplish here is quite simple, yet I am having some difficulty in achieving desired result.
Here is how my Logged In check works with Shared Preferences:
SplashScreenActivity:
if (splashViewModel.isUserLoggedIn()) {
startActivity(Intent(this, ProfileScreenActivity::class.java))
} else {
startActivity(Intent(this, LoginScreenActivity::class.java))
}
SplashViewModel:
fun isUserLoggedIn(): Boolean {
return MainRepository.isUserLoggedIn()
}
MainRepository:
fun isUserLoggedIn(): Boolean {
return MySharedPreferences.isUserLoggedIn()
}
MySharedPreferences:
fun isUserLoggedIn(): Boolean {
return preferences.getBoolean(ID_USER_LOGGED_IN, false)
}
As you can see - pretty simple and straightforward.
I was able to achieve similar thing with Room and LiveData, however, my implementation results in weird lag with SplashScreen and I think there might be something simpler when it comes to this.
Here is my Room & LiveData implementation:
SplashScreenActivity:
splashViewModel.isUserLoggedInRoom()
splashViewModel.isLoggedIn.observe(this, {
if (it == true) {
startActivity(Intent(this, ProfileScreenActivity::class.java))
} else if (it == false) {
startActivity(Intent(this, LoginScreenActivity::class.java))
}
})
SplashViewModel:
private val _isLoggedIn: MutableLiveData<Boolean> = MutableLiveData()
val isLoggedIn: LiveData<Boolean> = _isLoggedIn
fun isUserLoggedInRoom() {
viewModelScope.launch(IO) {
val response = MainRepository.isUserLoggedInRoom()
withContext(Main) {
_isLoggedIn.value = response
}
}
}
MainRepository:
fun isUserLoggedInRoom(): Boolean {
return loginInfoDao.isLoggedIn()
}
LoginInfoDao:
#Query("SELECT isLoggedIn FROM LoginInfo WHERE id=$LoginInfoID")
fun isLoggedIn(): Boolean
I would really appreciate if you could point me in the right direction here. Thanks!
Solution to this was simple: Build Release version of the apk and run it instead of debug one.

How to disable Spring Animation for Espresso Tests?

I'm using the Android spring animation in my project (see here). However, these animations are getting in the way of my espresso tests.
I already tried to disable these animations using the developer options in the phone, but they seem to not be affected by these settings.
Is there any way how I can disable them just for tests?
After struggling with a flaky test due to SpringAnimations I came up with three solutions:
Solution 1: Add a function that wraps creating your SpringAnimations
This is the most invasive in terms of changing existing code, but least complex method to follow:
You can check if animations are disabled at runtime:
fun animationsDisabled() =
Settings.Global.getFloat(
contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f,
) == 0.0f
Then selectively return a dummy animation that immediately finishes while also setting the value to it's final state:
fun <K : View?> createAnimation(
target: K,
property: FloatPropertyCompat<K>,
finalValue: Float
) = if (animationsDisabled() == false) {
SpringAnimation(target, property, finalValue).apply {
spring.dampingRatio = dampingRatio
spring.stiffness = stiffness
}
} else {
property.setValue(target, finalValue)
SpringAnimation(FloatValueHolder(0f)).apply{
spring = SpringForce(100f)
spring.dampingRatio = dampingRatio
spring.stiffness = stiffness
addUpdateListener { _, _, _ -> skipToEnd() }
}
}
}
Solution 2: Create an IdlingResource that tells Espresso if a DynamicAnimation is running
SpringAnimation and FlingAnimation both extend from DynamicAnimation, the class which is ignoring the system Animation Scale and causing issues here.
This solution isn't the prettiest as it uses reflection, but the implementation details it relies on haven't changed since DynamicAnimation was introduced.
Based on DataBindingIdlingResource:
import android.view.View
import androidx.dynamicanimation.animation.DynamicAnimation
import androidx.test.espresso.IdlingResource
import androidx.test.ext.junit.rules.ActivityScenarioRule
import java.util.UUID
// An espresso idling resource implementation that reports idle status for all DynamicAnimation instances
class DynamicAnimationIdlingResource(private val activityScenarioRule: ActivityScenarioRule<*>) :
IdlingResource {
// list of registered callbacks
private val idlingCallbacks = mutableListOf<IdlingResource.ResourceCallback>()
// give it a unique id to workaround an espresso bug where you cannot register/unregister
// an idling resource w/ the same name.
private val id = UUID.randomUUID().toString()
// holds whether isIdle is 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
override fun getName() = "DynamicAnimation $id"
override fun isIdleNow(): Boolean {
val idle = !getDynamicAnimations().any { it.isRunning }
#Suppress("LiftReturnOrAssignment")
if (idle) {
if (wasNotIdle) {
// notify observers to avoid espresso race detector
idlingCallbacks.forEach { it.onTransitionToIdle() }
}
wasNotIdle = false
} else {
wasNotIdle = true
activityScenarioRule.scenario.onActivity {
it.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 getDynamicAnimations(): List<DynamicAnimation<*>> {
val dynamicAnimations = mutableListOf<DynamicAnimation<*>>()
val animationHandlerClass = Class
.forName("androidx.dynamicanimation.animation.AnimationHandler")
val animationHandler =
animationHandlerClass
.getDeclaredMethod("getInstance")
.invoke(null)
val animationCallbacksField =
animationHandlerClass
.getDeclaredField("mAnimationCallbacks").apply {
isAccessible = true
}
val animationCallbacks =
animationCallbacksField.get(animationHandler) as ArrayList<*>
animationCallbacks.forEach {
if (it is DynamicAnimation<*>) {
dynamicAnimations.add(it)
}
}
return dynamicAnimations
}
}
For convenience a matching test rule:
/**
* A JUnit rule that registers an idling resource for all animations that use DynamicAnimations.
*/
class DynamicAnimationIdlingResourceRule(activityScenarioRule: ActivityScenarioRule<*>) : TestWatcher() {
private val idlingResource = DynamicAnimationIdlingResource(activityScenarioRule)
override fun finished(description: Description?) {
IdlingRegistry.getInstance().unregister(idlingResource)
super.finished(description)
}
override fun starting(description: Description?) {
IdlingRegistry.getInstance().register(idlingResource)
super.starting(description)
}
}
This isn't the perfect solution since it will still cause your tests to wait for animations despite changing the animation scale globally
If you have infinite animations based on SpringAnimations (by setting Damping to zero), this won't work as it'll always report to Espresso that an animation is running. You could work around that by casting the DynamicAnimation to a SpringAnimation and checking if Damping was set, but I felt like that's a rare enough case to not complicate things.
Solution 3: Force all SpringAnimations to skip to their last frame
Another reflection based solution, but this one completely disables the SpringAnimations. The trade-off is that theoretically Espresso can still try to interact in the 1 frame window between a SpringAnimation being asked to end, and it actually ending.
In practice I had to rerun the test hundreds of times in a row to get this to happen, at which point the animation may not even be the source of flakiness. So the trade-off is probably worth it if the animations are dragging down how long your tests take to complete:
private fun disableSpringAnimations() {
val animationHandlerClass = Class
.forName("androidx.dynamicanimation.animation.AnimationHandler")
val animationHandler =
animationHandlerClass
.getDeclaredMethod("getInstance")
.invoke(null)
val animationCallbacksField =
animationHandlerClass
.getDeclaredField("mAnimationCallbacks").apply {
isAccessible = true
}
CoroutineScope(Dispatchers.IO).launch {
while (true) {
withContext(Dispatchers.Main) {
val animationCallbacks =
animationCallbacksField.get(animationHandler) as ArrayList<*>
animationCallbacks.forEach {
val animation = it as? SpringAnimation
if (animation?.isRunning == true && animation.canSkipToEnd()) {
animation.skipToEnd()
animation.doAnimationFrame(100000L)
}
}
}
delay(16L)
}
}
}
Call this method in your #Before annotated function to have it run before each test.
In the SpringAnimation implementation, skipToEnd sets a flag that is not checked until the next call to doAnimationFrame, hence the animation.doAnimationFrame(100000L) call.

Anko doAsyncResult coroutines

I am new to anko and coroutines so excuse me if I am asking something trivial :)
So what I am trying to do is have the user click a button and then I want to download a JSON from the internet, store it locally and parse it. Since both operations can take considerable time I thought to use anko coroutines.
So first question is:
1. Can I use nested doAsync calls, calling the 2nd doAsync in the UIThread of the first one?
I tried it and it seems to work but it feels wrong so I was trying to find a more elegant way
Example:
doAsync {
downloadFileFromUrl(fileUrl)
uiThread {
doAsync {
IOUtils.parseFile(context!!)
val database = AppDatabase.getInstance(context!!)
val results = database.resultsDao().all
uiThread {
//show Results
}
}
}
}
2. While searching a solution for my problem I found doAsyncResult. If 1 it's not correct, is this is the correct approach? I tried already to use it but with Boolean I get errors. See below:
private fun downloadFileFromUrl(fileUrl: String): Boolean {
try{
//Download file. No doAsync calls here.
//The procedure just returns true if successful or false in case of any errors
return true
} catch (e: Exception) {
Log.e("Error: ", e.message)
return false
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
parseButton.setOnClickListener {
try {
val downloadFileResult: (AnkoAsyncContext<Boolean>.() -> Boolean) = {
::downloadFileFromUrl.invoke(fileUrl)
}
val downloadFileResultFutureValue: Future<Boolean> = doAsyncResult(null, downloadFileResult)
//Continue processing if downloadFileResultFutureValue is true
} catch (e: IOException) {
e.printStackTrace()
}
}
}
This line
val downloadFileResultFutureValue: Future<Boolean> = doAsyncResult(null, downloadFileResult)
does not compile with the following error which I don't understand how to fix:
Type inference failed: Cannot infer type parameter T in
fun <T, R> T.doAsyncResult
(
exceptionHandler: ((Throwable) → Unit)? = ...,
task: AnkoAsyncContext<T>.() → R
)
: Future<R>
None of the following substitutions
receiver: Boolean
arguments:
(
((Throwable) → Unit)?,
AnkoAsyncContext<Boolean>.() → Boolean
)
receiver: BlankFragment
arguments:
(
((Throwable) → Unit)?,
AnkoAsyncContext<BlankFragment>.() → Boolean
)
can be applied to
receiver: BlankFragment
arguments:
(
Nothing?,
AnkoAsyncContext<Boolean>.() → Boolean
)
Thanks in advance
Doing this:
doAsync {
// 1. Something
uiThread {
// 2. Nothing
doAsync {
Indeed doesn't make much sense, unless (2) is not nothing, and you just omitted some code.
If you didn't, you can just stay with this version:
doAsync {
downloadFileFromUrl(fileUrl)
IOUtils.parseFile(context!!)
val database = AppDatabase.getInstance(context!!)
val results = database.resultsDao().all
uiThread {
//show Results
}
}
Since parseFile() depends on downloadFileFromUrl() anyway, and everything runs in a coroutine, you don't become more concurrent by adding this back-and-forth.

android snackbar - how to test with roboelectric

From here we now know that robolectric does not have a shadow object but we can create a custom shadow object for a snackbar.It's ashame they have one for toast but not for snackbar.
I am showing a snackbar in my code when there is no network connection. I'd like to know how can i write a unit test (with robolectric as the test runner) that can verify that a snackbar gets shown when there is no network connection.
Its a little hard because the snackbar is not in xml. So when i declare my actually Activity controller it doesn't have a snackbar at that time.
You know how to test a toast we have ShadowToast.getTextOfLatestToast() i want one for snackBar
im currently using org.robolectric:robolectric:3.0-rc2 and dont see ShadowSnackbar.class available.
It's actually explained in the blogpost how to add the ShadowToast class to enable testing.
Add the ShadowSnackbar to your test sources;
Add the Snackbar class as an instrumented class in your custom Gradle test runner;
Add the ShadowSnackbar as a Shadow in your test;
In your app's code, you'll call on the Snackbar when no internet connection is available. Because of the configuration (e.g. intercepting of) the Snackbar as Instrumented class, the Shadow-variant of the class will be used. You'll be able to evaluate the result at that moment.
I posted a much simpler answer
you can just do:
val textView: TextView? = rootView.findSnackbarTextView()
assertThat(textView, `is`(notNullValue()))
Implementation:
/**
* #return a TextView if a snackbar is shown anywhere in the view hierarchy.
*
* NOTE: calling Snackbar.make() does not create a snackbar. Only calling #show() will create it.
*
* If the textView is not-null you can check its text.
*/
fun View.findSnackbarTextView(): TextView? {
val possibleSnackbarContentLayout = findSnackbarLayout()?.getChildAt(0) as? SnackbarContentLayout
return possibleSnackbarContentLayout
?.getChildAt(0) as? TextView
}
private fun View.findSnackbarLayout(): Snackbar.SnackbarLayout? {
when (this) {
is Snackbar.SnackbarLayout -> return this
!is ViewGroup -> return null
}
// otherwise traverse the children
// the compiler needs an explicit assert that `this` is an instance of ViewGroup
this as ViewGroup
(0 until childCount).forEach { i ->
val possibleSnackbarLayout = getChildAt(i).findSnackbarLayout()
if (possibleSnackbarLayout != null) return possibleSnackbarLayout
}
return null
}
this is what worked for me, but it was a very simple use case
#Implements(Snackbar::class)
class CustomShadowSnackbar {
companion object {
val shownSnackbars = mutableListOf<Snackbar>()
fun Snackbar.getTextMessage(): String {
val view = (this.view as ViewGroup)
.children
.first { it is SnackbarContentLayout } as SnackbarContentLayout
return view.messageView.text.toString()
}
fun clear() {
shownSnackbars.clear()
}
}
#RealObject
lateinit var snackbar: Snackbar
#Implementation
fun show() {
shownSnackbars.add(snackbar)
}
#Implementation
fun __constructor__(
context: Context,
parent: ViewGroup,
content: View,
contentViewCallback: ContentViewCallback) {
Shadow.invokeConstructor(
Snackbar::class.java,
snackbar,
ReflectionHelpers.ClassParameter(Context::class.java, context),
ReflectionHelpers.ClassParameter(ViewGroup::class.java, parent),
ReflectionHelpers.ClassParameter(View::class.java, content),
ReflectionHelpers.ClassParameter(ContentViewCallback::class.java, contentViewCallback)
)
}
}

Categories

Resources