Issue resolved (#9)

* Issue resolved

Took 2 hours 5 minutes

* Issue resolved with unit tests

Took 1 hour 31 minutes
This commit is contained in:
2022-08-30 23:38:09 +01:00
committed by GitHub
parent 14c45b87ca
commit 4b8f2944cb
27 changed files with 368 additions and 291 deletions

View File

@@ -1,12 +1,7 @@
package com.appttude.h_mal.easycc.data.network.api package com.appttude.h_mal.easycc.data.network.api
import com.appttude.h_mal.easycc.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query

View File

@@ -1,20 +1,14 @@
package com.appttude.h_mal.easycc.data.network.api package com.appttude.h_mal.easycc.data.network.api
import com.appttude.h_mal.easycc.data.network.interceptors.NetworkConnectionInterceptor
import com.appttude.h_mal.easycc.data.network.interceptors.QueryInterceptor
import com.appttude.h_mal.easycc.data.network.response.ResponseObject import com.appttude.h_mal.easycc.data.network.response.ResponseObject
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Response import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
/** /**
* Retrofit Network class to currency api calls * Retrofit Network class to currency api calls
*/ */
interface CurrencyApi : Api{ interface CurrencyApi : Api {
// Get rate from server with arguments passed in Repository // Get rate from server with arguments passed in Repository
@GET("convert?") @GET("convert?")

View File

@@ -5,7 +5,7 @@ import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Inject import javax.inject.Inject
class RemoteDataSource @Inject constructor(){ class RemoteDataSource @Inject constructor() {
fun <Api> buildApi( fun <Api> buildApi(
okkHttpclient: OkHttpClient, okkHttpclient: OkHttpClient,

View File

@@ -5,22 +5,22 @@ import com.appttude.h_mal.easycc.models.CurrencyModelInterface
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class CurrencyResponse( data class CurrencyResponse(
@field:SerializedName("date") @field:SerializedName("date")
val date: String? = null, val date: String? = null,
@field:SerializedName("amount") @field:SerializedName("amount")
val amount: Double? = null, val amount: Double? = null,
@field:SerializedName("rates") @field:SerializedName("rates")
var rates: Map<String, Double>? = null, var rates: Map<String, Double>? = null,
@field:SerializedName("base") @field:SerializedName("base")
val base: String? = null val base: String? = null
) : CurrencyModelInterface { ) : CurrencyModelInterface {
override fun getCurrencyModel(): CurrencyModel { override fun getCurrencyModel(): CurrencyModel {
return CurrencyModel( return CurrencyModel(
base, base,
rates?.iterator()?.next()?.key, rates?.iterator()?.next()?.key,
rates?.iterator()?.next()?.value ?: 0.0 rates?.iterator()?.next()?.value ?: 0.0
) )
} }
} }

View File

@@ -1,16 +1,13 @@
package com.appttude.h_mal.easycc.data.repository package com.appttude.h_mal.easycc.data.repository
import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.data.network.response.ResponseObject
/** /**
* Main entry point for accessing currency data. * Main entry point for accessing currency data.
*/ */
interface Repository { interface Repository {
suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): ResponseObject suspend fun getDataFromApi(fromCurrency: String, toCurrency: String): CurrencyModel
suspend fun getBackupDataFromApi(fromCurrency: String, toCurrency: String): CurrencyResponse
fun getConversionPair(): Pair<String?, String?> fun getConversionPair(): Pair<String?, String?>

View File

@@ -5,10 +5,10 @@ import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.data.network.SafeApiRequest import com.appttude.h_mal.easycc.data.network.SafeApiRequest
import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi
import com.appttude.h_mal.easycc.data.network.api.CurrencyApi import com.appttude.h_mal.easycc.data.network.api.CurrencyApi
import com.appttude.h_mal.easycc.data.network.response.CurrencyResponse
import com.appttude.h_mal.easycc.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider
import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.utils.convertPairsListToString import com.appttude.h_mal.easycc.utils.convertPairsListToString
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
/** /**
@@ -23,17 +23,19 @@ class RepositoryImpl @Inject constructor(
override suspend fun getDataFromApi( override suspend fun getDataFromApi(
fromCurrency: String, fromCurrency: String,
toCurrency: String toCurrency: String
): ResponseObject { ): CurrencyModel {
// Set currency pairs as correct string for api query eg. AUD_GBP return try {
val currencyPair = convertPairsListToString(fromCurrency, toCurrency) // Set currency pairs as correct string for api query eg. AUD_GBP
return responseUnwrap { api.getCurrencyRate(currencyPair) } val currencyPair = convertPairsListToString(fromCurrency, toCurrency)
} responseUnwrap { api.getCurrencyRate(currencyPair) }.getCurrencyModel()
} catch (e: IOException) {
override suspend fun getBackupDataFromApi( responseUnwrap {
fromCurrency: String, backUpApi.getCurrencyRate(
toCurrency: String fromCurrency,
): CurrencyResponse { toCurrency
return responseUnwrap { backUpApi.getCurrencyRate(fromCurrency, toCurrency) } )
}.getCurrencyModel()
}
} }
override fun getConversionPair(): Pair<String?, String?> { override fun getConversionPair(): Pair<String?, String?> {

View File

@@ -1,19 +0,0 @@
package com.appttude.h_mal.easycc.helper
import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.models.CurrencyModelInterface
import javax.inject.Inject
class CurrencyDataHelper @Inject constructor(
val repository: Repository
) {
suspend fun getDataFromApi(from: String, to: String): CurrencyModelInterface {
return try {
repository.getDataFromApi(from, to)
} catch (e: Exception) {
e.printStackTrace()
repository.getBackupDataFromApi(from, to)
}
}
}

View File

@@ -6,7 +6,6 @@ import com.appttude.h_mal.easycc.utils.trimToThree
import javax.inject.Inject import javax.inject.Inject
class WidgetHelper @Inject constructor( class WidgetHelper @Inject constructor(
private val helper: CurrencyDataHelper,
val repository: Repository val repository: Repository
) { ) {
@@ -16,7 +15,7 @@ class WidgetHelper @Inject constructor(
val s1 = pair.first?.trimToThree() ?: return null val s1 = pair.first?.trimToThree() ?: return null
val s2 = pair.second?.trimToThree() ?: return null val s2 = pair.second?.trimToThree() ?: return null
return helper.getDataFromApi(s1, s2).getCurrencyModel() return repository.getDataFromApi(s1, s2)
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return null return null

View File

@@ -3,12 +3,12 @@ package com.appttude.h_mal.easycc.models
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
data class CurrencyObject( data class CurrencyObject(
@SerializedName("id") @SerializedName("id")
var id: String, var id: String,
@SerializedName("fr") @SerializedName("fr")
var fr: String, var fr: String,
@SerializedName("to") @SerializedName("to")
var to: String, var to: String,
@SerializedName("val") @SerializedName("val")
var value: Double var value: Double
) )

View File

@@ -0,0 +1,96 @@
package com.appttude.h_mal.easycc.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.LayoutParams
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import androidx.appcompat.app.AppCompatActivity
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.utils.*
abstract class BaseActivity<V : BaseViewModel> : AppCompatActivity() {
private lateinit var loadingView: View
abstract val viewModel: V?
private var loading: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
configureObserver()
}
/**
* Creates a loading view which to be shown during async operations
*
* #setOnClickListener(null) is an ugly work around to prevent under being clicked during
* loading
*/
private fun instantiateLoadingView() {
loadingView = layoutInflater.inflate(R.layout.progress_layout, null)
loadingView.setOnClickListener(null)
addContentView(loadingView, LayoutParams(MATCH_PARENT, MATCH_PARENT))
loadingView.hide()
}
override fun onStart() {
super.onStart()
instantiateLoadingView()
}
fun <A : AppCompatActivity> startActivity(activity: Class<A>) {
val intent = Intent(this, activity)
startActivity(intent)
}
open fun onStarted() {
loadingView.fadeIn()
loading = true
}
/**
* Called in case of success or some data emitted from the liveData in viewModel
*/
open fun onSuccess(data: Any?) {
loadingView.fadeOut()
loading = false
}
/**
* Called in case of failure or some error emitted from the liveData in viewModel
*/
open fun onFailure(error: String?) {
error?.let { displayToast(it) }
loadingView.fadeOut()
loading = false
}
private fun configureObserver() {
viewModel?.uiState?.observe(this) {
when (it) {
is ViewState.HasStarted -> onStarted()
is ViewState.HasData<*> -> onSuccess(it.data.getContentIfNotHandled())
is ViewState.HasError -> onFailure(it.error.getContentIfNotHandled())
}
}
}
private fun View.fadeIn() = apply {
show()
triggerAnimation(android.R.anim.fade_in) {}
}
private fun View.fadeOut() = apply {
hide()
triggerAnimation(android.R.anim.fade_out) {}
}
override fun onBackPressed() {
if (!loading) super.onBackPressed()
}
}

View File

@@ -0,0 +1,22 @@
package com.appttude.h_mal.easycc.ui
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.appttude.h_mal.easycc.utils.Event
import com.appttude.h_mal.easycc.utils.ViewState
abstract class BaseViewModel : ViewModel() {
open val uiState: MutableLiveData<ViewState> = MutableLiveData()
fun onStart() {
uiState.postValue(ViewState.HasStarted)
}
fun <T : Any> onSuccess(result: T) {
uiState.postValue(ViewState.HasData(Event(result)))
}
protected fun onError(error: String) {
uiState.postValue(ViewState.HasError(Event(error)))
}
}

View File

@@ -9,7 +9,6 @@ import android.view.WindowManager
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ListView import android.widget.ListView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.SearchView
import com.appttude.h_mal.easycc.R import com.appttude.h_mal.easycc.R
/** /**
@@ -18,7 +17,7 @@ import com.appttude.h_mal.easycc.R
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class CustomDialogClass( class CustomDialogClass(
context: Context, context: Context,
private val clickListener: ClickListener val onSelect: (String) -> Unit
) : Dialog(context) { ) : Dialog(context) {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -30,7 +29,6 @@ class CustomDialogClass(
// Keyboard not to overlap dialog // Keyboard not to overlap dialog
window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) window!!.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
// array adapter for list of currencies in R.Strings
val arrayAdapter = val arrayAdapter =
ArrayAdapter.createFromResource( ArrayAdapter.createFromResource(
context, R.array.currency_arrays, context, R.array.currency_arrays,
@@ -52,13 +50,8 @@ class CustomDialogClass(
// interface selection back to calling activity // interface selection back to calling activity
list_view.setOnItemClickListener { adapterView, _, i, _ -> list_view.setOnItemClickListener { adapterView, _, i, _ ->
clickListener.onText(adapterView.getItemAtPosition(i).toString()) onSelect.invoke(adapterView.getItemAtPosition(i).toString())
dismiss() dismiss()
} }
} }
} }
// Interface to handle selection within dialog
interface ClickListener {
fun onText(currencyName: String)
}

View File

@@ -7,18 +7,16 @@ import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.databinding.ActivityMainBinding import com.appttude.h_mal.easycc.databinding.ActivityMainBinding
import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.ui.BaseActivity
import com.appttude.h_mal.easycc.utils.clearEditText import com.appttude.h_mal.easycc.utils.clearEditText
import com.appttude.h_mal.easycc.utils.displayToast
import com.appttude.h_mal.easycc.utils.hideView
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity(), View.OnClickListener { class MainActivity : BaseActivity<MainViewModel>(), View.OnClickListener {
private val viewModel: MainViewModel by viewModels() override val viewModel: MainViewModel by viewModels()
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -29,34 +27,22 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
* Prevent keyboard overlapping views * Prevent keyboard overlapping views
*/ */
window.setSoftInputMode( window.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN or WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
) )
viewModel.initiate(intent.extras) viewModel.initiate(intent.extras)
binding.currencyOne.text = viewModel.rateIdTo binding.currencyOne.text = viewModel.rateIdFrom
binding.currencyTwo.text = viewModel.rateIdFrom binding.currencyTwo.text = viewModel.rateIdTo
setUpListeners() setUpListeners()
setUpObservers()
} }
private fun setUpObservers() { override fun onSuccess(data: Any?) {
viewModel.operationStartedListener.observe(this) { super.onSuccess(data)
binding.progressBar.hideView(false) if (data is CurrencyModel) {
} binding.bottomInsertValues.clearEditText()
viewModel.operationFinishedListener.observe(this) { pair -> binding.topInsertValue.clearEditText()
// hide progress bar
binding.progressBar.hideView(true)
if (pair.first) {
// Operation was successful remove text in EditTexts
binding.bottomInsertValues.clearEditText()
binding.topInsertValue.clearEditText()
} else {
// Display Toast with error message returned from Viewmodel
pair.second?.let { displayToast(it) }
}
} }
} }
@@ -69,25 +55,21 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
} }
private fun showCustomDialog(view: View?) { private fun showCustomDialog(view: View?) {
CustomDialogClass(this, object : ClickListener { CustomDialogClass(this) {
override fun onText(currencyName: String) { (view as TextView).text = it
(view as TextView).text = currencyName viewModel.setCurrencyName(view.tag, it)
viewModel.setCurrencyName(view.tag, currencyName) }.show()
}
}).show()
} }
override fun onClick(view: View?) { override fun onClick(view: View?) {
showCustomDialog(view) showCustomDialog(view)
} }
// Text watcher applied to EditText @topInsertValue
private val textWatcherClass: TextWatcher = object : TextWatcher { private val textWatcherClass: TextWatcher = object : TextWatcher {
override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) { override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) {
// Remove text watcher on other text watcher to prevent infinite loop // Remove text watcher on other text watcher to prevent infinite loop
binding.bottomInsertValues.removeTextChangedListener(textWatcherClass2) binding.bottomInsertValues.removeTextChangedListener(textWatcherClass2)
// Clear any values if current EditText is empty
if (binding.topInsertValue.text.isNullOrEmpty()) if (binding.topInsertValue.text.isNullOrEmpty())
binding.bottomInsertValues.setText("") binding.bottomInsertValues.setText("")
} }
@@ -102,7 +84,6 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
private val textWatcherClass2: TextWatcher = object : TextWatcher { private val textWatcherClass2: TextWatcher = object : TextWatcher {
override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) { override fun onTextChanged(s: CharSequence, st: Int, b: Int, c: Int) {
binding.topInsertValue.removeTextChangedListener(textWatcherClass) binding.topInsertValue.removeTextChangedListener(textWatcherClass)
if (binding.bottomInsertValues.text.isNullOrEmpty()) if (binding.bottomInsertValues.text.isNullOrEmpty())
binding.topInsertValue.clearEditText() binding.topInsertValue.clearEditText()
@@ -115,4 +96,5 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
} }
} }
} }

View File

@@ -1,17 +1,12 @@
package com.appttude.h_mal.easycc.ui.main package com.appttude.h_mal.easycc.ui.main
import android.os.Bundle import android.os.Bundle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.helper.CurrencyDataHelper import com.appttude.h_mal.easycc.ui.BaseViewModel
import com.appttude.h_mal.easycc.utils.toTwoDpString import com.appttude.h_mal.easycc.utils.toTwoDpString
import com.appttude.h_mal.easycc.utils.trimToThree import com.appttude.h_mal.easycc.utils.trimToThree
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@@ -21,60 +16,48 @@ import javax.inject.Inject
*/ */
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
private val currencyDataHelper: CurrencyDataHelper,
private val repository: Repository private val repository: Repository
) : ViewModel() { ) : BaseViewModel() {
private val conversionPairs by lazy { repository.getConversionPair() } private val conversionPairs by lazy { repository.getConversionPair() }
// Viewbinding to textviews in @activity_main.xml // Viewbinded variables
var rateIdFrom: String? = null var rateIdFrom: String? = null
var rateIdTo: String? = null var rateIdTo: String? = null
//operation results livedata based on outcome of operation
val operationStartedListener = MutableLiveData<Boolean>()
val operationFinishedListener = MutableLiveData<Pair<Boolean, String?>>()
private var conversionRate: Double = 1.00 private var conversionRate: Double = 1.00
private fun getExchangeRate() { private fun getExchangeRate() {
operationStartedListener.postValue(true) onStart()
// view binded exchange rates selected null checked
if (rateIdFrom.isNullOrEmpty() || rateIdTo.isNullOrEmpty()) { if (rateIdFrom.isNullOrEmpty() || rateIdTo.isNullOrEmpty()) {
operationFinishedListener.postValue(Pair(false, "Select currencies")) onError("Select both currencies")
return return
} }
// No need to call api as it will return exchange rate as 1
if (rateIdFrom == rateIdTo) { if (rateIdFrom == rateIdTo) {
conversionRate = 1.00 conversionRate = 1.00
operationFinishedListener.postValue(Pair(true, null)) onError("Currency selections are the same")
return return
} }
viewModelScope.launch { viewModelScope.launch {
try { try {
// Non-null assertion (!!) as values have been null checked and have not changed // Non-null assertion (!!) as values have been null checked and have not changed
val exchangeResponse = currencyDataHelper.getDataFromApi( val exchangeResponse = repository.getDataFromApi(
rateIdFrom!!.trimToThree(), rateIdFrom!!.trimToThree(),
rateIdTo!!.trimToThree() rateIdTo!!.trimToThree()
) )
exchangeResponse.getCurrencyModel().let { exchangeResponse.let {
conversionRate = it.rate conversionRate = it.rate
repository.setConversionPair(rateIdFrom!!, rateIdTo!!) repository.setConversionPair(rateIdFrom!!, rateIdTo!!)
operationFinishedListener.postValue(Pair(true, null)) onSuccess(it)
return@launch return@launch
} }
} catch (e: IOException) { } catch (e: IOException) {
e.message?.let { e.message?.let { onError(it) }
operationFinishedListener.postValue(Pair(false, it))
return@launch
}
} }
operationFinishedListener.postValue(Pair(false, "Failed to retrieve rate"))
} }
} }

View File

@@ -7,11 +7,9 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding import com.appttude.h_mal.easycc.databinding.CurrencyAppWidgetConfigureBinding
import com.appttude.h_mal.easycc.ui.main.ClickListener import com.appttude.h_mal.easycc.ui.BaseActivity
import com.appttude.h_mal.easycc.ui.main.CustomDialogClass import com.appttude.h_mal.easycc.ui.main.CustomDialogClass
import com.appttude.h_mal.easycc.utils.displayToast
import com.appttude.h_mal.easycc.utils.transformIntToArray import com.appttude.h_mal.easycc.utils.transformIntToArray
import com.appttude.h_mal.easycc.widget.CurrencyAppWidgetKotlin import com.appttude.h_mal.easycc.widget.CurrencyAppWidgetKotlin
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@@ -20,16 +18,16 @@ import dagger.hilt.android.AndroidEntryPoint
* The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget. * The configuration screen for the [CurrencyAppWidgetKotlin] AppWidget.
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(), class CurrencyAppWidgetConfigureActivityKotlin : BaseActivity<WidgetViewModel>(),
View.OnClickListener { View.OnClickListener {
val viewModel: WidgetViewModel by viewModels() override val viewModel: WidgetViewModel by viewModels()
private lateinit var binding: CurrencyAppWidgetConfigureBinding private lateinit var binding: CurrencyAppWidgetConfigureBinding
private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID private var mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
public override fun onCreate(icicle: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(icicle) super.onCreate(savedInstanceState)
binding = CurrencyAppWidgetConfigureBinding.inflate(layoutInflater) binding = CurrencyAppWidgetConfigureBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -50,10 +48,8 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(),
finish() finish()
return return
} }
viewModel.initiate(mAppWidgetId) viewModel.initiate(mAppWidgetId)
setupObserver()
setupClickListener() setupClickListener()
} }
@@ -63,17 +59,9 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(),
binding.currencyTwo.setOnClickListener(this) binding.currencyTwo.setOnClickListener(this)
} }
private fun setupObserver() { override fun onSuccess(data: Any?) {
viewModel.operationFinishedListener.observe(this) { super.onSuccess(data)
displaySubmitDialog()
// it.first is a the success of the operation
if (it.first) {
displaySubmitDialog()
} else {
// failed operation - display toast with message from it.second
it.second?.let { message -> displayToast(message) }
}
}
} }
override fun onClick(view: View?) { override fun onClick(view: View?) {
@@ -98,12 +86,10 @@ class CurrencyAppWidgetConfigureActivityKotlin : AppCompatActivity(),
private fun showCustomDialog(view: View?) { private fun showCustomDialog(view: View?) {
CustomDialogClass(this, object : ClickListener { CustomDialogClass(this) {
override fun onText(currencyName: String) { (view as TextView).text = it
(view as TextView).text = currencyName viewModel.setCurrencyName(view.tag, it)
viewModel.setCurrencyName(view.tag, currencyName) }.show()
}
}).show()
} }
fun finishCurrencyWidgetActivity() { fun finishCurrencyWidgetActivity() {

View File

@@ -3,8 +3,6 @@ package com.appttude.h_mal.easycc.ui.widget
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import com.appttude.h_mal.easycc.R
import com.appttude.h_mal.easycc.databinding.ActivityMainBinding
import com.appttude.h_mal.easycc.databinding.ConfirmDialogBinding import com.appttude.h_mal.easycc.databinding.ConfirmDialogBinding

View File

@@ -1,8 +1,7 @@
package com.appttude.h_mal.easycc.ui.widget package com.appttude.h_mal.easycc.ui.widget
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.ui.BaseViewModel
import com.appttude.h_mal.easycc.utils.trimToThree import com.appttude.h_mal.easycc.utils.trimToThree
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@@ -10,7 +9,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class WidgetViewModel @Inject constructor( class WidgetViewModel @Inject constructor(
private val repository: Repository private val repository: Repository
) : ViewModel() { ) : BaseViewModel() {
private val defaultCurrency: String by lazy { repository.getCurrenciesList()[0] } private val defaultCurrency: String by lazy { repository.getCurrenciesList()[0] }
var appWidgetId: Int? = null var appWidgetId: Int? = null
@@ -20,7 +19,6 @@ class WidgetViewModel @Inject constructor(
var rateIdTo: String? = null var rateIdTo: String? = null
// Live data to feedback to @CurrencyAppWidgetConfigureActivityKotlin // Live data to feedback to @CurrencyAppWidgetConfigureActivityKotlin
val operationFinishedListener = MutableLiveData<Pair<Boolean, String?>>()
// Setup viewmodel app widget ID // Setup viewmodel app widget ID
// Set default values for text views // Set default values for text views
@@ -43,15 +41,14 @@ class WidgetViewModel @Inject constructor(
fun submitSelectionOnClick() { fun submitSelectionOnClick() {
if (rateIdTo == null || rateIdFrom == null) { if (rateIdTo == null || rateIdFrom == null) {
operationFinishedListener.value = Pair(false, "Selections incomplete") onError("Selections incomplete")
return return
} }
if (rateIdFrom == rateIdTo) { if (rateIdFrom == rateIdTo) {
operationFinishedListener.value = onError("Selected rates cannot be the same")
Pair(false, "Selected rates cannot be the same ${rateIdFrom}${rateIdTo}")
return return
} }
operationFinishedListener.value = Pair(true, null) onSuccess(Unit)
} }
fun setWidgetStored() { fun setWidgetStored() {

View File

@@ -0,0 +1,24 @@
package com.appttude.h_mal.easycc.utils
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}

View File

@@ -0,0 +1,7 @@
package com.appttude.h_mal.easycc.utils
sealed class ViewState {
object HasStarted : ViewState()
class HasData<T : Any>(val data: Event<T>) : ViewState()
class HasError(val error: Event<String>) : ViewState()
}

View File

@@ -2,21 +2,34 @@ package com.appttude.h_mal.easycc.utils
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.EditText import android.widget.EditText
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AnimRes
fun EditText.clearEditText() { fun EditText.clearEditText() {
this.setText("") this.setText("")
} }
fun View.hideView(vis: Boolean) {
visibility = if (vis) {
View.GONE
} else {
View.VISIBLE
}
}
fun Context.displayToast(message: String) { fun Context.displayToast(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show() Toast.makeText(this, message, Toast.LENGTH_LONG).show()
} }
fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) {
val animation = AnimationUtils.loadAnimation(context, id)
animation.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationEnd(animation: Animation?) = complete(this@triggerAnimation)
override fun onAnimationStart(a: Animation?) {}
override fun onAnimationRepeat(a: Animation?) {}
})
startAnimation(animation)
}
fun View.show() {
this.visibility = View.VISIBLE
}
fun View.hide() {
this.visibility = View.GONE
}

View File

@@ -1,7 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt" xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp" android:width="1080dp"
android:height="192dp" android:height="1920dp"
android:viewportWidth="1080" android:viewportWidth="1080"
android:viewportHeight="1920"> android:viewportHeight="1920">
<path android:pathData="M0,0L1080,0L1080,1920L0,1920L0,0Z"> <path android:pathData="M0,0L1080,0L1080,1920L0,1920L0,0Z">

View File

@@ -99,17 +99,4 @@
</LinearLayout> </LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:indeterminateTint="@color/colour_four"
android:indeterminateTintMode="src_atop"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#4D000000">
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
android:layout_gravity="center"
android:elevation="0.2dp"
android:indeterminateTint="@color/colour_four"
android:indeterminateTintMode="src_atop"/>
</FrameLayout>

View File

@@ -1,11 +1,13 @@
package com.appttude.h_mal.easycc.repository package com.appttude.h_mal.easycc.repository
import com.appttude.h_mal.easycc.data.network.SafeApiRequest
import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi import com.appttude.h_mal.easycc.data.network.api.BackupCurrencyApi
import com.appttude.h_mal.easycc.data.network.api.CurrencyApi import com.appttude.h_mal.easycc.data.network.api.CurrencyApi
import com.appttude.h_mal.easycc.data.network.response.ResponseObject import com.appttude.h_mal.easycc.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider import com.appttude.h_mal.easycc.data.prefs.PreferenceProvider
import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.data.repository.RepositoryImpl import com.appttude.h_mal.easycc.data.repository.RepositoryImpl
import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.utils.convertPairsListToString import com.appttude.h_mal.easycc.utils.convertPairsListToString
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.ResponseBody import okhttp3.ResponseBody
@@ -22,7 +24,7 @@ import java.io.IOException
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
class RepositoryNetworkTest { class RepositoryNetworkTest : SafeApiRequest() {
lateinit var repository: Repository lateinit var repository: Repository
@@ -49,15 +51,16 @@ class RepositoryNetworkTest {
//create a successful retrofit response //create a successful retrofit response
val mockCurrencyResponse = mock(ResponseObject::class.java) val mockCurrencyResponse = mock(ResponseObject::class.java)
val re = Response.success(mockCurrencyResponse) val re = Response.success(mockCurrencyResponse)
val currencyModel = mock(CurrencyModel::class.java)
//WHEN - loginApiRequest to return a successful response //WHEN - loginApiRequest to return a successful response
val currencyPair = convertPairsListToString(s1, s2) val currencyPair = convertPairsListToString(s1, s2)
Mockito.`when`(api.getCurrencyRate(currencyPair)).thenReturn(re) Mockito.`when`(api.getCurrencyRate(currencyPair)).thenReturn(re)
Mockito.`when`(responseUnwrap { api.getCurrencyRate(currencyPair) }.getCurrencyModel()).thenReturn(currencyModel)
//THEN - the unwrapped login response contains the correct values //THEN - the unwrapped login response contains the correct values
val currencyResponse = repository.getDataFromApi(s1, s2) val currencyResponse = repository.getDataFromApi(s1, s2)
assertNotNull(currencyResponse) assertEquals(currencyResponse, currencyModel)
assertEquals(currencyResponse, mockCurrencyResponse)
} }
@Test @Test
@@ -74,6 +77,7 @@ class RepositoryNetworkTest {
//WHEN //WHEN
val currencyPair = convertPairsListToString(s1, s2) val currencyPair = convertPairsListToString(s1, s2)
Mockito.`when`(api.getCurrencyRate(currencyPair)).thenAnswer { re } Mockito.`when`(api.getCurrencyRate(currencyPair)).thenAnswer { re }
Mockito.`when`(apiBackup.getCurrencyRate(s1, s2)).thenAnswer { re }
//THEN - assert exception is not null //THEN - assert exception is not null
val ioExceptionReturned = assertFailsWith<IOException> { val ioExceptionReturned = assertFailsWith<IOException> {

View File

@@ -0,0 +1,22 @@
package com.appttude.h_mal.easycc.ui
import androidx.lifecycle.MutableLiveData
import com.appttude.h_mal.easycc.utils.ViewState
abstract class BaseViewModelTest<V : BaseViewModel> {
abstract val viewModel: V?
open fun setUp() {
viewModel?.uiState?.observeForever {
when (it) {
is ViewState.HasStarted -> Unit
is ViewState.HasData<*> -> dataPost.postValue(it.data.getContentIfNotHandled())
is ViewState.HasError -> errorPost.postValue(it.error.getContentIfNotHandled())
}
}
}
var dataPost: MutableLiveData<Any> = MutableLiveData()
var errorPost: MutableLiveData<String> = MutableLiveData()
}

View File

@@ -1,17 +1,17 @@
package com.appttude.h_mal.easycc.ui.main package com.appttude.h_mal.easycc.ui.main
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.appttude.h_mal.easycc.data.network.response.ResponseObject
import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.helper.CurrencyDataHelper import com.appttude.h_mal.easycc.models.CurrencyModel
import com.appttude.h_mal.easycc.ui.BaseViewModelTest
import com.appttude.h_mal.easycc.utils.MainCoroutineRule import com.appttude.h_mal.easycc.utils.MainCoroutineRule
import com.appttude.h_mal.easycc.utils.observeOnce import com.appttude.h_mal.easycc.utils.observeOnce
import com.nhaarman.mockitokotlin2.doAnswer import com.nhaarman.mockitokotlin2.doAnswer
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.* import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
@@ -22,7 +22,7 @@ import org.mockito.MockitoAnnotations
import java.io.IOException import java.io.IOException
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class MainViewModelTest { class MainViewModelTest : BaseViewModelTest<MainViewModel>(){
// Run tasks synchronously // Run tasks synchronously
@get:Rule @get:Rule
@@ -31,27 +31,27 @@ class MainViewModelTest {
@get:Rule @get:Rule
var mainCoroutineRule = MainCoroutineRule() var mainCoroutineRule = MainCoroutineRule()
lateinit var viewModel: MainViewModel override lateinit var viewModel: MainViewModel
@Mock @Mock
lateinit var repository: Repository lateinit var repository: Repository
@Mock private val currencyOne = "AUD - Australian Dollar"
lateinit var helper: CurrencyDataHelper private val currencyTwo = "GBP - British Pound"
@Before @Before
fun setUp() { override fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
viewModel = MainViewModel(helper, repository) viewModel = MainViewModel(repository)
super.setUp()
} }
@Test @Test
fun initiate_validBundleValues_successResponse() = runBlocking { fun initiate_validBundleValues_successResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound"
val bundle = mock(Bundle()::class.java) val bundle = mock(Bundle()::class.java)
val responseObject = mock(ResponseObject::class.java) val responseObject = mock(CurrencyModel::class.java)
//WHEN //WHEN
Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne) Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne)
@@ -61,59 +61,50 @@ class MainViewModelTest {
//THEN //THEN
viewModel.initiate(bundle) viewModel.initiate(bundle)
viewModel.operationStartedListener.observeOnce {
assertEquals(true, it) dataPost.observeOnce {
} assertEquals(it, responseObject)
viewModel.operationFinishedListener.observeOnce {
assertEquals(true, it.first)
assertNull(it.second)
} }
} }
@Test @Test
fun initiate_invalidBundleValues_successfulResponse() = runBlocking { fun initiate_invalidBundleValues_successfulResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "corrupted data"
val currencyTwo = "corrupted data again"
val bundle = mock(Bundle()::class.java) val bundle = mock(Bundle()::class.java)
val error = "Corrupted data found" val error = "Corrupted data found"
//WHEN //WHEN
Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne) Mockito.`when`(bundle.getString("parse_1")).thenReturn(currencyOne)
Mockito.`when`(bundle.getString("parse_2")).thenReturn(currencyTwo) Mockito.`when`(bundle.getString("parse_2")).thenReturn(currencyTwo)
Mockito.`when`(helper.getDataFromApi(currencyOne, currencyTwo)) Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.doAnswer { throw IOException(error) } .doAnswer { throw IOException(error) }
//THEN //THEN
viewModel.initiate(bundle) viewModel.initiate(bundle)
viewModel.operationStartedListener.observeOnce {
assertEquals(true, it) errorPost.observeOnce {
} assertEquals(error, it)
viewModel.operationFinishedListener.observeOnce {
assertEquals(false, it.first)
assertEquals(it.second, error)
} }
} }
@Test @Test
fun initiate_sameBundleValues_successfulResponse() = runBlocking { fun initiate_sameBundleValues_successfulResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar"
val bundle = mock(Bundle()::class.java) val bundle = mock(Bundle()::class.java)
val responseObject = mock(CurrencyModel::class.java)
//WHEN //WHEN
Mockito.`when`(bundle.getString("parse_1")).thenReturn(null) Mockito.`when`(bundle.getString("parse_1")).thenReturn(null)
Mockito.`when`(bundle.getString("parse_2")).thenReturn(null) Mockito.`when`(bundle.getString("parse_2")).thenReturn(null)
Mockito.`when`(repository.getConversionPair()).thenReturn(Pair(currencyOne, currencyOne)) Mockito.`when`(repository.getConversionPair()).thenReturn(Pair(currencyOne, currencyOne))
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
.thenReturn(responseObject)
//THEN //THEN
viewModel.initiate(bundle) viewModel.initiate(bundle)
viewModel.operationStartedListener.observeOnce {
assertEquals(true, it) dataPost.observeOnce {
} assertEquals(responseObject, it)
viewModel.operationFinishedListener.observeOnce {
assertEquals(true, it.first)
assertNull(it.second)
} }
} }
@@ -129,12 +120,9 @@ class MainViewModelTest {
//THEN //THEN
viewModel.initiate(bundle) viewModel.initiate(bundle)
viewModel.operationStartedListener.observeOnce {
assertEquals(true, it) errorPost.observeOnce {
} assertEquals("Select both currencies", it)
viewModel.operationFinishedListener.observeOnce {
assertEquals(false, it.first)
assertEquals("Select currencies", it.second)
} }
} }
@@ -142,11 +130,9 @@ class MainViewModelTest {
@Test @Test
fun setCurrencyName_validValues_successResponse() = runBlocking { fun setCurrencyName_validValues_successResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound"
viewModel.rateIdTo = currencyTwo viewModel.rateIdTo = currencyTwo
val tag = "top" val tag = "top"
val responseObject = mock(ResponseObject::class.java) val responseObject = mock(CurrencyModel::class.java)
//WHEN //WHEN
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)) Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
@@ -154,24 +140,18 @@ class MainViewModelTest {
//THEN //THEN
viewModel.setCurrencyName(tag, currencyOne) viewModel.setCurrencyName(tag, currencyOne)
viewModel.operationStartedListener.observeOnce {
assertEquals(true, it) dataPost.observeOnce {
} assertEquals(responseObject, it)
viewModel.operationFinishedListener.observeOnce {
assertEquals(true, it.first)
Log.i("tag", "${it.first} ${it.second}")
assertNull(it.second)
} }
} }
@Test @Test
fun setCurrencyName_sameValues_successfulResponse() = runBlocking { fun setCurrencyName_sameValues_successfulResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar"
val currencyTwo = "GBP - British Pound"
viewModel.rateIdTo = currencyOne viewModel.rateIdTo = currencyOne
val tag = "top" val tag = "top"
val responseObject = mock(ResponseObject::class.java) val responseObject = mock(CurrencyModel::class.java)
//WHEN //WHEN
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)) Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo))
@@ -179,35 +159,27 @@ class MainViewModelTest {
//THEN //THEN
viewModel.setCurrencyName(tag, currencyOne) viewModel.setCurrencyName(tag, currencyOne)
viewModel.operationStartedListener.observeOnce { dataPost.observeOnce {
assertEquals(true, it) assertEquals(responseObject, it)
}
viewModel.operationFinishedListener.observeOnce {
assertEquals(true, it.first)
assertNull(it.second)
} }
} }
@Test @Test
fun setCurrencyName_invalidValues_unsuccessfulResponse() = runBlocking { fun setCurrencyName_invalidValues_unsuccessfulResponse() = runBlocking {
//GIVEN //GIVEN
val currencyOne = "AUD - Australian Dollar" val error = "Data is corrupted"
val currencyTwo = "GBP - British Pound" viewModel.rateIdTo = "corrupted"
val tag = "top" val tag = "top"
val responseObject = mock(ResponseObject::class.java) val responseObject = mock(CurrencyModel::class.java)
//WHEN //WHEN
Mockito.`when`(repository.getDataFromApi(currencyOne, currencyTwo)) Mockito.`when`(repository.getDataFromApi(currencyOne, "corrupted"))
.thenReturn(responseObject) .doAnswer { throw IOException(error) }
//THEN //THEN
viewModel.setCurrencyName(tag, currencyOne) viewModel.setCurrencyName(tag, currencyOne)
viewModel.operationStartedListener.observeOnce { errorPost.observeOnce {
assertEquals(true, it) assertEquals(error, it)
}
viewModel.operationFinishedListener.observeOnce {
assertEquals(false, it.first)
assertNotNull(it.second)
} }
} }

View File

@@ -2,6 +2,7 @@ package com.appttude.h_mal.easycc.ui.widget
import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.appttude.h_mal.easycc.data.repository.Repository import com.appttude.h_mal.easycc.data.repository.Repository
import com.appttude.h_mal.easycc.ui.BaseViewModelTest
import com.appttude.h_mal.easycc.utils.observeOnce import com.appttude.h_mal.easycc.utils.observeOnce
import org.junit.Assert.* import org.junit.Assert.*
import org.junit.Before import org.junit.Before
@@ -15,21 +16,23 @@ import org.mockito.MockitoAnnotations
private const val currencyOne = "AUD - Australian Dollar" private const val currencyOne = "AUD - Australian Dollar"
private const val currencyTwo = "GBP - British Pound" private const val currencyTwo = "GBP - British Pound"
class WidgetViewModelTest { class WidgetViewModelTest : BaseViewModelTest<WidgetViewModel>(){
// Run tasks synchronously // Run tasks synchronously
@get:Rule @get:Rule
val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var viewModel: WidgetViewModel override lateinit var viewModel: WidgetViewModel
@Mock @Mock
lateinit var repository: Repository lateinit var repository: Repository
@Before @Before
fun setUp() { override fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
viewModel = WidgetViewModel(repository) viewModel = WidgetViewModel(repository)
super.setUp()
} }
@Test @Test
@@ -75,7 +78,6 @@ class WidgetViewModelTest {
//THEN //THEN
val dialogResult = viewModel.getSubmitDialogMessage() val dialogResult = viewModel.getSubmitDialogMessage()
assertEquals(dialogResult, "Create widget for AUDGBP?") assertEquals(dialogResult, "Create widget for AUDGBP?")
} }
@Test @Test
@@ -86,9 +88,9 @@ class WidgetViewModelTest {
//THEN //THEN
viewModel.submitSelectionOnClick() viewModel.submitSelectionOnClick()
viewModel.operationFinishedListener.observeOnce {
assertEquals(it.first, true) dataPost.observeOnce {
assertNull(it.second) assert(it is Unit)
} }
} }
@@ -100,20 +102,23 @@ class WidgetViewModelTest {
//THEN //THEN
viewModel.submitSelectionOnClick() viewModel.submitSelectionOnClick()
viewModel.operationFinishedListener.observeOnce {
assertEquals(it.first, false) errorPost.observeOnce {
assertNotNull(it.second) assertEquals("Selected rates cannot be the same", it)
} }
} }
@Test @Test
fun submitSelectionOnClick_noInput_unsuccessfulResponse() { fun submitSelectionOnClick_noInput_unsuccessfulResponse() {
//GIVEN
viewModel.rateIdFrom = null
viewModel.rateIdTo = null
//THEN //THEN
viewModel.submitSelectionOnClick() viewModel.submitSelectionOnClick()
viewModel.operationFinishedListener.observeOnce {
assertEquals(it.first, false) errorPost.observeOnce {
assertNotNull(it.second) assertEquals("Selections incomplete", it)
} }
} }
} }