Espresso AppNotIdleException when using custom View

Viewed 83

I have a problem when I test my login screen which has custom EditText in it. When I try run the test, the test always loading and nothing happen until I get AppNotIdleException. I'd disabled my system animation when run the test, and finally try remove my custom EditText view and the test running.

Here is my code:

fragment_login.xml


    <?xml version="1.0" encoding="utf-8"?>
    
    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:keep="@layout/fragment_login">
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
    
            <com.airbnb.lottie.LottieAnimationView
                android:id="@+id/lottie_auth"
                android:layout_width="0dp"
                android:layout_height="250dp"
                android:layout_marginStart="32dp"
                android:layout_marginEnd="32dp"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                android:alpha="0"
                app:layout_constraintTop_toBottomOf="@+id/tv_greeting"
                app:lottie_autoPlay="true"
                app:lottie_loop="true"
                app:lottie_rawRes="@raw/watermelon_lottie" />
    
            <TextView
                android:id="@+id/tv_greeting"
                style="@style/TextAppearance.StoryApp.Headline"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="20dp"
                android:alpha="0"
                android:layout_marginTop="32dp"
                android:layout_marginEnd="20dp"
                android:text="@string/greeting2"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    
            <TextView
                android:id="@+id/tv_login"
                style="@style/TextAppearance.StoryApp.Title"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="20dp"
                android:alpha="0"
                android:layout_marginTop="16dp"
                android:text="@string/login"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/lottie_auth" />
    
            <com.ekotyoo.storyapp.ui.EmailEditText
                android:id="@+id/et_email"
                android:layout_width="0dp"
                android:layout_height="50dp"
                android:layout_marginStart="20dp"
                android:layout_marginTop="16dp"
                android:layout_marginEnd="20dp"
                android:alpha="0"
                android:inputType="textEmailAddress"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/tv_login" />
    
            <com.ekotyoo.storyapp.ui.PasswordEditText
                android:id="@+id/et_password"
                android:layout_width="0dp"
                android:layout_height="50dp"
                android:layout_marginStart="20dp"
                android:layout_marginTop="16dp"
                android:layout_marginEnd="20dp"
                android:alpha="0"
                android:inputType="textPassword"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/et_email" />
    
            <Button
                android:id="@+id/btn_login"
                style="@style/TextAppearance.StoryApp.Button"
                android:layout_width="0dp"
                android:layout_height="50dp"
                android:layout_marginStart="20dp"
                android:layout_marginTop="32dp"
                android:layout_marginEnd="20dp"
                android:alpha="0"
                android:background="@drawable/bg_button"
                android:text="@string/login"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/et_password" />
    
            <TextView
                android:id="@+id/tv_or_signup"
                style="@style/TextAppearance.StoryApp.Body1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="32dp"
                android:alpha="0"
                android:text="@string/or_signup"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/btn_login" />
    
            <ProgressBar
                android:id="@+id/loading_progress_bar"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:visibility="gone"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    
        </androidx.constraintlayout.widget.ConstraintLayout>
    </ScrollView>

PasswordEditText.kt:


    package com.ekotyoo.storyapp.ui
    
    
    import android.content.Context
    import android.graphics.Canvas
    import android.graphics.drawable.Drawable
    import android.text.method.PasswordTransformationMethod
    import android.util.AttributeSet
    import android.view.MotionEvent
    import android.view.View
    import androidx.appcompat.widget.AppCompatEditText
    import androidx.core.content.ContextCompat
    import androidx.core.widget.doOnTextChanged
    import com.ekotyoo.storyapp.R
    
    class PasswordEditText : AppCompatEditText, View.OnTouchListener {
    
        private val roundedBackground =
            ContextCompat.getDrawable(context, R.drawable.bg_custom_edittext)
        private lateinit var hidePasswordIcon: Drawable
        private lateinit var showPasswordIcon: Drawable
        private var isHidden = true
        private val passwordTransformationMethod = PasswordTransformationMethod.getInstance()
    
        constructor(context: Context) : super(context) {
            init()
        }
    
        constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
            init()
        }
    
        constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
            context,
            attrs,
            defStyleAttr
        ) {
            init()
        }
    
        private fun init() {
            setOnTouchListener(this)
            doOnTextChanged { text, _, _, _ ->
                text?.let {
                    if (it.length < 6 && it.isNotBlank()) setError(
                        context.applicationContext.getString(
                            R.string.password_less_than_6
                        ), null
                    )
                }
            }
            hidePasswordIcon =
                ContextCompat.getDrawable(context, R.drawable.ic_eye_disabled) as Drawable
            showPasswordIcon = ContextCompat.getDrawable(context, R.drawable.ic_eye) as Drawable
            hint = "Password"
            textSize = 14F
            textAlignment = View.TEXT_ALIGNMENT_VIEW_START
            background = roundedBackground
        }
    
        override fun onDraw(canvas: Canvas?) {
            super.onDraw(canvas)
            transformationMethod = if (isHidden) {
                showPasswordVisible()
                passwordTransformationMethod
            } else {
                showPasswordHidden()
                null
            }
        }
    
        private fun setDrawables(
            left: Drawable? = null,
            top: Drawable? = null,
            right: Drawable? = null,
            bottom: Drawable? = null,
        ) {
            setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom)
        }
    
        private fun showPasswordVisible() {
            setDrawables(null, null, null, null)
            setDrawables(null, null, showPasswordIcon, null)
        }
    
        private fun showPasswordHidden() {
            setDrawables(null, null, null, null)
            setDrawables(null, null, hidePasswordIcon, null)
        }
    
        override fun onTouch(view: View?, event: MotionEvent): Boolean {
            if (compoundDrawables[2] != null) {
                val toggleButtonStart: Float
                val toggleButtonEnd: Float
                var isToggleButtonClicked = false
    
                if (layoutDirection == View.LAYOUT_DIRECTION_RTL) {
                    toggleButtonEnd = (hidePasswordIcon.intrinsicWidth + paddingStart).toFloat()
                    if (event.x < toggleButtonEnd) isToggleButtonClicked = true
                } else {
                    toggleButtonStart = (width - paddingEnd - hidePasswordIcon.intrinsicWidth).toFloat()
                    if (event.x > toggleButtonStart) isToggleButtonClicked = true
                }
    
                if (isToggleButtonClicked) {
                    when (event.action) {
                        MotionEvent.ACTION_DOWN -> {
                            hidePasswordIcon = ContextCompat.getDrawable(
                                context,
                                R.drawable.ic_eye_disabled
                            ) as Drawable
                            showPasswordIcon =
                                ContextCompat.getDrawable(context, R.drawable.ic_eye) as Drawable
                            isHidden = !isHidden
                            return true
                        }
                    }
                }
            }
            return false
        }
    }

LoginFragment.kt


    package com.ekotyoo.storyapp.ui.login
    
    
    import android.animation.AnimatorSet
    import android.animation.ObjectAnimator
    import android.os.Bundle
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import androidx.core.widget.doAfterTextChanged
    import androidx.fragment.app.Fragment
    import androidx.fragment.app.viewModels
    import androidx.lifecycle.lifecycleScope
    import androidx.navigation.fragment.findNavController
    import com.ekotyoo.storyapp.R
    import com.ekotyoo.storyapp.databinding.FragmentLoginBinding
    import com.ekotyoo.storyapp.utils.Utils
    import com.ekotyoo.storyapp.utils.ViewModelFactory
    import kotlinx.coroutines.flow.MutableStateFlow
    import kotlinx.coroutines.flow.collect
    import kotlinx.coroutines.flow.combine
    import kotlinx.coroutines.launch
    
    class LoginFragment : Fragment() {
    
        private var _binding: FragmentLoginBinding? = null
        private val binding get() = _binding!!
    
        private val viewModel: LoginViewModel by viewModels {
            ViewModelFactory.getInstance(requireContext())
        }
    
        override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?
        ): View {
            _binding = FragmentLoginBinding.inflate(layoutInflater, container, false)
            return binding.root
        }
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            setupView()
            playAnimation()
            observeViewModel()
        }
    
        private fun setupView() {
            binding.btnLogin.setOnClickListener {
                val email = binding.etEmail.text.toString().trim()
                val password = binding.etPassword.text.toString().trim()
    
                viewModel.login(email, password)
            }
    
            binding.tvOrSignup.setOnClickListener {
                findNavController().navigate(R.id.action_loginFragment_to_signupFragment)
            }
    
            validateForm()
        }
    
        private fun validateForm() {
            val email = MutableStateFlow("")
            val password = MutableStateFlow("")
    
            with(binding) {
                etEmail.doAfterTextChanged { text -> email.value = text.toString().trim() }
                etPassword.doAfterTextChanged { text -> password.value = text.toString().trim() }
            }
    
            lifecycleScope.launch {
                combine(email, password) { e, p ->
                    Utils.validateEmail(e) && Utils.validatePassword(p)
                }.collect { isValid ->
                    binding.btnLogin.isEnabled = isValid
                }
            }
        }
    
        private fun playAnimation() {
            val greeting = ObjectAnimator.ofFloat(binding.tvGreeting, View.ALPHA, 1F).setDuration(500L)
            val lottie = ObjectAnimator.ofFloat(binding.lottieAuth, View.ALPHA, 1f).setDuration(500L)
            val loginAlpha = ObjectAnimator.ofFloat(binding.tvLogin, View.ALPHA, 1F).setDuration(500L)
            val loginTranslateX =
                ObjectAnimator.ofFloat(binding.tvLogin, View.TRANSLATION_X, -100F, 0F).setDuration(500L)
            val etEmailAlpha = ObjectAnimator.ofFloat(binding.etEmail, View.ALPHA, 1F).setDuration(500L)
            val etEmailTranslateX =
                ObjectAnimator.ofFloat(binding.etEmail, View.TRANSLATION_X, -100F, 0F).setDuration(500L)
            val etPassAlpha =
                ObjectAnimator.ofFloat(binding.etPassword, View.ALPHA, 1F).setDuration(500L)
            val etPassTranslateX =
                ObjectAnimator.ofFloat(binding.etPassword, View.TRANSLATION_X, -100F, 0F)
                    .setDuration(500L)
            val btnLoginAlpha =
                ObjectAnimator.ofFloat(binding.btnLogin, View.ALPHA, 1F).setDuration(500L)
            val btnLoginTranslateX =
                ObjectAnimator.ofFloat(binding.btnLogin, View.TRANSLATION_X, -100F, 0F)
                    .setDuration(500L)
            val orSignup = ObjectAnimator.ofFloat(binding.tvOrSignup, View.ALPHA, 1F).setDuration(500L)
    
            val login = AnimatorSet().apply { playTogether(loginAlpha, loginTranslateX) }
            val email = AnimatorSet().apply { playTogether(etEmailAlpha, etEmailTranslateX) }
            val password = AnimatorSet().apply { playTogether(etPassAlpha, etPassTranslateX) }
            val btnLogin = AnimatorSet().apply { playTogether(btnLoginAlpha, btnLoginTranslateX) }
    
            AnimatorSet().apply {
                playSequentially(greeting, lottie, login, email, password, btnLogin, orSignup)
                start()
            }
        }
    
        private fun observeViewModel() {
            viewModel.userModel.observe(viewLifecycleOwner) { userModel ->
                if (userModel?.isLoggedIn == true) {
                    findNavController().navigate(R.id.action_loginFragment_to_homeFragment)
                }
            }
    
            viewModel.errorMessage.observe(viewLifecycleOwner) { message ->
                Utils.showToast(requireContext(), message)
            }
    
            viewModel.isLoading.observe(viewLifecycleOwner) { state ->
                showLoading(state)
            }
        }
    
        private fun showLoading(isLoading: Boolean) {
            binding.loadingProgressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
        }
    
        override fun onDestroyView() {
            super.onDestroyView()
            _binding = null
        }
    }

MainActivityTest.kt


    package com.ekotyoo.storyapp.ui
    
    
    
    
    import androidx.recyclerview.widget.RecyclerView
    import androidx.test.espresso.Espresso.onView
    import androidx.test.espresso.Espresso.pressBack
    import androidx.test.espresso.IdlingRegistry
    import androidx.test.espresso.action.ViewActions.click
    import androidx.test.espresso.action.ViewActions.typeText
    import androidx.test.espresso.assertion.ViewAssertions.matches
    import androidx.test.espresso.contrib.RecyclerViewActions
    import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
    import androidx.test.espresso.matcher.ViewMatchers.withId
    import androidx.test.ext.junit.rules.ActivityScenarioRule
    import androidx.test.ext.junit.runners.AndroidJUnit4
    import androidx.test.filters.LargeTest
    import com.ekotyoo.storyapp.R
    import com.ekotyoo.storyapp.utils.EspressoIdlingResource
    import org.junit.After
    import org.junit.Before
    import org.junit.Rule
    import org.junit.Test
    import org.junit.runner.RunWith
    
    @RunWith(AndroidJUnit4::class)
    @LargeTest
    class MainActivityTest {
    
        @get:Rule
        val activity = ActivityScenarioRule(MainActivity::class.java)
    
        @Before
        fun setUp() {
            IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
        }
    
        @After
        fun tearDown() {
            IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
        }
    
        @Test
        fun load_story() {
            onView(withId(R.id.rv_story)).check(matches(isDisplayed()))
            onView(withId(R.id.rv_story)).perform(
                RecyclerViewActions.scrollToPosition<RecyclerView.ViewHolder>(
                    2
                )
            )
        }
    
        @Test
        fun go_to_detail_story() {
            onView(withId(R.id.rv_story)).check(matches(isDisplayed()))
            onView(withId(R.id.rv_story)).perform(
                RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
                    2,
                    click()
                )
            )
        }
    
        @Test
        fun go_to_map_story() {
            onView(withId(R.id.btn_map)).perform(click())
            onView(withId(R.id.btn_back)).check(matches(isDisplayed()))
            pressBack()
        }
    
        @Test
        fun go_to_post_story() {
            onView(withId(R.id.btn_add_story)).perform(click())
            onView(withId(R.id.btn_change_image)).perform(click())
            pressBack()
            onView(withId(R.id.et_caption)).perform(typeText("Hello World"))
            pressBack()
        }
    
        @Test
        fun open_popup_menu() {
            onView(withId(R.id.btn_menu)).perform(click())
        }
        
        @Test
        fun login() {
            onView(withId(R.id.et_email)).perform(typeText("email@mail.com"))
            onView(withId(R.id.et_password)).perform(typeText("123456"))
            onView(withId(R.id.btn_login)).perform(click())
        }
    }

0 Answers
Related