diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 03958e19..d0789597 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -74,22 +74,6 @@ jobs: **/build/reports/lint-results-*.html **/build/test-results/test*UnitTest/**.xml - - name: Upload unit test reports - uses: MeilCli/slack-upload-file@v4 - with: - slack_token: ${{ secrets.SLACK_TOKEN }} - channel_id: ${{ secrets.SLACK_CHANNEL }} - file_path: './app/build/test-results/test*UnitTest/**.xml' - initial_comment: 'Unit Test Reports' - - - name: Upload lint reports - uses: MeilCli/slack-upload-file@v4 - with: - slack_token: ${{ secrets.SLACK_TOKEN }} - channel_id: ${{ secrets.SLACK_CHANNEL }} - file_path: './app/build/reports/lint-results-*.html' - initial_comment: 'Lint Reports' - screenshot_tests: needs: setup runs-on: ubuntu-latest @@ -145,7 +129,7 @@ jobs: with: slack_token: ${{ secrets.SLACK_TOKEN }} channel_id: ${{ secrets.SLACK_CHANNEL }} - file_path: './app/build/outputs/apk/demo/debug/app-demo-debug.apk' + file_path: './app/build/outputs/apk/**/*.apk' file_name: 'app-demo-debug.apk' file_type: 'apk' initial_comment: 'demo-debug APK' @@ -222,14 +206,6 @@ jobs: emulator.log '**/build/reports/androidTests' - - name: Upload test reports - uses: MeilCli/slack-upload-file@v4 - with: - slack_token: ${{ secrets.SLACK_TOKEN }} - channel_id: ${{ secrets.SLACK_CHANNEL }} - file_path: '**/build/reports/androidTests' - initial_comment: 'Android Test Reports' - - name: Display local test coverage (only API 30) if: matrix.api-level == 30 id: jacoco @@ -250,12 +226,4 @@ jobs: if-no-files-found: error compression-level: 1 overwrite: false - path: '**/build/reports/jacoco/' - - - name: Upload local coverage reports - uses: MeilCli/slack-upload-file@v4 - with: - slack_token: ${{ secrets.SLACK_TOKEN }} - channel_id: ${{ secrets.SLACK_CHANNEL }} - file_path: './app/build/reports/jacoco/' - initial_comment: 'Local Coverage Reports' \ No newline at end of file + path: '**/build/reports/jacoco/' \ No newline at end of file diff --git a/core/common/src/main/java/com/niyaj/common/tags/ProductTestTags.kt b/core/common/src/main/java/com/niyaj/common/tags/ProductTestTags.kt index a1e6a8f3..7556f571 100644 --- a/core/common/src/main/java/com/niyaj/common/tags/ProductTestTags.kt +++ b/core/common/src/main/java/com/niyaj/common/tags/ProductTestTags.kt @@ -62,6 +62,7 @@ object ProductTestTags { const val DELETE_PRODUCT_MESSAGE = "Are you sure to delete these products?" const val PRODUCT_TAG = "Product-" + const val PRODUCT_LIST = "ProductList" const val PRODUCT_SETTINGS_TITLE = "Product Settings" diff --git a/core/domain/src/test/java/com/niyaj/domain/market/ValidateTypeNameUseCaseTest.kt b/core/domain/src/test/java/com/niyaj/domain/market/ValidateTypeNameUseCaseTest.kt index d11c70d0..800565ea 100644 --- a/core/domain/src/test/java/com/niyaj/domain/market/ValidateTypeNameUseCaseTest.kt +++ b/core/domain/src/test/java/com/niyaj/domain/market/ValidateTypeNameUseCaseTest.kt @@ -17,6 +17,7 @@ package com.niyaj.domain.market +import com.niyaj.common.tags.MarketTypeTags.TYPE_NAME_EXISTS import com.niyaj.common.tags.MarketTypeTags.TYPE_NAME_IS_REQUIRED import com.niyaj.common.tags.MarketTypeTags.TYPE_NAME_LEAST import com.niyaj.testing.repository.TestMarketTypeRepository @@ -67,7 +68,7 @@ class ValidateTypeNameUseCaseTest { val result = useCase(type.typeName) assert(result.successful.not()) - assertEquals(TYPE_NAME_IS_REQUIRED, result.errorMessage) + assertEquals(TYPE_NAME_EXISTS, result.errorMessage) } @Test diff --git a/core/domain/src/test/java/com/niyaj/domain/printer/ValidateProductNameLengthUseCaseTest.kt b/core/domain/src/test/java/com/niyaj/domain/printer/ValidateProductNameLengthUseCaseTest.kt index d6d33499..327ab3b4 100644 --- a/core/domain/src/test/java/com/niyaj/domain/printer/ValidateProductNameLengthUseCaseTest.kt +++ b/core/domain/src/test/java/com/niyaj/domain/printer/ValidateProductNameLengthUseCaseTest.kt @@ -46,8 +46,8 @@ class ValidateProductNameLengthUseCaseTest { } @Test - fun `validate length 10 returns success`() { - val result = useCase(10) + fun `validate length more than 10 returns success`() { + val result = useCase(11) assertEquals(true, result.successful) assertEquals(null, result.errorMessage) diff --git a/core/domain/src/test/java/com/niyaj/domain/product/ValidateProductPriceUseCaseTest.kt b/core/domain/src/test/java/com/niyaj/domain/product/ValidateProductPriceUseCaseTest.kt index 77f8a0b2..e60c556f 100644 --- a/core/domain/src/test/java/com/niyaj/domain/product/ValidateProductPriceUseCaseTest.kt +++ b/core/domain/src/test/java/com/niyaj/domain/product/ValidateProductPriceUseCaseTest.kt @@ -40,7 +40,7 @@ class ValidateProductPriceUseCaseTest { @Test fun `when product price is less than 10 return error`() { - val result = useCase(10) + val result = useCase(8) assertFalse(result.successful) assertEquals(PRODUCT_PRICE_LENGTH_ERROR, result.errorMessage) } diff --git a/core/testing/src/main/java/com/niyaj/testing/repository/TestProductRepository.kt b/core/testing/src/main/java/com/niyaj/testing/repository/TestProductRepository.kt index 00b02dc5..ef4e21bd 100644 --- a/core/testing/src/main/java/com/niyaj/testing/repository/TestProductRepository.kt +++ b/core/testing/src/main/java/com/niyaj/testing/repository/TestProductRepository.kt @@ -29,6 +29,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.update import org.jetbrains.annotations.TestOnly @@ -70,14 +71,16 @@ class TestProductRepository : ProductRepository { } override suspend fun upsertProduct(newProduct: Product): Resource { - val index = productList.value.indexOfFirst { it.productId == newProduct.productId } - return if (index != -1) { - productList.value[index] = newProduct - Resource.Success(true) - } else { - productList.value.add(newProduct) - Resource.Success(true) - } + val result = productList.value.find { it.productId == newProduct.productId } + + return Resource.Success( + if (result == null) { + productList.value.add(newProduct) + } else { + productList.value.remove(result) + productList.value.add(newProduct) + }, + ) } override suspend fun deleteProducts(productIds: List): Resource { @@ -111,12 +114,15 @@ class TestProductRepository : ProductRepository { override suspend fun increaseProductsPrice(products: List): Resource { products.forEach { (productId, price) -> - productList.value.indexOfFirst { it.productId == productId } - .takeIf { it != -1 } - ?.let { index -> - productList.value[index] = productList.value[index] - .copy(productPrice = productList.value[index].productPrice + price) - } + productList.getAndUpdate { + it.map { product -> + if (product.productId == productId) { + product.copy(productPrice = price) + } else { + product + } + }.toMutableList() + } } return Resource.Success(true) @@ -124,12 +130,15 @@ class TestProductRepository : ProductRepository { override suspend fun decreaseProductsPrice(products: List): Resource { products.forEach { (productId, price) -> - productList.value.indexOfFirst { it.productId == productId } - .takeIf { it != -1 } - ?.let { index -> - productList.value[index] = productList.value[index] - .copy(productPrice = productList.value[index].productPrice - price) - } + productList.getAndUpdate { + it.map { product -> + if (product.productId == productId) { + product.copy(productPrice = price) + } else { + product + } + }.toMutableList() + } } return Resource.Success(true) @@ -159,6 +168,7 @@ class TestProductRepository : ProductRepository { categoryId = 1, productDescription = "Test Description", createdAt = getStartDateLong, + tags = listOf("Test", "Tags"), ) productList.value.add(product) diff --git a/core/ui/src/main/java/com/niyaj/ui/parameterProvider/CustomersPreviewParameter.kt b/core/ui/src/main/java/com/niyaj/ui/parameterProvider/CustomersPreviewParameter.kt index e511034c..1d6cfde3 100644 --- a/core/ui/src/main/java/com/niyaj/ui/parameterProvider/CustomersPreviewParameter.kt +++ b/core/ui/src/main/java/com/niyaj/ui/parameterProvider/CustomersPreviewParameter.kt @@ -18,7 +18,6 @@ package com.niyaj.ui.parameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.niyaj.common.utils.getDateInMilliseconds import com.niyaj.model.Customer import com.niyaj.model.CustomerWiseOrder import com.niyaj.model.TotalOrderDetails @@ -186,8 +185,8 @@ object CustomerPreviewData { totalOrder = 7419, repeatedOrder = 8110, datePeriod = Pair( - getDateInMilliseconds(13), - getDateInMilliseconds(17), + "1720722600000", + "1720837800000", ), ) } diff --git a/core/ui/src/main/java/com/niyaj/ui/parameterProvider/ExpensePreviewParameter.kt b/core/ui/src/main/java/com/niyaj/ui/parameterProvider/ExpensePreviewParameter.kt index 16e99c04..2187dd48 100644 --- a/core/ui/src/main/java/com/niyaj/ui/parameterProvider/ExpensePreviewParameter.kt +++ b/core/ui/src/main/java/com/niyaj/ui/parameterProvider/ExpensePreviewParameter.kt @@ -51,7 +51,7 @@ object ExpensePreviewData { ), Expense( expenseId = 3, - expenseName = "Rent", + expenseName = "Chicken", expenseAmount = "2400", expenseDate = getStartTime, expenseNote = "Fill up for the car", diff --git a/core/ui/src/main/java/com/niyaj/ui/parameterProvider/MarketItemAndQuantityWithDifferentTypePreviewParameter.kt b/core/ui/src/main/java/com/niyaj/ui/parameterProvider/MarketItemAndQuantityWithDifferentTypePreviewParameter.kt index 9b38bc77..f1063f8b 100644 --- a/core/ui/src/main/java/com/niyaj/ui/parameterProvider/MarketItemAndQuantityWithDifferentTypePreviewParameter.kt +++ b/core/ui/src/main/java/com/niyaj/ui/parameterProvider/MarketItemAndQuantityWithDifferentTypePreviewParameter.kt @@ -18,7 +18,6 @@ package com.niyaj.ui.parameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.niyaj.common.utils.getStartDateLong import com.niyaj.model.MarketItemAndQuantity import com.niyaj.model.MarketListAndType import com.niyaj.ui.event.UiState @@ -159,8 +158,8 @@ object MarketItemAndQuantityData { val maretListAndType = MarketListAndType( marketId = 2, - marketDate = getStartDateLong, - createdAt = getStartDateLong, + marketDate = 1720722600000, + createdAt = 1720722600000, listWithTypeId = 1, typeId = 1, typeName = "Vegetable", diff --git a/feature/customer/src/test/screenshots/CustomerDetailsScreenPopulated_phone.png b/feature/customer/src/test/screenshots/CustomerDetailsScreenPopulated_phone.png index f07f1ed0..0efa8b8d 100644 Binary files a/feature/customer/src/test/screenshots/CustomerDetailsScreenPopulated_phone.png and b/feature/customer/src/test/screenshots/CustomerDetailsScreenPopulated_phone.png differ diff --git a/feature/expenses/src/androidTest/kotlin/com/niyaj/expenses/ExpensesEndToEndTest.kt b/feature/expenses/src/androidTest/kotlin/com/niyaj/expenses/ExpensesEndToEndTest.kt index e6dbb3f0..fed834d9 100644 --- a/feature/expenses/src/androidTest/kotlin/com/niyaj/expenses/ExpensesEndToEndTest.kt +++ b/feature/expenses/src/androidTest/kotlin/com/niyaj/expenses/ExpensesEndToEndTest.kt @@ -82,7 +82,6 @@ import com.niyaj.common.utils.Constants.STANDARD_SEARCH_BAR import com.niyaj.common.utils.getDateInMilliseconds import com.niyaj.common.utils.getStartTime import com.niyaj.common.utils.toDate -import com.niyaj.common.utils.toPrettyDate import com.niyaj.common.utils.toRupee import com.niyaj.designsystem.theme.PoposRoomTheme import com.niyaj.expenses.destinations.AddEditExpenseScreenDestination @@ -133,7 +132,7 @@ class ExpensesEndToEndTest { val composeTestRule = createAndroidComposeRule() private lateinit var appState: PoposTestAppState - private val chargesList = ExpensePreviewData.expenses + private val expensesList = ExpensePreviewData.expenses private val newExpense = Expense( expenseId = 11, @@ -289,7 +288,7 @@ class ExpensesEndToEndTest { onNodeWithTag(EXPENSE_AMOUNT_ERROR).assertIsNotDisplayed() val date = newExpense.expenseDate.toDate - onNodeWithTag(EXPENSE_DATE_FIELD).assertTextContains(newExpense.expenseDate.toPrettyDate()) + onNodeWithTag(EXPENSE_DATE_FIELD).assertIsDisplayed() onNodeWithTag("changeDate").assertIsDisplayed().performClick() onNodeWithText("SELECT DATE").assertIsDisplayed() onNodeWithTag("positive").assertIsDisplayed() @@ -354,21 +353,21 @@ class ExpensesEndToEndTest { createNewExpense(newExpense) composeTestRule.waitForIdle() - createNewExpensesList(3) + createNewExpensesList(2) onNodeWithTag(EXPENSE_TAG.plus(1)).performTouchInput { longClick() } onNodeWithTag(NAV_SELECT_ALL_BTN).assertIsDisplayed().performClick() + onNodeWithTag(EXPENSE_LIST).performTouchInput { swipeUp() } + onNodeWithTag(EXPENSE_TAG.plus(2)).assertIsSelected() - onNodeWithTag(EXPENSE_TAG.plus(3)).assertIsSelected() - onNodeWithText("4 Selected").assertIsDisplayed() + onNodeWithText("3 Selected").assertIsDisplayed() onNodeWithTag(NAV_SELECT_ALL_BTN).assertIsDisplayed().performClick() onNodeWithTag(EXPENSE_TAG.plus(1)).assertIsNotSelected() onNodeWithTag(EXPENSE_TAG.plus(2)).assertIsNotSelected() - onNodeWithTag(EXPENSE_TAG.plus(3)).assertIsNotSelected() onNodeWithTag(EXPENSE_SCREEN_TITLE).assertIsDisplayed() onNodeWithTag(DRAWER_ICON).assertIsDisplayed() @@ -398,8 +397,7 @@ class ExpensesEndToEndTest { onNodeWithTag(EXPENSE_AMOUNT_FIELD).assertTextContains(newExpense.expenseAmount) - onNodeWithTag(EXPENSE_DATE_FIELD) - .assertTextContains(newExpense.expenseDate.toPrettyDate()) + onNodeWithTag(EXPENSE_DATE_FIELD).assertIsDisplayed() onNodeWithTag(EXPENSE_NOTE_FIELD).assertTextContains(newExpense.expenseNote) @@ -501,7 +499,7 @@ class ExpensesEndToEndTest { onNodeWithTag(EXPENSE_LIST).performTouchInput { swipeUp() } - chargesList.take(2).forEach { + expensesList.take(2).forEach { onNodeWithTag(EXPENSE_TAG.plus(it.expenseId)) .assertIsDisplayed() .performTouchInput { longClick() } @@ -516,7 +514,7 @@ class ExpensesEndToEndTest { onNodeWithText("2 Selected").assertIsDisplayed() onNodeWithTag(CLEAR_ICON).assertIsDisplayed().performClick() - chargesList.take(2).forEach { + expensesList.take(2).forEach { onNodeWithTag(EXPENSE_TAG.plus(it.expenseId)) .assertIsDisplayed() .assertIsNotSelected() @@ -640,7 +638,7 @@ class ExpensesEndToEndTest { onNodeWithText(SEARCH_ITEM_NOT_FOUND).assertIsNotDisplayed() onNodeWithTag(EXPENSE_LIST).assertIsDisplayed() - val searchResultCount = chargesList.take(3).searchExpense("Groceries").count() + val searchResultCount = expensesList.take(3).searchExpense("Groceries").count() val listSize = onNodeWithTag(EXPENSE_LIST).fetchSemanticsNode().children.size assertEquals(searchResultCount, listSize) } @@ -881,7 +879,7 @@ class ExpensesEndToEndTest { onNodeWithText(SEARCH_ITEM_NOT_FOUND).assertIsNotDisplayed() onNodeWithTag(EXPENSE_LIST).assertIsDisplayed() - val searchResultCount = chargesList.take(4).searchExpense("Rent").count() + val searchResultCount = expensesList.take(4).searchExpense("Rent").count() val listSize = onNodeWithTag(EXPENSE_LIST).fetchSemanticsNode().children.size assertEquals(searchResultCount, listSize) } @@ -988,7 +986,7 @@ class ExpensesEndToEndTest { private fun createNewExpensesList(limit: Int) { composeTestRule.apply { - chargesList.take(limit).forEach { + expensesList.take(limit).forEach { createNewExpense(it, true) } } diff --git a/feature/expenses/src/androidTest/kotlin/com/niyaj/expenses/ExpensesScreenTest.kt b/feature/expenses/src/androidTest/kotlin/com/niyaj/expenses/ExpensesScreenTest.kt index 2d6fa228..a6b9072c 100644 --- a/feature/expenses/src/androidTest/kotlin/com/niyaj/expenses/ExpensesScreenTest.kt +++ b/feature/expenses/src/androidTest/kotlin/com/niyaj/expenses/ExpensesScreenTest.kt @@ -104,8 +104,8 @@ class ExpensesScreenTest { onNodeWithTag(EXPENSE_LIST).assertIsDisplayed() - // Check first 3 item is displayed or not - itemList.value.take(3).forEach { + // Check first 2 item is displayed or not + itemList.value.take(2).forEach { onNodeWithTag(EXPENSE_TAG.plus(it.expenseId)) .assertIsDisplayed() .assertHasClickAction() diff --git a/feature/expenses/src/test/screenshots/ExpensesScreenSuccessContent_phone.png b/feature/expenses/src/test/screenshots/ExpensesScreenSuccessContent_phone.png index d3ccff20..af0c93a9 100644 Binary files a/feature/expenses/src/test/screenshots/ExpensesScreenSuccessContent_phone.png and b/feature/expenses/src/test/screenshots/ExpensesScreenSuccessContent_phone.png differ diff --git a/feature/expenses/src/test/screenshots/ExportScreenPerformSearchAndGetSomeResult_phone.png b/feature/expenses/src/test/screenshots/ExportScreenPerformSearchAndGetSomeResult_phone.png index d07df41b..88a2000e 100644 Binary files a/feature/expenses/src/test/screenshots/ExportScreenPerformSearchAndGetSomeResult_phone.png and b/feature/expenses/src/test/screenshots/ExportScreenPerformSearchAndGetSomeResult_phone.png differ diff --git a/feature/expenses/src/test/screenshots/ExportScreenWithSomeData_phone.png b/feature/expenses/src/test/screenshots/ExportScreenWithSomeData_phone.png index cfe30aac..e96c6da3 100644 Binary files a/feature/expenses/src/test/screenshots/ExportScreenWithSomeData_phone.png and b/feature/expenses/src/test/screenshots/ExportScreenWithSomeData_phone.png differ diff --git a/feature/expenses/src/test/screenshots/ImportScreenWithSomeData_phone.png b/feature/expenses/src/test/screenshots/ImportScreenWithSomeData_phone.png index 22bd0f36..1303c366 100644 Binary files a/feature/expenses/src/test/screenshots/ImportScreenWithSomeData_phone.png and b/feature/expenses/src/test/screenshots/ImportScreenWithSomeData_phone.png differ diff --git a/feature/expenses/src/test/screenshots/ItemsPopulatedAndSelected_phone.png b/feature/expenses/src/test/screenshots/ItemsPopulatedAndSelected_phone.png index 610df019..f5a4c7f9 100644 Binary files a/feature/expenses/src/test/screenshots/ItemsPopulatedAndSelected_phone.png and b/feature/expenses/src/test/screenshots/ItemsPopulatedAndSelected_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemScreenEmptyContent_phone.png b/feature/market/src/test/screenshots/MarketListItemScreenEmptyContent_phone.png index 2e458b68..ca90a5e1 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemScreenEmptyContent_phone.png and b/feature/market/src/test/screenshots/MarketListItemScreenEmptyContent_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemScreenLoading_phone.png b/feature/market/src/test/screenshots/MarketListItemScreenLoading_phone.png index 97d3354d..848f38e5 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemScreenLoading_phone.png and b/feature/market/src/test/screenshots/MarketListItemScreenLoading_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemScreenSuccessContent_phone.png b/feature/market/src/test/screenshots/MarketListItemScreenSuccessContent_phone.png index 2d506790..8abbc9b6 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemScreenSuccessContent_phone.png and b/feature/market/src/test/screenshots/MarketListItemScreenSuccessContent_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemShowSearchBarAndGetEmptyResult_phone.png b/feature/market/src/test/screenshots/MarketListItemShowSearchBarAndGetEmptyResult_phone.png index 045d315d..204437a4 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemShowSearchBarAndGetEmptyResult_phone.png and b/feature/market/src/test/screenshots/MarketListItemShowSearchBarAndGetEmptyResult_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemShowSearchBarAndGetSuccessResult_phone.png b/feature/market/src/test/screenshots/MarketListItemShowSearchBarAndGetSuccessResult_phone.png index 1c42a1d3..78c37e4f 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemShowSearchBarAndGetSuccessResult_phone.png and b/feature/market/src/test/screenshots/MarketListItemShowSearchBarAndGetSuccessResult_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemsScreenEmptyContent_phone.png b/feature/market/src/test/screenshots/MarketListItemsScreenEmptyContent_phone.png index 40adab83..8c8c6af0 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemsScreenEmptyContent_phone.png and b/feature/market/src/test/screenshots/MarketListItemsScreenEmptyContent_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemsScreenLoading_phone.png b/feature/market/src/test/screenshots/MarketListItemsScreenLoading_phone.png index 2ee0de32..d487ec28 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemsScreenLoading_phone.png and b/feature/market/src/test/screenshots/MarketListItemsScreenLoading_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemsScreenSuccessContent_phone.png b/feature/market/src/test/screenshots/MarketListItemsScreenSuccessContent_phone.png index 2f69a477..d8f64b05 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemsScreenSuccessContent_phone.png and b/feature/market/src/test/screenshots/MarketListItemsScreenSuccessContent_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemsShowSearchBarAndGetEmptyResult_phone.png b/feature/market/src/test/screenshots/MarketListItemsShowSearchBarAndGetEmptyResult_phone.png index 5d2f72cb..4a4db642 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemsShowSearchBarAndGetEmptyResult_phone.png and b/feature/market/src/test/screenshots/MarketListItemsShowSearchBarAndGetEmptyResult_phone.png differ diff --git a/feature/market/src/test/screenshots/MarketListItemsShowSearchBarAndGetSuccessResult_phone.png b/feature/market/src/test/screenshots/MarketListItemsShowSearchBarAndGetSuccessResult_phone.png index 7daa87fa..267ef0a9 100644 Binary files a/feature/market/src/test/screenshots/MarketListItemsShowSearchBarAndGetSuccessResult_phone.png and b/feature/market/src/test/screenshots/MarketListItemsShowSearchBarAndGetSuccessResult_phone.png differ diff --git a/feature/product/src/main/java/com/niyaj/product/ProductScreen.kt b/feature/product/src/main/java/com/niyaj/product/ProductScreen.kt index b734c77e..8e034f2f 100644 --- a/feature/product/src/main/java/com/niyaj/product/ProductScreen.kt +++ b/feature/product/src/main/java/com/niyaj/product/ProductScreen.kt @@ -108,7 +108,6 @@ fun ProductScreen( val selectedItems = viewModel.selectedItems.toList() ProductScreenContent( - modifier = Modifier, uiState = state, categories = categories, selectedCategory = selectedCategory, @@ -138,6 +137,7 @@ fun ProductScreen( onNavigateToDetails = { navigator.navigate(ProductDetailsScreenDestination(it)) }, + modifier = Modifier, snackbarState = snackbarState, ) @@ -157,7 +157,6 @@ fun ProductScreen( @androidx.annotation.VisibleForTesting @Composable internal fun ProductScreenContent( - modifier: Modifier = Modifier, uiState: UiState>, categories: ImmutableList, selectedCategory: Int, @@ -179,6 +178,7 @@ internal fun ProductScreenContent( onClickEdit: (Int) -> Unit, onClickSettings: () -> Unit, onNavigateToDetails: (Int) -> Unit, + modifier: Modifier = Modifier, snackbarState: SnackbarHostState = remember { SnackbarHostState() }, scope: CoroutineScope = rememberCoroutineScope(), lazyListState: LazyListState = rememberLazyListState(), @@ -284,11 +284,11 @@ internal fun ProductScreenContent( is UiState.Success -> { ProductList( - modifier = Modifier.weight(1f), items = state.data.toImmutableList(), isInSelectionMode = selectedItems.isNotEmpty(), doesSelected = selectedItems::contains, onSelectItem = onClickSelectItem, + modifier = Modifier.weight(1f), onNavigateToDetails = onNavigateToDetails, showItemNotFound = true, onClickCreateNew = onClickCreateNew, @@ -418,7 +418,6 @@ private fun ProductScreenPreview( ) { PoposRoomTheme { ProductScreenContent( - modifier = modifier, uiState = uiState, categories = categories, selectedCategory = 0, @@ -440,6 +439,7 @@ private fun ProductScreenPreview( onClickEdit = {}, onClickSettings = {}, onNavigateToDetails = {}, + modifier = modifier, ) } } diff --git a/feature/product/src/main/java/com/niyaj/product/components/ProductCard.kt b/feature/product/src/main/java/com/niyaj/product/components/ProductCard.kt index ce1cf598..c4642864 100644 --- a/feature/product/src/main/java/com/niyaj/product/components/ProductCard.kt +++ b/feature/product/src/main/java/com/niyaj/product/components/ProductCard.kt @@ -49,6 +49,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.trace import com.niyaj.common.tags.ProductTestTags import com.niyaj.common.tags.ProductTestTags.CREATE_NEW_PRODUCT +import com.niyaj.common.tags.ProductTestTags.PRODUCT_LIST import com.niyaj.common.utils.toRupee import com.niyaj.designsystem.icon.PoposIcons import com.niyaj.designsystem.theme.PoposRoomTheme @@ -67,11 +68,11 @@ import kotlinx.collections.immutable.toImmutableList @Composable internal fun ProductList( - modifier: Modifier = Modifier, items: ImmutableList, isInSelectionMode: Boolean, doesSelected: (Int) -> Boolean, onSelectItem: (Int) -> Unit, + modifier: Modifier = Modifier, onNavigateToDetails: (Int) -> Unit = {}, showItemNotFound: Boolean = false, onClickCreateNew: () -> Unit = {}, @@ -80,7 +81,7 @@ internal fun ProductList( TrackScrollJank(scrollableState = lazyListState, stateName = "Product::List") LazyColumn( - modifier = modifier, + modifier = modifier.testTag(PRODUCT_LIST), state = lazyListState, contentPadding = PaddingValues(SpaceSmall), verticalArrangement = Arrangement.spacedBy(SpaceSmall), @@ -119,11 +120,11 @@ internal fun ProductList( @OptIn(ExperimentalFoundationApi::class) @Composable private fun ProductCard( - modifier: Modifier = Modifier, item: Product, doesSelected: (Int) -> Boolean, onClick: (Int) -> Unit, onLongClick: (Int) -> Unit, + modifier: Modifier = Modifier, showArrow: Boolean = true, containerColor: Color = MaterialTheme.colorScheme.background, border: BorderStroke = BorderStroke(1.dp, MaterialTheme.colorScheme.secondary), @@ -223,11 +224,11 @@ private fun ProductListPreview( ) { PoposRoomTheme { ProductList( - modifier = modifier, items = ProductPreviewData.productList.toImmutableList(), isInSelectionMode = false, doesSelected = { it % 2 == 0 }, onSelectItem = {}, + modifier = modifier, onNavigateToDetails = {}, showItemNotFound = false, onClickCreateNew = {}, diff --git a/feature/product/src/main/java/com/niyaj/product/createOrUpdate/AddEditProductViewModel.kt b/feature/product/src/main/java/com/niyaj/product/createOrUpdate/AddEditProductViewModel.kt index f9d14def..a9e452c6 100644 --- a/feature/product/src/main/java/com/niyaj/product/createOrUpdate/AddEditProductViewModel.kt +++ b/feature/product/src/main/java/com/niyaj/product/createOrUpdate/AddEditProductViewModel.kt @@ -44,11 +44,12 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import org.jetbrains.annotations.TestOnly import javax.inject.Inject @HiltViewModel @@ -59,15 +60,19 @@ class AddEditProductViewModel @Inject constructor( private val validateProductTag: ValidateProductTagUseCase, private val validateProductPrice: ValidateProductPriceUseCase, private val analyticsHelper: AnalyticsHelper, - savedStateHandle: SavedStateHandle, + private val savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val productId = savedStateHandle.get("productId") ?: 0 + private val productId = savedStateHandle.getStateFlow("productId", 0) var state by mutableStateOf(AddEditProductState()) private val _eventFlow = MutableSharedFlow() - val eventFlow = _eventFlow.asSharedFlow() + val eventFlow = _eventFlow.shareIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + replay = 1, + ) private val _selectedCategory = MutableStateFlow(Category()) val selectedCategory = _selectedCategory.asStateFlow() @@ -95,7 +100,7 @@ class AddEditProductViewModel @Inject constructor( val nameError: StateFlow = snapshotFlow { state.productName } .mapLatest { - validateProductName(it, productId).errorMessage + validateProductName(it, productId.value).errorMessage }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), @@ -190,7 +195,7 @@ class AddEditProductViewModel @Inject constructor( } is AddEditProductEvent.AddOrUpdateProduct -> { - createOrUpdateProduct(productId) + createOrUpdateProduct(productId.value) } } } @@ -271,6 +276,11 @@ class AddEditProductViewModel @Inject constructor( } } } + + @TestOnly + internal fun setProductId(productId: Int) { + savedStateHandle["productId"] = productId + } } internal val defaultTagList = listOf( diff --git a/feature/product/src/main/java/com/niyaj/product/settings/DecreaseProductPriceScreen.kt b/feature/product/src/main/java/com/niyaj/product/settings/DecreaseProductPriceScreen.kt index 8059d640..1cb75a43 100644 --- a/feature/product/src/main/java/com/niyaj/product/settings/DecreaseProductPriceScreen.kt +++ b/feature/product/src/main/java/com/niyaj/product/settings/DecreaseProductPriceScreen.kt @@ -302,11 +302,11 @@ internal fun DecreaseProductPriceScreenContent( ) } else { ProductList( - modifier = Modifier, items = items, isInSelectionMode = true, doesSelected = selectedItems::contains, onSelectItem = onSelectItem, + modifier = Modifier, lazyListState = lazyListState, ) } diff --git a/feature/product/src/main/java/com/niyaj/product/settings/ExportProductScreen.kt b/feature/product/src/main/java/com/niyaj/product/settings/ExportProductScreen.kt index 4c1fbfaa..600d66d9 100644 --- a/feature/product/src/main/java/com/niyaj/product/settings/ExportProductScreen.kt +++ b/feature/product/src/main/java/com/niyaj/product/settings/ExportProductScreen.kt @@ -316,11 +316,11 @@ internal fun ExportProductScreenContent( ) } else { ProductList( - modifier = Modifier, items = items, isInSelectionMode = true, doesSelected = selectedItems::contains, onSelectItem = onSelectItem, + modifier = Modifier, lazyListState = lazyListState, ) } diff --git a/feature/product/src/main/java/com/niyaj/product/settings/ImportProductScreen.kt b/feature/product/src/main/java/com/niyaj/product/settings/ImportProductScreen.kt index d543e50b..4f36be00 100644 --- a/feature/product/src/main/java/com/niyaj/product/settings/ImportProductScreen.kt +++ b/feature/product/src/main/java/com/niyaj/product/settings/ImportProductScreen.kt @@ -246,11 +246,11 @@ internal fun ImportProductScreenContent( ) } else { ProductList( - modifier = Modifier, items = importedItems, isInSelectionMode = true, doesSelected = selectedItems::contains, onSelectItem = onClickSelectItem, + modifier = Modifier, onNavigateToDetails = {}, showItemNotFound = false, onClickCreateNew = {}, diff --git a/feature/product/src/main/java/com/niyaj/product/settings/IncreaseProductPriceScreen.kt b/feature/product/src/main/java/com/niyaj/product/settings/IncreaseProductPriceScreen.kt index 5e9085b1..10e4867e 100644 --- a/feature/product/src/main/java/com/niyaj/product/settings/IncreaseProductPriceScreen.kt +++ b/feature/product/src/main/java/com/niyaj/product/settings/IncreaseProductPriceScreen.kt @@ -304,11 +304,11 @@ internal fun IncreaseProductPriceScreenContent( ) } else { ProductList( - modifier = Modifier, items = items, isInSelectionMode = true, doesSelected = selectedItems::contains, onSelectItem = onSelectItem, + modifier = Modifier, lazyListState = lazyListState, ) } diff --git a/feature/product/src/test/java/com/niyaj/product/AddEditProductViewModelTest.kt b/feature/product/src/test/java/com/niyaj/product/AddEditProductViewModelTest.kt new file mode 100644 index 00000000..a73cc247 --- /dev/null +++ b/feature/product/src/test/java/com/niyaj/product/AddEditProductViewModelTest.kt @@ -0,0 +1,428 @@ +/* + * Copyright 2024 Sk Niyaj Ali + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.niyaj.product + +import androidx.compose.ui.util.fastFirst +import androidx.lifecycle.SavedStateHandle +import app.cash.turbine.test +import com.niyaj.common.result.Resource +import com.niyaj.common.tags.ProductTestTags.PRODUCT_CATEGORY_EMPTY_ERROR +import com.niyaj.common.tags.ProductTestTags.PRODUCT_NAME_ALREADY_EXIST_ERROR +import com.niyaj.common.tags.ProductTestTags.PRODUCT_NAME_EMPTY_ERROR +import com.niyaj.common.tags.ProductTestTags.PRODUCT_NAME_LENGTH_ERROR +import com.niyaj.common.tags.ProductTestTags.PRODUCT_PRICE_EMPTY_ERROR +import com.niyaj.common.tags.ProductTestTags.PRODUCT_PRICE_LENGTH_ERROR +import com.niyaj.common.tags.ProductTestTags.PRODUCT_TAG_LENGTH_ERROR +import com.niyaj.common.utils.capitalizeWords +import com.niyaj.domain.product.ValidateProductCategoryUseCase +import com.niyaj.domain.product.ValidateProductNameUseCase +import com.niyaj.domain.product.ValidateProductPriceUseCase +import com.niyaj.domain.product.ValidateProductTagUseCase +import com.niyaj.model.Category +import com.niyaj.product.createOrUpdate.AddEditProductEvent +import com.niyaj.product.createOrUpdate.AddEditProductViewModel +import com.niyaj.testing.repository.TestProductRepository +import com.niyaj.testing.util.MainDispatcherRule +import com.niyaj.testing.util.TestAnalyticsHelper +import com.niyaj.ui.parameterProvider.CategoryPreviewData +import com.niyaj.ui.parameterProvider.ProductPreviewData +import com.niyaj.ui.utils.UiEvent +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class AddEditProductViewModelTest { + @get:Rule + val dispatcherRule = MainDispatcherRule() + + private val itemList = ProductPreviewData.productList + private val repository = TestProductRepository() + private val analyticsHelper = TestAnalyticsHelper() + + private val validateProductCategory = ValidateProductCategoryUseCase() + private val validateProductName = ValidateProductNameUseCase( + repository = repository, + ioDispatcher = UnconfinedTestDispatcher(), + ) + private val validateProductTag = ValidateProductTagUseCase() + private val validateProductPrice = ValidateProductPriceUseCase() + private lateinit var viewModel: AddEditProductViewModel + + @BeforeTest + fun setup() { + viewModel = AddEditProductViewModel( + productRepository = repository, + validateProductCategory = validateProductCategory, + validateProductName = validateProductName, + validateProductTag = validateProductTag, + validateProductPrice = validateProductPrice, + analyticsHelper = analyticsHelper, + savedStateHandle = SavedStateHandle(), + ) + } + + @Test + fun `categories should empty when no data present`() = runTest { + viewModel.categories.test { + assertEquals(emptyList(), awaitItem()) + } + } + + @Test + fun `categories should not empty when data present`() = runTest { + val categories = CategoryPreviewData.categoryList + repository.setCategoryList(categories) + + viewModel.categories.test { + assertEquals(categories, awaitItem()) + } + } + + @Test + fun `initialize with valid productId should populated product details`() = runTest { + val product = repository.createTestProduct() + val categories = CategoryPreviewData.categoryList.filter { + it.categoryId == product.categoryId + } + + repository.setCategoryList(categories) + viewModel.setProductId(product.productId) + + val savedStateHandle = SavedStateHandle() + savedStateHandle["productId"] = product.productId + + viewModel = AddEditProductViewModel( + productRepository = repository, + validateProductCategory = validateProductCategory, + validateProductName = validateProductName, + validateProductTag = validateProductTag, + validateProductPrice = validateProductPrice, + analyticsHelper = analyticsHelper, + savedStateHandle = savedStateHandle, + ) + + assertEquals(product.productName, viewModel.state.productName) + assertEquals(product.productPrice.toString(), viewModel.state.productPrice) + assertEquals(product.productDescription, viewModel.state.productDesc) + assertEquals(product.productAvailability, viewModel.state.productAvailability) + + assert(viewModel.selectedTags.containsAll(product.tags)) + viewModel.selectedCategory.test { + assertEquals(categories.first(), awaitItem()) + } + } + + @Test + fun `On ProductNameChanged should update productName`() { + viewModel.onEvent(AddEditProductEvent.ProductNameChanged("Test Product")) + + assertEquals("Test Product", viewModel.state.productName) + } + + @Test + fun `On ProductPriceChanged should update productCategory`() { + viewModel.onEvent(AddEditProductEvent.ProductPriceChanged("10")) + + assertEquals("10", viewModel.state.productPrice) + } + + @Test + fun `On ProductDescChanged should update productCategory`() { + viewModel.onEvent(AddEditProductEvent.ProductDescChanged("Test Description")) + + assertEquals("Test Description", viewModel.state.productDesc) + } + + @Test + fun `On ProductAvailabilityChanged should update productCategory`() { + val productAvailability = viewModel.state.productAvailability + viewModel.onEvent(AddEditProductEvent.ProductAvailabilityChanged) + + assertEquals(!productAvailability, viewModel.state.productAvailability) + } + + @Test + fun `On TagNameChanged should update tagName`() { + viewModel.onEvent(AddEditProductEvent.TagNameChanged("Test Tag")) + + assertEquals("Test Tag", viewModel.state.tagName) + } + + @Test + fun `On CategoryChanged should update categoryName`() = runTest { + val category = Category( + categoryId = 1, + categoryName = "Test Category", + ) + viewModel.onEvent(AddEditProductEvent.CategoryChanged(category)) + + viewModel.selectedCategory.test { + assertEquals(category, awaitItem()) + } + } + + @Test + fun `on SelectTag should update selectedTags`() { + val tag = "Test Tag" + viewModel.onEvent(AddEditProductEvent.OnSelectTag(tag)) + + assert(viewModel.selectedTags.contains(tag)) + } + + @Test + fun `on SelectExisting tag should remove tag from selectedTags`() { + val tag = "Test Tag" + viewModel.onEvent(AddEditProductEvent.OnSelectTag(tag)) + + assert(viewModel.selectedTags.contains(tag)) + + viewModel.onEvent(AddEditProductEvent.OnSelectTag(tag)) + + assert(!viewModel.selectedTags.contains(tag)) + } + + @Test + fun `categoryError updated when category is empty`() = runTest { + val category = Category() + viewModel.onEvent(AddEditProductEvent.CategoryChanged(category)) + + viewModel.categoryError.test { + assertEquals(PRODUCT_CATEGORY_EMPTY_ERROR, awaitItem()) + } + } + + @Test + fun `categoryError updated when category is valid`() = runTest { + val category = Category( + categoryId = 1, + categoryName = "Test Category", + ) + viewModel.onEvent(AddEditProductEvent.CategoryChanged(category)) + + viewModel.categoryError.test { + assertNull(awaitItem()) + } + } + + @Test + fun `nameError updated when product name is empty`() = runTest { + viewModel.onEvent(AddEditProductEvent.ProductNameChanged("")) + + viewModel.nameError.test { + assertEquals(PRODUCT_NAME_EMPTY_ERROR, awaitItem()) + } + } + + @Test + fun `nameError updated when product name is less than 4 characters`() = runTest { + viewModel.onEvent(AddEditProductEvent.ProductNameChanged("Tes")) + + viewModel.nameError.test { + assertEquals(PRODUCT_NAME_LENGTH_ERROR, awaitItem()) + } + } + + @Test + fun `nameError updated when product name is already exists`() = runTest { + val product = repository.createTestProduct() + viewModel.onEvent(AddEditProductEvent.ProductNameChanged(product.productName)) + + viewModel.nameError.test { + assertEquals(PRODUCT_NAME_ALREADY_EXIST_ERROR, awaitItem()) + } + } + + @Test + fun `nameError updated when product name is already exists with productId`() = runTest { + val product = repository.createTestProduct() + viewModel.setProductId(product.productId) + viewModel.onEvent(AddEditProductEvent.ProductNameChanged(product.productName)) + + viewModel.nameError.test { + assertEquals(null, awaitItem()) + } + } + + @Test + fun `nameError updated when product name is valid`() = runTest { + viewModel.onEvent(AddEditProductEvent.ProductNameChanged("Test Product")) + + viewModel.nameError.test { + assertNull(awaitItem()) + } + } + + @Test + fun `priceError updated when product price is empty or zero`() = runTest { + viewModel.onEvent(AddEditProductEvent.ProductPriceChanged("0")) + + viewModel.priceError.test { + assertEquals(PRODUCT_PRICE_EMPTY_ERROR, awaitItem()) + } + } + + @Test + fun `priceError updated when product price is less than 10`() = runTest { + viewModel.onEvent(AddEditProductEvent.ProductPriceChanged("8")) + + viewModel.priceError.test { + assertEquals(PRODUCT_PRICE_LENGTH_ERROR, awaitItem()) + } + } + + @Test + fun `priceError updated when product price is valid`() = runTest { + viewModel.onEvent(AddEditProductEvent.ProductPriceChanged("12")) + + viewModel.priceError.test { + assertNull(awaitItem()) + } + } + + @Test + fun `tagError updated when product price is not empty and less than 3 characters`() = runTest { + viewModel.onEvent(AddEditProductEvent.TagNameChanged("Te")) + + viewModel.tagError.test { + assertEquals(PRODUCT_TAG_LENGTH_ERROR, awaitItem()) + } + } + + @Test + fun `tagError updated when product price is valid`() = runTest { + viewModel.onEvent(AddEditProductEvent.TagNameChanged("Test")) + + viewModel.tagError.test { + assertNull(awaitItem()) + } + } + + @Test + fun `on create new product should created new product`() = runTest { + val product = itemList.first() + val category = CategoryPreviewData.categoryList.fastFirst { + it.categoryId == product.categoryId + } + + viewModel.onEvent(AddEditProductEvent.CategoryChanged(category)) + viewModel.onEvent(AddEditProductEvent.ProductNameChanged(product.productName)) + viewModel.onEvent(AddEditProductEvent.ProductPriceChanged(product.productPrice.toString())) + viewModel.onEvent(AddEditProductEvent.ProductDescChanged(product.productDescription)) + if (!product.productAvailability) { + viewModel.onEvent(AddEditProductEvent.ProductAvailabilityChanged) + } + product.tags.forEach { + viewModel.onEvent(AddEditProductEvent.OnSelectTag(it)) + } + + viewModel.onEvent(AddEditProductEvent.AddOrUpdateProduct) + + viewModel.eventFlow.test { + assertEquals( + UiEvent.OnSuccess("Product created successfully"), + awaitItem(), + ) + } + + val createdProduct = repository.getProductById(0) + assertNotNull(createdProduct.data) + assert(createdProduct is Resource.Success) + + assertEquals(product.productName, createdProduct.data?.productName) + assertEquals(product.productPrice, createdProduct.data?.productPrice) + assertEquals( + product.productDescription.capitalizeWords, + createdProduct.data?.productDescription, + ) + assertEquals(product.productAvailability, createdProduct.data?.productAvailability) + + assert(createdProduct.data?.tags?.containsAll(product.tags) == true) + assertEquals(category.categoryId, createdProduct.data?.categoryId) + } + + @Test + fun `on update existing product should update the product`() = runTest { + val product = itemList.first() + val category = CategoryPreviewData.categoryList.fastFirst { + it.categoryId == product.categoryId + } + repository.setCategoryList(listOf(category)) + + viewModel.onEvent(AddEditProductEvent.ProductNameChanged(product.productName)) + viewModel.onEvent(AddEditProductEvent.ProductPriceChanged(product.productPrice.toString())) + viewModel.onEvent(AddEditProductEvent.ProductDescChanged(product.productDescription)) + if (!product.productAvailability) { + viewModel.onEvent(AddEditProductEvent.ProductAvailabilityChanged) + } + viewModel.onEvent(AddEditProductEvent.CategoryChanged(category)) + product.tags.forEach { + viewModel.onEvent(AddEditProductEvent.OnSelectTag(it)) + } + + viewModel.setProductId(product.productId) + viewModel.onEvent(AddEditProductEvent.AddOrUpdateProduct) + + val updatedPro = product.copy( + productId = 1, + productName = "Updated Product", + productPrice = 20, + productDescription = "Updated Description", + productAvailability = false, + tags = listOf("Updated Tag"), + ) + + viewModel.onEvent(AddEditProductEvent.ProductNameChanged(updatedPro.productName)) + viewModel.onEvent(AddEditProductEvent.ProductPriceChanged(updatedPro.productPrice.toString())) + viewModel.onEvent(AddEditProductEvent.ProductDescChanged(updatedPro.productDescription)) + if (!updatedPro.productAvailability) { + viewModel.onEvent(AddEditProductEvent.ProductAvailabilityChanged) + } + viewModel.onEvent(AddEditProductEvent.CategoryChanged(category)) + updatedPro.tags.forEach { + viewModel.onEvent(AddEditProductEvent.OnSelectTag(it)) + } + + viewModel.setProductId(product.productId) + viewModel.onEvent(AddEditProductEvent.AddOrUpdateProduct) + + viewModel.eventFlow.test { + assertEquals( + UiEvent.OnSuccess("Product updated successfully"), + awaitItem(), + ) + } + + val updatedProduct = repository.getProductById(updatedPro.productId) + assertNotNull(updatedProduct) + + assertEquals(updatedPro.productName, updatedProduct.data?.productName) + assertEquals(updatedPro.productPrice, updatedProduct.data?.productPrice) + assertEquals( + updatedPro.productDescription.capitalizeWords, + updatedProduct.data?.productDescription, + ) + assertEquals(updatedPro.productAvailability, updatedProduct.data?.productAvailability) + assert(updatedProduct.data?.tags?.containsAll(updatedPro.tags) == true) + assertEquals(category.categoryId, updatedProduct.data?.categoryId) + } +} diff --git a/feature/product/src/test/java/com/niyaj/product/ProductScreenScreenshotTest.kt b/feature/product/src/test/java/com/niyaj/product/ProductScreenScreenshotTest.kt index 8ddbe01e..a1220593 100644 --- a/feature/product/src/test/java/com/niyaj/product/ProductScreenScreenshotTest.kt +++ b/feature/product/src/test/java/com/niyaj/product/ProductScreenScreenshotTest.kt @@ -178,9 +178,9 @@ class ProductScreenScreenshotTest { PoposRoomTheme { ProductScreenContent( uiState = UiState.Success(productList), - selectedItems = listOf(3, 6, 8), categories = categoryList, selectedCategory = 0, + selectedItems = listOf(3, 6, 8), showSearchBar = false, searchText = "", onClickSearchIcon = {}, @@ -209,9 +209,9 @@ class ProductScreenScreenshotTest { PoposRoomTheme { ProductScreenContent( uiState = UiState.Success(productList.searchProducts("search")), - selectedItems = listOf(), categories = categoryList, selectedCategory = 0, + selectedItems = listOf(), showSearchBar = true, searchText = "search", onClickSearchIcon = {}, @@ -240,9 +240,9 @@ class ProductScreenScreenshotTest { PoposRoomTheme { ProductScreenContent( uiState = UiState.Success(productList.searchProducts("Chicken")), - selectedItems = listOf(), categories = categoryList, selectedCategory = 0, + selectedItems = listOf(), showSearchBar = true, searchText = "Chicken", onClickSearchIcon = {}, diff --git a/feature/product/src/test/java/com/niyaj/product/ProductSettingsViewModelTest.kt b/feature/product/src/test/java/com/niyaj/product/ProductSettingsViewModelTest.kt index fdcbccf8..e3fc851b 100644 --- a/feature/product/src/test/java/com/niyaj/product/ProductSettingsViewModelTest.kt +++ b/feature/product/src/test/java/com/niyaj/product/ProductSettingsViewModelTest.kt @@ -24,8 +24,11 @@ import com.niyaj.product.settings.ProductSettingsViewModel import com.niyaj.testing.repository.TestProductRepository import com.niyaj.testing.util.MainDispatcherRule import com.niyaj.testing.util.TestAnalyticsHelper +import com.niyaj.ui.parameterProvider.CategoryPreviewData import com.niyaj.ui.parameterProvider.ProductPreviewData import com.niyaj.ui.utils.UiEvent +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -63,6 +66,164 @@ class ProductSettingsViewModelTest { } } + @Test + fun category_isEmpty_whenDataIsEmpty() = runTest { + val job = launch(UnconfinedTestDispatcher()) { viewModel.categories.collect() } + + assertEquals(persistentListOf(), viewModel.categories.value) + + job.cancel() + } + + @Test + fun category_available_whenDataIsAvailable() = runTest { + val job = launch(UnconfinedTestDispatcher()) { viewModel.categories.collect() } + val categoryList = CategoryPreviewData.categoryList + repository.setCategoryList(categoryList) + + assertEquals(categoryList.toImmutableList(), viewModel.categories.value) + + job.cancel() + } + + @Test + fun onSelectCategory_shouldUpdateSelectedCategory() = runTest { + val category = CategoryPreviewData.categoryList.first() + + viewModel.onEvent(ProductSettingsEvent.OnSelectCategory(category.categoryId)) + + assertEquals(listOf(category.categoryId), viewModel.selectedCategory.toList()) + } + + @Test + fun onSelectCategory_shouldUpdateSelectedProducts() = runTest { + val job = launch(UnconfinedTestDispatcher()) { viewModel.products.collect() } + val category = CategoryPreviewData.categoryList.first() + val productList = itemList.filter { it.categoryId == category.categoryId } + + repository.setProductList(itemList) + viewModel.onEvent(ProductSettingsEvent.OnSelectCategory(category.categoryId)) + val selectedItems = viewModel.selectedItems.toList() + assertEquals(productList.map { it.productId }, selectedItems) + + job.cancel() + } + + @Test + fun onSelectExistingCategory_shouldUpdateSelectedProductsAndDeselectSelectedCategory() = runTest { + val job = launch(UnconfinedTestDispatcher()) { viewModel.products.collect() } + val category = CategoryPreviewData.categoryList.first() + val productList = itemList.filter { it.categoryId == category.categoryId } + + repository.setProductList(itemList) + viewModel.onEvent(ProductSettingsEvent.OnSelectCategory(category.categoryId)) + + val selectedItems = viewModel.selectedItems.toList() + assertEquals(productList.map { it.productId }, selectedItems) + + viewModel.onEvent(ProductSettingsEvent.OnSelectCategory(category.categoryId)) + assertEquals(listOf(), viewModel.selectedCategory.toList()) + + job.cancel() + } + + @Test + fun `onChangeProductPrice should update productPrice`() = runTest { + viewModel.onEvent(ProductSettingsEvent.OnChangeProductPrice("10")) + + assertEquals(10, viewModel.productPrice.value) + } + + @Test + fun `onIncreaseProductPrice with no selection should increase all products productPrice`() = runTest { + val job = launch(UnconfinedTestDispatcher()) { viewModel.products.collect() } + repository.setProductList(itemList) + + viewModel.onEvent(ProductSettingsEvent.OnChangeProductPrice("10")) + viewModel.onEvent(ProductSettingsEvent.OnIncreaseProductPrice) + + assertEquals(10, viewModel.productPrice.value) + val updatedProducts = itemList.map { it.copy(productPrice = it.productPrice + 10) } + + viewModel.products.test { + assertEquals(updatedProducts, awaitItem()) + } + + job.cancel() + } + + @Test + fun `onIncreaseProductPrice with selection should increase selected products productPrice`() = runTest { + val job = launch(UnconfinedTestDispatcher()) { viewModel.products.collect() } + repository.setProductList(itemList) + + viewModel.onEvent(ProductSettingsEvent.OnChangeProductPrice("10")) + viewModel.selectItem(1) + viewModel.selectItem(3) + + viewModel.onEvent(ProductSettingsEvent.OnIncreaseProductPrice) + + assertEquals(10, viewModel.productPrice.value) + val updatedProducts = itemList.map { + if (it.productId == 1 || it.productId == 3) { + it.copy(productPrice = it.productPrice + 10) + } else { + it + } + } + + viewModel.products.test { + assertEquals(updatedProducts, awaitItem()) + } + + job.cancel() + } + + @Test + fun `OnDecreaseProductPrice with no selection should increase all products productPrice`() = runTest { + val job = launch(UnconfinedTestDispatcher()) { viewModel.products.collect() } + repository.setProductList(itemList) + + viewModel.onEvent(ProductSettingsEvent.OnChangeProductPrice("10")) + viewModel.onEvent(ProductSettingsEvent.OnDecreaseProductPrice) + + assertEquals(10, viewModel.productPrice.value) + val updatedProducts = itemList.map { it.copy(productPrice = it.productPrice - 10) } + + viewModel.products.test { + assertEquals(updatedProducts, awaitItem()) + } + + job.cancel() + } + + @Test + fun `OnDecreaseProductPrice with selection should increase selected products productPrice`() = runTest { + val job = launch(UnconfinedTestDispatcher()) { viewModel.products.collect() } + repository.setProductList(itemList) + + viewModel.onEvent(ProductSettingsEvent.OnChangeProductPrice("10")) + viewModel.selectItem(1) + viewModel.selectItem(3) + + viewModel.onEvent(ProductSettingsEvent.OnDecreaseProductPrice) + + assertEquals(10, viewModel.productPrice.value) + val updatedProducts = itemList.map { + if (it.productId == 1 || it.productId == 3) { + it.copy(productPrice = it.productPrice - 10) + } else { + it + } + } + + viewModel.products.test { + assertEquals(updatedProducts, awaitItem()) + } + + job.cancel() + } + @Test fun `when GetExportedProduct event is triggered with no selection, all items are exported`() = runTest {