From c38a5413f9a41d3b3270297e87f65e3cd70a03e4 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Thu, 18 Sep 2025 18:07:37 +0200 Subject: [PATCH 01/23] Add logs to error case when failing to check Bookings tab visibility --- .../android/ui/bookings/tab/BookingsTabController.kt | 3 ++- .../src/main/kotlin/com/woocommerce/android/util/WooLog.kt | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt index e8e938ea9029..26a2546a6f2d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.lifecycleScope import com.woocommerce.android.R import com.woocommerce.android.databinding.ActivityMainBinding import com.woocommerce.android.ui.main.MainActivity +import com.woocommerce.android.util.WooLog import kotlinx.coroutines.launch import javax.inject.Inject @@ -39,7 +40,7 @@ class BookingsTabController @Inject constructor( binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = it } .onFailure { - // TODO log error or track errors? + WooLog.w(WooLog.T.BOOKINGS, "Failed to check if bookings tab should be visible: ${it.message}") } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/util/WooLog.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/util/WooLog.kt index 7495b4ddcf88..0a6c512f69a2 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/util/WooLog.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/util/WooLog.kt @@ -50,7 +50,8 @@ object WooLog { GOOGLE_ADS, POS, CUSTOM_FIELDS, - SHIPPING_LABELS + SHIPPING_LABELS, + BOOKINGS } // Breaking convention to be consistent with org.wordpress.android.util.AppLog From c89ba2d140c3cdf611af69d7bc88a1ef4fb12e95 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Thu, 18 Sep 2025 18:07:51 +0200 Subject: [PATCH 02/23] Add new extension function to check site is CIAB --- .../kotlin/com/woocommerce/android/ciab/CIABExtensions.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt new file mode 100644 index 000000000000..e4c8cdbfad4c --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt @@ -0,0 +1,7 @@ +package com.woocommerce.android.ciab + +import com.woocommerce.android.ciab.CIABSiteGateKeeper.Companion.CIAB_GARDEN_NAME +import com.woocommerce.android.tools.SelectedSite + +fun SelectedSite.isCurrentSiteCIAB(): Boolean = + this.getOrNull()?.let { it.isGardenSite && it.gardenName == CIAB_GARDEN_NAME } ?: false From 84ff7b1ca7212b13e040e3571cede370fe1e15b2 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Thu, 18 Sep 2025 18:08:13 +0200 Subject: [PATCH 03/23] Check if current site is CIAB before displaying bookings tab --- .../android/ciab/CIABSiteGateKeeper.kt | 9 +-------- .../android/ui/bookings/tab/ShowBookingsTab.kt | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt index 15935dd760c6..a361e74dd436 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt @@ -1,6 +1,5 @@ package com.woocommerce.android.ciab -import androidx.annotation.VisibleForTesting import com.woocommerce.android.tools.SelectedSite import javax.inject.Inject @@ -11,20 +10,14 @@ class CIABSiteGateKeeper @Inject constructor(private val selectedSite: SelectedS ): Boolean { // For now, all affected features are unsupported in CIAB. // If there are exceptions in the future, we can handle them here. - return !isCurrentSiteCIAB() + return !selectedSite.isCurrentSiteCIAB() } fun isFeatureUnsupported(feature: CIABAffectedFeature): Boolean { return !isFeatureSupported(feature) } - private fun isCurrentSiteCIAB(): Boolean { - val site = selectedSite.getOrNull() ?: return false - return site.isGardenSite && site.gardenName == CIAB_GARDEN_NAME - } - companion object Companion { - @VisibleForTesting const val CIAB_GARDEN_NAME = "commerce" } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt index 93628e43ad3f..06752ecc5fd0 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt @@ -1,13 +1,21 @@ package com.woocommerce.android.ui.bookings.tab +import com.woocommerce.android.ciab.isCurrentSiteCIAB +import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.util.FeatureFlag import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject -class ShowBookingsTab @Inject constructor() { - suspend operator fun invoke(): Result = withContext(Dispatchers.IO) { - // Add here: Fetch if site has any published bookable product AND if site is CIAB - return@withContext Result.success(FeatureFlag.BOOKINGS_MVP.isEnabled()) - } +class ShowBookingsTab @Inject constructor( + private val selectedSite: SelectedSite +) { + + suspend operator fun invoke(): Result = + withContext(Dispatchers.IO) { + Result.success( + selectedSite.isCurrentSiteCIAB() && + FeatureFlag.BOOKINGS_MVP.isEnabled() + ) + } } From f99c836ef7ca0b3d08b8e468601b5b37ef744a88 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Fri, 19 Sep 2025 15:52:09 +0200 Subject: [PATCH 04/23] Refactor extension function to apply only to SiteModel --- .../kotlin/com/woocommerce/android/ciab/CIABExtensions.kt | 5 ++--- .../com/woocommerce/android/ciab/CIABSiteGateKeeper.kt | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt index e4c8cdbfad4c..4d6199d93076 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt @@ -1,7 +1,6 @@ package com.woocommerce.android.ciab import com.woocommerce.android.ciab.CIABSiteGateKeeper.Companion.CIAB_GARDEN_NAME -import com.woocommerce.android.tools.SelectedSite +import org.wordpress.android.fluxc.model.SiteModel -fun SelectedSite.isCurrentSiteCIAB(): Boolean = - this.getOrNull()?.let { it.isGardenSite && it.gardenName == CIAB_GARDEN_NAME } ?: false +fun SiteModel.isCIABSite() = isGardenSite && gardenName == CIAB_GARDEN_NAME diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt index a361e74dd436..e20cd6f66f31 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt @@ -10,13 +10,16 @@ class CIABSiteGateKeeper @Inject constructor(private val selectedSite: SelectedS ): Boolean { // For now, all affected features are unsupported in CIAB. // If there are exceptions in the future, we can handle them here. - return !selectedSite.isCurrentSiteCIAB() + return !isCurrentSiteCIAB() } fun isFeatureUnsupported(feature: CIABAffectedFeature): Boolean { return !isFeatureSupported(feature) } + private fun isCurrentSiteCIAB(): Boolean = + selectedSite.getOrNull()?.isCIABSite() ?: false + companion object Companion { const val CIAB_GARDEN_NAME = "commerce" } From 838ea0f6e3b19adf1b2b771c2a755bd48e8eed29 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Fri, 19 Sep 2025 16:07:22 +0200 Subject: [PATCH 05/23] Link Bookings tab visibility to Bookable published bookable products --- .../ui/bookings/tab/BookingsTabController.kt | 6 +-- .../ui/bookings/tab/ShowBookingsTab.kt | 44 ++++++++++++++----- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt index 26a2546a6f2d..170a3acea100 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.lifecycleScope import com.woocommerce.android.R import com.woocommerce.android.databinding.ActivityMainBinding import com.woocommerce.android.ui.main.MainActivity -import com.woocommerce.android.util.WooLog import kotlinx.coroutines.launch import javax.inject.Inject @@ -36,12 +35,9 @@ class BookingsTabController @Inject constructor( private fun checkBookingsTabVisibility() { activity.lifecycleScope.launch { showBookingsTab() - .onSuccess { + .collect { binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = it } - .onFailure { - WooLog.w(WooLog.T.BOOKINGS, "Failed to check if bookings tab should be visible: ${it.message}") - } } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt index 06752ecc5fd0..916345c67b98 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt @@ -1,21 +1,45 @@ package com.woocommerce.android.ui.bookings.tab -import com.woocommerce.android.ciab.isCurrentSiteCIAB +import com.woocommerce.android.ciab.isCIABSite import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.products.ProductStatus +import com.woocommerce.android.ui.products.list.ProductListRepository import com.woocommerce.android.util.FeatureFlag -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onStart +import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption import javax.inject.Inject class ShowBookingsTab @Inject constructor( - private val selectedSite: SelectedSite + private val selectedSite: SelectedSite, + private val productListRepository: ProductListRepository ) { - suspend operator fun invoke(): Result = - withContext(Dispatchers.IO) { - Result.success( - selectedSite.isCurrentSiteCIAB() && - FeatureFlag.BOOKINGS_MVP.isEnabled() + operator fun invoke(): Flow { + return combine( + selectedSite.observe(), + productListRepository + .observeProductsCount( + filterOptions = mapOf( + ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, + ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE + ), + excludeSampleProducts = true + ), + ) { site, productsCount -> + productsCount > 0 && + site?.isCIABSite() == true && + FeatureFlag.BOOKINGS_MVP.isEnabled() + }.onStart { + productListRepository.fetchProductList( + productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE) ) - } + }.distinctUntilChanged() + } + + companion object { + private const val BOOKING_PRODUCT_TYPE = "booking" + } } From aed415a57605df90913d66deb3b46b15487d55be Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Fri, 19 Sep 2025 16:11:54 +0200 Subject: [PATCH 06/23] Add logs when refreshing bookable products fails --- .../woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt index 916345c67b98..28731d167672 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt @@ -5,6 +5,7 @@ import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.products.ProductStatus import com.woocommerce.android.ui.products.list.ProductListRepository import com.woocommerce.android.util.FeatureFlag +import com.woocommerce.android.util.WooLog import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -35,7 +36,9 @@ class ShowBookingsTab @Inject constructor( }.onStart { productListRepository.fetchProductList( productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE) - ) + ).onFailure { + WooLog.w(WooLog.T.BOOKINGS, "Failed to fetch bookable products" ) + } }.distinctUntilChanged() } From ccf36cde7e5a2777d5eb1e26270c2b87a4c2b62f Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Fri, 19 Sep 2025 16:28:14 +0200 Subject: [PATCH 07/23] Rename class --- .../android/ui/bookings/tab/BookingsTabController.kt | 4 ++-- .../{ShowBookingsTab.kt => ObserveBookingsTabVisibility.kt} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/{ShowBookingsTab.kt => ObserveBookingsTabVisibility.kt} (96%) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt index 170a3acea100..0e4c2ceaa571 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject class BookingsTabController @Inject constructor( - private val showBookingsTab: ShowBookingsTab + private val observeBookingsTabVisibility: ObserveBookingsTabVisibility ) : DefaultLifecycleObserver { private lateinit var activity: MainActivity private lateinit var binding: ActivityMainBinding @@ -34,7 +34,7 @@ class BookingsTabController @Inject constructor( private fun checkBookingsTabVisibility() { activity.lifecycleScope.launch { - showBookingsTab() + observeBookingsTabVisibility() .collect { binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = it } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt similarity index 96% rename from WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt rename to WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt index 28731d167672..d87cda70b4bb 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTab.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.onStart import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption import javax.inject.Inject -class ShowBookingsTab @Inject constructor( +class ObserveBookingsTabVisibility @Inject constructor( private val selectedSite: SelectedSite, private val productListRepository: ProductListRepository ) { From cab6f524c50a978716af4e373430d9eec4c73f1a Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Fri, 19 Sep 2025 16:38:34 +0200 Subject: [PATCH 08/23] Add tests --- .../ui/bookings/tab/ShowBookingsTabTest.kt | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTabTest.kt diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTabTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTabTest.kt new file mode 100644 index 000000000000..722a7a04ec4f --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTabTest.kt @@ -0,0 +1,143 @@ +package com.woocommerce.android.ui.bookings.tab + +import app.cash.turbine.test +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.products.list.ProductListRepository +import com.woocommerce.android.viewmodel.BaseUnitTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.argThat +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption + +@OptIn(ExperimentalCoroutinesApi::class) +class ShowBookingsTabTest : BaseUnitTest() { + + @Mock lateinit var selectedSite: SelectedSite + @Mock lateinit var productListRepository: ProductListRepository + + private lateinit var sut: ObserveBookingsTabVisibility + + private lateinit var siteFlow: MutableStateFlow + private lateinit var countFlow: MutableStateFlow + + @Before + fun setup() { + siteFlow = MutableStateFlow(null) + countFlow = MutableStateFlow(0) + whenever(selectedSite.observe()).thenReturn(siteFlow) + whenever( + productListRepository.observeProductsCount( + filterOptions = any(), + excludeSampleProducts = any() + ) + ).thenReturn(countFlow) + + sut = ObserveBookingsTabVisibility(selectedSite, productListRepository) + } + + @Test + fun `given CIAB site and bookings available, when invoke, then emits true`() = testBlocking { + val site = ciabSite() + siteFlow.value = site + countFlow.value = 2 + + sut().test { + // onStart should trigger a fetch with booking type + verify(productListRepository).fetchProductList( + loadMore = any(), + productFilterOptions = argThat { this[ProductFilterOption.TYPE] == "booking" }, + excludedProductIds = any(), + sortType = org.mockito.kotlin.anyOrNull() + ) + // Then + awaitItem() + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given zero booking products, when invoke, then emits false`() = testBlocking { + // Given + siteFlow.value = ciabSite() + countFlow.value = 0 + + // When/Then + sut().test { + val value = awaitItem() + assert(!value) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given non-CIAB site, when invoke, then emits false`() = testBlocking { + // Given + siteFlow.value = nonCiabSite() + countFlow.value = 10 + + // When/Then + sut().test { + val value = awaitItem() + assert(!value) + cancelAndIgnoreRemainingEvents() + } + } + + + @Test + fun `given same inputs produce same result, when values update, then only emits once`() = testBlocking { + // Given initial true conditions + siteFlow.value = ciabSite() + countFlow.value = 2 + + sut().test { + // First emission should be true + val first = awaitItem() + assert(first) + + // When inputs change but computed value remains true + countFlow.value = 3 // still true + siteFlow.value = ciabSite() // new instance but still CIAB + + // Then no new emissions due to distinctUntilChanged + expectNoEvents() + + // Now change to false + countFlow.value = 0 + val next = awaitItem() + assert(!next) + + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given fetch fails onStart, when invoke, emits based on persisted values`() = testBlocking { + siteFlow.value = ciabSite() + countFlow.value = 1 + + // When/Then + sut().test { + // It should still emit true based on current flows + assert(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + private fun ciabSite(): SiteModel = SiteModel().apply { + setIsGardenSite(true) + setGardenName("commerce") + } + + private fun nonCiabSite(): SiteModel = SiteModel().apply { + setIsGardenSite(false) + setGardenName("other") + } +} From f3b097351deb8ac064544772f22cb5c54e3e82a9 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Fri, 19 Sep 2025 17:57:36 +0200 Subject: [PATCH 09/23] Add unit tests --- .../tab/ObserveBookingsTabVisibility.kt | 4 +- .../tab/ObserveBookingsTabVisibilityTest.kt | 176 ++++++++++++++++++ .../ui/bookings/tab/ShowBookingsTabTest.kt | 143 -------------- 3 files changed, 179 insertions(+), 144 deletions(-) create mode 100644 WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt delete mode 100644 WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTabTest.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt index d87cda70b4bb..f2361cd089f0 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.ui.bookings.tab +import androidx.annotation.VisibleForTesting import com.woocommerce.android.ciab.isCIABSite import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.products.ProductStatus @@ -43,6 +44,7 @@ class ObserveBookingsTabVisibility @Inject constructor( } companion object { - private const val BOOKING_PRODUCT_TYPE = "booking" + @VisibleForTesting + const val BOOKING_PRODUCT_TYPE = "booking" } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt new file mode 100644 index 000000000000..f14f548f62b9 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt @@ -0,0 +1,176 @@ +package com.woocommerce.android.ui.bookings.tab + +import app.cash.turbine.test +import com.woocommerce.android.tools.SelectedSite +import com.woocommerce.android.ui.bookings.tab.ObserveBookingsTabVisibility.Companion.BOOKING_PRODUCT_TYPE +import com.woocommerce.android.ui.products.ProductStatus +import com.woocommerce.android.ui.products.list.ProductListRepository +import com.woocommerce.android.viewmodel.BaseUnitTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.wordpress.android.fluxc.model.SiteModel +import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption + +@OptIn(ExperimentalCoroutinesApi::class) +class ObserveBookingsTabVisibilityTest : BaseUnitTest() { + + private val bookableProdsFilterOptions = mapOf( + ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, + ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE + ) + private val siteFlow = MutableStateFlow(null) + private val bookableProdsCountFlow = MutableStateFlow(0L) + + private val selectedSite: SelectedSite = mock { + on { observe() }.thenReturn(siteFlow) + } + private val productListRepository: ProductListRepository = mock { + on { + observeProductsCount( + filterOptions = bookableProdsFilterOptions, + excludeSampleProducts = true + ) + }.thenReturn(bookableProdsCountFlow) + } + + private lateinit var sut: ObserveBookingsTabVisibility + + suspend fun setup(prepareMocks: suspend () -> Unit = {}) { + prepareMocks() + sut = ObserveBookingsTabVisibility(selectedSite, productListRepository) + } + + @Test + fun `when invoke is called, then bookable products are fetched`() = testBlocking { + siteFlow.value = ciabSite() + bookableProdsCountFlow.value = 2 + + setup() + + sut().test { + verify(productListRepository).fetchProductList( + loadMore = false, + productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE), + excludedProductIds = emptyList(), + sortType = null + ) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given CIAB site and bookings products published, when invoke, then emits true`() = testBlocking { + siteFlow.value = ciabSite() + bookableProdsCountFlow.value = 1 + + setup() + + sut().test { + val showBookingTabValue = awaitItem() + assertTrue(showBookingTabValue) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given zero booking products, when invoke, then emits false`() = testBlocking { + siteFlow.value = ciabSite() + bookableProdsCountFlow.value = 0 + + setup() + + sut().test { + val showBookingTabValue = awaitItem() + assertFalse(showBookingTabValue) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given non-CIAB site, when invoke, then emits false`() = testBlocking { + siteFlow.value = nonCiabSite() + bookableProdsCountFlow.value = 10 + + setup() + + sut().test { + val showBookingTabValue = awaitItem() + assertFalse(showBookingTabValue) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given non-Commerce Garden CIAB site, when invoke, then emits false`() = testBlocking { + siteFlow.value = nonCommerceGardenSite() + bookableProdsCountFlow.value = 10 + + setup() + + sut().test { + val showBookingTabValue = awaitItem() + assertFalse(showBookingTabValue) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given same inputs produce same result, when values update, then only emits once`() = testBlocking { + siteFlow.value = ciabSite() + bookableProdsCountFlow.value = 2 + + setup() + + sut().test { + val firstEmission = awaitItem() + assertTrue(firstEmission) + + // When inputs change but computed value remains true + bookableProdsCountFlow.value = 3 // still true + siteFlow.value = ciabSite() // new instance but still CIAB + + // Then no new emissions due to distinctUntilChanged + expectNoEvents() + + // Now change to false + bookableProdsCountFlow.value = 0 + val secondEmission = awaitItem() + assertFalse(secondEmission) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun `given bookable products fetch fails onStart, when invoke, then emits based on persisted values`() = + testBlocking { + siteFlow.value = ciabSite() + bookableProdsCountFlow.value = 1 + + setup() + + sut().test { + assert(awaitItem()) + cancelAndIgnoreRemainingEvents() + } + } + + private fun ciabSite(): SiteModel = SiteModel().apply { + setIsGardenSite(true) + gardenName = "commerce" + } + + private fun nonCiabSite(): SiteModel = SiteModel().apply { + setIsGardenSite(false) + gardenName = "commerce" + } + + private fun nonCommerceGardenSite(): SiteModel = SiteModel().apply { + setIsGardenSite(true) + gardenName = "other" + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTabTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTabTest.kt deleted file mode 100644 index 722a7a04ec4f..000000000000 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ShowBookingsTabTest.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.woocommerce.android.ui.bookings.tab - -import app.cash.turbine.test -import com.woocommerce.android.tools.SelectedSite -import com.woocommerce.android.ui.products.list.ProductListRepository -import com.woocommerce.android.viewmodel.BaseUnitTest -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import org.junit.Before -import org.junit.Test -import org.mockito.Mock -import org.mockito.kotlin.any -import org.mockito.kotlin.argThat -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import org.wordpress.android.fluxc.model.SiteModel -import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption - -@OptIn(ExperimentalCoroutinesApi::class) -class ShowBookingsTabTest : BaseUnitTest() { - - @Mock lateinit var selectedSite: SelectedSite - @Mock lateinit var productListRepository: ProductListRepository - - private lateinit var sut: ObserveBookingsTabVisibility - - private lateinit var siteFlow: MutableStateFlow - private lateinit var countFlow: MutableStateFlow - - @Before - fun setup() { - siteFlow = MutableStateFlow(null) - countFlow = MutableStateFlow(0) - whenever(selectedSite.observe()).thenReturn(siteFlow) - whenever( - productListRepository.observeProductsCount( - filterOptions = any(), - excludeSampleProducts = any() - ) - ).thenReturn(countFlow) - - sut = ObserveBookingsTabVisibility(selectedSite, productListRepository) - } - - @Test - fun `given CIAB site and bookings available, when invoke, then emits true`() = testBlocking { - val site = ciabSite() - siteFlow.value = site - countFlow.value = 2 - - sut().test { - // onStart should trigger a fetch with booking type - verify(productListRepository).fetchProductList( - loadMore = any(), - productFilterOptions = argThat { this[ProductFilterOption.TYPE] == "booking" }, - excludedProductIds = any(), - sortType = org.mockito.kotlin.anyOrNull() - ) - // Then - awaitItem() - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `given zero booking products, when invoke, then emits false`() = testBlocking { - // Given - siteFlow.value = ciabSite() - countFlow.value = 0 - - // When/Then - sut().test { - val value = awaitItem() - assert(!value) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `given non-CIAB site, when invoke, then emits false`() = testBlocking { - // Given - siteFlow.value = nonCiabSite() - countFlow.value = 10 - - // When/Then - sut().test { - val value = awaitItem() - assert(!value) - cancelAndIgnoreRemainingEvents() - } - } - - - @Test - fun `given same inputs produce same result, when values update, then only emits once`() = testBlocking { - // Given initial true conditions - siteFlow.value = ciabSite() - countFlow.value = 2 - - sut().test { - // First emission should be true - val first = awaitItem() - assert(first) - - // When inputs change but computed value remains true - countFlow.value = 3 // still true - siteFlow.value = ciabSite() // new instance but still CIAB - - // Then no new emissions due to distinctUntilChanged - expectNoEvents() - - // Now change to false - countFlow.value = 0 - val next = awaitItem() - assert(!next) - - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `given fetch fails onStart, when invoke, emits based on persisted values`() = testBlocking { - siteFlow.value = ciabSite() - countFlow.value = 1 - - // When/Then - sut().test { - // It should still emit true based on current flows - assert(awaitItem()) - cancelAndIgnoreRemainingEvents() - } - } - - private fun ciabSite(): SiteModel = SiteModel().apply { - setIsGardenSite(true) - setGardenName("commerce") - } - - private fun nonCiabSite(): SiteModel = SiteModel().apply { - setIsGardenSite(false) - setGardenName("other") - } -} From 802ffd65d0572a28d7367b98fdeb5a282a9c41bb Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Fri, 19 Sep 2025 18:35:52 +0200 Subject: [PATCH 10/23] Ensure site is selected before observing bookings count --- .../ui/bookings/tab/BookingsTabController.kt | 15 +++++-- .../tab/ObserveBookingsTabVisibility.kt | 43 ++++++++----------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt index 0e4c2ceaa571..72cca1ad41e0 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt @@ -5,12 +5,15 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.woocommerce.android.R import com.woocommerce.android.databinding.ActivityMainBinding +import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.main.MainActivity +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch import javax.inject.Inject class BookingsTabController @Inject constructor( - private val observeBookingsTabVisibility: ObserveBookingsTabVisibility + private val observeBookingsTabVisibility: ObserveBookingsTabVisibility, + private val selectedSite: SelectedSite ) : DefaultLifecycleObserver { private lateinit var activity: MainActivity private lateinit var binding: ActivityMainBinding @@ -34,9 +37,13 @@ class BookingsTabController @Inject constructor( private fun checkBookingsTabVisibility() { activity.lifecycleScope.launch { - observeBookingsTabVisibility() - .collect { - binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = it + selectedSite.observe() + .filter { it != null } + .collect { siteModel -> + observeBookingsTabVisibility(siteModel!!) + .collect { + binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = it + } } } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt index f2361cd089f0..b2f1cbb8f964 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt @@ -2,46 +2,41 @@ package com.woocommerce.android.ui.bookings.tab import androidx.annotation.VisibleForTesting import com.woocommerce.android.ciab.isCIABSite -import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.products.ProductStatus import com.woocommerce.android.ui.products.list.ProductListRepository import com.woocommerce.android.util.FeatureFlag import com.woocommerce.android.util.WooLog import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption import javax.inject.Inject class ObserveBookingsTabVisibility @Inject constructor( - private val selectedSite: SelectedSite, private val productListRepository: ProductListRepository ) { - operator fun invoke(): Flow { - return combine( - selectedSite.observe(), - productListRepository - .observeProductsCount( - filterOptions = mapOf( - ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, - ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE - ), - excludeSampleProducts = true - ), - ) { site, productsCount -> + operator fun invoke(siteModel: SiteModel): Flow = productListRepository.observeProductsCount( + filterOptions = mapOf( + ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, + ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE + ), + excludeSampleProducts = true + ).onStart { + productListRepository.fetchProductList( + productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE) + ).onFailure { + WooLog.w(WooLog.T.BOOKINGS, "Failed to fetch bookable products") + } + } + .map { productsCount -> productsCount > 0 && - site?.isCIABSite() == true && + siteModel?.isCIABSite() == true && FeatureFlag.BOOKINGS_MVP.isEnabled() - }.onStart { - productListRepository.fetchProductList( - productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE) - ).onFailure { - WooLog.w(WooLog.T.BOOKINGS, "Failed to fetch bookable products" ) - } - }.distinctUntilChanged() - } + } + .distinctUntilChanged() companion object { @VisibleForTesting From a120eed847ab675180941897802b02cc9d3b84a3 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Fri, 19 Sep 2025 18:44:09 +0200 Subject: [PATCH 11/23] Fix unit tests after refactor --- .../tab/ObserveBookingsTabVisibilityTest.kt | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt index f14f548f62b9..a30e3043a32b 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt @@ -1,7 +1,6 @@ package com.woocommerce.android.ui.bookings.tab import app.cash.turbine.test -import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.bookings.tab.ObserveBookingsTabVisibility.Companion.BOOKING_PRODUCT_TYPE import com.woocommerce.android.ui.products.ProductStatus import com.woocommerce.android.ui.products.list.ProductListRepository @@ -23,12 +22,8 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE ) - private val siteFlow = MutableStateFlow(null) private val bookableProdsCountFlow = MutableStateFlow(0L) - private val selectedSite: SelectedSite = mock { - on { observe() }.thenReturn(siteFlow) - } private val productListRepository: ProductListRepository = mock { on { observeProductsCount( @@ -42,17 +37,16 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { suspend fun setup(prepareMocks: suspend () -> Unit = {}) { prepareMocks() - sut = ObserveBookingsTabVisibility(selectedSite, productListRepository) + sut = ObserveBookingsTabVisibility(productListRepository) } @Test fun `when invoke is called, then bookable products are fetched`() = testBlocking { - siteFlow.value = ciabSite() bookableProdsCountFlow.value = 2 setup() - sut().test { + sut(ciabSite()).test { verify(productListRepository).fetchProductList( loadMore = false, productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE), @@ -65,12 +59,11 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given CIAB site and bookings products published, when invoke, then emits true`() = testBlocking { - siteFlow.value = ciabSite() bookableProdsCountFlow.value = 1 setup() - sut().test { + sut(ciabSite()).test { val showBookingTabValue = awaitItem() assertTrue(showBookingTabValue) cancelAndIgnoreRemainingEvents() @@ -78,13 +71,12 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { } @Test - fun `given zero booking products, when invoke, then emits false`() = testBlocking { - siteFlow.value = ciabSite() + fun `given CIAB site with zero booking products, when invoke, then emits false`() = testBlocking { bookableProdsCountFlow.value = 0 setup() - sut().test { + sut(ciabSite()).test { val showBookingTabValue = awaitItem() assertFalse(showBookingTabValue) cancelAndIgnoreRemainingEvents() @@ -93,12 +85,11 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given non-CIAB site, when invoke, then emits false`() = testBlocking { - siteFlow.value = nonCiabSite() bookableProdsCountFlow.value = 10 setup() - sut().test { + sut(nonCiabSite()).test { val showBookingTabValue = awaitItem() assertFalse(showBookingTabValue) cancelAndIgnoreRemainingEvents() @@ -107,12 +98,11 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given non-Commerce Garden CIAB site, when invoke, then emits false`() = testBlocking { - siteFlow.value = nonCommerceGardenSite() bookableProdsCountFlow.value = 10 setup() - sut().test { + sut(nonCommerceGardenSite()).test { val showBookingTabValue = awaitItem() assertFalse(showBookingTabValue) cancelAndIgnoreRemainingEvents() @@ -121,19 +111,15 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given same inputs produce same result, when values update, then only emits once`() = testBlocking { - siteFlow.value = ciabSite() bookableProdsCountFlow.value = 2 setup() - sut().test { + sut(ciabSite()).test { val firstEmission = awaitItem() assertTrue(firstEmission) - // When inputs change but computed value remains true bookableProdsCountFlow.value = 3 // still true - siteFlow.value = ciabSite() // new instance but still CIAB - // Then no new emissions due to distinctUntilChanged expectNoEvents() @@ -148,12 +134,11 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given bookable products fetch fails onStart, when invoke, then emits based on persisted values`() = testBlocking { - siteFlow.value = ciabSite() bookableProdsCountFlow.value = 1 setup() - sut().test { + sut(ciabSite()).test { assert(awaitItem()) cancelAndIgnoreRemainingEvents() } From eb13d9eeb06fd3f41b4ce5f1bdbf35a698cf44ce Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Mon, 22 Sep 2025 18:12:17 +0200 Subject: [PATCH 12/23] Move new is isCIABSite to the correct file --- .../kotlin/com/woocommerce/android/ciab/CIABExtensions.kt | 6 ------ .../com/woocommerce/android/ciab/CIABSiteGateKeeper.kt | 1 + .../com/woocommerce/android/extensions/SiteModelExt.kt | 3 +++ .../android/ui/bookings/tab/ObserveBookingsTabVisibility.kt | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt deleted file mode 100644 index 4d6199d93076..000000000000 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABExtensions.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.woocommerce.android.ciab - -import com.woocommerce.android.ciab.CIABSiteGateKeeper.Companion.CIAB_GARDEN_NAME -import org.wordpress.android.fluxc.model.SiteModel - -fun SiteModel.isCIABSite() = isGardenSite && gardenName == CIAB_GARDEN_NAME diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt index e20cd6f66f31..b706ef58fcd8 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ciab/CIABSiteGateKeeper.kt @@ -1,5 +1,6 @@ package com.woocommerce.android.ciab +import com.woocommerce.android.extensions.isCIABSite import com.woocommerce.android.tools.SelectedSite import javax.inject.Inject diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/SiteModelExt.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/SiteModelExt.kt index 2317052b4af7..946e01e31b5e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/SiteModelExt.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/SiteModelExt.kt @@ -1,6 +1,7 @@ package com.woocommerce.android.extensions import android.text.TextUtils +import com.woocommerce.android.ciab.CIABSiteGateKeeper.Companion.CIAB_GARDEN_NAME import com.woocommerce.android.ui.plans.domain.FREE_TRIAL_PLAN_ID import com.woocommerce.android.util.WooLog import org.wordpress.android.fluxc.model.SiteModel @@ -78,3 +79,5 @@ val SiteModel?.isSitePublic: Boolean val SiteModel.isEligibleForAI: Boolean get() = isWPComAtomic || planActiveFeatures.orEmpty().contains("ai-assistant") + +fun SiteModel.isCIABSite() = isGardenSite && gardenName == CIAB_GARDEN_NAME diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt index b2f1cbb8f964..b526b249bc70 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt @@ -1,7 +1,7 @@ package com.woocommerce.android.ui.bookings.tab import androidx.annotation.VisibleForTesting -import com.woocommerce.android.ciab.isCIABSite +import com.woocommerce.android.extensions.isCIABSite import com.woocommerce.android.ui.products.ProductStatus import com.woocommerce.android.ui.products.list.ProductListRepository import com.woocommerce.android.util.FeatureFlag From 6c84c240fba5adfa14fa9b23af03d3af1497cb83 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Mon, 22 Sep 2025 18:13:47 +0200 Subject: [PATCH 13/23] Use filterNotNull function --- .../android/ui/bookings/tab/BookingsTabController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt index 72cca1ad41e0..cff7114f5bad 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt @@ -7,7 +7,7 @@ import com.woocommerce.android.R import com.woocommerce.android.databinding.ActivityMainBinding import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.main.MainActivity -import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import javax.inject.Inject @@ -38,7 +38,7 @@ class BookingsTabController @Inject constructor( private fun checkBookingsTabVisibility() { activity.lifecycleScope.launch { selectedSite.observe() - .filter { it != null } + .filterNotNull() .collect { siteModel -> observeBookingsTabVisibility(siteModel!!) .collect { From 5610a8435550a8cf7ac939e3e14900128518c7de Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Mon, 22 Sep 2025 18:21:24 +0200 Subject: [PATCH 14/23] Remove unneeded nullable expression and simplified boolean condition --- .../android/ui/bookings/tab/ObserveBookingsTabVisibility.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt index b526b249bc70..53ebb4a9f681 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt @@ -32,9 +32,7 @@ class ObserveBookingsTabVisibility @Inject constructor( } } .map { productsCount -> - productsCount > 0 && - siteModel?.isCIABSite() == true && - FeatureFlag.BOOKINGS_MVP.isEnabled() + productsCount > 0 && siteModel.isCIABSite() && FeatureFlag.BOOKINGS_MVP.isEnabled() } .distinctUntilChanged() From 2ad102ad397840ef4c7ee4fcafe17d800c832ccf Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Mon, 22 Sep 2025 18:26:07 +0200 Subject: [PATCH 15/23] Avoid fetching bookings over the network if site is not CIAB --- .../tab/ObserveBookingsTabVisibility.kt | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt index 53ebb4a9f681..80204d214368 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt @@ -8,6 +8,8 @@ import com.woocommerce.android.util.FeatureFlag import com.woocommerce.android.util.WooLog import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import org.wordpress.android.fluxc.model.SiteModel @@ -18,23 +20,30 @@ class ObserveBookingsTabVisibility @Inject constructor( private val productListRepository: ProductListRepository ) { - operator fun invoke(siteModel: SiteModel): Flow = productListRepository.observeProductsCount( - filterOptions = mapOf( - ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, - ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE - ), - excludeSampleProducts = true - ).onStart { - productListRepository.fetchProductList( - productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE) - ).onFailure { - WooLog.w(WooLog.T.BOOKINGS, "Failed to fetch bookable products") + operator fun invoke(siteModel: SiteModel): Flow = flow { + val isCIABSite = FeatureFlag.BOOKINGS_MVP.isEnabled() && siteModel.isCIABSite() + if (!isCIABSite) { + emit(false) + } else { + emitAll( + productListRepository.observeProductsCount( + filterOptions = mapOf( + ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, + ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE + ), + excludeSampleProducts = true + ) + .map { count -> count > 0 } + .onStart { + productListRepository.fetchProductList( + productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE) + ).onFailure { + WooLog.w(WooLog.T.BOOKINGS, "Failed to fetch bookable products") + } + }.distinctUntilChanged() + ) } } - .map { productsCount -> - productsCount > 0 && siteModel.isCIABSite() && FeatureFlag.BOOKINGS_MVP.isEnabled() - } - .distinctUntilChanged() companion object { @VisibleForTesting From 41a3fc6733f050aa1cf6b01df86cbc80e65e4313 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Tue, 23 Sep 2025 09:09:37 +0200 Subject: [PATCH 16/23] Make onStart logic async to avoid delaying the emission of first event --- .../bookings/tab/ObserveBookingsTabVisibility.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt index 80204d214368..21d59427b908 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt @@ -1,23 +1,27 @@ package com.woocommerce.android.ui.bookings.tab import androidx.annotation.VisibleForTesting +import com.woocommerce.android.di.AppCoroutineScope import com.woocommerce.android.extensions.isCIABSite import com.woocommerce.android.ui.products.ProductStatus import com.woocommerce.android.ui.products.list.ProductListRepository import com.woocommerce.android.util.FeatureFlag import com.woocommerce.android.util.WooLog +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption import javax.inject.Inject class ObserveBookingsTabVisibility @Inject constructor( - private val productListRepository: ProductListRepository + private val productListRepository: ProductListRepository, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) { operator fun invoke(siteModel: SiteModel): Flow = flow { @@ -35,10 +39,12 @@ class ObserveBookingsTabVisibility @Inject constructor( ) .map { count -> count > 0 } .onStart { - productListRepository.fetchProductList( - productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE) - ).onFailure { - WooLog.w(WooLog.T.BOOKINGS, "Failed to fetch bookable products") + appCoroutineScope.launch { + productListRepository.fetchProductList( + productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE) + ).onFailure { + WooLog.w(WooLog.T.BOOKINGS, "Failed to fetch bookable products") + } } }.distinctUntilChanged() ) From a3e6f537626b2162af36fdc934ac952e15f5dba4 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Tue, 23 Sep 2025 09:19:33 +0200 Subject: [PATCH 17/23] Reactive implementation enables removing lifecycle observer We just need to ensure BookingsTabController subscribes to ObserveBookingsTabVisibility when the app starts, and unsubscribes when destroyed.activity.lifecycleScope takes care of this already so no need to make the class lifecycle aware. --- .../ui/bookings/tab/BookingsTabController.kt | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt index cff7114f5bad..fb505d56394b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt @@ -1,7 +1,5 @@ package com.woocommerce.android.ui.bookings.tab -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import com.woocommerce.android.R import com.woocommerce.android.databinding.ActivityMainBinding @@ -14,7 +12,7 @@ import javax.inject.Inject class BookingsTabController @Inject constructor( private val observeBookingsTabVisibility: ObserveBookingsTabVisibility, private val selectedSite: SelectedSite -) : DefaultLifecycleObserver { +) { private lateinit var activity: MainActivity private lateinit var binding: ActivityMainBinding @@ -24,23 +22,15 @@ class BookingsTabController @Inject constructor( ) { this.activity = activity this.binding = binding - activity.lifecycle.addObserver(this) - } - - override fun onResume(owner: LifecycleOwner) { checkBookingsTabVisibility() } - override fun onDestroy(owner: LifecycleOwner) { - owner.lifecycle.removeObserver(this) - } - private fun checkBookingsTabVisibility() { activity.lifecycleScope.launch { selectedSite.observe() .filterNotNull() .collect { siteModel -> - observeBookingsTabVisibility(siteModel!!) + observeBookingsTabVisibility(siteModel) .collect { binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = it } From 9e03aae86488e8aa2046aaf7a63586db931a90db Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Tue, 23 Sep 2025 09:28:36 +0200 Subject: [PATCH 18/23] Fix unit tests --- .../ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt index a30e3043a32b..4a60c5eb1532 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt @@ -7,6 +7,7 @@ import com.woocommerce.android.ui.products.list.ProductListRepository import com.woocommerce.android.viewmodel.BaseUnitTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -17,6 +18,7 @@ import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption @OptIn(ExperimentalCoroutinesApi::class) class ObserveBookingsTabVisibilityTest : BaseUnitTest() { + private val testScope = TestScope(coroutinesTestRule.testDispatcher) private val bookableProdsFilterOptions = mapOf( ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, @@ -37,7 +39,10 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { suspend fun setup(prepareMocks: suspend () -> Unit = {}) { prepareMocks() - sut = ObserveBookingsTabVisibility(productListRepository) + sut = ObserveBookingsTabVisibility( + productListRepository, + appCoroutineScope = testScope + ) } @Test From f05fba9c28493c7531724c8af53ea8d6fcf8b010 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Tue, 23 Sep 2025 13:13:56 +0200 Subject: [PATCH 19/23] Remove nesting flow collection to avoid potential issues --- .../ui/bookings/tab/BookingsTabController.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt index fb505d56394b..97f13482ae89 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt @@ -5,7 +5,9 @@ import com.woocommerce.android.R import com.woocommerce.android.databinding.ActivityMainBinding import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.main.MainActivity +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import javax.inject.Inject @@ -25,16 +27,17 @@ class BookingsTabController @Inject constructor( checkBookingsTabVisibility() } + @OptIn(ExperimentalCoroutinesApi::class) private fun checkBookingsTabVisibility() { activity.lifecycleScope.launch { selectedSite.observe() - .filterNotNull() - .collect { siteModel -> - observeBookingsTabVisibility(siteModel) - .collect { - binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = it - } - } + .filterNotNull() + .flatMapLatest { siteModel -> + observeBookingsTabVisibility(siteModel) + } + .collect { isVisible -> + binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = isVisible + } } } } From 7a5b2c02abf7df33deb18a7b69bc5d081eb1fe08 Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Tue, 23 Sep 2025 17:59:15 +0200 Subject: [PATCH 20/23] Fix detekt issue --- .../ui/bookings/tab/BookingsTabController.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt index 97f13482ae89..963dcc7fce6d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt @@ -31,13 +31,13 @@ class BookingsTabController @Inject constructor( private fun checkBookingsTabVisibility() { activity.lifecycleScope.launch { selectedSite.observe() - .filterNotNull() - .flatMapLatest { siteModel -> - observeBookingsTabVisibility(siteModel) - } - .collect { isVisible -> - binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = isVisible - } + .filterNotNull() + .flatMapLatest { siteModel -> + observeBookingsTabVisibility(siteModel) + } + .collect { isVisible -> + binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = isVisible + } } } } From e1227bc4bf22e4b663fc5cd395c21eee9b803bfa Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Wed, 24 Sep 2025 16:23:42 +0200 Subject: [PATCH 21/23] Moves logic to observe site changes to ObserveBookingsTabVisibility --- .../ui/bookings/tab/BookingsTabController.kt | 14 ++------------ .../bookings/tab/ObserveBookingsTabVisibility.kt | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt index 963dcc7fce6d..e403f740df44 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/BookingsTabController.kt @@ -3,17 +3,12 @@ package com.woocommerce.android.ui.bookings.tab import androidx.lifecycle.lifecycleScope import com.woocommerce.android.R import com.woocommerce.android.databinding.ActivityMainBinding -import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.main.MainActivity -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch import javax.inject.Inject class BookingsTabController @Inject constructor( - private val observeBookingsTabVisibility: ObserveBookingsTabVisibility, - private val selectedSite: SelectedSite + private val observeBookingsTabVisibility: ObserveBookingsTabVisibility ) { private lateinit var activity: MainActivity private lateinit var binding: ActivityMainBinding @@ -27,14 +22,9 @@ class BookingsTabController @Inject constructor( checkBookingsTabVisibility() } - @OptIn(ExperimentalCoroutinesApi::class) private fun checkBookingsTabVisibility() { activity.lifecycleScope.launch { - selectedSite.observe() - .filterNotNull() - .flatMapLatest { siteModel -> - observeBookingsTabVisibility(siteModel) - } + observeBookingsTabVisibility() .collect { isVisible -> binding.bottomNav.menu.findItem(R.id.bookings)?.isVisible = isVisible } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt index 21d59427b908..7952642f8f6f 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt @@ -3,14 +3,18 @@ package com.woocommerce.android.ui.bookings.tab import androidx.annotation.VisibleForTesting import com.woocommerce.android.di.AppCoroutineScope import com.woocommerce.android.extensions.isCIABSite +import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.products.ProductStatus import com.woocommerce.android.ui.products.list.ProductListRepository import com.woocommerce.android.util.FeatureFlag import com.woocommerce.android.util.WooLog import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -21,10 +25,20 @@ import javax.inject.Inject class ObserveBookingsTabVisibility @Inject constructor( private val productListRepository: ProductListRepository, + private val selectedSite: SelectedSite, @AppCoroutineScope private val appCoroutineScope: CoroutineScope, ) { - operator fun invoke(siteModel: SiteModel): Flow = flow { + @OptIn(ExperimentalCoroutinesApi::class) + operator fun invoke(): Flow = + selectedSite.observe() + .filterNotNull() + .flatMapLatest { siteModel -> + observeBookingTabVisibility(siteModel) + } + + + private fun observeBookingTabVisibility(siteModel: SiteModel): Flow = flow { val isCIABSite = FeatureFlag.BOOKINGS_MVP.isEnabled() && siteModel.isCIABSite() if (!isCIABSite) { emit(false) From 81a856c38afb6d95840976e2f5ebc0144bdd27ab Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Wed, 24 Sep 2025 16:34:29 +0200 Subject: [PATCH 22/23] Fix unit tests --- .../tab/ObserveBookingsTabVisibilityTest.kt | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt index 4a60c5eb1532..378dd04400c0 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibilityTest.kt @@ -1,6 +1,7 @@ package com.woocommerce.android.ui.bookings.tab import app.cash.turbine.test +import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.bookings.tab.ObserveBookingsTabVisibility.Companion.BOOKING_PRODUCT_TYPE import com.woocommerce.android.ui.products.ProductStatus import com.woocommerce.android.ui.products.list.ProductListRepository @@ -11,15 +12,20 @@ import kotlinx.coroutines.test.TestScope import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.store.WCProductStore.ProductFilterOption @OptIn(ExperimentalCoroutinesApi::class) class ObserveBookingsTabVisibilityTest : BaseUnitTest() { private val testScope = TestScope(coroutinesTestRule.testDispatcher) - + private val selectedSite: SelectedSite = mock() + private val selectedSiteFlow = MutableStateFlow(null) private val bookableProdsFilterOptions = mapOf( ProductFilterOption.STATUS to ProductStatus.PUBLISH.value, ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE @@ -39,24 +45,26 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { suspend fun setup(prepareMocks: suspend () -> Unit = {}) { prepareMocks() + whenever(selectedSite.observe()).thenReturn(selectedSiteFlow) sut = ObserveBookingsTabVisibility( - productListRepository, - appCoroutineScope = testScope + productListRepository = productListRepository, + selectedSite = selectedSite, + appCoroutineScope = testScope, ) } @Test - fun `when invoke is called, then bookable products are fetched`() = testBlocking { + fun `given site is CIAB, when invoke is called, then bookable products are fetched`() = testBlocking { bookableProdsCountFlow.value = 2 - + selectedSiteFlow.value = ciabSite() setup() - sut(ciabSite()).test { + sut().test { verify(productListRepository).fetchProductList( - loadMore = false, - productFilterOptions = mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE), - excludedProductIds = emptyList(), - sortType = null + loadMore = any(), + productFilterOptions = eq(mapOf(ProductFilterOption.TYPE to BOOKING_PRODUCT_TYPE)), + excludedProductIds = any(), + sortType = anyOrNull() ) cancelAndIgnoreRemainingEvents() } @@ -65,10 +73,10 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given CIAB site and bookings products published, when invoke, then emits true`() = testBlocking { bookableProdsCountFlow.value = 1 - + selectedSiteFlow.value = ciabSite() setup() - sut(ciabSite()).test { + sut().test { val showBookingTabValue = awaitItem() assertTrue(showBookingTabValue) cancelAndIgnoreRemainingEvents() @@ -78,10 +86,10 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given CIAB site with zero booking products, when invoke, then emits false`() = testBlocking { bookableProdsCountFlow.value = 0 - + selectedSiteFlow.value = ciabSite() setup() - sut(ciabSite()).test { + sut().test { val showBookingTabValue = awaitItem() assertFalse(showBookingTabValue) cancelAndIgnoreRemainingEvents() @@ -91,10 +99,10 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given non-CIAB site, when invoke, then emits false`() = testBlocking { bookableProdsCountFlow.value = 10 - + selectedSiteFlow.value = nonCiabSite() setup() - sut(nonCiabSite()).test { + sut().test { val showBookingTabValue = awaitItem() assertFalse(showBookingTabValue) cancelAndIgnoreRemainingEvents() @@ -104,10 +112,10 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given non-Commerce Garden CIAB site, when invoke, then emits false`() = testBlocking { bookableProdsCountFlow.value = 10 - + selectedSiteFlow.value = nonCommerceGardenSite() setup() - sut(nonCommerceGardenSite()).test { + sut().test { val showBookingTabValue = awaitItem() assertFalse(showBookingTabValue) cancelAndIgnoreRemainingEvents() @@ -117,10 +125,10 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { @Test fun `given same inputs produce same result, when values update, then only emits once`() = testBlocking { bookableProdsCountFlow.value = 2 - + selectedSiteFlow.value = ciabSite() setup() - sut(ciabSite()).test { + sut().test { val firstEmission = awaitItem() assertTrue(firstEmission) // When inputs change but computed value remains true @@ -140,10 +148,10 @@ class ObserveBookingsTabVisibilityTest : BaseUnitTest() { fun `given bookable products fetch fails onStart, when invoke, then emits based on persisted values`() = testBlocking { bookableProdsCountFlow.value = 1 - + selectedSiteFlow.value = ciabSite() setup() - sut(ciabSite()).test { + sut().test { assert(awaitItem()) cancelAndIgnoreRemainingEvents() } From 34a3bb1ff38e766278f01e4e7c2188b2833ff0de Mon Sep 17 00:00:00 2001 From: jorgemucientesfayos Date: Wed, 24 Sep 2025 16:50:35 +0200 Subject: [PATCH 23/23] Fix detekt indentation issue --- .../android/ui/bookings/tab/ObserveBookingsTabVisibility.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt index 7952642f8f6f..d98fc3c90103 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/bookings/tab/ObserveBookingsTabVisibility.kt @@ -37,7 +37,6 @@ class ObserveBookingsTabVisibility @Inject constructor( observeBookingTabVisibility(siteModel) } - private fun observeBookingTabVisibility(siteModel: SiteModel): Flow = flow { val isCIABSite = FeatureFlag.BOOKINGS_MVP.isEnabled() && siteModel.isCIABSite() if (!isCIABSite) {