Reputation: 1106
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
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