From 41140c6178111ec30019149fa1d86fe1c4d11ba7 Mon Sep 17 00:00:00 2001 From: rahulsainani Date: Tue, 28 Nov 2023 13:42:22 +0100 Subject: [PATCH] Bug 1866992 - Add product recommendations exposure telemetry (cherry picked from commit daf7931aee0e24763e302203ddf36b6a8e434d8a) --- app/metrics.yaml | 42 ++++ app/nimbus.fml.yaml | 5 + .../shopping/ShoppingExperienceFeature.kt | 8 + .../ReviewQualityCheckMiddlewareProvider.kt | 2 + .../ReviewQualityCheckNetworkMiddleware.kt | 11 +- ...ReviewQualityCheckPreferencesMiddleware.kt | 7 + .../middleware/ReviewQualityCheckService.kt | 12 +- .../store/ReviewQualityCheckAction.kt | 2 + .../shopping/store/ReviewQualityCheckState.kt | 2 + .../shopping/store/ReviewQualityCheckStore.kt | 2 + ...QualityCheckBottomSheetStateFeatureTest.kt | 1 + .../fake/FakeReviewQualityCheckService.kt | 6 +- .../fake/FakeShoppingExperienceFeature.kt | 4 + .../DefaultReviewQualityCheckServiceTest.kt | 129 +++++++++++++ ...viewQualityCheckTelemetryMiddlewareTest.kt | 22 ++- .../store/ReviewQualityCheckStoreTest.kt | 179 ++++++++++++++++-- 16 files changed, 409 insertions(+), 25 deletions(-) diff --git a/app/metrics.yaml b/app/metrics.yaml index 3b0faa3d6..140f36711 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -10923,6 +10923,48 @@ shopping: metadata: tags: - Shopping + ads_exposure: + type: event + description: | + On a supported product page, the review checker showed analysis, + and the ads exposure pref was enabled, or review checker ads were enabled, + and when we tried to fetch an ad from the ad server, an ad was available. + Does not indicate whether the ad was actually shown. + send_in_pings: + - events + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1866992 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4622#issuecomment-1829905076 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Shopping + surface_no_ads_available: + type: event + description: | + On a supported product page, the review checker + showed analysis, and review checker ads were enabled, + but when we tried to fetch an ad from the ad server, + no ad was available. + send_in_pings: + - events + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1866992 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/4622#issuecomment-1829905076 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: never + metadata: + tags: + - Shopping shopping.settings: component_opted_out: diff --git a/app/nimbus.fml.yaml b/app/nimbus.fml.yaml index c4dec93a2..5203597cf 100644 --- a/app/nimbus.fml.yaml +++ b/app/nimbus.fml.yaml @@ -405,11 +405,16 @@ features: description: if true, recommended products feature is enabled to be shown to the user based on their preference. type: Boolean default: false + product-recommendations-exposure: + description: if true, we want to record recommended products inventory for opted-in users, even if product recommendations are disabled. + type: Boolean + default: false defaults: - channel: developer value: enabled: true product-recommendations: true + product-recommendations-exposure: true print: description: A feature for printing from the share or browser menu. diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ShoppingExperienceFeature.kt b/app/src/main/java/org/mozilla/fenix/shopping/ShoppingExperienceFeature.kt index 4eef7176e..fd06ac5a1 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/ShoppingExperienceFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/ShoppingExperienceFeature.kt @@ -15,6 +15,11 @@ interface ShoppingExperienceFeature { * Returns true if the shopping experience feature is enabled. */ val isEnabled: Boolean + + /** + * Returns true if product recommendations exposure nimbus flag is enabled. + */ + val isProductRecommendationsExposureEnabled: Boolean } /** @@ -24,4 +29,7 @@ class DefaultShoppingExperienceFeature : ShoppingExperienceFeature { override val isEnabled get() = FxNimbus.features.shoppingExperience.value().enabled + + override val isProductRecommendationsExposureEnabled: Boolean + get() = FxNimbus.features.shoppingExperience.value().productRecommendationsExposure } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt b/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt index 7a7a16c22..e4cb6e1f5 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/di/ReviewQualityCheckMiddlewareProvider.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.CoroutineScope import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.tabs.TabsUseCases import org.mozilla.fenix.components.AppStore +import org.mozilla.fenix.shopping.DefaultShoppingExperienceFeature import org.mozilla.fenix.shopping.middleware.DefaultNetworkChecker import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckPreferences import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckService @@ -58,6 +59,7 @@ object ReviewQualityCheckMiddlewareProvider { reviewQualityCheckPreferences = DefaultReviewQualityCheckPreferences(settings), reviewQualityCheckVendorsService = DefaultReviewQualityCheckVendorsService(browserStore), appStore = appStore, + shoppingExperienceFeature = DefaultShoppingExperienceFeature(), scope = scope, ) diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNetworkMiddleware.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNetworkMiddleware.kt index 15148fa96..fea0937b9 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNetworkMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckNetworkMiddleware.kt @@ -170,10 +170,15 @@ class ReviewQualityCheckNetworkMiddleware( private suspend fun Store.updateRecommendedProductState() { val currentState = state if (currentState is ReviewQualityCheckState.OptedIn && - currentState.productRecommendationsPreference == true + (currentState.productRecommendationsExposure || (currentState.productRecommendationsPreference == true)) ) { - reviewQualityCheckService.productRecommendation().toRecommendedProductState().also { - dispatch(UpdateRecommendedProduct(it)) + val productRecommendation = reviewQualityCheckService.productRecommendation( + currentState.productRecommendationsPreference ?: false, + ) + if (currentState.productRecommendationsPreference == true) { + productRecommendation.toRecommendedProductState().also { + dispatch(UpdateRecommendedProduct(it)) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt index e93f48336..825b1ad23 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckPreferencesMiddleware.kt @@ -16,6 +16,7 @@ import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction.InfoCardEx import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction.SettingsCardExpanded import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.components.appstate.shopping.ShoppingState.CardState +import org.mozilla.fenix.shopping.ShoppingExperienceFeature import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction import org.mozilla.fenix.shopping.store.ReviewQualityCheckMiddleware import org.mozilla.fenix.shopping.store.ReviewQualityCheckState @@ -29,12 +30,14 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn * @param reviewQualityCheckVendorsService The [ReviewQualityCheckVendorsService] instance for * getting the list of product vendors. * @param appStore The [AppStore] instance for dispatching [ShoppingAction]s. + * @param shoppingExperienceFeature The [ShoppingExperienceFeature] instance to get feature flags. * @param scope The [CoroutineScope] to use for launching coroutines. */ class ReviewQualityCheckPreferencesMiddleware( private val reviewQualityCheckPreferences: ReviewQualityCheckPreferences, private val reviewQualityCheckVendorsService: ReviewQualityCheckVendorsService, private val appStore: AppStore, + private val shoppingExperienceFeature: ShoppingExperienceFeature, private val scope: CoroutineScope, ) : ReviewQualityCheckMiddleware { @@ -75,6 +78,8 @@ class ReviewQualityCheckPreferencesMiddleware( ReviewQualityCheckAction.OptInCompleted( isProductRecommendationsEnabled = isProductRecommendationsEnabled, + productRecommendationsExposure = + shoppingExperienceFeature.isProductRecommendationsExposureEnabled, productVendor = reviewQualityCheckVendorsService.productVendor(), isHighlightsExpanded = savedCardState.isHighlightsExpanded, isInfoExpanded = savedCardState.isInfoExpanded, @@ -95,6 +100,8 @@ class ReviewQualityCheckPreferencesMiddleware( store.dispatch( ReviewQualityCheckAction.OptInCompleted( isProductRecommendationsEnabled = isProductRecommendationsEnabled, + productRecommendationsExposure = + shoppingExperienceFeature.isProductRecommendationsExposureEnabled, productVendor = reviewQualityCheckVendorsService.productVendor(), isHighlightsExpanded = false, isInfoExpanded = false, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckService.kt b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckService.kt index 21e4fbbcf..e3af842f1 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckService.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckService.kt @@ -11,6 +11,7 @@ import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.shopping.ProductAnalysis import mozilla.components.concept.engine.shopping.ProductRecommendation import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.GleanMetrics.Shopping import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -50,7 +51,7 @@ interface ReviewQualityCheckService { * * @return [ProductRecommendation] if request succeeds, null otherwise. */ - suspend fun productRecommendation(): ProductRecommendation? + suspend fun productRecommendation(shouldRecordAvailableTelemetry: Boolean): ProductRecommendation? /** * Sends a click attribution event for a given product aid. @@ -128,13 +129,20 @@ class DefaultReviewQualityCheckService( override fun selectedTabUrl(): String? = browserStore.state.selectedTab?.content?.url - override suspend fun productRecommendation(): ProductRecommendation? = + override suspend fun productRecommendation(shouldRecordAvailableTelemetry: Boolean): ProductRecommendation? = withContext(Dispatchers.Main) { suspendCoroutine { continuation -> browserStore.state.selectedTab?.let { tab -> tab.engineState.engineSession?.requestProductRecommendations( url = tab.content.url, onResult = { + if (it.isEmpty()) { + if (shouldRecordAvailableTelemetry) { + Shopping.surfaceNoAdsAvailable.record() + } + } else { + Shopping.adsExposure.record() + } // Return the first available recommendation since ui requires only // one recommendation. continuation.resume(it.firstOrNull()) diff --git a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt index 4798ebfea..67f07ad08 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckAction.kt @@ -63,6 +63,7 @@ sealed interface ReviewQualityCheckAction : Action { * * @property isProductRecommendationsEnabled Reflects the user preference update to display * recommended product. Null when product recommendations feature is disabled. + * @property productRecommendationsExposure Whether product recommendations exposure is enabled. * @property productVendor The vendor of the product. * @property isHighlightsExpanded Whether the highlights card should be expanded. * @property isInfoExpanded Whether the info card should be expanded. @@ -70,6 +71,7 @@ sealed interface ReviewQualityCheckAction : Action { */ data class OptInCompleted( val isProductRecommendationsEnabled: Boolean?, + val productRecommendationsExposure: Boolean, val productVendor: ReviewQualityCheckState.ProductVendor, val isHighlightsExpanded: Boolean, val isInfoExpanded: Boolean, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt index 47b8018dd..ce9a390d4 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckState.kt @@ -41,6 +41,7 @@ sealed interface ReviewQualityCheckState : State { * @property productRecommendationsPreference User preference whether to show product * recommendations. True if product recommendations should be shown. Null indicates that product * recommendations are disabled. + * @property productRecommendationsExposure Whether product recommendations exposure is enabled. * @property productVendor The vendor of the product. * @property isSettingsExpanded Whether or not the settings card is expanded. * @property isInfoExpanded Whether or not the info card is expanded. @@ -49,6 +50,7 @@ sealed interface ReviewQualityCheckState : State { data class OptedIn( val productReviewState: ProductReviewState = ProductReviewState.Loading, val productRecommendationsPreference: Boolean?, + val productRecommendationsExposure: Boolean, val productVendor: ProductVendor, val isSettingsExpanded: Boolean = false, val isInfoExpanded: Boolean = false, diff --git a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt index f18566f5d..e0c598b95 100644 --- a/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt +++ b/app/src/main/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStore.kt @@ -48,6 +48,7 @@ private fun mapStateForUpdateAction( if (state is ReviewQualityCheckState.OptedIn) { state.copy( productRecommendationsPreference = action.isProductRecommendationsEnabled, + productRecommendationsExposure = action.productRecommendationsExposure, isHighlightsExpanded = action.isHighlightsExpanded, isInfoExpanded = action.isInfoExpanded, isSettingsExpanded = action.isSettingsExpanded, @@ -55,6 +56,7 @@ private fun mapStateForUpdateAction( } else { ReviewQualityCheckState.OptedIn( productRecommendationsPreference = action.isProductRecommendationsEnabled, + productRecommendationsExposure = action.productRecommendationsExposure, productVendor = action.productVendor, isHighlightsExpanded = action.isHighlightsExpanded, isInfoExpanded = action.isInfoExpanded, diff --git a/app/src/test/java/org/mozilla/fenix/shopping/ReviewQualityCheckBottomSheetStateFeatureTest.kt b/app/src/test/java/org/mozilla/fenix/shopping/ReviewQualityCheckBottomSheetStateFeatureTest.kt index f8bd15735..74627775c 100644 --- a/app/src/test/java/org/mozilla/fenix/shopping/ReviewQualityCheckBottomSheetStateFeatureTest.kt +++ b/app/src/test/java/org/mozilla/fenix/shopping/ReviewQualityCheckBottomSheetStateFeatureTest.kt @@ -34,6 +34,7 @@ class ReviewQualityCheckBottomSheetStateFeatureTest { store.dispatch( ReviewQualityCheckAction.OptInCompleted( isProductRecommendationsEnabled = true, + productRecommendationsExposure = true, productVendor = ReviewQualityCheckState.ProductVendor.WALMART, isHighlightsExpanded = false, isInfoExpanded = false, diff --git a/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeReviewQualityCheckService.kt b/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeReviewQualityCheckService.kt index 4030f7521..a0f365958 100644 --- a/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeReviewQualityCheckService.kt +++ b/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeReviewQualityCheckService.kt @@ -14,7 +14,7 @@ class FakeReviewQualityCheckService( private val reanalysis: AnalysisStatusDto? = null, private val status: AnalysisStatusDto? = null, private val selectedTabUrl: String? = null, - private val productRecommendation: ProductRecommendation? = null, + private val productRecommendation: () -> ProductRecommendation? = { null }, private val recordClick: (String) -> Unit = {}, private val recordImpression: (String) -> Unit = {}, ) : ReviewQualityCheckService { @@ -33,7 +33,9 @@ class FakeReviewQualityCheckService( override fun selectedTabUrl(): String? = selectedTabUrl - override suspend fun productRecommendation(): ProductRecommendation? = productRecommendation + override suspend fun productRecommendation(shouldRecordAvailableTelemetry: Boolean): ProductRecommendation? { + return productRecommendation.invoke() + } override suspend fun recordRecommendedProductClick(productAid: String) { recordClick(productAid) diff --git a/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeShoppingExperienceFeature.kt b/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeShoppingExperienceFeature.kt index 613cbd157..572b18ea3 100644 --- a/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeShoppingExperienceFeature.kt +++ b/app/src/test/java/org/mozilla/fenix/shopping/fake/FakeShoppingExperienceFeature.kt @@ -8,8 +8,12 @@ import org.mozilla.fenix.shopping.ShoppingExperienceFeature class FakeShoppingExperienceFeature( private val enabled: Boolean = true, + private val productRecommendationsExposureEnabled: Boolean = true, ) : ShoppingExperienceFeature { override val isEnabled: Boolean get() = enabled + + override val isProductRecommendationsExposureEnabled: Boolean + get() = productRecommendationsExposureEnabled } diff --git a/app/src/test/java/org/mozilla/fenix/shopping/middleware/DefaultReviewQualityCheckServiceTest.kt b/app/src/test/java/org/mozilla/fenix/shopping/middleware/DefaultReviewQualityCheckServiceTest.kt index 1c74f4bda..56f74a5f4 100644 --- a/app/src/test/java/org/mozilla/fenix/shopping/middleware/DefaultReviewQualityCheckServiceTest.kt +++ b/app/src/test/java/org/mozilla/fenix/shopping/middleware/DefaultReviewQualityCheckServiceTest.kt @@ -12,18 +12,30 @@ import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.shopping.ProductAnalysis +import mozilla.components.concept.engine.shopping.ProductRecommendation +import mozilla.components.service.glean.testing.GleanTestRule +import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.GleanMetrics.Shopping +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.shopping.ProductAnalysisTestData +import org.mozilla.fenix.shopping.ProductRecommendationTestData +@RunWith(FenixRobolectricTestRunner::class) class DefaultReviewQualityCheckServiceTest { @get:Rule val coroutinesTestRule = MainCoroutineRule() + @get:Rule + val gleanTestRule = GleanTestRule(testContext) + @Test fun `GIVEN fetch is called WHEN onResult is invoked with the expected type THEN product analysis returns the same data`() = runTest { @@ -110,4 +122,121 @@ class DefaultReviewQualityCheckServiceTest { assertEquals(expected, actual) } + + @Test + fun `GIVEN product recommendations is called WHEN onResult is invoked with the result THEN recommendations returns the data and exposure is called`() = + runTest { + val engineSession = mockk() + val expected = ProductRecommendationTestData.productRecommendation() + val productRecommendations = listOf(expected) + + every { + engineSession.requestProductRecommendations(any(), any(), any()) + }.answers { + secondArg<(List) -> Unit>().invoke(productRecommendations) + } + + val tab = createTab( + url = "https://www.shopping.org/product", + id = "test-tab", + engineSession = engineSession, + ) + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + + val tested = DefaultReviewQualityCheckService(BrowserStore(browserState)) + + val actual = tested.productRecommendation(false) + + assertEquals(expected, actual) + assertNotNull(Shopping.adsExposure.testGetValue()) + } + + @Test + fun `GIVEN product recommendations is called WHEN onResult is invoked with a empty list and telemetry should be recorded THEN recommendations returns null and no ads available event is called`() = + runTest { + val engineSession = mockk() + + every { + engineSession.requestProductRecommendations(any(), any(), any()) + }.answers { + secondArg<(List) -> Unit>().invoke(emptyList()) + } + + val tab = createTab( + url = "https://www.shopping.org/product", + id = "test-tab", + engineSession = engineSession, + ) + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + + val tested = DefaultReviewQualityCheckService(BrowserStore(browserState)) + + val actual = tested.productRecommendation(true) + + assertNull(actual) + assertNotNull(Shopping.surfaceNoAdsAvailable.testGetValue()) + } + + @Test + fun `GIVEN product recommendations is called WHEN onResult is invoked with a empty list and telemetry should not be recorded THEN recommendations returns null and no ads available event is not called`() = + runTest { + val engineSession = mockk() + + every { + engineSession.requestProductRecommendations(any(), any(), any()) + }.answers { + secondArg<(List) -> Unit>().invoke(emptyList()) + } + + val tab = createTab( + url = "https://www.shopping.org/product", + id = "test-tab", + engineSession = engineSession, + ) + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + + val tested = DefaultReviewQualityCheckService(BrowserStore(browserState)) + + val actual = tested.productRecommendation(false) + + assertNull(actual) + assertNull(Shopping.surfaceNoAdsAvailable.testGetValue()) + } + + @Test + fun `GIVEN product recommendations is called WHEN onException is invoked THEN recommendations returns null`() = + runTest { + val engineSession = mockk() + + every { + engineSession.requestProductRecommendations(any(), any(), any()) + }.answers { + thirdArg<(Throwable) -> Unit>().invoke(RuntimeException()) + } + + val tab = createTab( + url = "https://www.shopping.org/product", + id = "test-tab", + engineSession = engineSession, + ) + val browserState = BrowserState( + tabs = listOf(tab), + selectedTabId = tab.id, + ) + + val tested = DefaultReviewQualityCheckService(BrowserStore(browserState)) + + val actual = tested.productRecommendation(false) + + assertNull(actual) + } } diff --git a/app/src/test/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckTelemetryMiddlewareTest.kt b/app/src/test/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckTelemetryMiddlewareTest.kt index 5495d8af2..e9d81fe0b 100644 --- a/app/src/test/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckTelemetryMiddlewareTest.kt +++ b/app/src/test/java/org/mozilla/fenix/shopping/middleware/ReviewQualityCheckTelemetryMiddlewareTest.kt @@ -11,7 +11,7 @@ import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.libstate.ext.waitUntilIdle import mozilla.components.support.test.robolectric.testContext -import org.junit.Assert +import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before @@ -67,8 +67,8 @@ class ReviewQualityCheckTelemetryMiddlewareTest { assertNotNull(Shopping.surfaceClosed.testGetValue()) val event = Shopping.surfaceClosed.testGetValue()!! - Assert.assertEquals(1, event.size) - Assert.assertEquals(BottomSheetDismissSource.CLICK_OUTSIDE.sourceName, event.single().extra?.getValue("source")) + assertEquals(1, event.size) + assertEquals(BottomSheetDismissSource.CLICK_OUTSIDE.sourceName, event.single().extra?.getValue("source")) } @Test @@ -78,8 +78,8 @@ class ReviewQualityCheckTelemetryMiddlewareTest { assertNotNull(Shopping.surfaceDisplayed.testGetValue()) val event = Shopping.surfaceDisplayed.testGetValue()!! - Assert.assertEquals(1, event.size) - Assert.assertEquals(BottomSheetViewState.HALF_VIEW.state, event.single().extra?.getValue("view")) + assertEquals(1, event.size) + assertEquals(BottomSheetViewState.HALF_VIEW.state, event.single().extra?.getValue("view")) } @Test @@ -127,6 +127,7 @@ class ReviewQualityCheckTelemetryMiddlewareTest { val tested = ReviewQualityCheckStore( initialState = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = true, + productRecommendationsExposure = true, productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, isHighlightsExpanded = false, ), @@ -146,6 +147,7 @@ class ReviewQualityCheckTelemetryMiddlewareTest { val tested = ReviewQualityCheckStore( initialState = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = true, + productRecommendationsExposure = true, productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, isHighlightsExpanded = true, ), @@ -165,6 +167,7 @@ class ReviewQualityCheckTelemetryMiddlewareTest { val tested = ReviewQualityCheckStore( initialState = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = true, + productRecommendationsExposure = true, productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, isSettingsExpanded = false, ), @@ -184,6 +187,7 @@ class ReviewQualityCheckTelemetryMiddlewareTest { val tested = ReviewQualityCheckStore( initialState = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = true, + productRecommendationsExposure = true, productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, isSettingsExpanded = true, ), @@ -261,6 +265,7 @@ class ReviewQualityCheckTelemetryMiddlewareTest { analysisStatus = AnalysisPresent.AnalysisStatus.UP_TO_DATE, ), productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, ), middleware = listOf( @@ -318,6 +323,7 @@ class ReviewQualityCheckTelemetryMiddlewareTest { analysisStatus = AnalysisPresent.AnalysisStatus.NEEDS_ANALYSIS, ), productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, ), middleware = listOf( @@ -355,6 +361,7 @@ class ReviewQualityCheckTelemetryMiddlewareTest { val tested = ReviewQualityCheckStore( initialState = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, isHighlightsExpanded = false, ), @@ -366,7 +373,7 @@ class ReviewQualityCheckTelemetryMiddlewareTest { tested.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation).joinBlocking() tested.waitUntilIdle() - assertNotNull(Shopping.SurfaceAdsSettingToggledExtra("enabled")) + assertEquals("enabled", Shopping.surfaceAdsSettingToggled.testGetValue()!!.first().extra!!["action"]) } @Test @@ -374,6 +381,7 @@ class ReviewQualityCheckTelemetryMiddlewareTest { val tested = ReviewQualityCheckStore( initialState = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = true, + productRecommendationsExposure = true, productVendor = ReviewQualityCheckState.ProductVendor.AMAZON, isHighlightsExpanded = false, ), @@ -385,6 +393,6 @@ class ReviewQualityCheckTelemetryMiddlewareTest { tested.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation).joinBlocking() tested.waitUntilIdle() - assertNotNull(Shopping.SurfaceAdsSettingToggledExtra("disabled")) + assertEquals("disabled", Shopping.surfaceAdsSettingToggled.testGetValue()!!.first().extra!!["action"]) } } diff --git a/app/src/test/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStoreTest.kt b/app/src/test/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStoreTest.kt index 91acad868..56e86ffd8 100644 --- a/app/src/test/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/shopping/store/ReviewQualityCheckStoreTest.kt @@ -25,10 +25,12 @@ import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.components.appstate.shopping.ShoppingState import org.mozilla.fenix.shopping.ProductAnalysisTestData import org.mozilla.fenix.shopping.ProductRecommendationTestData +import org.mozilla.fenix.shopping.ShoppingExperienceFeature import org.mozilla.fenix.shopping.fake.FakeNetworkChecker import org.mozilla.fenix.shopping.fake.FakeReviewQualityCheckPreferences import org.mozilla.fenix.shopping.fake.FakeReviewQualityCheckService import org.mozilla.fenix.shopping.fake.FakeReviewQualityCheckVendorsService +import org.mozilla.fenix.shopping.fake.FakeShoppingExperienceFeature import org.mozilla.fenix.shopping.middleware.AnalysisStatusDto import org.mozilla.fenix.shopping.middleware.NetworkChecker import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckNetworkMiddleware @@ -99,6 +101,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -143,6 +146,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = null, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -172,6 +176,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = true, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -188,7 +193,7 @@ class ReviewQualityCheckStoreTest { ), reviewQualityCheckService = FakeReviewQualityCheckService( productAnalysis = { ProductAnalysisTestData.productAnalysis() }, - productRecommendation = ProductRecommendationTestData.productRecommendation(), + productRecommendation = { ProductRecommendationTestData.productRecommendation() }, ), ), ) @@ -203,6 +208,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, productReviewState = ProductAnalysisTestData.analysisPresent( recommendedProductState = ReviewQualityCheckState.RecommendedProductState.Initial, @@ -246,6 +252,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, isHighlightsExpanded = false, isSettingsExpanded = true, @@ -279,6 +286,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, isSettingsExpanded = true, ) @@ -322,6 +330,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, isSettingsExpanded = false, ) @@ -356,6 +365,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, isInfoExpanded = true, ) @@ -399,6 +409,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, isInfoExpanded = false, ) @@ -434,6 +445,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, isHighlightsExpanded = true, ) @@ -478,6 +490,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, isHighlightsExpanded = false, ) @@ -509,6 +522,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productReviewState = ProductAnalysisTestData.analysisPresent(), productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -534,6 +548,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productReviewState = ReviewQualityCheckState.OptedIn.ProductReviewState.Error.GenericError, productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -559,6 +574,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productReviewState = ReviewQualityCheckState.OptedIn.ProductReviewState.Error.NetworkError, productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -586,6 +602,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productReviewState = ReviewQualityCheckState.OptedIn.ProductReviewState.Error.GenericError, productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -629,6 +646,7 @@ class ReviewQualityCheckStoreTest { analysisStatus = AnalysisStatus.NEEDS_ANALYSIS, ), productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -658,6 +676,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productReviewState = ProductAnalysisTestData.analysisPresent(), productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -691,6 +710,7 @@ class ReviewQualityCheckStoreTest { val expected = ReviewQualityCheckState.OptedIn( productReviewState = ReviewQualityCheckState.OptedIn.ProductReviewState.Error.NotEnoughReviews, productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -734,6 +754,7 @@ class ReviewQualityCheckStoreTest { analysisStatus = AnalysisStatus.REANALYZING, ), productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) @@ -784,6 +805,7 @@ class ReviewQualityCheckStoreTest { analysisStatus = AnalysisStatus.NEEDS_ANALYSIS, ), productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -793,6 +815,7 @@ class ReviewQualityCheckStoreTest { analysisStatus = AnalysisStatus.REANALYZING, ), productRecommendationsPreference = false, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertFalse(observedState.contains(notExpected)) @@ -841,7 +864,7 @@ class ReviewQualityCheckStoreTest { ), reviewQualityCheckService = FakeReviewQualityCheckService( productAnalysis = { ProductAnalysisTestData.productAnalysis() }, - productRecommendation = ProductRecommendationTestData.productRecommendation(), + productRecommendation = { ProductRecommendationTestData.productRecommendation() }, ), ), ) @@ -857,12 +880,138 @@ class ReviewQualityCheckStoreTest { recommendedProductState = ProductRecommendationTestData.product(), ), productRecommendationsPreference = true, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) } } + @Test + fun `GIVEN product recommendations are disabled WHEN a product analysis is fetched successfully and exposure is set to true THEN product recommendation should also be fetched`() = + runTest { + setAndResetLocale { + var productRecommendationFetched = false + val tested = ReviewQualityCheckStore( + middleware = provideReviewQualityCheckMiddleware( + reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences( + isEnabled = true, + isProductRecommendationsEnabled = false, + ), + shoppingExperienceFeature = FakeShoppingExperienceFeature( + productRecommendationsExposureEnabled = true, + ), + reviewQualityCheckService = FakeReviewQualityCheckService( + productAnalysis = { ProductAnalysisTestData.productAnalysis() }, + productRecommendation = { + productRecommendationFetched = true + ProductRecommendationTestData.productRecommendation() + }, + ), + ), + ) + tested.waitUntilIdle() + dispatcher.scheduler.advanceUntilIdle() + tested.waitUntilIdle() + tested.dispatch(ReviewQualityCheckAction.FetchProductAnalysis).joinBlocking() + tested.waitUntilIdle() + dispatcher.scheduler.advanceUntilIdle() + + val expected = ReviewQualityCheckState.OptedIn( + productReviewState = ProductAnalysisTestData.analysisPresent(), + productRecommendationsPreference = false, + productRecommendationsExposure = true, + productVendor = ProductVendor.BEST_BUY, + ) + assertEquals(expected, tested.state) + assertTrue(productRecommendationFetched) + } + } + + @Test + fun `GIVEN product recommendations are disabled WHEN a product analysis is fetched successfully and exposure is set to false THEN product recommendation should not be fetched and displayed`() = + runTest { + setAndResetLocale { + var productRecommendationFetched = false + val tested = ReviewQualityCheckStore( + middleware = provideReviewQualityCheckMiddleware( + reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences( + isEnabled = true, + isProductRecommendationsEnabled = false, + ), + shoppingExperienceFeature = FakeShoppingExperienceFeature( + productRecommendationsExposureEnabled = false, + ), + reviewQualityCheckService = FakeReviewQualityCheckService( + productAnalysis = { ProductAnalysisTestData.productAnalysis() }, + productRecommendation = { + productRecommendationFetched = true + ProductRecommendationTestData.productRecommendation() + }, + ), + ), + ) + tested.waitUntilIdle() + dispatcher.scheduler.advanceUntilIdle() + tested.waitUntilIdle() + tested.dispatch(ReviewQualityCheckAction.FetchProductAnalysis).joinBlocking() + tested.waitUntilIdle() + dispatcher.scheduler.advanceUntilIdle() + + val expected = ReviewQualityCheckState.OptedIn( + productReviewState = ProductAnalysisTestData.analysisPresent(), + productRecommendationsPreference = false, + productRecommendationsExposure = false, + productVendor = ProductVendor.BEST_BUY, + ) + assertEquals(expected, tested.state) + assertFalse(productRecommendationFetched) + } + } + + @Test + fun `GIVEN product recommendations are enabled WHEN a product analysis is fetched successfully and exposure is set to false THEN product recommendation should be fetched and displayed`() = + runTest { + setAndResetLocale { + var productRecommendationFetched = false + val tested = ReviewQualityCheckStore( + middleware = provideReviewQualityCheckMiddleware( + reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences( + isEnabled = true, + isProductRecommendationsEnabled = true, + ), + shoppingExperienceFeature = FakeShoppingExperienceFeature( + productRecommendationsExposureEnabled = false, + ), + reviewQualityCheckService = FakeReviewQualityCheckService( + productAnalysis = { ProductAnalysisTestData.productAnalysis() }, + productRecommendation = { + productRecommendationFetched = true + ProductRecommendationTestData.productRecommendation() + }, + ), + ), + ) + tested.waitUntilIdle() + dispatcher.scheduler.advanceUntilIdle() + tested.waitUntilIdle() + tested.dispatch(ReviewQualityCheckAction.FetchProductAnalysis).joinBlocking() + tested.waitUntilIdle() + dispatcher.scheduler.advanceUntilIdle() + + val expected = ReviewQualityCheckState.OptedIn( + productReviewState = ProductAnalysisTestData.analysisPresent( + recommendedProductState = ProductRecommendationTestData.product(), + ), + productRecommendationsPreference = true, + productRecommendationsExposure = false, + productVendor = ProductVendor.BEST_BUY, + ) + assertEquals(expected, tested.state) + assertTrue(productRecommendationFetched) + } + } + @Test fun `GIVEN product recommendations are enabled WHEN a product analysis is fetched successfully and product recommendation fails THEN product recommendations state should be initial`() = runTest { @@ -874,7 +1023,7 @@ class ReviewQualityCheckStoreTest { ), reviewQualityCheckService = FakeReviewQualityCheckService( productAnalysis = { ProductAnalysisTestData.productAnalysis() }, - productRecommendation = null, + productRecommendation = { null }, ), ), ) @@ -890,6 +1039,7 @@ class ReviewQualityCheckStoreTest { recommendedProductState = ReviewQualityCheckState.RecommendedProductState.Initial, ), productRecommendationsPreference = true, + productRecommendationsExposure = true, productVendor = ProductVendor.BEST_BUY, ) assertEquals(expected, tested.state) @@ -898,7 +1048,8 @@ class ReviewQualityCheckStoreTest { @Test fun `GIVEN product recommendations are enabled WHEN product analysis fails THEN product recommendations should not be fetched`() = runTest { - val captureActionsMiddleware = CaptureActionsMiddleware() + val captureActionsMiddleware = + CaptureActionsMiddleware() val tested = ReviewQualityCheckStore( middleware = provideReviewQualityCheckMiddleware( reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences( @@ -907,7 +1058,7 @@ class ReviewQualityCheckStoreTest { ), reviewQualityCheckService = FakeReviewQualityCheckService( productAnalysis = { null }, - productRecommendation = ProductRecommendationTestData.productRecommendation(), + productRecommendation = { ProductRecommendationTestData.productRecommendation() }, ), ) + captureActionsMiddleware, ) @@ -933,9 +1084,11 @@ class ReviewQualityCheckStoreTest { ), reviewQualityCheckService = FakeReviewQualityCheckService( productAnalysis = { ProductAnalysisTestData.productAnalysis() }, - productRecommendation = ProductRecommendationTestData.productRecommendation( - aid = "342", - ), + productRecommendation = { + ProductRecommendationTestData.productRecommendation( + aid = "342", + ) + }, recordClick = { productClicked = it }, @@ -970,9 +1123,11 @@ class ReviewQualityCheckStoreTest { ), reviewQualityCheckService = FakeReviewQualityCheckService( productAnalysis = { ProductAnalysisTestData.productAnalysis() }, - productRecommendation = ProductRecommendationTestData.productRecommendation( - aid = "342", - ), + productRecommendation = { + ProductRecommendationTestData.productRecommendation( + aid = "342", + ) + }, recordImpression = { productViewed = it }, @@ -997,6 +1152,7 @@ class ReviewQualityCheckStoreTest { reviewQualityCheckVendorsService: FakeReviewQualityCheckVendorsService = FakeReviewQualityCheckVendorsService(), reviewQualityCheckService: ReviewQualityCheckService = FakeReviewQualityCheckService(), networkChecker: NetworkChecker = FakeNetworkChecker(), + shoppingExperienceFeature: ShoppingExperienceFeature = FakeShoppingExperienceFeature(), appStore: AppStore = AppStore(), ): List { return listOf( @@ -1004,6 +1160,7 @@ class ReviewQualityCheckStoreTest { reviewQualityCheckPreferences = reviewQualityCheckPreferences, reviewQualityCheckVendorsService = reviewQualityCheckVendorsService, appStore = appStore, + shoppingExperienceFeature = shoppingExperienceFeature, scope = this.scope, ), ReviewQualityCheckNetworkMiddleware(