Praveen P.
Praveen P.

Reputation: 1106

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
    }
}

Upvotes: 0

Views: 89

Answers (1)

gosr
gosr

Reputation: 4708

The way I normally do it is to use another app module and put the UI tests there. For example, if app module depends of lib module, put the UI tests in app module.

But you could also create another separate "app lib testing" module (which would be an additional app type module), which would have the single responsibility of testing your library module.

I am not aware of more elegant solutions.

Upvotes: 2

Related Questions