Merge pull request #120 from kirmanak/delete-recipe
Add "delete recipe" button
This commit is contained in:
@@ -90,4 +90,9 @@ class MealieDataSourceWrapper @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteRecipe(recipeSlug: String) = when (getVersion()) {
|
||||||
|
ServerVersion.V0 -> v0Source.deleteRecipe(recipeSlug)
|
||||||
|
ServerVersion.V1 -> v1Source.deleteRecipe(recipeSlug)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,4 +19,6 @@ interface RecipeRepo {
|
|||||||
suspend fun refreshRecipes()
|
suspend fun refreshRecipes()
|
||||||
|
|
||||||
suspend fun updateIsRecipeFavorite(recipeSlug: String, isFavorite: Boolean): Result<Unit>
|
suspend fun updateIsRecipeFavorite(recipeSlug: String, isFavorite: Boolean): Result<Unit>
|
||||||
|
|
||||||
|
suspend fun deleteRecipe(entity: RecipeSummaryEntity): Result<Unit>
|
||||||
}
|
}
|
||||||
@@ -19,4 +19,6 @@ interface RecipeStorage {
|
|||||||
suspend fun queryRecipeInfo(recipeId: String): FullRecipeEntity?
|
suspend fun queryRecipeInfo(recipeId: String): FullRecipeEntity?
|
||||||
|
|
||||||
suspend fun updateFavoriteRecipes(favorites: List<String>)
|
suspend fun updateFavoriteRecipes(favorites: List<String>)
|
||||||
|
|
||||||
|
suspend fun deleteRecipe(entity: RecipeSummaryEntity)
|
||||||
}
|
}
|
||||||
@@ -80,4 +80,9 @@ class RecipeStorageImpl @Inject constructor(
|
|||||||
recipeDao.setNonFavorite(favorites)
|
recipeDao.setNonFavorite(favorites)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteRecipe(entity: RecipeSummaryEntity) {
|
||||||
|
logger.v { "deleteRecipeBySlug() called with: entity = $entity" }
|
||||||
|
recipeDao.deleteRecipe(entity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -78,11 +78,24 @@ class RecipeRepoImpl @Inject constructor(
|
|||||||
): Result<Unit> = runCatchingExceptCancel {
|
): Result<Unit> = runCatchingExceptCancel {
|
||||||
logger.v { "updateIsRecipeFavorite() called with: recipeSlug = $recipeSlug, isFavorite = $isFavorite" }
|
logger.v { "updateIsRecipeFavorite() called with: recipeSlug = $recipeSlug, isFavorite = $isFavorite" }
|
||||||
dataSource.updateIsRecipeFavorite(recipeSlug, isFavorite)
|
dataSource.updateIsRecipeFavorite(recipeSlug, isFavorite)
|
||||||
mediator.onFavoritesChange()
|
val favorites = dataSource.getFavoriteRecipes()
|
||||||
|
storage.updateFavoriteRecipes(favorites)
|
||||||
|
pagingSourceFactory.invalidate()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
logger.e(it) { "Can't update recipe's is favorite status" }
|
logger.e(it) { "Can't update recipe's is favorite status" }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteRecipe(
|
||||||
|
entity: RecipeSummaryEntity
|
||||||
|
): Result<Unit> = runCatchingExceptCancel {
|
||||||
|
logger.v { "deleteRecipe() called with: entity = $entity" }
|
||||||
|
dataSource.deleteRecipe(entity.slug)
|
||||||
|
storage.deleteRecipe(entity)
|
||||||
|
pagingSourceFactory.invalidate()
|
||||||
|
}.onFailure {
|
||||||
|
logger.e(it) { "Can't delete recipe" }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val LOAD_PAGE_SIZE = 50
|
private const val LOAD_PAGE_SIZE = 50
|
||||||
private const val INITIAL_LOAD_PAGE_SIZE = LOAD_PAGE_SIZE * 3
|
private const val INITIAL_LOAD_PAGE_SIZE = LOAD_PAGE_SIZE * 3
|
||||||
|
|||||||
@@ -83,10 +83,4 @@ class RecipesRemoteMediator @Inject constructor(
|
|||||||
recipes.size
|
recipes.size
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun onFavoritesChange() {
|
|
||||||
logger.v { "onFavoritesChange() called" }
|
|
||||||
val favorites = network.getFavoriteRecipes()
|
|
||||||
storage.updateFavoriteRecipes(favorites)
|
|
||||||
pagingSourceFactory.invalidate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -8,4 +8,6 @@ interface RecipeDataSource {
|
|||||||
suspend fun getFavoriteRecipes(): List<String>
|
suspend fun getFavoriteRecipes(): List<String>
|
||||||
|
|
||||||
suspend fun updateIsRecipeFavorite(recipeSlug: String, isFavorite: Boolean)
|
suspend fun updateIsRecipeFavorite(recipeSlug: String, isFavorite: Boolean)
|
||||||
|
|
||||||
|
suspend fun deleteRecipe(recipeSlug: String)
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,10 @@ class RecipeViewHolder @AssistedInject constructor(
|
|||||||
override val recipeSummaryEntity: RecipeSummaryEntity
|
override val recipeSummaryEntity: RecipeSummaryEntity
|
||||||
) : ClickEvent()
|
) : ClickEvent()
|
||||||
|
|
||||||
|
data class DeleteClick(
|
||||||
|
override val recipeSummaryEntity: RecipeSummaryEntity
|
||||||
|
) : ClickEvent()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val loadingPlaceholder by lazy {
|
private val loadingPlaceholder by lazy {
|
||||||
@@ -62,6 +66,7 @@ class RecipeViewHolder @AssistedInject constructor(
|
|||||||
logger.d { "bind: item clicked $entity" }
|
logger.d { "bind: item clicked $entity" }
|
||||||
clickListener(ClickEvent.RecipeClick(entity))
|
clickListener(ClickEvent.RecipeClick(entity))
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.favoriteIcon.isVisible = showFavoriteIcon
|
binding.favoriteIcon.isVisible = showFavoriteIcon
|
||||||
binding.favoriteIcon.setOnClickListener {
|
binding.favoriteIcon.setOnClickListener {
|
||||||
clickListener(ClickEvent.FavoriteClick(entity))
|
clickListener(ClickEvent.FavoriteClick(entity))
|
||||||
@@ -80,6 +85,10 @@ class RecipeViewHolder @AssistedInject constructor(
|
|||||||
R.string.view_holder_recipe_non_favorite_content_description
|
R.string.view_holder_recipe_non_favorite_content_description
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
binding.deleteIcon.setOnClickListener {
|
||||||
|
clickListener(ClickEvent.DeleteClick(item))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package gq.kirmanak.mealient.ui.recipes
|
package gq.kirmanak.mealient.ui.recipes
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
@@ -13,6 +14,7 @@ import androidx.paging.LoadState
|
|||||||
import androidx.paging.PagingDataAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import gq.kirmanak.mealient.R
|
import gq.kirmanak.mealient.R
|
||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
@@ -55,9 +57,15 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) {
|
|||||||
checkedMenuItemId = R.id.recipes_list
|
checkedMenuItemId = R.id.recipes_list
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModel.showFavoriteIcon.observe(viewLifecycleOwner) { showFavoriteIcon ->
|
collectWhenViewResumed(viewModel.showFavoriteIcon) { showFavoriteIcon ->
|
||||||
setupRecipeAdapter(showFavoriteIcon)
|
setupRecipeAdapter(showFavoriteIcon)
|
||||||
}
|
}
|
||||||
|
collectWhenViewResumed(viewModel.deleteRecipeResult) {
|
||||||
|
logger.d { "Delete recipe result is $it" }
|
||||||
|
if (it.isFailure) {
|
||||||
|
showLongToast(R.string.fragment_recipes_delete_recipe_failed)
|
||||||
|
}
|
||||||
|
}
|
||||||
hideKeyboardOnScroll()
|
hideKeyboardOnScroll()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +108,9 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) {
|
|||||||
is RecipeViewHolder.ClickEvent.RecipeClick -> {
|
is RecipeViewHolder.ClickEvent.RecipeClick -> {
|
||||||
onRecipeClicked(it.recipeSummaryEntity)
|
onRecipeClicked(it.recipeSummaryEntity)
|
||||||
}
|
}
|
||||||
|
is RecipeViewHolder.ClickEvent.DeleteClick -> {
|
||||||
|
onDeleteClick(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +150,27 @@ class RecipesListFragment : Fragment(R.layout.fragment_recipes_list) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onDeleteClick(event: RecipeViewHolder.ClickEvent) {
|
||||||
|
logger.v { "onDeleteClick() called with: event = $event" }
|
||||||
|
val entity = event.recipeSummaryEntity
|
||||||
|
val message = getString(
|
||||||
|
R.string.fragment_recipes_delete_recipe_confirm_dialog_message, entity.name
|
||||||
|
)
|
||||||
|
val onPositiveClick = DialogInterface.OnClickListener { _, _ ->
|
||||||
|
viewModel.onDeleteConfirm(entity)
|
||||||
|
}
|
||||||
|
val positiveBtnResId = R.string.fragment_recipes_delete_recipe_confirm_dialog_positive_btn
|
||||||
|
val titleResId = R.string.fragment_recipes_delete_recipe_confirm_dialog_title
|
||||||
|
val negativeBtnResId = R.string.fragment_recipes_delete_recipe_confirm_dialog_negative_btn
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(titleResId)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(positiveBtnResId, onPositiveClick)
|
||||||
|
.setNegativeButton(negativeBtnResId) { _, _ -> }
|
||||||
|
.show()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private fun onFavoriteClick(event: RecipeViewHolder.ClickEvent) {
|
private fun onFavoriteClick(event: RecipeViewHolder.ClickEvent) {
|
||||||
logger.v { "onFavoriteClick() called with: event = $event" }
|
logger.v { "onFavoriteClick() called with: event = $event" }
|
||||||
viewModel.onFavoriteIconClick(event.recipeSummaryEntity).observe(viewLifecycleOwner) {
|
viewModel.onFavoriteIconClick(event.recipeSummaryEntity).observe(viewLifecycleOwner) {
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package gq.kirmanak.mealient.ui.recipes
|
|||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
|
||||||
import androidx.lifecycle.liveData
|
import androidx.lifecycle.liveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.paging.PagingData
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
@@ -12,8 +12,16 @@ import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
|||||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||||
import gq.kirmanak.mealient.extensions.valueUpdatesOnly
|
import gq.kirmanak.mealient.extensions.valueUpdatesOnly
|
||||||
import gq.kirmanak.mealient.logging.Logger
|
import gq.kirmanak.mealient.logging.Logger
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@@ -23,8 +31,18 @@ class RecipesListViewModel @Inject constructor(
|
|||||||
private val logger: Logger,
|
private val logger: Logger,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val pagingData = recipeRepo.createPager().flow.cachedIn(viewModelScope)
|
val pagingData: Flow<PagingData<RecipeSummaryEntity>> = recipeRepo.createPager().flow
|
||||||
val showFavoriteIcon = authRepo.isAuthorizedFlow.asLiveData()
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
val showFavoriteIcon: StateFlow<Boolean> = authRepo.isAuthorizedFlow
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||||
|
|
||||||
|
private val _deleteRecipeResult = MutableSharedFlow<Result<Unit>>(
|
||||||
|
replay = 0,
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
val deleteRecipeResult: SharedFlow<Result<Unit>> get() = _deleteRecipeResult
|
||||||
|
|
||||||
init {
|
init {
|
||||||
authRepo.isAuthorizedFlow.valueUpdatesOnly().onEach { hasAuthorized ->
|
authRepo.isAuthorizedFlow.valueUpdatesOnly().onEach { hasAuthorized ->
|
||||||
@@ -49,4 +67,13 @@ class RecipesListViewModel @Inject constructor(
|
|||||||
isFavorite = recipeSummaryEntity.isFavorite.not(),
|
isFavorite = recipeSummaryEntity.isFavorite.not(),
|
||||||
).also { emit(it) }
|
).also { emit(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onDeleteConfirm(recipeSummaryEntity: RecipeSummaryEntity) {
|
||||||
|
logger.v { "onDeleteConfirm() called with: recipeSummaryEntity = $recipeSummaryEntity" }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result = recipeRepo.deleteRecipe(recipeSummaryEntity)
|
||||||
|
logger.d { "onDeleteConfirm: delete result is $result" }
|
||||||
|
_deleteRecipeResult.emit(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
10
app/src/main/res/drawable/ic_delete.xml
Normal file
10
app/src/main/res/drawable/ic_delete.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="32dp"
|
||||||
|
android:height="32dp"
|
||||||
|
android:tint="?attr/colorPrimary"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M7,21Q6.175,21 5.588,20.413Q5,19.825 5,19V6H4V4H9V3H15V4H20V6H19V19Q19,19.825 18.413,20.413Q17.825,21 17,21ZM17,6H7V19Q7,19 7,19Q7,19 7,19H17Q17,19 17,19Q17,19 17,19ZM9,17H11V8H9ZM13,17H15V8H13ZM7,6V19Q7,19 7,19Q7,19 7,19Q7,19 7,19Q7,19 7,19Z" />
|
||||||
|
</vector>
|
||||||
@@ -38,9 +38,8 @@
|
|||||||
app:layout_constraintDimensionRatio="2:1"
|
app:layout_constraintDimensionRatio="2:1"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/favorite_icon"
|
app:layout_constraintTop_toBottomOf="@id/delete_icon"
|
||||||
app:layout_constraintVertical_chainStyle="packed"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
app:layout_goneMarginTop="@dimen/margin_medium"
|
|
||||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||||
tools:srcCompat="@drawable/placeholder_recipe" />
|
tools:srcCompat="@drawable/placeholder_recipe" />
|
||||||
|
|
||||||
@@ -54,6 +53,19 @@
|
|||||||
app:layout_constraintEnd_toEndOf="@id/image"
|
app:layout_constraintEnd_toEndOf="@id/image"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:srcCompat="@drawable/ic_favorite_unfilled"
|
tools:srcCompat="@drawable/ic_favorite_unfilled"
|
||||||
tools:visibility="gone" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/delete_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/margin_medium"
|
||||||
|
android:layout_marginVertical="@dimen/margin_small"
|
||||||
|
android:contentDescription="@string/view_holder_recipe_delete_content_description"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/image"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/favorite_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_goneMarginEnd="0dp"
|
||||||
|
app:srcCompat="@drawable/ic_delete" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -57,4 +57,10 @@
|
|||||||
<string name="content_description_activity_share_recipe_progress">Индикатор прогресса</string>
|
<string name="content_description_activity_share_recipe_progress">Индикатор прогресса</string>
|
||||||
<string name="view_holder_recipe_favorite_content_description">Добавлен в избранное</string>
|
<string name="view_holder_recipe_favorite_content_description">Добавлен в избранное</string>
|
||||||
<string name="view_holder_recipe_non_favorite_content_description">Не добавлен в избранное</string>
|
<string name="view_holder_recipe_non_favorite_content_description">Не добавлен в избранное</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_failed">Не удалось удалить рецепт</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_confirm_dialog_title">Удалить рецепт</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_confirm_dialog_message">Вы уверены, что хотите удалить %1$s? Удаление необратимо.</string>
|
||||||
|
<string name="view_holder_recipe_delete_content_description">Удалить рецепт</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_confirm_dialog_positive_btn">Подтвердить</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_confirm_dialog_negative_btn">Отмена</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -50,6 +50,11 @@
|
|||||||
<string name="fragment_recipes_load_failure_toast_unexpected_response">unexpected response</string>
|
<string name="fragment_recipes_load_failure_toast_unexpected_response">unexpected response</string>
|
||||||
<string name="fragment_recipes_load_failure_toast_no_connection">no connection</string>
|
<string name="fragment_recipes_load_failure_toast_no_connection">no connection</string>
|
||||||
<string name="fragment_recipes_favorite_update_failed">Favorite status update failed</string>
|
<string name="fragment_recipes_favorite_update_failed">Favorite status update failed</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_failed">Recipe removal failed</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_confirm_dialog_title">Delete recipe</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_confirm_dialog_message">Are you sure you want to delete %1$s? This cannot be undone.</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_confirm_dialog_positive_btn">Confirm</string>
|
||||||
|
<string name="fragment_recipes_delete_recipe_confirm_dialog_negative_btn">Cancel</string>
|
||||||
<string name="menu_navigation_drawer_change_url">Change URL</string>
|
<string name="menu_navigation_drawer_change_url">Change URL</string>
|
||||||
<string name="search_recipes_hint">Search recipes</string>
|
<string name="search_recipes_hint">Search recipes</string>
|
||||||
<string name="menu_navigation_drawer_header" translatable="false">@string/app_name</string>
|
<string name="menu_navigation_drawer_header" translatable="false">@string/app_name</string>
|
||||||
@@ -60,4 +65,5 @@
|
|||||||
<string name="content_description_activity_share_recipe_progress">Progress indicator</string>
|
<string name="content_description_activity_share_recipe_progress">Progress indicator</string>
|
||||||
<string name="view_holder_recipe_favorite_content_description">Item is favorite</string>
|
<string name="view_holder_recipe_favorite_content_description">Item is favorite</string>
|
||||||
<string name="view_holder_recipe_non_favorite_content_description">Item is not favorite</string>
|
<string name="view_holder_recipe_non_favorite_content_description">Item is not favorite</string>
|
||||||
|
<string name="view_holder_recipe_delete_content_description">Delete recipe</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -8,9 +8,11 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
|||||||
import gq.kirmanak.mealient.datasource.NetworkError.Unauthorized
|
import gq.kirmanak.mealient.datasource.NetworkError.Unauthorized
|
||||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_FULL_RECIPE_INFO
|
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_FULL_RECIPE_INFO
|
||||||
|
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY
|
||||||
import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY
|
import gq.kirmanak.mealient.test.RecipeImplTestData.FULL_CAKE_INFO_ENTITY
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.coVerifyOrder
|
||||||
import io.mockk.impl.annotations.MockK
|
import io.mockk.impl.annotations.MockK
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -70,19 +72,25 @@ class RecipeRepoTest : BaseUnitTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when remove favorite recipe expect correct sequence`() = runTest {
|
fun `when remove favorite recipe expect correct sequence`() = runTest {
|
||||||
|
coEvery { dataSource.getFavoriteRecipes() } returns listOf("porridge")
|
||||||
subject.updateIsRecipeFavorite("cake", false)
|
subject.updateIsRecipeFavorite("cake", false)
|
||||||
coVerify {
|
coVerify {
|
||||||
dataSource.updateIsRecipeFavorite(eq("cake"), eq(false))
|
dataSource.updateIsRecipeFavorite(eq("cake"), eq(false))
|
||||||
remoteMediator.onFavoritesChange()
|
dataSource.getFavoriteRecipes()
|
||||||
|
storage.updateFavoriteRecipes(eq(listOf("porridge")))
|
||||||
|
pagingSourceFactory.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when add favorite recipe expect correct sequence`() = runTest {
|
fun `when add favorite recipe expect correct sequence`() = runTest {
|
||||||
|
coEvery { dataSource.getFavoriteRecipes() } returns listOf("porridge", "cake")
|
||||||
subject.updateIsRecipeFavorite("porridge", true)
|
subject.updateIsRecipeFavorite("porridge", true)
|
||||||
coVerify {
|
coVerify {
|
||||||
dataSource.updateIsRecipeFavorite(eq("porridge"), eq(true))
|
dataSource.updateIsRecipeFavorite(eq("porridge"), eq(true))
|
||||||
remoteMediator.onFavoritesChange()
|
dataSource.getFavoriteRecipes()
|
||||||
|
storage.updateFavoriteRecipes(eq(listOf("porridge", "cake")))
|
||||||
|
pagingSourceFactory.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +100,7 @@ class RecipeRepoTest : BaseUnitTest() {
|
|||||||
dataSource.updateIsRecipeFavorite(any(), any())
|
dataSource.updateIsRecipeFavorite(any(), any())
|
||||||
} throws Unauthorized(IOException())
|
} throws Unauthorized(IOException())
|
||||||
subject.updateIsRecipeFavorite("porridge", true)
|
subject.updateIsRecipeFavorite("porridge", true)
|
||||||
coVerify(inverse = true) { remoteMediator.onFavoritesChange() }
|
coVerify(inverse = true) { dataSource.getFavoriteRecipes() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -100,4 +108,21 @@ class RecipeRepoTest : BaseUnitTest() {
|
|||||||
subject.refreshRecipes()
|
subject.refreshRecipes()
|
||||||
coVerify { remoteMediator.updateRecipes(eq(0), eq(150), eq(LoadType.REFRESH)) }
|
coVerify { remoteMediator.updateRecipes(eq(0), eq(150), eq(LoadType.REFRESH)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when delete recipe expect correct sequence`() = runTest {
|
||||||
|
subject.deleteRecipe(CAKE_RECIPE_SUMMARY_ENTITY)
|
||||||
|
coVerifyOrder {
|
||||||
|
dataSource.deleteRecipe(eq("cake"))
|
||||||
|
storage.deleteRecipe(eq(CAKE_RECIPE_SUMMARY_ENTITY))
|
||||||
|
pagingSourceFactory.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when delete recipe remotely fails expect it isn't deleted locally`() = runTest {
|
||||||
|
coEvery { dataSource.deleteRecipe(any()) } throws Unauthorized(IOException())
|
||||||
|
subject.deleteRecipe(CAKE_RECIPE_SUMMARY_ENTITY)
|
||||||
|
coVerify(inverse = true) { storage.deleteRecipe(any()) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -144,27 +144,6 @@ class RecipesRemoteMediatorTest : BaseUnitTest() {
|
|||||||
coVerify { storage.refreshAll(TEST_RECIPE_SUMMARY_ENTITIES) }
|
coVerify { storage.refreshAll(TEST_RECIPE_SUMMARY_ENTITIES) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when favorites change expect network call`() = runTest {
|
|
||||||
coEvery { dataSource.getFavoriteRecipes() } returns listOf("cake", "porridge")
|
|
||||||
subject.onFavoritesChange()
|
|
||||||
coVerify { dataSource.getFavoriteRecipes() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when favorites change expect storage update`() = runTest {
|
|
||||||
coEvery { dataSource.getFavoriteRecipes() } returns listOf("cake", "porridge")
|
|
||||||
subject.onFavoritesChange()
|
|
||||||
coVerify { storage.updateFavoriteRecipes(eq(listOf("cake", "porridge"))) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `when favorites change expect factory invalidation`() = runTest {
|
|
||||||
coEvery { dataSource.getFavoriteRecipes() } returns listOf("cake", "porridge")
|
|
||||||
subject.onFavoritesChange()
|
|
||||||
coVerify { pagingSourceFactory.invalidate() }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when recipe update requested but favorite fails expect non-zero updates`() = runTest {
|
fun `when recipe update requested but favorite fails expect non-zero updates`() = runTest {
|
||||||
coEvery { dataSource.getFavoriteRecipes() } throws Unauthorized(IOException())
|
coEvery { dataSource.getFavoriteRecipes() } throws Unauthorized(IOException())
|
||||||
|
|||||||
@@ -5,15 +5,23 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||||
import gq.kirmanak.mealient.test.BaseUnitTest
|
import gq.kirmanak.mealient.test.BaseUnitTest
|
||||||
|
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_RECIPE_SUMMARY_ENTITY
|
||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.impl.annotations.MockK
|
import io.mockk.impl.annotations.MockK
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.toList
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class RecipesListViewModelTest : BaseUnitTest() {
|
class RecipesListViewModelTest : BaseUnitTest() {
|
||||||
@@ -24,6 +32,12 @@ class RecipesListViewModelTest : BaseUnitTest() {
|
|||||||
@MockK(relaxed = true)
|
@MockK(relaxed = true)
|
||||||
lateinit var recipeRepo: RecipeRepo
|
lateinit var recipeRepo: RecipeRepo
|
||||||
|
|
||||||
|
@Before
|
||||||
|
override fun setUp() {
|
||||||
|
super.setUp()
|
||||||
|
every { authRepo.isAuthorizedFlow } returns flowOf(true)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when authRepo isAuthorized changes to true expect that recipes are refreshed`() {
|
fun `when authRepo isAuthorized changes to true expect that recipes are refreshed`() {
|
||||||
every { authRepo.isAuthorizedFlow } returns flowOf(false, true)
|
every { authRepo.isAuthorizedFlow } returns flowOf(false, true)
|
||||||
@@ -40,14 +54,12 @@ class RecipesListViewModelTest : BaseUnitTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when authRepo isAuthorized doesn't change expect that recipes are not refreshed`() {
|
fun `when authRepo isAuthorized doesn't change expect that recipes are not refreshed`() {
|
||||||
every { authRepo.isAuthorizedFlow } returns flowOf(true)
|
|
||||||
createSubject()
|
createSubject()
|
||||||
coVerify(inverse = true) { recipeRepo.refreshRecipes() }
|
coVerify(inverse = true) { recipeRepo.refreshRecipes() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when refreshRecipeInfo succeeds expect successful result`() = runTest {
|
fun `when refreshRecipeInfo succeeds expect successful result`() = runTest {
|
||||||
every { authRepo.isAuthorizedFlow } returns flowOf(true)
|
|
||||||
val slug = "cake"
|
val slug = "cake"
|
||||||
coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit)
|
coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit)
|
||||||
val actual = createSubject().refreshRecipeInfo(slug).asFlow().first()
|
val actual = createSubject().refreshRecipeInfo(slug).asFlow().first()
|
||||||
@@ -56,7 +68,6 @@ class RecipesListViewModelTest : BaseUnitTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when refreshRecipeInfo succeeds expect call to repo`() = runTest {
|
fun `when refreshRecipeInfo succeeds expect call to repo`() = runTest {
|
||||||
every { authRepo.isAuthorizedFlow } returns flowOf(true)
|
|
||||||
val slug = "cake"
|
val slug = "cake"
|
||||||
coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit)
|
coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns Result.success(Unit)
|
||||||
createSubject().refreshRecipeInfo(slug).asFlow().first()
|
createSubject().refreshRecipeInfo(slug).asFlow().first()
|
||||||
@@ -65,7 +76,6 @@ class RecipesListViewModelTest : BaseUnitTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when refreshRecipeInfo fails expect result with error`() = runTest {
|
fun `when refreshRecipeInfo fails expect result with error`() = runTest {
|
||||||
every { authRepo.isAuthorizedFlow } returns flowOf(true)
|
|
||||||
val slug = "cake"
|
val slug = "cake"
|
||||||
val result = Result.failure<Unit>(RuntimeException())
|
val result = Result.failure<Unit>(RuntimeException())
|
||||||
coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns result
|
coEvery { recipeRepo.refreshRecipeInfo(eq(slug)) } returns result
|
||||||
@@ -73,5 +83,38 @@ class RecipesListViewModelTest : BaseUnitTest() {
|
|||||||
assertThat(actual).isEqualTo(result)
|
assertThat(actual).isEqualTo(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when delete recipe expect successful result in flow`() = runTest {
|
||||||
|
coEvery { recipeRepo.deleteRecipe(any()) } returns Result.success(Unit)
|
||||||
|
val subject = createSubject()
|
||||||
|
val results = runTestAndCollectFlow(subject.deleteRecipeResult) {
|
||||||
|
subject.onDeleteConfirm(CAKE_RECIPE_SUMMARY_ENTITY)
|
||||||
|
}
|
||||||
|
assertThat(results.single().isSuccess).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when delete recipe expect failed result in flow`() = runTest {
|
||||||
|
coEvery { recipeRepo.deleteRecipe(any()) } returns Result.failure(IOException())
|
||||||
|
val subject = createSubject()
|
||||||
|
val results = runTestAndCollectFlow(subject.deleteRecipeResult) {
|
||||||
|
subject.onDeleteConfirm(CAKE_RECIPE_SUMMARY_ENTITY)
|
||||||
|
}
|
||||||
|
assertThat(results.single().isFailure).isTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> TestScope.runTestAndCollectFlow(
|
||||||
|
flow: Flow<T>,
|
||||||
|
block: () -> Unit,
|
||||||
|
): List<T> {
|
||||||
|
val results = mutableListOf<T>()
|
||||||
|
val collectJob = launch(UnconfinedTestDispatcher(testScheduler)) {
|
||||||
|
flow.toList(results)
|
||||||
|
}
|
||||||
|
block()
|
||||||
|
collectJob.cancel()
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
private fun createSubject() = RecipesListViewModel(recipeRepo, authRepo, logger)
|
private fun createSubject() = RecipesListViewModel(recipeRepo, authRepo, logger)
|
||||||
}
|
}
|
||||||
@@ -15,9 +15,7 @@ import kotlinx.coroutines.flow.take
|
|||||||
import kotlinx.coroutines.flow.toList
|
import kotlinx.coroutines.flow.toList
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.rules.Timeout
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
class ShareRecipeViewModelTest : BaseUnitTest() {
|
class ShareRecipeViewModelTest : BaseUnitTest() {
|
||||||
@@ -27,9 +25,6 @@ class ShareRecipeViewModelTest : BaseUnitTest() {
|
|||||||
|
|
||||||
lateinit var subject: ShareRecipeViewModel
|
lateinit var subject: ShareRecipeViewModel
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val timeoutRule: Timeout = Timeout.seconds(5)
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
override fun setUp() {
|
override fun setUp() {
|
||||||
super.setUp()
|
super.setUp()
|
||||||
|
|||||||
@@ -46,4 +46,7 @@ interface RecipeDao {
|
|||||||
|
|
||||||
@Query("UPDATE recipe_summaries SET is_favorite = 0 WHERE slug NOT IN (:favorites)")
|
@Query("UPDATE recipe_summaries SET is_favorite = 0 WHERE slug NOT IN (:favorites)")
|
||||||
suspend fun setNonFavorite(favorites: List<String>)
|
suspend fun setNonFavorite(favorites: List<String>)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteRecipe(entity: RecipeSummaryEntity)
|
||||||
}
|
}
|
||||||
@@ -46,4 +46,6 @@ interface MealieDataSourceV0 {
|
|||||||
suspend fun removeFavoriteRecipe(userId: Int, recipeSlug: String)
|
suspend fun removeFavoriteRecipe(userId: Int, recipeSlug: String)
|
||||||
|
|
||||||
suspend fun addFavoriteRecipe(userId: Int, recipeSlug: String)
|
suspend fun addFavoriteRecipe(userId: Int, recipeSlug: String)
|
||||||
|
|
||||||
|
suspend fun deleteRecipe(slug: String)
|
||||||
}
|
}
|
||||||
@@ -115,4 +115,12 @@ class MealieDataSourceV0Impl @Inject constructor(
|
|||||||
logMethod = { "addFavoriteRecipe" },
|
logMethod = { "addFavoriteRecipe" },
|
||||||
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override suspend fun deleteRecipe(
|
||||||
|
slug: String
|
||||||
|
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.deleteRecipe(slug) },
|
||||||
|
logMethod = { "deleteRecipe" },
|
||||||
|
logParameters = { "slug = $slug" }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,4 +55,9 @@ interface MealieServiceV0 {
|
|||||||
@Path("userId") userId: Int,
|
@Path("userId") userId: Int,
|
||||||
@Path("recipeSlug") recipeSlug: String
|
@Path("recipeSlug") recipeSlug: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@DELETE("/api/recipes/{slug}")
|
||||||
|
suspend fun deleteRecipe(
|
||||||
|
@Path("slug") slug: String
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -54,4 +54,5 @@ interface MealieDataSourceV1 {
|
|||||||
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
|
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
|
||||||
|
|
||||||
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
|
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
|
||||||
|
suspend fun deleteRecipe(slug: String)
|
||||||
}
|
}
|
||||||
@@ -126,5 +126,13 @@ class MealieDataSourceV1Impl @Inject constructor(
|
|||||||
logMethod = { "addFavoriteRecipe" },
|
logMethod = { "addFavoriteRecipe" },
|
||||||
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
override suspend fun deleteRecipe(
|
||||||
|
slug: String
|
||||||
|
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
|
||||||
|
block = { service.deleteRecipe(slug) },
|
||||||
|
logMethod = { "deleteRecipe" },
|
||||||
|
logParameters = { "slug = $slug" }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,4 +61,9 @@ interface MealieServiceV1 {
|
|||||||
@Path("userId") userId: String,
|
@Path("userId") userId: String,
|
||||||
@Path("recipeSlug") recipeSlug: String
|
@Path("recipeSlug") recipeSlug: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@DELETE("/api/recipes/{slug}")
|
||||||
|
suspend fun deleteRecipe(
|
||||||
|
@Path("slug") slug: String
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -7,20 +7,23 @@ import io.mockk.MockKAnnotations
|
|||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
||||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
import org.junit.rules.Timeout
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
open class BaseUnitTest {
|
open class BaseUnitTest {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule(order = 0)
|
||||||
val instantExecutorRule = InstantTaskExecutorRule()
|
val instantExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
|
@get:Rule(order = 1)
|
||||||
|
val timeoutRule: Timeout = Timeout.seconds(10)
|
||||||
|
|
||||||
protected val logger: Logger = FakeLogger()
|
protected val logger: Logger = FakeLogger()
|
||||||
|
|
||||||
lateinit var dispatchers: AppDispatchers
|
lateinit var dispatchers: AppDispatchers
|
||||||
@@ -30,10 +33,10 @@ open class BaseUnitTest {
|
|||||||
MockKAnnotations.init(this)
|
MockKAnnotations.init(this)
|
||||||
Dispatchers.setMain(UnconfinedTestDispatcher())
|
Dispatchers.setMain(UnconfinedTestDispatcher())
|
||||||
dispatchers = object : AppDispatchers {
|
dispatchers = object : AppDispatchers {
|
||||||
override val io: CoroutineDispatcher = StandardTestDispatcher()
|
override val io: CoroutineDispatcher = UnconfinedTestDispatcher()
|
||||||
override val main: CoroutineDispatcher = StandardTestDispatcher()
|
override val main: CoroutineDispatcher = UnconfinedTestDispatcher()
|
||||||
override val default: CoroutineDispatcher = StandardTestDispatcher()
|
override val default: CoroutineDispatcher = UnconfinedTestDispatcher()
|
||||||
override val unconfined: CoroutineDispatcher = StandardTestDispatcher()
|
override val unconfined: CoroutineDispatcher = UnconfinedTestDispatcher()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user