diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterHistoryMapper.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterHistoryMapper.kt new file mode 100644 index 00000000000..d6906aadcb5 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterHistoryMapper.kt @@ -0,0 +1,48 @@ +package com.woocommerce.android.ui.products.filter + +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import javax.inject.Inject + +/** + * Encodes/decodes a [ProductFilterResult] to and from the opaque `payload` string persisted in the + * filter history table. + * + * The payload is a JSON serialization of the selected slugs; Gson omits null fields, so logically + * identical selections produce identical JSON and dedup reliably. The category name is only persisted + * when a category id is actually selected, so clearing the category to "Any" can't leak a stale name + * into the payload (which would break dedup). Decoding tolerates missing/unknown fields (all data-class + * fields default to null). + */ +class ProductFilterHistoryMapper @Inject constructor( + private val gson: Gson +) { + fun toPayload(filter: ProductFilterResult): String = gson.toJson(filter.toData()) + + fun fromPayload(payload: String): ProductFilterResult? = + runCatching { gson.fromJson(payload, ProductFilterHistoryData::class.java) }.getOrNull()?.toResult() + + private fun ProductFilterResult.toData() = ProductFilterHistoryData( + stockStatus = stockStatus, + productStatus = productStatus, + productType = productType, + productCategory = productCategory, + productCategoryName = productCategoryName?.takeIf { productCategory != null } + ) + + private fun ProductFilterHistoryData.toResult() = ProductFilterResult( + stockStatus = stockStatus, + productType = productType, + productStatus = productStatus, + productCategory = productCategory, + productCategoryName = productCategoryName + ) + + data class ProductFilterHistoryData( + @SerializedName("stock_status") val stockStatus: String? = null, + @SerializedName("product_status") val productStatus: String? = null, + @SerializedName("product_type") val productType: String? = null, + @SerializedName("product_category") val productCategory: String? = null, + @SerializedName("product_category_name") val productCategoryName: String? = null + ) +} diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListFragment.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListFragment.kt index e15a9cf46ae..4391c734db2 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListFragment.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListFragment.kt @@ -14,12 +14,17 @@ import com.woocommerce.android.R import com.woocommerce.android.analytics.AnalyticsEvent import com.woocommerce.android.analytics.AnalyticsTracker import com.woocommerce.android.databinding.FragmentProductFilterListBinding +import com.woocommerce.android.extensions.handleResult import com.woocommerce.android.extensions.navigateBackWithResult import com.woocommerce.android.extensions.navigateSafely import com.woocommerce.android.extensions.takeIfNotEqualTo import com.woocommerce.android.ui.base.BaseFragment +import com.woocommerce.android.ui.filters.FilterHistoryType +import com.woocommerce.android.ui.filters.FilterHistoryViewModel +import com.woocommerce.android.ui.filters.SavedFilter import com.woocommerce.android.ui.main.AppBarStatus import com.woocommerce.android.ui.main.MainActivity +import com.woocommerce.android.ui.products.filter.ProductFilterListViewModel.OpenFilterHistory import com.woocommerce.android.ui.products.list.ProductListFragment import com.woocommerce.android.viewmodel.MultiLiveEvent import dagger.hilt.android.AndroidEntryPoint @@ -84,10 +89,12 @@ class ProductFilterListFragment : override fun onCreateMenu(menu: Menu, inflater: MenuInflater) { inflater.inflate(R.menu.menu_clear, menu) + inflater.inflate(R.menu.menu_filter_history, menu) } override fun onPrepareMenu(menu: Menu) { updateClearButtonVisibility(menu.findItem(R.id.menu_clear)) + menu.findItem(R.id.menu_filter_history).isVisible = viewModel.isFilterHistoryEnabled } override fun onMenuItemSelected(item: MenuItem): Boolean { @@ -98,6 +105,12 @@ class ProductFilterListFragment : updateClearButtonVisibility(item) true } + + R.id.menu_filter_history -> { + viewModel.onFilterHistoryButtonClicked() + true + } + else -> false } } @@ -116,13 +129,25 @@ class ProductFilterListFragment : is MultiLiveEvent.Event.ExitWithResult<*> -> { navigateBackWithResult(ProductListFragment.PRODUCT_FILTER_RESULT_KEY, event.data) } + is OpenFilterHistory -> navigateToFilterHistory() else -> event.isHandled = false } } + handleResult(FilterHistoryViewModel.FILTER_HISTORY_RESULT_KEY) { + viewModel.onPastFilterSelected(it) + } + viewModel.loadFilters() } + private fun navigateToFilterHistory() { + findNavController().navigateSafely( + ProductFilterListFragmentDirections + .actionProductFilterListFragmentToFilterHistoryFragment(FilterHistoryType.PRODUCTS) + ) + } + private fun showProductFilterList(productFilterList: List) { productFilterListAdapter.filterList = productFilterList } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListViewModel.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListViewModel.kt index 8cc01fe9849..df16818d806 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListViewModel.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListViewModel.kt @@ -16,11 +16,14 @@ import com.woocommerce.android.model.sortCategories import com.woocommerce.android.tools.NetworkStatus import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.common.PluginRepository +import com.woocommerce.android.ui.filters.SavedFilter import com.woocommerce.android.ui.products.ProductStatus import com.woocommerce.android.ui.products.ProductStockStatus import com.woocommerce.android.ui.products.ProductType import com.woocommerce.android.ui.products.categories.ProductCategoriesRepository import com.woocommerce.android.ui.products.filter.ProductFilterListViewModel.FilterListOptionItemUiModel.DefaultFilterListOptionItemUiModel +import com.woocommerce.android.util.FeatureFlag +import com.woocommerce.android.util.FeatureFlagRepository import com.woocommerce.android.viewmodel.LiveDataDelegate import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.ResourceProvider @@ -50,12 +53,17 @@ class ProductFilterListViewModel @Inject constructor( private val pluginRepository: PluginRepository, private val selectedSite: SelectedSite, private val analyticsTracker: AnalyticsTrackerWrapper, + private val saveProductFilterToHistory: SaveProductFilterToHistory, + private val productFilterHistoryMapper: ProductFilterHistoryMapper, + featureFlagRepository: FeatureFlagRepository, ) : ScopedViewModel(savedState) { companion object { private const val KEY_PRODUCT_FILTER_OPTIONS = "key_product_filter_options" private const val KEY_PRODUCT_FILTER_SELECTED_CATEGORY_NAME = "key_product_filter_selected_category_name" } + val isFilterHistoryEnabled: Boolean = featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY) + private val arguments: ProductFilterListFragmentArgs by savedState.navArgs() private var pluginsInformation: Map = HashMap() @@ -314,9 +322,31 @@ class ProductFilterListViewModel @Inject constructor( productCategory = getFilterByProductCategory(), productCategoryName = selectedCategoryName ) + saveProductFilterToHistory(result) triggerEvent(MultiLiveEvent.Event.ExitWithResult(result)) } + fun onFilterHistoryButtonClicked() { + analyticsTracker.track( + AnalyticsEvent.FILTER_HISTORY_BUTTON_TAPPED, + mapOf(AnalyticsTracker.KEY_SOURCE to AnalyticsTracker.VALUE_FILTER_HISTORY_SOURCE_PRODUCTS) + ) + triggerEvent(OpenFilterHistory) + } + + fun onPastFilterSelected(savedFilter: SavedFilter) { + val result = productFilterHistoryMapper.fromPayload(savedFilter.payload) ?: return + productFilterOptions.clear() + result.stockStatus?.let { productFilterOptions[STOCK_STATUS] = it } + result.productStatus?.let { productFilterOptions[STATUS] = it } + result.productType?.let { productFilterOptions[TYPE] = it } + result.productCategory?.let { productFilterOptions[CATEGORY] = it } + selectedCategoryName = result.productCategoryName + savedState[KEY_PRODUCT_FILTER_OPTIONS] = productFilterOptions + savedState[KEY_PRODUCT_FILTER_SELECTED_CATEGORY_NAME] = selectedCategoryName + loadFilters() + } + private fun buildFilterListItemUiModel(): List { val filterListItems = mutableListOf() filterListItems.add( @@ -450,6 +480,8 @@ class ProductFilterListViewModel @Inject constructor( }?.filterOptionListItems = categoryOptions } + object OpenFilterHistory : MultiLiveEvent.Event() + @Parcelize data class ProductFilterListViewState( val screenTitle: String? = null, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/SaveProductFilterToHistory.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/SaveProductFilterToHistory.kt new file mode 100644 index 00000000000..725fd75e007 --- /dev/null +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/products/filter/SaveProductFilterToHistory.kt @@ -0,0 +1,62 @@ +package com.woocommerce.android.ui.products.filter + +import androidx.annotation.StringRes +import com.woocommerce.android.di.AppCoroutineScope +import com.woocommerce.android.ui.filters.FilterHistoryRepository +import com.woocommerce.android.ui.filters.FilterHistoryType +import com.woocommerce.android.ui.products.ProductStatus +import com.woocommerce.android.ui.products.ProductStockStatus +import com.woocommerce.android.ui.products.ProductType +import com.woocommerce.android.util.FeatureFlag +import com.woocommerce.android.util.FeatureFlagRepository +import com.woocommerce.android.util.WooLog +import com.woocommerce.android.viewmodel.ResourceProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * Persists the given product filter selection to the filter history. + * + * Fire-and-forget on the application scope so it survives the filter screen being dismissed, letting + * callers navigate away immediately without awaiting the DB write. Failures are logged rather than + * propagated — persisting history is best-effort and must never crash the app. + * + * Owns the human-readable label resolution (slug → localized label) so the [ProductFilterHistoryMapper] + * stays a pure payload codec, matching the order-list use case. + */ +class SaveProductFilterToHistory @Inject constructor( + private val featureFlagRepository: FeatureFlagRepository, + private val filterHistoryRepository: FilterHistoryRepository, + private val productFilterHistoryMapper: ProductFilterHistoryMapper, + private val resourceProvider: ResourceProvider, + @AppCoroutineScope private val appCoroutineScope: CoroutineScope +) { + operator fun invoke(filter: ProductFilterResult) { + if (!featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)) return + if (!filter.hasSelection()) return + appCoroutineScope.launch { + runCatching { + filterHistoryRepository.save( + type = FilterHistoryType.PRODUCTS, + payload = productFilterHistoryMapper.toPayload(filter), + readableString = buildReadableString(filter) + ) + }.onFailure { WooLog.e(WooLog.T.PRODUCTS, "Failed to save product filter to history", it) } + } + } + + private fun buildReadableString(filter: ProductFilterResult): String = + listOfNotNull( + filter.stockStatus?.let { label(ProductStockStatus.fromString(it).stringResource) }, + filter.productStatus?.let { status -> ProductStatus.fromString(status)?.let { label(it.stringResource) } }, + filter.productType?.let { label(ProductType.fromString(it).stringResource) }, + filter.productCategoryName?.takeIf { filter.productCategory != null } + ).joinToString(separator = ", ") + + private fun label(@StringRes resId: Int): String? = + resId.takeIf { it != 0 }?.let { resourceProvider.getString(it) } + + private fun ProductFilterResult.hasSelection(): Boolean = + stockStatus != null || productStatus != null || productType != null || productCategory != null +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterHistoryMapperTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterHistoryMapperTest.kt new file mode 100644 index 00000000000..337df3102ef --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterHistoryMapperTest.kt @@ -0,0 +1,58 @@ +package com.woocommerce.android.ui.products.filter + +import com.google.gson.Gson +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class ProductFilterHistoryMapperTest { + private val sut = ProductFilterHistoryMapper(Gson()) + + @Test + fun `given a full selection, when encoding then decoding, then it round-trips`() { + val filter = ProductFilterResult( + stockStatus = "instock", + productType = "simple", + productStatus = "draft", + productCategory = "5", + productCategoryName = "Shoes" + ) + + assertThat(sut.fromPayload(sut.toPayload(filter))).isEqualTo(filter) + } + + @Test + fun `given identical selections, when encoded, then payloads are identical`() { + val a = ProductFilterResult("instock", null, "draft", null, null) + val b = ProductFilterResult("instock", null, "draft", null, null) + + assertThat(sut.toPayload(a)).isEqualTo(sut.toPayload(b)) + } + + @Test + fun `given an invalid payload, when decoding, then null is returned`() { + assertThat(sut.fromPayload("not-json")).isNull() + } + + @Test + fun `given an empty json payload, when decoding, then a filter with no selection is returned`() { + assertThat(sut.fromPayload("{}")).isEqualTo(ProductFilterResult(null, null, null, null, null)) + } + + @Test + fun `given a category name without a category id, when encoding, then the name is dropped`() { + val withStaleName = ProductFilterResult("instock", null, null, null, "Any") + val withoutName = ProductFilterResult("instock", null, null, null, null) + + // The stale name must not leak into the payload, otherwise it breaks dedup against the same + // stock-only filter saved from another path. + assertThat(sut.toPayload(withStaleName)).isEqualTo(sut.toPayload(withoutName)) + assertThat(sut.fromPayload(sut.toPayload(withStaleName))?.productCategoryName).isNull() + } + + @Test + fun `given a category name with a category id, when encoding, then the name is kept`() { + val filter = ProductFilterResult(null, null, null, "5", "Shoes") + + assertThat(sut.fromPayload(sut.toPayload(filter))?.productCategoryName).isEqualTo("Shoes") + } +} diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListViewModelTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListViewModelTest.kt index 48d32c8ae23..32e26824e44 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListViewModelTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/ProductFilterListViewModelTest.kt @@ -8,16 +8,20 @@ import com.woocommerce.android.model.WooPlugin import com.woocommerce.android.tools.NetworkStatus import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.common.PluginRepository +import com.woocommerce.android.ui.filters.SavedFilter import com.woocommerce.android.ui.products.ProductType import com.woocommerce.android.ui.products.categories.ProductCategoriesRepository import com.woocommerce.android.ui.products.filter.ProductFilterListViewModel.FilterListOptionItemUiModel +import com.woocommerce.android.util.FeatureFlagRepository import com.woocommerce.android.viewmodel.BaseUnitTest +import com.woocommerce.android.viewmodel.MultiLiveEvent import com.woocommerce.android.viewmodel.ResourceProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import org.assertj.core.api.Assertions import org.junit.Before import org.junit.Test import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -37,6 +41,9 @@ class ProductFilterListViewModelTest : BaseUnitTest() { private lateinit var productFilterListViewModel: ProductFilterListViewModel private lateinit var pluginRepository: PluginRepository private lateinit var analyticsTrackerWrapper: AnalyticsTrackerWrapper + private lateinit var saveProductFilterToHistory: SaveProductFilterToHistory + private lateinit var productFilterHistoryMapper: ProductFilterHistoryMapper + private lateinit var featureFlagRepository: FeatureFlagRepository private val siteModel: SiteModel = SiteModel().apply { id = 123 } private val selectedSiteMock: SelectedSite = mock { on { getIfExists() }.doReturn(siteModel) @@ -51,6 +58,9 @@ class ProductFilterListViewModelTest : BaseUnitTest() { networkStatus = mock() pluginRepository = mock() analyticsTrackerWrapper = mock() + saveProductFilterToHistory = mock() + productFilterHistoryMapper = mock() + featureFlagRepository = mock() productFilterListViewModel = ProductFilterListViewModel( savedState = ProductFilterListFragmentArgs( selectedStockStatus = "instock", @@ -64,12 +74,77 @@ class ProductFilterListViewModelTest : BaseUnitTest() { networkStatus = networkStatus, pluginRepository = pluginRepository, selectedSite = selectedSiteMock, - analyticsTracker = analyticsTrackerWrapper + analyticsTracker = analyticsTrackerWrapper, + saveProductFilterToHistory = saveProductFilterToHistory, + productFilterHistoryMapper = productFilterHistoryMapper, + featureFlagRepository = featureFlagRepository ) whenever(resourceProvider.getString(any())).thenReturn("") } + @Test + fun `when show products is clicked, then the current filter is saved and returned`() { + productFilterListViewModel.onShowProductsClicked() + + val expected = ProductFilterResult( + stockStatus = "instock", + productType = "any", + productStatus = "published", + productCategory = "1", + productCategoryName = "any" + ) + val captor = argumentCaptor() + verify(saveProductFilterToHistory).invoke(captor.capture()) + Assertions.assertThat(captor.firstValue).isEqualTo(expected) + + val event = productFilterListViewModel.event.value + Assertions.assertThat(event).isInstanceOf(MultiLiveEvent.Event.ExitWithResult::class.java) + Assertions.assertThat((event as MultiLiveEvent.Event.ExitWithResult<*>).data).isEqualTo(expected) + } + + @Test + fun `when filter history button is clicked, then entry point is tracked and history is opened`() { + productFilterListViewModel.onFilterHistoryButtonClicked() + + verify(analyticsTrackerWrapper).track( + AnalyticsEvent.FILTER_HISTORY_BUTTON_TAPPED, + mapOf(AnalyticsTracker.KEY_SOURCE to AnalyticsTracker.VALUE_FILTER_HISTORY_SOURCE_PRODUCTS) + ) + Assertions.assertThat(productFilterListViewModel.event.value) + .isEqualTo(ProductFilterListViewModel.OpenFilterHistory) + } + + @Test + fun `when a past filter is selected, then all filter options are repopulated`() { + val applied = ProductFilterResult( + stockStatus = "outofstock", + productType = "simple", + productStatus = "draft", + productCategory = "7", + productCategoryName = "Boots" + ) + whenever(productFilterHistoryMapper.fromPayload("payload")).thenReturn(applied) + + productFilterListViewModel.onPastFilterSelected(SavedFilter(readableString = "r", payload = "payload")) + productFilterListViewModel.onShowProductsClicked() + + // onShowProductsClicked rebuilds the result from the repopulated map + selectedCategoryName, + // so an equal result proves every field (incl. the category name) was applied. + val captor = argumentCaptor() + verify(saveProductFilterToHistory).invoke(captor.capture()) + Assertions.assertThat(captor.firstValue).isEqualTo(applied) + } + + @Test + fun `given an undecodable past filter, when selected, then the current selection is unchanged`() { + whenever(productFilterHistoryMapper.fromPayload("bad")).thenReturn(null) + + productFilterListViewModel.onPastFilterSelected(SavedFilter(readableString = "r", payload = "bad")) + + Assertions.assertThat(productFilterListViewModel.getFilterString()).isEqualTo("instock, any, published, 1") + } + @Test fun `when filters are loaded, then display product status filter`() { val productFilters = mutableListOf() diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/SaveProductFilterToHistoryTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/SaveProductFilterToHistoryTest.kt new file mode 100644 index 00000000000..86d480221e3 --- /dev/null +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/products/filter/SaveProductFilterToHistoryTest.kt @@ -0,0 +1,101 @@ +package com.woocommerce.android.ui.products.filter + +import com.woocommerce.android.ui.filters.FilterHistoryRepository +import com.woocommerce.android.ui.filters.FilterHistoryType +import com.woocommerce.android.util.FeatureFlag +import com.woocommerce.android.util.FeatureFlagRepository +import com.woocommerce.android.viewmodel.BaseUnitTest +import com.woocommerce.android.viewmodel.ResourceProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +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 + +@OptIn(ExperimentalCoroutinesApi::class) +class SaveProductFilterToHistoryTest : BaseUnitTest() { + private val featureFlagRepository: FeatureFlagRepository = mock() + private val filterHistoryRepository: FilterHistoryRepository = mock() + private val productFilterHistoryMapper: ProductFilterHistoryMapper = mock { + on { toPayload(any()) } doReturn PAYLOAD + } + private val resourceProvider: ResourceProvider = mock { + on { getString(any()) } doReturn "Label" + } + + private val sut = SaveProductFilterToHistory( + featureFlagRepository = featureFlagRepository, + filterHistoryRepository = filterHistoryRepository, + productFilterHistoryMapper = productFilterHistoryMapper, + resourceProvider = resourceProvider, + appCoroutineScope = CoroutineScope(coroutinesTestRule.testDispatcher) + ) + + @Test + fun `given feature flag disabled, when invoked, then nothing is saved`() = testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(false) + + sut(ProductFilterResult("instock", null, null, null, null)) + + verify(filterHistoryRepository, never()).save(any(), any(), any()) + } + + @Test + fun `given no filter is selected, when invoked, then nothing is saved`() = testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(true) + + sut(ProductFilterResult(null, null, null, null, null)) + + verify(filterHistoryRepository, never()).save(any(), any(), any()) + } + + @Test + fun `given only a category name is set, when invoked, then nothing is saved`() = testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(true) + + sut(ProductFilterResult(null, null, null, null, "Shoes")) + + verify(filterHistoryRepository, never()).save(any(), any(), any()) + } + + @Test + fun `given a filter is selected, when invoked, then it is saved with the resolved label`() = testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(true) + + sut(ProductFilterResult("instock", null, null, null, null)) + + verify(filterHistoryRepository).save(FilterHistoryType.PRODUCTS, PAYLOAD, "Label") + } + + @Test + fun `given an unresolvable slug, when building the label, then it is dropped from the readable string`() = + testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(true) + + // "weirdstatus" maps to ProductStockStatus.Custom (stringResource == 0) → dropped; + // only the valid product type resolves, so the readable string is a single "Label". + sut(ProductFilterResult(stockStatus = "weirdstatus", productType = "simple", null, null, null)) + + verify(filterHistoryRepository).save(eq(FilterHistoryType.PRODUCTS), eq(PAYLOAD), eq("Label")) + } + + @Test + fun `given a category name without a category id, when building the label, then the name is dropped`() = + testBlocking { + whenever(featureFlagRepository.isEnabled(FeatureFlag.FILTER_HISTORY)).thenReturn(true) + + sut(ProductFilterResult(stockStatus = "instock", null, null, null, productCategoryName = "Any")) + + // Readable is just the stock label, not "Label, Any". + verify(filterHistoryRepository).save(eq(FilterHistoryType.PRODUCTS), eq(PAYLOAD), eq("Label")) + } + + private companion object { + const val PAYLOAD = "payload" + } +}