From 810affa55fb070be1282fa188c4b68ba2eebff31 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Wed, 30 Apr 2025 11:30:52 +0200 Subject: [PATCH 01/12] Add new event objects --- .../ui/woopos/util/analytics/WooPosAnalyticsEvent.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt index 0ad62a573aa..f499b60993a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt @@ -105,6 +105,18 @@ sealed class WooPosAnalyticsEvent : IAnalyticsEvent { data object SimpleProductExplanationDialogShown : Event() { override val name: String = "simple_products_explanation_dialog_shown" } + data object SearchButtonTapped : Event() { + override val name: String = "search_button_tapped" + } + data object PreSearchRecentTermTapped : Event() { + override val name: String = "pre_search_recent_term_tapped" + } + data object KeyboardDismissedInSearch : Event() { + override val name: String = "keyboard_dismissed_in_search" + } + data object ItemsNextPageLoaded : Event() { + override val name: String = "items_next_page_loaded" + } } sealed class PaymentFlowTrackerEvent : WooPosAnalyticsEvent() { From 0a0ee74fa38e2723cbb16f55a56d375028245837 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Wed, 30 Apr 2025 13:20:55 +0200 Subject: [PATCH 02/12] Track SearchButtonTapped --- .../home/items/WooPosItemsSearchHelper.kt | 20 ++++- .../home/items/WooPosItemsViewModelTest.kt | 79 +++++++++++++++++++ 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt index 75b70bfc73f..fdb29832225 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt @@ -7,10 +7,17 @@ import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewState.SearchState +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchButtonTapped +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.viewmodel.ResourceProvider import dagger.hilt.android.scopes.ActivityRetainedScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -18,7 +25,8 @@ import javax.inject.Inject class WooPosItemsSearchHelper @Inject constructor( private val resourceProvider: ResourceProvider, private val childToParentEventSender: WooPosChildrenToParentEventSender, - private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver + private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver, + private val analyticsTracker: WooPosAnalyticsTracker, ) { private lateinit var coroutineScope: CoroutineScope private lateinit var viewStateFlow: MutableStateFlow @@ -30,6 +38,16 @@ class WooPosItemsSearchHelper @Inject constructor( this.coroutineScope = coroutineScope this.viewStateFlow = viewStateFlow listenEventsFromParent() + viewStateFlow + .map { it.search } + .distinctUntilChanged() + .map { it is SearchState.Visible && it.state is WooPosSearchInputState.Open } + .distinctUntilChanged() + .filter { it } + .onEach { + analyticsTracker.track(SearchButtonTapped) + } + .launchIn(coroutineScope) } private fun listenEventsFromParent() { diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt index 175acf3f501..ad351db1519 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt @@ -6,7 +6,11 @@ import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearch import com.woocommerce.android.ui.woopos.featureflags.WooPosIsProductsSearchEnabled import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchButtonTapped +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -15,6 +19,7 @@ import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -43,6 +48,9 @@ class WooPosItemsViewModelTest { on { defaultTabs }.thenReturn(tabs) } + private val analyticsTracker: WooPosAnalyticsTracker = mock() + private lateinit var _viewState: MutableStateFlow + @Before fun setup() { whenever(searchHelper.getInitialSearchState(any())).thenReturn( @@ -50,6 +58,13 @@ class WooPosItemsViewModelTest { state = WooPosSearchInputState.Closed ) ) + + _viewState = MutableStateFlow( + WooPosItemsViewState.ProductList( + tabs = tabsHelper.defaultTabs, + search = searchHelper.getInitialSearchState(isProductsSearchEnabled()), + ) + ) } @Test @@ -180,6 +195,70 @@ class WooPosItemsViewModelTest { assertThat(value).isInstanceOf(WooPosItemsViewState.CouponList::class.java) } } + + @Test + fun `when search state changes from closed to open, then SearchButtonTapped event is tracked`() = runTest { + // GIVEN + createViewModel() + + val initialState = WooPosItemsViewState.ProductList( + tabs = tabsHelper.defaultTabs, + search = WooPosItemsViewState.SearchState.Visible( + state = WooPosSearchInputState.Closed + ) + ) + _viewState.value = initialState + + // WHEN - Change state to open + val newState = WooPosItemsViewState.ProductList( + tabs = tabsHelper.defaultTabs, + search = WooPosItemsViewState.SearchState.Visible( + state = WooPosSearchInputState.Open( + input = WooPosSearchInputState.Open.Input.Hint(""), + isLoading = false + ) + ) + ) + _viewState.value = newState + advanceUntilIdle() + + // THEN + verify(analyticsTracker).track(SearchButtonTapped) + } + + @Test + fun `when search state changes but not from closed to open, event is not tracked`() = runTest { + // GIVEN + createViewModel() + + val initialState = WooPosItemsViewState.ProductList( + tabs = tabsHelper.defaultTabs, + search = WooPosItemsViewState.SearchState.Visible( + state = WooPosSearchInputState.Open( + input = WooPosSearchInputState.Open.Input.Hint(""), + isLoading = false + ) + ) + ) + _viewState.value = initialState + + // WHEN + val newState = WooPosItemsViewState.ProductList( + tabs = tabsHelper.defaultTabs, + search = WooPosItemsViewState.SearchState.Visible( + state = WooPosSearchInputState.Open( + input = WooPosSearchInputState.Open.Input.Query("test", 4), + isLoading = false + ) + ) + ) + _viewState.value = newState + advanceUntilIdle() + + // THEN + verify(analyticsTracker, never()).track(SearchButtonTapped) + } + private fun createViewModel() = WooPosItemsViewModel( wooPosItemsNavigator, From bc502d5a55a04af2eea5e43274d4c27a58eb2720 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Wed, 30 Apr 2025 13:25:38 +0200 Subject: [PATCH 03/12] Clean up code --- .../home/items/WooPosItemsSearchHelper.kt | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt index fdb29832225..53d9014c6f3 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelper.kt @@ -38,16 +38,7 @@ class WooPosItemsSearchHelper @Inject constructor( this.coroutineScope = coroutineScope this.viewStateFlow = viewStateFlow listenEventsFromParent() - viewStateFlow - .map { it.search } - .distinctUntilChanged() - .map { it is SearchState.Visible && it.state is WooPosSearchInputState.Open } - .distinctUntilChanged() - .filter { it } - .onEach { - analyticsTracker.track(SearchButtonTapped) - } - .launchIn(coroutineScope) + observeAndTrackSearchInputStateOpen(viewStateFlow, coroutineScope) } private fun listenEventsFromParent() { @@ -183,6 +174,25 @@ class WooPosItemsSearchHelper @Inject constructor( ) } + private fun observeAndTrackSearchInputStateOpen( + viewStateFlow: MutableStateFlow, + coroutineScope: CoroutineScope + ) { + viewStateFlow + .map { it.search } + .distinctUntilChanged() + .map { it is SearchState.Visible && it.state is WooPosSearchInputState.Open } + .distinctUntilChanged() + .filter { it } + .onEach { + val event = SearchButtonTapped.apply { + addProperties(mapOf("item_list_type" to "products")) + } + analyticsTracker.track(event) + } + .launchIn(coroutineScope) + } + private fun getCurrentContentState(): WooPosItemsViewState { return viewStateFlow.value } From fd11633e09a1b1b03d80dbf1d86be0ab03668026 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Wed, 30 Apr 2025 17:10:30 +0200 Subject: [PATCH 04/12] Add source param to ItemAddedToCart event --- .../home/WooPosHomeChildToParentCommunication.kt | 6 +++++- .../home/WooPosHomeParentToChildCommunication.kt | 4 +++- .../ui/woopos/home/WooPosHomeViewModel.kt | 2 +- .../ui/woopos/home/cart/WooPosCartViewModel.kt | 13 ++++++++----- .../home/items/coupons/WooPosCouponsViewModel.kt | 6 +++++- .../items/products/WooPosProductsViewModel.kt | 6 +++++- .../items/search/WooPosItemsSearchViewModel.kt | 4 +++- .../variations/WooPosVariationsViewModel.kt | 4 +++- .../util/analytics/WooPosAnalyticsEvent.kt | 16 +++++++++++++++- 9 files changed, 48 insertions(+), 13 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt index 16581c32259..1bccf61958b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeChildToParentCommunication.kt @@ -1,6 +1,7 @@ package com.woocommerce.android.ui.woopos.home import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import dagger.hilt.android.scopes.ActivityRetainedScoped import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -25,7 +26,10 @@ sealed class ChildToParentEvent { ) : ChildToParentEvent() data object BackFromCheckoutToCartClicked : ChildToParentEvent() - data class ItemClickedInProductSelector(val itemData: WooPosItemsViewModel.ItemClickedData) : ChildToParentEvent() + data class ItemClickedInProductSelector( + val itemData: WooPosItemsViewModel.ItemClickedData, + val source: WooPosItemSource = WooPosItemSource.PRODUCT_LIST + ) : ChildToParentEvent() data object NewTransactionClicked : ChildToParentEvent() data object PaymentCollecting : ChildToParentEvent() data object PaymentInProgress : ChildToParentEvent() diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt index af5282fda47..820ccd13326 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeParentToChildCommunication.kt @@ -1,6 +1,7 @@ package com.woocommerce.android.ui.woopos.home import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import dagger.hilt.android.scopes.ActivityRetainedScoped import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -22,7 +23,8 @@ class WooPosParentToChildrenCommunication @Inject constructor() : sealed class ParentToChildrenEvent { data object BackFromCheckoutToCartClicked : ParentToChildrenEvent() data class ItemClickedInProductSelector( - val itemData: WooPosItemsViewModel.ItemClickedData + val itemData: WooPosItemsViewModel.ItemClickedData, + val source: WooPosItemSource ) : ParentToChildrenEvent() data class CheckoutClicked( diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt index 9b9e62a74bc..f3275e8f2ee 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/WooPosHomeViewModel.kt @@ -129,7 +129,7 @@ class WooPosHomeViewModel @Inject constructor( is ChildToParentEvent.ItemClickedInProductSelector -> { sendEventToChildren( - ItemClickedInProductSelector(event.itemData) + ItemClickedInProductSelector(itemData = event.itemData, source = event.source) ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt index fbad972595b..28dda5a8ab5 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModel.kt @@ -27,6 +27,7 @@ import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Eve import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.CheckoutTapped import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ClearCartTapped import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.InteractionWithCustomerStarted +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemRemovedFromCart import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEventConstant import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker @@ -199,12 +200,14 @@ class WooPosCartViewModel @Inject constructor( analyticsTracker.track(InteractionWithCustomerStarted) } _state.value = updateStateWithNewItem(itemClicked.await()) - WooPosAnalyticsEvent.Event.ItemAddedToCart.addProperties( - mapOf( - WooPosAnalyticsEventConstant.PRODUCT_TYPE to event.itemData.posItemNameForAnalytics() + + val source = WooPosItemSource.toAnalyticsString(event.source) + val itemAddedEvent = WooPosAnalyticsEvent.Event.ItemAddedToCart(source).apply { + addProperties( + mapOf(WooPosAnalyticsEventConstant.PRODUCT_TYPE to event.itemData.posItemNameForAnalytics()) ) - ) - analyticsTracker.track(WooPosAnalyticsEvent.Event.ItemAddedToCart) + } + analyticsTracker.track(itemAddedEvent) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/coupons/WooPosCouponsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/coupons/WooPosCouponsViewModel.kt index 97b081a2b87..ff38f403477 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/coupons/WooPosCouponsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/coupons/WooPosCouponsViewModel.kt @@ -14,6 +14,7 @@ import com.woocommerce.android.ui.woopos.home.items.coupons.WooPosCouponsUIEvent import com.woocommerce.android.ui.woopos.home.items.coupons.WooPosCouponsUIEvent.RetryTriggered import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -96,7 +97,10 @@ class WooPosCouponsViewModel @Inject constructor( viewModelScope.launch { fromChildToParentEventSender.sendToParent( // CouponsProject: rename ItemClickedInProductSelector to ItemClicked - ChildToParentEvent.ItemClickedInProductSelector(ItemClickedData.Coupon(event.couponId)) + ChildToParentEvent.ItemClickedInProductSelector( + itemData = ItemClickedData.Coupon(event.couponId), + source = WooPosItemSource.COUPON_LIST + ) ) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt index 36f4cb3b4b9..4d8601c1d4d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt @@ -14,6 +14,7 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosProductsViewState import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ProductsPullToRefreshTriggered import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice @@ -252,7 +253,10 @@ class WooPosProductsViewModel @Inject constructor( } private fun onItemClicked(itemData: ItemClickedData) { - sendEventToParent(ChildToParentEvent.ItemClickedInProductSelector(itemData)) + sendEventToParent(ChildToParentEvent.ItemClickedInProductSelector( + itemData = itemData, + source = WooPosItemSource.PRODUCT_LIST + )) } private fun sendEventToParent(event: ChildToParentEvent) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt index 7a512ee8524..1191434f7c3 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt @@ -16,6 +16,7 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel.ItemCli import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -204,7 +205,8 @@ class WooPosItemsSearchViewModel @Inject constructor( viewModelScope.launch { childToParentEventSender.sendToParent( ChildToParentEvent.ItemClickedInProductSelector( - ItemClickedData.Product.Simple(id = item.id) + itemData = ItemClickedData.Product.Simple(id = item.id), + source = WooPosItemSource.SEARCH_RESULT ) ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt index c6f324b277e..86936d2db88 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt @@ -14,6 +14,7 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState import com.woocommerce.android.ui.woopos.home.items.WooPosVariationsViewState +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.VariationsPullToRefreshTriggered import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice @@ -208,7 +209,8 @@ class WooPosVariationsViewModel @Inject constructor( private fun onVariationClicked(productId: Long, variationId: Long) { sendEventToParent( ChildToParentEvent.ItemClickedInProductSelector( - WooPosItemsViewModel.ItemClickedData.Product.Variation(productId, variationId) + itemData = WooPosItemsViewModel.ItemClickedData.Product.Variation(productId, variationId), + source = WooPosItemSource.PRODUCT_LIST ) ) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt index f499b60993a..629f75979c0 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt @@ -77,8 +77,22 @@ sealed class WooPosAnalyticsEvent : IAnalyticsEvent { data object InteractionWithCustomerStarted : Event() { override val name: String = "interaction_with_customer_started" } - data object ItemAddedToCart : Event() { + data class ItemAddedToCart(val source: String = "list") : Event() { override val name: String = "item_added_to_cart" + + init { + addProperties(mapOf("source" to source)) + } + + enum class WooPosItemSource(val value: String) { + PRODUCT_LIST("list"), + SEARCH_RESULT("search_result"), + COUPON_LIST("coupons"); + + companion object { + fun toAnalyticsString(source: WooPosItemSource): String = source.value + } + } } data object ItemRemovedFromCart : Event() { override val name: String = "item_removed_from_cart" From afbc8d2d31b0d42ddef4b1b4fccc186fe24c8740 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Wed, 30 Apr 2025 17:35:27 +0200 Subject: [PATCH 05/12] Add pre_search_list source param to ItemAddedToCart event --- .../home/items/search/WooPosItemsSearchViewModel.kt | 9 +++++---- .../ui/woopos/util/analytics/WooPosAnalyticsEvent.kt | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt index 1191434f7c3..c33fd4ade66 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt @@ -63,7 +63,7 @@ class WooPosItemsSearchViewModel @Inject constructor( fun onUIEvent(event: WooPosItemsSearchUiEvent) { when (event) { WooPosItemsSearchUiEvent.OnNextPageRequested -> onEndOfListReached() - is WooPosItemsSearchUiEvent.OnItemClicked -> handleItemClicked(event.item) + is WooPosItemsSearchUiEvent.OnItemClicked -> handleItemClicked(event.item, WooPosItemSource.SEARCH_RESULT) WooPosItemsSearchUiEvent.LoadingErrorRetryButtonClicked -> { val currentState = _viewState.value as? WooPosItemsSearchViewState.Error ?: return performSearch(currentState.searchQuery) @@ -82,7 +82,7 @@ class WooPosItemsSearchViewModel @Inject constructor( is WooPosItemsSearchUiEvent.OnPopularItemClicked -> { viewModelScope.launch { emptyStateRepository.addPopularItemsToCache() - handleItemClicked(event.item) + handleItemClicked(event.item, WooPosItemSource.POPULAR_PRODUCTS) } } } @@ -199,14 +199,14 @@ class WooPosItemsSearchViewModel @Inject constructor( } } - private fun handleItemClicked(item: WooPosItemSelectionViewState) { + private fun handleItemClicked(item: WooPosItemSelectionViewState, source: WooPosItemSource) { when (item) { is WooPosItemSelectionViewState.Product.Simple -> { viewModelScope.launch { childToParentEventSender.sendToParent( ChildToParentEvent.ItemClickedInProductSelector( itemData = ItemClickedData.Product.Simple(id = item.id), - source = WooPosItemSource.SEARCH_RESULT + source = source, ) ) } @@ -225,6 +225,7 @@ class WooPosItemsSearchViewModel @Inject constructor( ) ) ) + // TODO: @samiuelson track item clicked on variations list } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt index 629f75979c0..b6852080106 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt @@ -87,6 +87,7 @@ sealed class WooPosAnalyticsEvent : IAnalyticsEvent { enum class WooPosItemSource(val value: String) { PRODUCT_LIST("list"), SEARCH_RESULT("search_result"), + POPULAR_PRODUCTS("pre_search_list"), COUPON_LIST("coupons"); companion object { From 13c1a39e42b83d55659904a99abe4b43bf83f09b Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 1 May 2025 12:15:18 +0200 Subject: [PATCH 06/12] Make `source` property dynamic in variations VM Track ItemClicked event in variations screen with real source (products, search) --- .../home/items/WooPosItemNavigationData.kt | 3 + .../items/products/WooPosProductsViewModel.kt | 11 +-- .../search/WooPosItemsSearchViewModel.kt | 2 +- .../variations/WooPosVariationsScreen.kt | 4 +- .../variations/WooPosVariationsViewModel.kt | 6 +- .../home/cart/WooPosCartViewModelTest.kt | 70 ++++++++++++------- .../home/items/WooPosItemsSearchHelperTest.kt | 5 +- .../WooPosLeftPaneScreensViewModelTest.kt | 4 ++ .../products/WooPosProductsViewModelTest.kt | 47 +++++++++++-- .../search/WooPosItemsSearchViewModelTest.kt | 34 ++++++++- .../WooPosVariationsViewModelTest.kt | 45 ++++++++---- .../WooPosAnalyticsEventTrackerTest.kt | 4 +- 12 files changed, 180 insertions(+), 55 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemNavigationData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemNavigationData.kt index 1ca6e1a5ef0..e149bd4353d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemNavigationData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemNavigationData.kt @@ -1,9 +1,12 @@ package com.woocommerce.android.ui.woopos.home.items +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource + sealed class WooPosItemNavigationData(open val id: Long) { data class VariableProductData( override val id: Long, val name: String, val numOfVariations: Int, + val source: WooPosItemSource, ) : WooPosItemNavigationData(id) } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt index 4d8601c1d4d..40734849907 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModel.kt @@ -102,6 +102,7 @@ class WooPosProductsViewModel @Inject constructor( id = event.item.id, name = event.item.name, numOfVariations = event.item.numOfVariations, + source = WooPosItemSource.PRODUCT_LIST ) ) ) @@ -253,10 +254,12 @@ class WooPosProductsViewModel @Inject constructor( } private fun onItemClicked(itemData: ItemClickedData) { - sendEventToParent(ChildToParentEvent.ItemClickedInProductSelector( - itemData = itemData, - source = WooPosItemSource.PRODUCT_LIST - )) + sendEventToParent( + ChildToParentEvent.ItemClickedInProductSelector( + itemData = itemData, + source = WooPosItemSource.PRODUCT_LIST + ) + ) } private fun sendEventToParent(event: ChildToParentEvent) { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt index c33fd4ade66..c95340b4b48 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt @@ -222,10 +222,10 @@ class WooPosItemsSearchViewModel @Inject constructor( id = item.id, name = item.name, numOfVariations = item.numOfVariations, + source = WooPosItemSource.SEARCH_RESULT ) ) ) - // TODO: @samiuelson track item clicked on variations list } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsScreen.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsScreen.kt index bc2ae74d126..b4e05ffb891 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsScreen.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsScreen.kt @@ -48,6 +48,7 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosItemsEmptyList import com.woocommerce.android.ui.woopos.home.items.WooPosItemsLoadingIndicator import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState import com.woocommerce.android.ui.woopos.home.items.WooPosVariationsViewState +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -61,7 +62,7 @@ fun WooPosVariationsScreen( key = variableProductData.id.toString() ) LaunchedEffect(variableProductData.id) { - viewModel.init(variableProductData.id) + viewModel.init(variableProductData.id, variableProductData.source) } val state = viewModel.viewState WooPosVariationsScreens( @@ -297,6 +298,7 @@ fun WooPosVariationsScreenPreview() { id = 0, name = "Variable Product", numOfVariations = 20, + source = WooPosItemSource.PRODUCT_LIST, ), state = productState, ) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt index 86936d2db88..fb36ed13e9a 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt @@ -48,11 +48,13 @@ class WooPosVariationsViewModel @Inject constructor( ) private var fetchJob: Job? = null + private var variationsSource: WooPosItemSource = WooPosItemSource.PRODUCT_LIST @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal var loadMoreJob: Job? = null - fun init(productId: Long) { + fun init(productId: Long, source: WooPosItemSource) { + this.variationsSource = source viewModelScope.launch { variationsDataSource.resetState() } @@ -210,7 +212,7 @@ class WooPosVariationsViewModel @Inject constructor( sendEventToParent( ChildToParentEvent.ItemClickedInProductSelector( itemData = WooPosItemsViewModel.ItemClickedData.Product.Variation(productId, variationId), - source = WooPosItemSource.PRODUCT_LIST + source = variationsSource ) ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt index a057386e42d..e07013bbd68 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt @@ -19,6 +19,8 @@ import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Eve import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.CheckoutTapped import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ClearCartTapped import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.InteractionWithCustomerStarted +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource.Companion.toAnalyticsString import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEventConstant import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTrackingDataKeeper @@ -117,7 +119,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -156,7 +159,8 @@ class WooPosCartViewModelTest { WooPosItemsViewModel.ItemClickedData.Product.Variation( id = variation.remoteVariationId, productId = variation.remoteProductId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -180,7 +184,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Coupon( id = 1L, - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -209,7 +214,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -246,7 +252,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Coupon( id = 1L, - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -286,7 +293,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -376,7 +384,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -437,14 +446,16 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product1.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) parentToChildrenEventsMutableFlow.emit( ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product2.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -465,7 +476,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product3.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -507,7 +519,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -527,7 +540,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Coupon( id = 1L, - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -581,7 +595,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) @@ -642,12 +657,14 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) // THEN - verify(analyticsTracker).track(WooPosAnalyticsEvent.Event.ItemAddedToCart) + verify(analyticsTracker) + .track(WooPosAnalyticsEvent.Event.ItemAddedToCart(source = toAnalyticsString(WooPosItemSource.PRODUCT_LIST))) } @Test @@ -663,12 +680,13 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Coupon( id = 1L, - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) // THEN - verify(analyticsTracker).track(WooPosAnalyticsEvent.Event.ItemAddedToCart) + verify(analyticsTracker).track(WooPosAnalyticsEvent.Event.ItemAddedToCart()) } @Test @@ -691,14 +709,15 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) // THEN verify(analyticsTracker).track( argThat { - this == WooPosAnalyticsEvent.Event.ItemAddedToCart && + this == WooPosAnalyticsEvent.Event.ItemAddedToCart() && ( this as WooPosAnalyticsEvent.Event.ItemAddedToCart ).properties[WooPosAnalyticsEventConstant.PRODUCT_TYPE] == "simple" @@ -734,14 +753,15 @@ class WooPosCartViewModelTest { WooPosItemsViewModel.ItemClickedData.Product.Variation( id = variation.remoteProductId, productId = variation.remoteProductId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) // THEN verify(analyticsTracker).track( argThat { - this == WooPosAnalyticsEvent.Event.ItemAddedToCart && + this == WooPosAnalyticsEvent.Event.ItemAddedToCart() && ( this as WooPosAnalyticsEvent.Event.ItemAddedToCart ).properties[WooPosAnalyticsEventConstant.PRODUCT_TYPE] == "variation" @@ -762,14 +782,15 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Coupon( id = 1L, - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) // THEN verify(analyticsTracker).track( argThat { - this == WooPosAnalyticsEvent.Event.ItemAddedToCart && + this == WooPosAnalyticsEvent.Event.ItemAddedToCart() && ( this as WooPosAnalyticsEvent.Event.ItemAddedToCart ).properties[WooPosAnalyticsEventConstant.PRODUCT_TYPE] == "coupon" @@ -793,7 +814,8 @@ class WooPosCartViewModelTest { ParentToChildrenEvent.ItemClickedInProductSelector( WooPosItemsViewModel.ItemClickedData.Product.Simple( id = product.remoteId - ) + ), + source = WooPosItemSource.PRODUCT_LIST ) ) return Pair(sut, states) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelperTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelperTest.kt index 805fcc75aca..c7d6e76f0ab 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelperTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelperTest.kt @@ -7,6 +7,7 @@ import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.viewmodel.ResourceProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow @@ -31,6 +32,7 @@ class WooPosItemsSearchHelperTest { private val mockResourceProvider: ResourceProvider = mock() private val mockChildToParentEventSender: WooPosChildrenToParentEventSender = mock() private val mockParentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock() + private val mockAnalyticsTracker: WooPosAnalyticsTracker = mock() private lateinit var viewStateFlow: MutableStateFlow private lateinit var searchHelper: WooPosItemsSearchHelper @@ -44,7 +46,8 @@ class WooPosItemsSearchHelperTest { searchHelper = WooPosItemsSearchHelper( resourceProvider = mockResourceProvider, childToParentEventSender = mockChildToParentEventSender, - parentToChildrenEventReceiver = mockParentToChildrenEventReceiver + parentToChildrenEventReceiver = mockParentToChildrenEventReceiver, + analyticsTracker = mockAnalyticsTracker, ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/navigation/WooPosLeftPaneScreensViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/navigation/WooPosLeftPaneScreensViewModelTest.kt index c0dbd07426f..18f6007f765 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/navigation/WooPosLeftPaneScreensViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/navigation/WooPosLeftPaneScreensViewModelTest.kt @@ -2,6 +2,7 @@ package com.woocommerce.android.ui.woopos.home.items.navigation import com.woocommerce.android.ui.woopos.home.items.WooPosItemNavigationData.VariableProductData import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runTest @@ -41,6 +42,7 @@ class WooPosLeftPaneScreensViewModelTest { 1L, "Product Name", numOfVariations = 10, + source = WooPosItemSource.PRODUCT_LIST, ) navigationEvents.emit(WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen(product)) @@ -58,6 +60,7 @@ class WooPosLeftPaneScreensViewModelTest { 1L, "Product Name", numOfVariations = 10, + source = WooPosItemSource.PRODUCT_LIST, ) navigationEvents.emit(WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen(product)) navigationEvents.emit(WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateBackToItemListScreen) @@ -71,6 +74,7 @@ class WooPosLeftPaneScreensViewModelTest { 1L, "Product Name", numOfVariations = 10, + source = WooPosItemSource.PRODUCT_LIST, ) navigationEvents.emit(WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen(product)) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModelTest.kt index 220154f695b..21de7546b06 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/products/WooPosProductsViewModelTest.kt @@ -6,13 +6,14 @@ import com.woocommerce.android.ui.woopos.home.ChildToParentEvent import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender import com.woocommerce.android.ui.woopos.home.items.WooPosContentViewState import com.woocommerce.android.ui.woopos.home.items.WooPosItemNavigationData -import com.woocommerce.android.ui.woopos.home.items.WooPosItemSelectionViewState.Product +import com.woocommerce.android.ui.woopos.home.items.WooPosItemSelectionViewState import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.WooPosProductsViewState import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule -import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ProductsPullToRefreshTriggered +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -104,7 +105,7 @@ class WooPosProductsViewModelTest { val value = awaitItem() as WooPosProductsViewState.Content @Suppress("UNCHECKED_CAST") - val items = value.items as List + val items = value.items as List assertThat(items).hasSize(2) assertThat(items[0].id).isEqualTo(1) assertThat(items[0].name).isEqualTo("Product 1") @@ -195,7 +196,7 @@ class WooPosProductsViewModelTest { @Test fun `when product clicked, then send event to parent`() = runTest { // GIVEN - val product = Product.Simple(id = 1, name = "", price = "", imageUrl = null) + val product = WooPosItemSelectionViewState.Product.Simple(id = 1, name = "", price = "", imageUrl = null) val viewModel = createViewModel() // WHEN @@ -299,7 +300,7 @@ class WooPosProductsViewModelTest { viewModel.onUIEvent(WooPosProductsUIEvent.PullToRefreshTriggered) // THEN - verify(analyticsTracker).track(ProductsPullToRefreshTriggered) + verify(analyticsTracker).track(WooPosAnalyticsEvent.Event.ProductsPullToRefreshTriggered) } @Test @@ -326,7 +327,7 @@ class WooPosProductsViewModelTest { // WHEN viewModel.onUIEvent( WooPosProductsUIEvent.ItemClicked( - Product.Variable( + WooPosItemSelectionViewState.Product.Variable( id = 1L, name = "Product 1", numOfVariations = 10, @@ -344,6 +345,7 @@ class WooPosProductsViewModelTest { id = 1, name = "Product 1", numOfVariations = 10, + source = WooPosItemSource.PRODUCT_LIST ) ) ) @@ -385,7 +387,9 @@ class WooPosProductsViewModelTest { // THEN val value = awaitItem() as WooPosProductsViewState.Content - assertThat(value.items.filterIsInstance().size).isEqualTo(1) + assertThat( + value.items.filterIsInstance().size + ).isEqualTo(1) } } @@ -462,6 +466,35 @@ class WooPosProductsViewModelTest { verify(productsDataSource, never()).loadMore() } + @Test + fun `when variable product is clicked from product list, then navigation event uses product list source`() = runTest { + // GIVEN + val viewModel = createViewModel() + val item = WooPosItemSelectionViewState.Product.Variable( + id = 1, + name = "Product", + price = "$10", + imageUrl = null, + numOfVariations = 2, + variationIds = emptyList() + ) + + // WHEN + viewModel.onUIEvent(WooPosProductsUIEvent.ItemClicked(item)) + + // THEN + verify(wooPosItemsNavigator).sendNavigationEvent( + WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen( + WooPosItemNavigationData.VariableProductData( + id = 1L, + name = "Product", + numOfVariations = 2, + source = WooPosItemSource.PRODUCT_LIST + ) + ) + ) + } + private fun createViewModel(): WooPosProductsViewModel { return WooPosProductsViewModel( productsDataSource = productsDataSource, diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt index 94aa5060d0d..99d7f7e16d0 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt @@ -7,12 +7,14 @@ import com.woocommerce.android.ui.woopos.home.ParentToChildrenEvent import com.woocommerce.android.ui.woopos.home.WooPosChildrenToParentEventSender import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceiver import com.woocommerce.android.ui.woopos.home.items.WooPosItemNavigationData.VariableProductData +import com.woocommerce.android.ui.woopos.home.items.WooPosItemSelectionViewState import com.woocommerce.android.ui.woopos.home.items.WooPosItemSelectionViewState.Product import com.woocommerce.android.ui.woopos.home.items.WooPosItemsViewModel.ItemClickedData import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -615,7 +617,8 @@ class WooPosItemsSearchViewModelTest { VariableProductData( id = 1, name = "Variable Product", - numOfVariations = 3 + numOfVariations = 3, + source = WooPosItemSource.SEARCH_RESULT, ) ) ) @@ -712,6 +715,35 @@ class WooPosItemsSearchViewModelTest { ) } + @Test + fun `when variable product is clicked from search, then navigation event uses search source`() = runTest { + // GIVEN + val viewModel = createViewModel() + val item = WooPosItemSelectionViewState.Product.Variable( + id = 1, + name = "Product", + price = "$10", + imageUrl = null, + numOfVariations = 2, + variationIds = emptyList() + ) + + // WHEN + viewModel.onUIEvent(WooPosItemsSearchUiEvent.OnItemClicked(item)) + + // THEN + verify(mockNavigator).sendNavigationEvent( + NavigateToVariationsScreen( + VariableProductData( + id = 1L, + name = "Product", + numOfVariations = 2, + source = WooPosItemSource.SEARCH_RESULT + ) + ) + ) + } + private fun mockSuccessfulSearch(query: String, products: List) { wheneverBlocking { mockDataSource.searchLocalProducts(query) }.thenReturn(emptyList()) wheneverBlocking { mockDataSource.searchRemoteProducts(query) }.thenReturn(Result.success(products)) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/variations/WooPosVariationsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/variations/WooPosVariationsViewModelTest.kt index 84c582c4c16..e11406f3b59 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/variations/WooPosVariationsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/variations/WooPosVariationsViewModelTest.kt @@ -15,6 +15,7 @@ import com.woocommerce.android.ui.woopos.home.items.variations.WooPosVariationsU import com.woocommerce.android.ui.woopos.home.items.variations.WooPosVariationsViewModel import com.woocommerce.android.ui.woopos.home.items.variations.getNameForPOS import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.VariationsPullToRefreshTriggered import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice @@ -67,7 +68,7 @@ class WooPosVariationsViewModelTest { // WHEN val viewModel = createViewModel() - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) viewModel.viewState.test { // THEN @@ -90,7 +91,7 @@ class WooPosVariationsViewModelTest { // WHEN val viewModel = createViewModel() - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) viewModel.viewState.test { // THEN @@ -113,7 +114,7 @@ class WooPosVariationsViewModelTest { // WHEN val viewModel = createViewModel() - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) viewModel.viewState.test { // THEN @@ -133,7 +134,7 @@ class WooPosVariationsViewModelTest { // WHEN val viewModel = createViewModel() - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) viewModel.viewState.test { // THEN assertThat(awaitItem()).isEqualTo(WooPosVariationsViewState.Empty()) @@ -149,7 +150,7 @@ class WooPosVariationsViewModelTest { // WHEN val viewModel = createViewModel() - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) viewModel.viewState.test { // THEN @@ -204,7 +205,7 @@ class WooPosVariationsViewModelTest { whenever(variationsDataSource.loadMore(any())).thenReturn(Result.success(emptyList())) val viewModel = createViewModel() - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) advanceUntilIdle() // WHEN @@ -241,7 +242,7 @@ class WooPosVariationsViewModelTest { ) val viewModel = createViewModel() - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) viewModel.onUIEvent(WooPosVariationsUIEvents.EndOfItemsListReached(123L, 10)) // THEN @@ -273,7 +274,7 @@ class WooPosVariationsViewModelTest { ) val viewModel = createViewModel() - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) advanceUntilIdle() viewModel.onUIEvent(WooPosVariationsUIEvents.EndOfItemsListReached(123L, 10)) advanceUntilIdle() @@ -301,7 +302,7 @@ class WooPosVariationsViewModelTest { val viewModel = createViewModel() val activeJob = Job() viewModel.loadMoreJob = activeJob - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) viewModel.onUIEvent(WooPosVariationsUIEvents.EndOfItemsListReached(1L, 10)) viewModel.viewState.test { @@ -325,7 +326,7 @@ class WooPosVariationsViewModelTest { // WHEN val viewModel = createViewModel() - viewModel.init(1L) + viewModel.init(1L, WooPosItemSource.PRODUCT_LIST) viewModel.viewState.test { // THEN @@ -335,9 +336,10 @@ class WooPosVariationsViewModelTest { } @Test - fun `given variation clicked, when item clicked, then send event to parent`() = runTest { + fun `given variation clicked and source is product list, when item clicked, then sends event with product list source`() = runTest { // GIVEN val viewModel = createViewModel() + viewModel.init(123L, WooPosItemSource.PRODUCT_LIST) // WHEN viewModel.onUIEvent(WooPosVariationsUIEvents.OnItemClicked(123L, 1L)) @@ -345,7 +347,26 @@ class WooPosVariationsViewModelTest { // THEN verify(fromChildToParentEventSender).sendToParent( ChildToParentEvent.ItemClickedInProductSelector( - WooPosItemsViewModel.ItemClickedData.Product.Variation(123L, 1L) + itemData = WooPosItemsViewModel.ItemClickedData.Product.Variation(123L, 1L), + source = WooPosItemSource.PRODUCT_LIST + ) + ) + } + + @Test + fun `given variation clicked and source is search result, when item clicked, then sends event with search result source`() = runTest { + // GIVEN + val viewModel = createViewModel() + viewModel.init(123L, WooPosItemSource.SEARCH_RESULT) + + // WHEN + viewModel.onUIEvent(WooPosVariationsUIEvents.OnItemClicked(123L, 1L)) + + // THEN + verify(fromChildToParentEventSender).sendToParent( + ChildToParentEvent.ItemClickedInProductSelector( + itemData = WooPosItemsViewModel.ItemClickedData.Product.Variation(123L, 1L), + source = WooPosItemSource.SEARCH_RESULT ) ) } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEventTrackerTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEventTrackerTest.kt index 1fad253a81d..8fc7515919c 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEventTrackerTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEventTrackerTest.kt @@ -21,7 +21,7 @@ class WooPosAnalyticsEventTrackerTest { @Test fun `given an event, when track is called, then it should track the event via wrapper`() = runTest { // GIVEN - val event = WooPosAnalyticsEvent.Event.ItemAddedToCart + val event = WooPosAnalyticsEvent.Event.ItemAddedToCart() // WHEN tracker.track(event) @@ -58,7 +58,7 @@ class WooPosAnalyticsEventTrackerTest { @Test fun `given an event and common properties, when track is called, then it should track the event with common properties`() = runTest { // GIVEN - val event = WooPosAnalyticsEvent.Event.ItemAddedToCart + val event = WooPosAnalyticsEvent.Event.ItemAddedToCart() val commonProperties = mapOf("test" to "test") whenever(commonPropertiesProvider.commonProperties).thenReturn(commonProperties) From 642d372affca52a30cdc10820a1dc2b0c8c5d188 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 1 May 2025 13:14:22 +0200 Subject: [PATCH 07/12] Update tests --- .../home/items/search/WooPosItemsSearchViewModelTest.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt index 99d7f7e16d0..f7c1379c740 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt @@ -589,7 +589,8 @@ class WooPosItemsSearchViewModelTest { // THEN verify(mockChildToParentEventSender).sendToParent( ChildToParentEvent.ItemClickedInProductSelector( - ItemClickedData.Product.Simple(id = 1) + itemData = ItemClickedData.Product.Simple(id = 1), + source = WooPosItemSource.SEARCH_RESULT ) ) } @@ -710,7 +711,8 @@ class WooPosItemsSearchViewModelTest { verify(mockEmptyStateProvider).addPopularItemsToCache() verify(mockChildToParentEventSender).sendToParent( ChildToParentEvent.ItemClickedInProductSelector( - ItemClickedData.Product.Simple(id = simpleProduct.id) + ItemClickedData.Product.Simple(id = simpleProduct.id), + source = WooPosItemSource.POPULAR_PRODUCTS ) ) } From e85b59603d8cd913965171776082fea2f6504760 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 1 May 2025 16:39:20 +0200 Subject: [PATCH 08/12] Fix coroutine cancellations in test --- .../home/items/WooPosItemsSearchHelperTest.kt | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelperTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelperTest.kt index c7d6e76f0ab..b0a041092b3 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelperTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsSearchHelperTest.kt @@ -9,6 +9,7 @@ import com.woocommerce.android.ui.woopos.home.WooPosParentToChildrenEventReceive import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.viewmodel.ResourceProvider +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf @@ -82,7 +83,7 @@ class WooPosItemsSearchHelperTest { // GIVEN val searchQuery = "test query" val cursorPosition = searchQuery.length - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) // WHEN searchHelper.onSearchChanged(searchQuery, cursorPosition) @@ -99,7 +100,7 @@ class WooPosItemsSearchHelperTest { // GIVEN val searchQuery = "test query" val cursorPosition = searchQuery.length - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) // WHEN searchHelper.onSearchChanged(searchQuery, cursorPosition) @@ -119,7 +120,7 @@ class WooPosItemsSearchHelperTest { // GIVEN val emptyQuery = "" val cursorPosition = 0 - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) // WHEN searchHelper.onSearchChanged(emptyQuery, cursorPosition) @@ -134,7 +135,7 @@ class WooPosItemsSearchHelperTest { @Test fun `given open search state, when onCloseSearchClicked called, then updates to closed state`() = runTest { // GIVEN - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) searchHelper.onSearchChanged("initial query", "initial query".length) // WHEN @@ -150,7 +151,7 @@ class WooPosItemsSearchHelperTest { fun `given search with query, when onClearSearchClicked called, then resets to initial open state with hint`() = runTest { // GIVEN - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) searchHelper.onSearchChanged("initial query", "initial query".length) // WHEN @@ -171,7 +172,7 @@ class WooPosItemsSearchHelperTest { ) // WHEN - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) advanceUntilIdle() // THEN @@ -189,7 +190,7 @@ class WooPosItemsSearchHelperTest { ) // WHEN - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) // THEN val currentState = viewStateFlow.value as WooPosItemsViewState.ProductList @@ -201,10 +202,11 @@ class WooPosItemsSearchHelperTest { @Test fun `when animation completes, then hasAnimationPlayed flag is set to true`() = runTest { // GIVEN - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) // WHEN searchHelper.onAnimationComplete() + advanceUntilIdle() // THEN val currentState = viewStateFlow.value as WooPosItemsViewState.ProductList @@ -216,7 +218,7 @@ class WooPosItemsSearchHelperTest { @Test fun `given animation completed, when search changed, then hasAnimationPlayed is preserved`() = runTest { // GIVEN - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) searchHelper.onAnimationComplete() // WHEN @@ -236,7 +238,7 @@ class WooPosItemsSearchHelperTest { @Test fun `given animation has played, when onClearSearchClicked, then hasAnimationPlayed is false`() = runTest { // GIVEN - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) searchHelper.onAnimationComplete() @@ -253,7 +255,7 @@ class WooPosItemsSearchHelperTest { @Test fun `given animation complete, when order successful paid, then state is closed`() = runTest { // GIVEN - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) searchHelper.onAnimationComplete() // WHEN @@ -264,7 +266,7 @@ class WooPosItemsSearchHelperTest { ) ) ) - searchHelper.initialize(this, viewStateFlow) + searchHelper.initialize(CoroutineScope(coroutinesTestRule.testDispatcher), viewStateFlow) advanceUntilIdle() // THEN From 611b7ff424ea58ee48b65219e619128235c511e3 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Thu, 1 May 2025 16:52:39 +0200 Subject: [PATCH 09/12] Clean up tests --- .../home/items/WooPosItemsViewModelTest.kt | 78 ------------------- 1 file changed, 78 deletions(-) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt index ad351db1519..da0859c0978 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/WooPosItemsViewModelTest.kt @@ -6,11 +6,7 @@ import com.woocommerce.android.ui.woopos.common.composeui.component.WooPosSearch import com.woocommerce.android.ui.woopos.featureflags.WooPosIsProductsSearchEnabled import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule -import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.SearchButtonTapped -import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -19,7 +15,6 @@ import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -48,9 +43,6 @@ class WooPosItemsViewModelTest { on { defaultTabs }.thenReturn(tabs) } - private val analyticsTracker: WooPosAnalyticsTracker = mock() - private lateinit var _viewState: MutableStateFlow - @Before fun setup() { whenever(searchHelper.getInitialSearchState(any())).thenReturn( @@ -58,13 +50,6 @@ class WooPosItemsViewModelTest { state = WooPosSearchInputState.Closed ) ) - - _viewState = MutableStateFlow( - WooPosItemsViewState.ProductList( - tabs = tabsHelper.defaultTabs, - search = searchHelper.getInitialSearchState(isProductsSearchEnabled()), - ) - ) } @Test @@ -196,69 +181,6 @@ class WooPosItemsViewModelTest { } } - @Test - fun `when search state changes from closed to open, then SearchButtonTapped event is tracked`() = runTest { - // GIVEN - createViewModel() - - val initialState = WooPosItemsViewState.ProductList( - tabs = tabsHelper.defaultTabs, - search = WooPosItemsViewState.SearchState.Visible( - state = WooPosSearchInputState.Closed - ) - ) - _viewState.value = initialState - - // WHEN - Change state to open - val newState = WooPosItemsViewState.ProductList( - tabs = tabsHelper.defaultTabs, - search = WooPosItemsViewState.SearchState.Visible( - state = WooPosSearchInputState.Open( - input = WooPosSearchInputState.Open.Input.Hint(""), - isLoading = false - ) - ) - ) - _viewState.value = newState - advanceUntilIdle() - - // THEN - verify(analyticsTracker).track(SearchButtonTapped) - } - - @Test - fun `when search state changes but not from closed to open, event is not tracked`() = runTest { - // GIVEN - createViewModel() - - val initialState = WooPosItemsViewState.ProductList( - tabs = tabsHelper.defaultTabs, - search = WooPosItemsViewState.SearchState.Visible( - state = WooPosSearchInputState.Open( - input = WooPosSearchInputState.Open.Input.Hint(""), - isLoading = false - ) - ) - ) - _viewState.value = initialState - - // WHEN - val newState = WooPosItemsViewState.ProductList( - tabs = tabsHelper.defaultTabs, - search = WooPosItemsViewState.SearchState.Visible( - state = WooPosSearchInputState.Open( - input = WooPosSearchInputState.Open.Input.Query("test", 4), - isLoading = false - ) - ) - ) - _viewState.value = newState - advanceUntilIdle() - - // THEN - verify(analyticsTracker, never()).track(SearchButtonTapped) - } - private fun createViewModel() = WooPosItemsViewModel( wooPosItemsNavigator, From 473d69eccf43048cbdc2f5b1b298a9c3afce865b Mon Sep 17 00:00:00 2001 From: samiuelson Date: Fri, 2 May 2025 12:04:28 +0200 Subject: [PATCH 10/12] Track ItemsNextPageLoaded --- .../search/WooPosItemsSearchViewModel.kt | 16 +++++++++ .../variations/WooPosVariationsViewModel.kt | 15 ++++++++ .../search/WooPosItemsSearchViewModelTest.kt | 34 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt index c95340b4b48..9d303463baa 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt @@ -17,6 +17,8 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemsNextPageLoaded +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -39,6 +41,7 @@ class WooPosItemsSearchViewModel @Inject constructor( private val parentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver, private val navigator: WooPosItemsNavigator, private val searchHelper: WooPosItemsSearchHelper, + private val analyticsTracker: WooPosAnalyticsTracker, ) : ViewModel() { private val _viewState = MutableStateFlow(WooPosItemsSearchViewState.Empty) @@ -190,6 +193,7 @@ class WooPosItemsSearchViewModel @Inject constructor( loadMoreJob = viewModelScope.launch { val result = dataSource.loadMore(query = currentState.searchQuery) _viewState.value = if (result.isSuccess) { + trackItemsNextPageLoaded() result.getOrThrow().toContentState( searchQuery = currentState.searchQuery, ) @@ -199,6 +203,18 @@ class WooPosItemsSearchViewModel @Inject constructor( } } + private suspend fun trackItemsNextPageLoaded() { + val event = ItemsNextPageLoaded.apply { + addProperties( + mapOf( + "item_list_type" to "products", + "search" to "true" + ) + ) + } + analyticsTracker.track(event) + } + private fun handleItemClicked(item: WooPosItemSelectionViewState, source: WooPosItemSource) { when (item) { is WooPosItemSelectionViewState.Product.Simple -> { diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt index fb36ed13e9a..43501d82c22 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/variations/WooPosVariationsViewModel.kt @@ -15,6 +15,7 @@ import com.woocommerce.android.ui.woopos.home.items.WooPosPaginationState import com.woocommerce.android.ui.woopos.home.items.WooPosPullToRefreshState import com.woocommerce.android.ui.woopos.home.items.WooPosVariationsViewState import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemsNextPageLoaded import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.VariationsPullToRefreshTriggered import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice @@ -170,6 +171,7 @@ class WooPosVariationsViewModel @Inject constructor( loadMoreJob = viewModelScope.launch { val result = variationsDataSource.loadMore(productId) _viewState.value = if (result.isSuccess) { + trackItemsNextPageLoaded() WooPosVariationsViewState.Content( items = result.getOrThrow().map { WooPosItemSelectionViewState.Product.Variation( @@ -187,6 +189,19 @@ class WooPosVariationsViewModel @Inject constructor( } } + private suspend fun trackItemsNextPageLoaded() { + val event = ItemsNextPageLoaded.apply { + val isSearch: Boolean = variationsSource == WooPosItemSource.SEARCH_RESULT + addProperties( + mapOf( + "item_list_type" to "variations", + "search" to "$isSearch" + ) + ) + } + analyticsTracker.track(event) + } + fun onUIEvent(event: WooPosVariationsUIEvents) { when (event) { is WooPosVariationsUIEvents.EndOfItemsListReached -> { diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt index f7c1379c740..a9965148b16 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt @@ -15,6 +15,8 @@ import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNaviga import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen import com.woocommerce.android.ui.woopos.util.WooPosCoroutineTestRule import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemsNextPageLoaded +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay @@ -30,6 +32,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any +import org.mockito.kotlin.argThat import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -51,6 +54,7 @@ class WooPosItemsSearchViewModelTest { private val mockParentToChildrenEventReceiver: WooPosParentToChildrenEventReceiver = mock() private val mockNavigator: WooPosItemsNavigator = mock() private val mockSearchHelper: com.woocommerce.android.ui.woopos.home.items.WooPosItemsSearchHelper = mock() + private val mockAnalyticsTracker: WooPosAnalyticsTracker = mock() private val defaultQuery = "test query" private val defaultProduct = ProductTestUtils.generateProduct( @@ -343,6 +347,35 @@ class WooPosItemsSearchViewModelTest { } } + @Test + fun `given content state and more pages available, when end of list reached, then track ItemsNextPageLoaded event`() = runTest { + // GIVEN + val additionalProduct = ProductTestUtils.generateProduct( + productId = 2, + productName = "Test Product 2", + amount = "20.0", + productType = "simple" + ) + + mockSuccessfulSearch(defaultQuery, listOf(defaultProduct)) + mockSuccessfulPagination(defaultQuery, listOf(additionalProduct)) + + // WHEN + val viewModel = createViewModel() + advanceTimeBy(600) + viewModel.onUIEvent(WooPosItemsSearchUiEvent.OnNextPageRequested) + advanceUntilIdle() + + // THEN + verify(mockAnalyticsTracker).track( + argThat { event -> + event is ItemsNextPageLoaded && + event.properties["item_list_type"] == "products" && + event.properties["search"] == "true" + } + ) + } + @Test fun `given content state when load more fails, then pagination state is error`() = runTest { // GIVEN @@ -798,5 +831,6 @@ class WooPosItemsSearchViewModelTest { parentToChildrenEventReceiver = mockParentToChildrenEventReceiver, navigator = mockNavigator, searchHelper = mockSearchHelper, + analyticsTracker = mockAnalyticsTracker, ) } From ab08526252fee40cbcec9b693f3282e62775ba87 Mon Sep 17 00:00:00 2001 From: samiuelson Date: Fri, 2 May 2025 12:20:04 +0200 Subject: [PATCH 11/12] Track PreSearchRecentTermTapped --- .../home/items/search/WooPosItemsSearchViewModel.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt index 9d303463baa..8a0ca45942c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModel.kt @@ -18,6 +18,7 @@ import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNaviga import com.woocommerce.android.ui.woopos.home.items.navigation.WooPosItemsNavigator.WooPosItemsScreenNavigationEvent.NavigateToVariationsScreen import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemAddedToCart.WooPosItemSource import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.ItemsNextPageLoaded +import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsEvent.Event.PreSearchRecentTermTapped import com.woocommerce.android.ui.woopos.util.analytics.WooPosAnalyticsTracker import com.woocommerce.android.ui.woopos.util.format.WooPosFormatPrice import dagger.hilt.android.lifecycle.HiltViewModel @@ -86,6 +87,7 @@ class WooPosItemsSearchViewModel @Inject constructor( viewModelScope.launch { emptyStateRepository.addPopularItemsToCache() handleItemClicked(event.item, WooPosItemSource.POPULAR_PRODUCTS) + trackPopularItemClicked() } } } @@ -215,6 +217,17 @@ class WooPosItemsSearchViewModel @Inject constructor( analyticsTracker.track(event) } + private suspend fun trackPopularItemClicked() { + val event = PreSearchRecentTermTapped.apply { + addProperties( + mapOf( + "item_list_type" to "products", + ) + ) + } + analyticsTracker.track(event) + } + private fun handleItemClicked(item: WooPosItemSelectionViewState, source: WooPosItemSource) { when (item) { is WooPosItemSelectionViewState.Product.Simple -> { From 32bbb449845675274306af6a0956a9d2cb20503a Mon Sep 17 00:00:00 2001 From: samiuelson Date: Fri, 2 May 2025 13:02:53 +0200 Subject: [PATCH 12/12] Satisfy detekt's complaints --- .../android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt | 3 --- .../android/ui/woopos/home/cart/WooPosCartViewModelTest.kt | 4 +++- .../home/items/search/WooPosItemsSearchViewModelTest.kt | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt index b6852080106..ed976e163fc 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/analytics/WooPosAnalyticsEvent.kt @@ -126,9 +126,6 @@ sealed class WooPosAnalyticsEvent : IAnalyticsEvent { data object PreSearchRecentTermTapped : Event() { override val name: String = "pre_search_recent_term_tapped" } - data object KeyboardDismissedInSearch : Event() { - override val name: String = "keyboard_dismissed_in_search" - } data object ItemsNextPageLoaded : Event() { override val name: String = "items_next_page_loaded" } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt index e07013bbd68..c648a6016c5 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/cart/WooPosCartViewModelTest.kt @@ -664,7 +664,9 @@ class WooPosCartViewModelTest { // THEN verify(analyticsTracker) - .track(WooPosAnalyticsEvent.Event.ItemAddedToCart(source = toAnalyticsString(WooPosItemSource.PRODUCT_LIST))) + .track( + WooPosAnalyticsEvent.Event.ItemAddedToCart(source = toAnalyticsString(WooPosItemSource.PRODUCT_LIST)) + ) } @Test diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt index a9965148b16..ef5cafec8e3 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/home/items/search/WooPosItemsSearchViewModelTest.kt @@ -370,8 +370,8 @@ class WooPosItemsSearchViewModelTest { verify(mockAnalyticsTracker).track( argThat { event -> event is ItemsNextPageLoaded && - event.properties["item_list_type"] == "products" && - event.properties["search"] == "true" + event.properties["item_list_type"] == "products" && + event.properties["search"] == "true" } ) }