samedi 3 octobre 2020

How to UI test a Custom Layout in a library module that doesn't have any Activities or Fragments?

I'd line to UI test a Custom Layout in a library module that doesn't have any Activities or Fragments. Is that even possible? All the information I can find about UI testing with Espresso needs an ActivityScenario or Fragment scenario, which I don't have in this case.

Here's the code of my Custom layout class, if that helps


class BuffView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
    : LinearLayout(context, attrs, defStyleAttr) {

    private val buffView: LinearLayout = inflate(context, R.layout.buff_view, this) as LinearLayout

    private var errorListener: ErrorListener? = null

    private val apiErrorHandler = ApiErrorHandler()
    private val getBuffUseCase = GetBuffUseCase(apiErrorHandler)
    /**
     * Handler for the time intervals between requests
     */
    private val intervalsHandler = Handler()

    private var countDownTimer: CountDownTimer? = null

    private var buffIdCount = 1
    /**
     * Indicates whether the API request should be made or not
     */
    private var getBuffs = false

    fun init() {
        getBuffs = true
        getBuff()
    }

    private fun getBuff() {
        if (!getBuffs) return
        getBuffUseCase.invoke(Params(buffIdCount.toLong()), object : UseCaseResponse<Buff> {
            override fun onSuccess(result: Buff) {
                if (isDataValid(result)) displayBuff(result) else hideBuffView()
            }

            override fun onError(errorModel: ErrorModel?) {
                errorListener?.onError(
                    errorModel?.message ?: context.getString(R.string.generic_error_message)
                )
                hideBuffView()
            }
        })

        if (buffIdCount < NUM_BUFFS_TO_BE_FETCHED ) {
            intervalsHandler.postDelayed({
                buffIdCount++
                getBuff()
                stopCountDownTimer()
            }, REQUEST_BUFF_INTERVAL_TIME)
        }
    }

    private fun isDataValid(buff: Buff): Boolean {
        if (buff.author.firstName.isNullOrEmpty() && buff.author.lastName.isNullOrEmpty()) {
            showErrorInvalidData(context.getString(R.string.author_reason_error_message))
            return false
        }

        if (buff.question == null || buff.question.title.isNullOrEmpty()) {
            showErrorInvalidData(context.getString(R.string.question_reason_error_message))
            return false
        }

        if (buff.timeToShow == null || buff.timeToShow < 0) {
            showErrorInvalidData(context.getString(R.string.timer_reason_error_message))
            return false
        }

        if (buff.answers == null || buff.answers.size < 2) {
            showErrorInvalidData(context.getString(R.string.answers_reason_error_message))
            return false
        }
        return true
    }

    private fun showErrorInvalidData(reason: String) {
        errorListener?.onError(context.getString(R.string.data_incomplete_error_message, reason))
    }

    private fun displayBuff(buff: Buff) {
        setBuffQuestion(buff.question!!.title!!)
        setBuffAuthor(buff.author)
        setBuffAnswers(buff.answers!!)
        setBuffProgressBar(buff.timeToShow!!)
        setBuffCloseButton()
        invalidate()
        showBuffView()
    }

    private fun setBuffQuestion(question: String) {
        questionText.text = question
    }

    private fun setBuffAuthor(author: Buff.Author) {
        val firstName = author.firstName ?: ""
        val lastName = author.lastName ?: ""
        val fullName = "$firstName $lastName"
        authorName.text = fullName

        author.image?.let { authorImageUrl ->
            Glide.with(context)
                .load(authorImageUrl)
                .into(authorImage)
        }
    }

    /**
     * Adds a new answerView for each element in answers
     * */
    private fun setBuffAnswers(answers: List<Buff.Answer>) {
        val answersContainer = findViewById<LinearLayout>(R.id.answersContainer)
        answersContainer.removeAllViews()
        for(answer in answers) {
            val answerView: View = LayoutInflater.from(answersContainer.context).inflate(
                R.layout.buff_answer,
                answersContainer,
                false
            )

            answer.answerImage?.x0?.url?.let {
                Glide.with(context)
                    .load(it)
                    .into(answerView.answerImage)
            }

            answerView.setOnClickListener {
                answerView.background = ContextCompat.getDrawable(
                    context,
                    R.drawable.answer_selected_bg
                )
                answerView.answerText.setTextColor(
                    ContextCompat.getColor(
                        context,
                        android.R.color.white
                    )
                )
                //freeze timer on answer selected
                stopCountDownTimer()

                //hide buff_content view 2 seconds after an answer has been selected
                it.postDelayed({
                    hideBuffView()
                }, HIDE_BUFF_AFTER_SELECTED_ANSWER_DURATION)
            }

            answerView.answerText?.text = answer.title
            answersContainer.addView(answerView)
        }
    }

    private fun setBuffProgressBar(timeToShow: Int) {
        questionTimeProgress.max = timeToShow
        countDownTimer = object : CountDownTimer(
            timeToShow * ONE_SECOND_INTERVAL,
            ONE_SECOND_INTERVAL
        ) {
            override fun onTick(millisUntilFinished: Long) {
                questionTimeText.text = (millisUntilFinished / ONE_SECOND_INTERVAL).toString()
                questionTimeProgress.progress = timeToShow - (millisUntilFinished / ONE_SECOND_INTERVAL).toInt()
            }

            override fun onFinish() {
                hideBuffView()
            }
        }.start()
    }

    private fun showBuffView() {
        buffView.visibility = VISIBLE
        val entryAnimation = AnimationUtils.loadAnimation(context, R.anim.entry_anim)
        buffView.startAnimation(entryAnimation)
    }

    private fun hideBuffView() {
        buffView.visibility = GONE
        val exitAnimation = AnimationUtils.loadAnimation(context, R.anim.exit_anim)
        buffView.startAnimation(exitAnimation)
    }

    private fun setBuffCloseButton() {
        buffCloseImageButton.setOnClickListener {
            hideBuffView()
            stopCountDownTimer()
        }
    }

    private fun stopCountDownTimer() {
        countDownTimer?.cancel()
    }

    fun addErrorListener(errorListener: ErrorListener) {
        this.errorListener = errorListener
    }
}

Aucun commentaire:

Enregistrer un commentaire