diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepository.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepository.kt index 45a53e0..849b258 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepository.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepository.kt @@ -6,6 +6,7 @@ package dev.marlonlom.apps.cappajv.features.catalog_detail import dev.marlonlom.apps.cappajv.core.database.datasource.LocalDataSource +import dev.marlonlom.apps.cappajv.core.database.entities.CatalogFavoriteItem import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem import dev.marlonlom.apps.cappajv.core.database.entities.CatalogPunctuation import kotlinx.coroutines.CoroutineDispatcher @@ -16,17 +17,23 @@ import kotlinx.coroutines.flow.combine /** * Data class definition for catalog item detail. * - * @property product product detail - * @property points product points list + * @author marlonlom + * + * @property product Catalog product detail + * @property isFavorite True/False if catalog product is marked as favorite. + * @property points Catalog product points list */ data class CatalogDetail( val product: CatalogItem, + val isFavorite: Boolean, val points: List ) /** * Catalog details repository class. * + * @author marlonlom + * * @property localDataSource local data source dependency * @property coroutineDispatcher coroutine dispatcher */ @@ -39,14 +46,19 @@ class CatalogDetailRepository( coroutineDispatcher.run { return combine( localDataSource.findProduct(itemId), + localDataSource.isFavorite(itemId), localDataSource.getPunctuations(itemId) - ) { product, points -> + ) { product, isFavorite, points -> try { return@combine product?.let { if (product.id == -1L) { null } else { - CatalogDetail(product, points) + CatalogDetail( + product = product, + isFavorite = isFavorite > 0, + points = points + ) } } } catch (e: Exception) { @@ -56,4 +68,18 @@ class CatalogDetailRepository( } } + /** + * Inserts a catalog item marked as favorite. + * + * @param favoriteItem Catalog favorite item to be saved. + */ + suspend fun saveFavorite(favoriteItem: CatalogFavoriteItem) = localDataSource.insertFavoriteProduct(favoriteItem) + + /** + * Deletes a catalog item marked as favorite, using its provided id. + * + * @param catalogId Catalog item id. + */ + suspend fun deleteFavorite(catalogId: Long) = localDataSource.deleteFavorite(catalogId) + } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRoute.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRoute.kt index 1b47863..49de7f8 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRoute.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRoute.kt @@ -22,6 +22,8 @@ import org.koin.androidx.compose.koinViewModel * @author marlonlom * * @param appState Application ui state. + * @param appContentCallbacks Application content callbacks. + * @param isRouting True/False if should navigate through routing. * @param catalogId Selected catalog item id. * @param viewModel Catalog detail viewmodel. */ @@ -48,7 +50,8 @@ fun CatalogDetailRoute( CatalogDetailRouteScreen( appState = appState, appContentCallbacks = appContentCallbacks, - isRouting = isRouting, detailUiState = detailUiState, + isRouting = isRouting, + onCatalogItemFavoriteChanged = viewModel::toggleFavorite, ) } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModel.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModel.kt index f296be4..f4250f9 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModel.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModel.kt @@ -8,6 +8,8 @@ package dev.marlonlom.apps.cappajv.features.catalog_detail import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import dev.marlonlom.apps.cappajv.core.database.entities.CatalogFavoriteItem +import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState.Found import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState.NotFound import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -15,6 +17,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** * Catalog detail view model class. @@ -51,10 +54,41 @@ class CatalogDetailViewModel( savedStateHandle[CATALOG_DETAIL_ID_KEY] = itemId } + /** + * Handles favorite state for catalog detail item. + * + * @param product Catalog detail item. + * @param isFavorite True/False for catalog detail item to be marked as favorite. + */ + fun toggleFavorite( + product: CatalogItem, + isFavorite: Boolean + ) { + viewModelScope.launch { + if (isFavorite) { + val favoriteItem = product.let { + CatalogFavoriteItem( + it.id, + it.title, + it.picture, + it.category, + it.samplePunctuation, + it.punctuationsCount + ) + } + repository.saveFavorite(favoriteItem) + } else { + repository.deleteFavorite(product.id) + } + } + } + companion object { + /** Constant for default catalog item id. */ private const val NO_CATALOG_ID = 0L + /** Constant for default catalog item id key. */ private const val CATALOG_DETAIL_ID_KEY = "CATALOG_DETAIL_ID" } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/parts/CatalogDetailButtonsBar.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/parts/CatalogDetailButtonsBar.kt index 7fdbf65..73f145a 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/parts/CatalogDetailButtonsBar.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/parts/CatalogDetailButtonsBar.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material.icons.rounded.FavoriteBorder import androidx.compose.material.icons.rounded.Share import androidx.compose.material3.Icon @@ -35,13 +36,17 @@ import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState * * @param appState Application ui state. * @param appContentCallbacks Application content callbacks. - * @param product Catalog detailed information + * @param product Catalog detailed information. + * @param isFavorite True/False if catalog detail item is favorite. + * @param onCatalogItemFavoriteChanged Action for catalog detail item favorite state changed. */ @Composable fun CatalogDetailButtonsBar( appState: CappajvAppState, appContentCallbacks: AppContentCallbacks, product: CatalogItem, + isFavorite: Boolean, + onCatalogItemFavoriteChanged: (CatalogItem, Boolean) -> Unit, ) { Row( modifier = Modifier @@ -50,10 +55,13 @@ fun CatalogDetailButtonsBar( verticalAlignment = Alignment.CenterVertically, ) { OutlinedButton( - onClick = { /*TODO*/ }, + onClick = { onCatalogItemFavoriteChanged(product, !isFavorite) }, shape = MaterialTheme.shapes.small, ) { - Icon(imageVector = Icons.Rounded.FavoriteBorder, contentDescription = null) + Icon( + imageVector = if (isFavorite) Icons.Rounded.Favorite else Icons.Rounded.FavoriteBorder, + contentDescription = null + ) Spacer(modifier = Modifier.width(10.dp)) Text(text = stringResource(id = R.string.text_catalog_detail_button_favorite)) } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/screens/CatalogDetailRouteScreen.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/screens/CatalogDetailRouteScreen.kt index ec9ec44..88c0540 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/screens/CatalogDetailRouteScreen.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/screens/CatalogDetailRouteScreen.kt @@ -7,22 +7,36 @@ package dev.marlonlom.apps.cappajv.features.catalog_detail.screens import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable +import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState import dev.marlonlom.apps.cappajv.ui.main.AppContentCallbacks import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState +/** + * Catalog detail route screen content composable ui. + * + * @author marlonlom + * + * @param appState Application ui state. + * @param appContentCallbacks Application content callbacks. + * @param detailUiState Catalog detail ui state. + * @param isRouting True/False if should navigate through routing. + * @param onCatalogItemFavoriteChanged Action for catalog detail item favorite state changed. + */ @ExperimentalFoundationApi @Composable fun CatalogDetailRouteScreen( appState: CappajvAppState, appContentCallbacks: AppContentCallbacks, + detailUiState: CatalogDetailUiState, isRouting: Boolean, - detailUiState: CatalogDetailUiState + onCatalogItemFavoriteChanged: (CatalogItem, Boolean) -> Unit, ) = when { else -> DefaultPortraitCatalogDetailScreen( appState = appState, appContentCallbacks = appContentCallbacks, detailUiState = detailUiState, - isRouting = isRouting + isRouting = isRouting, + onCatalogItemFavoriteChanged = onCatalogItemFavoriteChanged ) } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/screens/DefaultPortraitCatalogDetailScreen.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/screens/DefaultPortraitCatalogDetailScreen.kt index 31bc53d..b8554fc 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/screens/DefaultPortraitCatalogDetailScreen.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/screens/DefaultPortraitCatalogDetailScreen.kt @@ -17,13 +17,22 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState import dev.marlonlom.apps.cappajv.features.catalog_detail.parts.CatalogDetailTopBar import dev.marlonlom.apps.cappajv.features.catalog_detail.slots.CatalogDetailResultsSlot import dev.marlonlom.apps.cappajv.ui.main.AppContentCallbacks import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState - +/** + * @author marlonlom + * + * @param appState Application ui state. + * @param appContentCallbacks Application content callbacks. + * @param detailUiState Catalog detail ui state. + * @param isRouting True/False if should navigate through routing. + * @param onCatalogItemFavoriteChanged Action for catalog detail item favorite state changed. + */ @ExperimentalFoundationApi @Composable fun DefaultPortraitCatalogDetailScreen( @@ -31,6 +40,7 @@ fun DefaultPortraitCatalogDetailScreen( appContentCallbacks: AppContentCallbacks, detailUiState: CatalogDetailUiState, isRouting: Boolean, + onCatalogItemFavoriteChanged: (CatalogItem, Boolean) -> Unit, ) { val contentHorizontalPadding = when { appState.isLandscape.not().and(appState.isMediumWidth) -> 40.dp @@ -46,6 +56,11 @@ fun DefaultPortraitCatalogDetailScreen( horizontalAlignment = Alignment.CenterHorizontally, ) { CatalogDetailTopBar(appState, isRouting) - CatalogDetailResultsSlot(appState, appContentCallbacks, detailUiState) + CatalogDetailResultsSlot( + appState = appState, + appContentCallbacks = appContentCallbacks, + detailUiState = detailUiState, + onCatalogItemFavoriteChanged = onCatalogItemFavoriteChanged + ) } } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/CatalogDetailResultsSlot.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/CatalogDetailResultsSlot.kt index bc06649..1e5fa03 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/CatalogDetailResultsSlot.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/CatalogDetailResultsSlot.kt @@ -8,19 +8,36 @@ package dev.marlonlom.apps.cappajv.features.catalog_detail.slots import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState import dev.marlonlom.apps.cappajv.ui.main.AppContentCallbacks import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState +/** + * Catalog detail results slot composable ui. + * + * @author marlonlom + * + * @param appState Application ui state. + * @param appContentCallbacks Application content callbacks. + * @param detailUiState Catalog detail ui state. + * @param onCatalogItemFavoriteChanged Action for catalog detail item favorite state changed. + */ @ExperimentalFoundationApi @Composable fun CatalogDetailResultsSlot( appState: CappajvAppState, appContentCallbacks: AppContentCallbacks, detailUiState: CatalogDetailUiState, + onCatalogItemFavoriteChanged: (CatalogItem, Boolean) -> Unit, ) = when (detailUiState) { is CatalogDetailUiState.Found -> { - FoundCatalogDetailSlot(appState, appContentCallbacks, detailUiState) + FoundCatalogDetailSlot( + appState = appState, + appContentCallbacks = appContentCallbacks, + detailUiState = detailUiState, + onCatalogItemFavoriteChanged = onCatalogItemFavoriteChanged + ) } CatalogDetailUiState.NotFound -> Text("Select from list for view its detailed information.") diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/FoundCatalogDetailHeadingSlot.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/FoundCatalogDetailHeadingSlot.kt index e587a69..8cdc283 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/FoundCatalogDetailHeadingSlot.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/FoundCatalogDetailHeadingSlot.kt @@ -25,12 +25,16 @@ import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState * @param appState Application ui state. * @param appContentCallbacks Application content callbacks. * @param product Catalog detailed information. + * @param isFavorite True/False if catalog detail item is favorite. + * @param onCatalogItemFavoriteChanged Action for catalog detail item favorite state changed. */ @Composable fun FoundCatalogDetailHeadingSlot( appState: CappajvAppState, appContentCallbacks: AppContentCallbacks, product: CatalogItem, + isFavorite: Boolean, + onCatalogItemFavoriteChanged: (CatalogItem, Boolean) -> Unit, ) = Column( horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -42,8 +46,10 @@ fun FoundCatalogDetailHeadingSlot( CatalogDetailProductCategoryText(product) CatalogDetailButtonsBar( appState = appState, - product = product, appContentCallbacks = appContentCallbacks, + product = product, + isFavorite = isFavorite, + onCatalogItemFavoriteChanged = onCatalogItemFavoriteChanged, ) CatalogDetailProductDescriptionText(product) } diff --git a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/FoundCatalogDetailSlot.kt b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/FoundCatalogDetailSlot.kt index 0676148..3c672df 100644 --- a/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/FoundCatalogDetailSlot.kt +++ b/apps/mobile-app/src/main/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/slots/FoundCatalogDetailSlot.kt @@ -7,6 +7,7 @@ package dev.marlonlom.apps.cappajv.features.catalog_detail.slots import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable +import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState import dev.marlonlom.apps.cappajv.ui.main.AppContentCallbacks import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState @@ -17,7 +18,9 @@ import dev.marlonlom.apps.cappajv.ui.main.CappajvAppState * @author marlonlom * * @param appState Application ui state. + * @param appContentCallbacks Application content callbacks. * @param detailUiState Found catalog item information. + * @param onCatalogItemFavoriteChanged Action for catalog detail item favorite state changed. */ @ExperimentalFoundationApi @Composable @@ -25,8 +28,15 @@ fun FoundCatalogDetailSlot( appState: CappajvAppState, appContentCallbacks: AppContentCallbacks, detailUiState: CatalogDetailUiState.Found, + onCatalogItemFavoriteChanged: (CatalogItem, Boolean) -> Unit, ) { - val (product, points) = detailUiState.detail - FoundCatalogDetailHeadingSlot(appState, appContentCallbacks, product) + val (product, isFavorite, points) = detailUiState.detail + FoundCatalogDetailHeadingSlot( + appState = appState, + appContentCallbacks = appContentCallbacks, + product = product, + isFavorite = isFavorite, + onCatalogItemFavoriteChanged = onCatalogItemFavoriteChanged + ) FoundCatalogDetailPointsSlot(appState, points) } diff --git a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/FakeLocalDataSource.kt b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/FakeLocalDataSource.kt index 7c5f06a..2948c14 100644 --- a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/FakeLocalDataSource.kt +++ b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/FakeLocalDataSource.kt @@ -111,8 +111,8 @@ internal class FakeLocalDataSource( override fun insertAllProducts(vararg products: CatalogItem) = Unit - override fun insertAllFavoriteProducts(vararg favoriteItems: CatalogFavoriteItem) { - localFavoriteItems.addAll(favoriteItems) + override suspend fun insertFavoriteProduct(favoriteItem: CatalogFavoriteItem) { + localFavoriteItems.add(favoriteItem) } override fun insertAllPunctuations(vararg punctuations: CatalogPunctuation) = Unit @@ -127,18 +127,8 @@ internal class FakeLocalDataSource( } override fun deleteAllPunctuations() = Unit + override fun isFavorite(productId: Long): Flow = flowOf( + localFavoriteItems.count { it.id == productId } + ) - companion object { - val NONE = CatalogItem( - id = -1, - title = "", - slug = "", - titleNormalized = "", - picture = "", - category = "", - detail = "", - samplePunctuation = "", - punctuationsCount = 0 - ) - } } diff --git a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepositoryTest.kt b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepositoryTest.kt index 5d82794..d4708f8 100644 --- a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepositoryTest.kt +++ b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailRepositoryTest.kt @@ -7,14 +7,17 @@ package dev.marlonlom.apps.cappajv.features.catalog_detail import dev.marlonlom.apps.cappajv.core.catalog_source.CatalogDataService import dev.marlonlom.apps.cappajv.core.database.FakeLocalDataSource +import dev.marlonlom.apps.cappajv.core.database.entities.CatalogFavoriteItem import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem import dev.marlonlom.apps.cappajv.ui.util.slug import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue +import org.junit.Assert.fail import org.junit.Before import org.junit.Test import java.util.Locale @@ -57,4 +60,45 @@ internal class CatalogDetailRepositoryTest { assertNull(foundProduct) } + @Test + fun `Should mark single product item as favorite`() = runTest { + val favoriteItem = CatalogFavoriteItem( + id = 15396L, + title = "Granizado", + picture = "https://juanvaldez.com/wp-content/uploads/2022/10/Granizado-juan-Valdez.jpg", + category = "Category one", + samplePunctuation = "", + punctuationsCount = 0, + ) + repository.saveFavorite(favoriteItem) + val foundProduct = repository.find(favoriteItem.id).first() + assertNotNull(foundProduct) + if (foundProduct != null) { + assertTrue(foundProduct.isFavorite) + } else { + fail() + } + } + + @Test + fun `Should mark single product item as not favorite after marking it`() = runTest { + val favoriteItem = CatalogFavoriteItem( + id = 15396L, + title = "Granizado", + picture = "https://juanvaldez.com/wp-content/uploads/2022/10/Granizado-juan-Valdez.jpg", + category = "Category one", + samplePunctuation = "", + punctuationsCount = 0, + ) + repository.saveFavorite(favoriteItem) + repository.deleteFavorite(favoriteItem.id) + val foundProduct = repository.find(favoriteItem.id).first() + assertNotNull(foundProduct) + if (foundProduct != null) { + assertFalse(foundProduct.isFavorite) + } else { + fail() + } + } + } diff --git a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModelTest.kt b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModelTest.kt index daaacfa..09f36e7 100644 --- a/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModelTest.kt +++ b/apps/mobile-app/src/test/kotlin/dev/marlonlom/apps/cappajv/features/catalog_detail/CatalogDetailViewModelTest.kt @@ -8,8 +8,10 @@ package dev.marlonlom.apps.cappajv.features.catalog_detail import dev.marlonlom.apps.cappajv.core.catalog_source.CatalogDataService import dev.marlonlom.apps.cappajv.core.database.FakeLocalDataSource import dev.marlonlom.apps.cappajv.core.database.datasource.LocalDataSource +import dev.marlonlom.apps.cappajv.core.database.entities.CatalogItem import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState.Found import dev.marlonlom.apps.cappajv.features.catalog_detail.CatalogDetailUiState.NotFound +import dev.marlonlom.apps.cappajv.ui.util.slug import dev.marlonlom.apps.cappajv.util.MainDispatcherRule import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -23,7 +25,9 @@ import org.junit.Rule import org.junit.Test import org.mockito.ArgumentMatchers.anyLong import org.mockito.MockitoAnnotations +import org.mockito.kotlin.doSuspendableAnswer import org.mockito.kotlin.mock +import org.mockito.kotlin.stub import java.util.Locale @ExperimentalCoroutinesApi @@ -43,6 +47,17 @@ internal class CatalogDetailViewModelTest { on(mock.getPunctuations(anyLong())).thenAnswer { fakeLocalDataSource.getPunctuations(it.getArgument(0)) } + on(mock.isFavorite(anyLong())).thenAnswer { + fakeLocalDataSource.isFavorite(it.getArgument(0)) + } + mock.stub { + onBlocking { mock.insertFavoriteProduct(org.mockito.kotlin.any()) }.doSuspendableAnswer { + fakeLocalDataSource.insertFavoriteProduct(it.getArgument(0)) + } + onBlocking { mock.deleteFavorite(anyLong()) }.doSuspendableAnswer { + fakeLocalDataSource.deleteFavorite(it.getArgument(0)) + } + } } private lateinit var viewModel: CatalogDetailViewModel @@ -81,4 +96,31 @@ internal class CatalogDetailViewModelTest { assertNotNull(detailState) assertTrue(detailState == NotFound) } + + @Test + fun `Should mark single product item as favorite`() = runTest { + val catalogItem = CatalogItem( + id = 15396L, + title = "Granizado", + slug = "Granizado".slug, + titleNormalized = "Granizado".slug.replace("-", " "), + picture = "https://juanvaldez.com/wp-content/uploads/2022/10/Granizado-juan-Valdez.jpg", + category = "Category one", + detail = ",", + samplePunctuation = "", + punctuationsCount = 0, + ) + viewModel.toggleFavorite(catalogItem, true) + viewModel.find(catalogItem.id) + when (val detailState = viewModel.detail.value) { + is Found -> { + assertNotNull(detailState.detail.product) + assertEquals(catalogItem.id, detailState.detail.product.id) + assertTrue(detailState.detail.points.isNotEmpty()) + assertTrue(detailState.detail.isFavorite) + } + + NotFound -> fail("Not found") + } + } } diff --git a/features/core/database/src/androidTest/kotlin/dev/marlonlom/apps/cappajv/core/database/CatalogFavoriteItemsDaoTest.kt b/features/core/database/src/androidTest/kotlin/dev/marlonlom/apps/cappajv/core/database/CatalogFavoriteItemsDaoTest.kt index 329f966..b292186 100644 --- a/features/core/database/src/androidTest/kotlin/dev/marlonlom/apps/cappajv/core/database/CatalogFavoriteItemsDaoTest.kt +++ b/features/core/database/src/androidTest/kotlin/dev/marlonlom/apps/cappajv/core/database/CatalogFavoriteItemsDaoTest.kt @@ -59,7 +59,7 @@ internal class CatalogFavoriteItemsDaoTest { samplePunctuation = "", punctuationsCount = 0, ) - dao.insertAll(entity) + dao.insert(entity) val list = dao.getFavoriteItems().first() Truth.assertThat(list).contains(entity) } @@ -74,7 +74,7 @@ internal class CatalogFavoriteItemsDaoTest { samplePunctuation = "", punctuationsCount = 0, ) - dao.insertAll(entity) + dao.insert(entity) dao.deleteAll() val list = dao.getFavoriteItems().first() Truth.assertThat(list).isEmpty() @@ -101,11 +101,33 @@ internal class CatalogFavoriteItemsDaoTest { punctuationsCount = 0, ) ) - dao.insertAll(*entities) + entities.forEach { + dao.insert(it) + } dao.delete(2L) val list = dao.getFavoriteItems().first() Truth.assertThat(list).isNotEmpty() Truth.assertThat(list).contains(remainingItem) } + @Test + fun shouldInsertThenFailCheckThatIsFavoriteItem() = runBlocking { + val actual = dao.isFavorite(1234L).first() + Truth.assertThat(actual).isEqualTo(0) + } + + @Test + fun shouldInsertThenSuccessCheckThatIsFavoriteItem() = runBlocking { + val entity = CatalogFavoriteItem( + id = 1L, + title = "Pod", + picture = "https://noimage.no.com/no.png", + category = "CategoryOne", + samplePunctuation = "", + punctuationsCount = 0, + ) + dao.insert(entity) + val actual = dao.isFavorite(entity.id).first() + Truth.assertThat(actual).isEqualTo(1) + } } diff --git a/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/dao/CatalogFavoriteItemsDao.kt b/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/dao/CatalogFavoriteItemsDao.kt index 3425d78..fc530b8 100644 --- a/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/dao/CatalogFavoriteItemsDao.kt +++ b/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/dao/CatalogFavoriteItemsDao.kt @@ -6,6 +6,7 @@ package dev.marlonlom.apps.cappajv.core.database.dao import androidx.room.Dao +import androidx.room.Insert import androidx.room.Query import androidx.room.Upsert import dev.marlonlom.apps.cappajv.core.database.entities.CatalogFavoriteItem @@ -29,12 +30,12 @@ interface CatalogFavoriteItemsDao { fun getFavoriteItems(): Flow> /** - * Upsert product items. + * Inserts catalog product item. * - * @param products product items as typed array. + * @param product Catalog product item. */ - @Upsert - fun insertAll(vararg products: CatalogFavoriteItem) + @Insert + suspend fun insert(product: CatalogFavoriteItem) /** * Deletes all product items in local storage. @@ -50,4 +51,13 @@ interface CatalogFavoriteItemsDao { @Query("DELETE FROM catalog_item_favorite WHERE id = :productId") suspend fun delete(productId: Long) + /** + * Returns 1 if a product with the provided ID exists as a favorite, otherwise returns 0. + * + * @param productId Product item id. + * @return Number that indicates if product id exists as favorite, as Flow. + */ + @Query("SELECT COUNT(f.id) FROM catalog_item_favorite f WHERE f.id = :productId") + fun isFavorite(productId: Long): Flow + } diff --git a/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/LocalDataSource.kt b/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/LocalDataSource.kt index c958e79..8613987 100644 --- a/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/LocalDataSource.kt +++ b/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/LocalDataSource.kt @@ -65,9 +65,9 @@ interface LocalDataSource { /** * Insert all favorite catalog products. * - * @param favoriteItems Favorite catalog products as typed array. + * @param favoriteItem Favorite catalog products as typed array. */ - fun insertAllFavoriteProducts(vararg favoriteItems: CatalogFavoriteItem) + suspend fun insertFavoriteProduct(favoriteItem: CatalogFavoriteItem) /** * Insert all punctuations. @@ -98,6 +98,13 @@ interface LocalDataSource { */ fun deleteAllPunctuations() + /** + * Returns 1 if a product with the provided ID exists as a favorite, otherwise returns 0. + * + * @param productId Product item id. + * @return Number that indicates if product id exists as favorite, as Flow. + */ + fun isFavorite(productId: Long): Flow } diff --git a/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/LocalDataSourceImpl.kt b/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/LocalDataSourceImpl.kt index 9033439..fc12c89 100644 --- a/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/LocalDataSourceImpl.kt +++ b/features/core/database/src/main/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/LocalDataSourceImpl.kt @@ -38,8 +38,8 @@ class LocalDataSourceImpl( override fun getPunctuations(productId: Long) = catalogPunctuationsDao.findByProduct(productId) - override fun insertAllFavoriteProducts(vararg favoriteItems: CatalogFavoriteItem) = - catalogFavoriteItemsDao.insertAll(*favoriteItems) + override suspend fun insertFavoriteProduct(favoriteItem: CatalogFavoriteItem) = + catalogFavoriteItemsDao.insert(favoriteItem) override fun getFavorites(): Flow> = catalogFavoriteItemsDao.getFavoriteItems() @@ -60,4 +60,6 @@ class LocalDataSourceImpl( override fun deleteAllPunctuations() = catalogPunctuationsDao.deleteAll() + override fun isFavorite(productId: Long): Flow = catalogFavoriteItemsDao.isFavorite(productId) + } diff --git a/features/core/database/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/dao/FakeCatalogFavoriteItemsDao.kt b/features/core/database/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/dao/FakeCatalogFavoriteItemsDao.kt index 9dd997a..be31c46 100644 --- a/features/core/database/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/dao/FakeCatalogFavoriteItemsDao.kt +++ b/features/core/database/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/dao/FakeCatalogFavoriteItemsDao.kt @@ -22,8 +22,8 @@ internal class FakeCatalogFavoriteItemsDao( override fun getFavoriteItems(): Flow> = flowOf(list) - override fun insertAll(vararg products: CatalogFavoriteItem) { - list.addAll(products) + override suspend fun insert(product: CatalogFavoriteItem) { + list.add(product) } override fun deleteAll() { @@ -33,4 +33,6 @@ internal class FakeCatalogFavoriteItemsDao( override suspend fun delete(productId: Long) { list.removeIf { it.id == productId } } + + override fun isFavorite(productId: Long): Flow = flowOf(list.count { it.id == productId }) } diff --git a/features/core/database/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/CatalogFavoritesLocalDataSourceTest.kt b/features/core/database/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/CatalogFavoritesLocalDataSourceTest.kt index 385d397..8508b4f 100644 --- a/features/core/database/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/CatalogFavoritesLocalDataSourceTest.kt +++ b/features/core/database/src/test/kotlin/dev/marlonlom/apps/cappajv/core/database/datasource/CatalogFavoritesLocalDataSourceTest.kt @@ -11,6 +11,7 @@ import dev.marlonlom.apps.cappajv.core.database.dao.FakeCatalogPunctuationsDao import dev.marlonlom.apps.cappajv.core.database.dao.FakeCatalogSearchDao import dev.marlonlom.apps.cappajv.core.database.entities.CatalogFavoriteItem import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -50,7 +51,7 @@ internal class CatalogFavoritesLocalDataSourceTest { samplePunctuation = "", punctuationsCount = 0, ) - dataSource.insertAllFavoriteProducts(product) + dataSource.insertFavoriteProduct(product) dataSource.getFavorites().collect { items -> assertNotNull(items) assertTrue(items.isNotEmpty()) @@ -73,7 +74,7 @@ internal class CatalogFavoritesLocalDataSourceTest { samplePunctuation = "", punctuationsCount = 0, ) - dataSource.insertAllFavoriteProducts(product) + dataSource.insertFavoriteProduct(product) dataSource.deleteAllFavorites() dataSource.getFavorites() .filter { list -> list.indexOfFirst { it.id == 1L } >= 0 } @@ -102,7 +103,9 @@ internal class CatalogFavoritesLocalDataSourceTest { punctuationsCount = 0, ) ) - dataSource.insertAllFavoriteProducts(*products) + products.forEach { + dataSource.insertFavoriteProduct(it) + } dataSource.deleteFavorite(2L) dataSource.getFavorites() .collect { list -> @@ -111,4 +114,25 @@ internal class CatalogFavoritesLocalDataSourceTest { } } + @Test + fun `Should insert then fail check that is favorite item`() = runBlocking { + val actual = dataSource.isFavorite(1234L).first() + assertEquals(0, actual) + } + + @Test + fun shouldInsertThenSuccessCheckThatIsFavoriteItem() = runBlocking { + val entity = CatalogFavoriteItem( + id = 1L, + title = "Pod", + picture = "https://noimage.no.com/no.png", + category = "CategoryOne", + samplePunctuation = "", + punctuationsCount = 0, + ) + dataSource.insertFavoriteProduct(entity) + val actual = dataSource.isFavorite(entity.id).first() + assertEquals(1, actual) + } + }