2
0
mirror of https://github.com/fork-maintainers/iceraven-browser synced 2024-11-15 18:12:54 +00:00

Bug 1867061 - Display and hide rc product recommendations on toggle

This commit is contained in:
rahulsainani 2023-12-01 17:43:53 +01:00 committed by mergify[bot]
parent 6217bb0a3a
commit ba9bbd9ab9
5 changed files with 270 additions and 48 deletions

View File

@ -80,37 +80,16 @@ class ReviewQualityCheckNetworkMiddleware(
ReviewQualityCheckAction.AnalyzeProduct,
ReviewQualityCheckAction.RestoreReanalysis,
-> {
val reanalysis = reviewQualityCheckService.reanalyzeProduct()
store.onReanalyze()
}
if (reanalysis == null) {
store.updateProductReviewState(ProductReviewState.Error.GenericError)
return@launch
}
val status = pollForAnalysisStatus()
if (status == null ||
status == AnalysisStatusDto.PENDING ||
status == AnalysisStatusDto.IN_PROGRESS
ReviewQualityCheckAction.ToggleProductRecommendation -> {
val state = store.state
if (state is ReviewQualityCheckState.OptedIn &&
state.productReviewState is ProductReviewState.AnalysisPresent &&
state.productRecommendationsPreference == true
) {
// poll failed, reset to previous state
val state = store.state
if (state is ReviewQualityCheckState.OptedIn) {
if (state.productReviewState is ProductReviewState.NoAnalysisPresent) {
store.updateProductReviewState(ProductReviewState.NoAnalysisPresent())
} else if (state.productReviewState is ProductReviewState.AnalysisPresent) {
store.updateProductReviewState(
state.productReviewState.copy(
analysisStatus = AnalysisStatus.NEEDS_ANALYSIS,
),
)
}
}
} else {
// poll succeeded, update state
val productAnalysis = reviewQualityCheckService.fetchProductReview()
val productReviewState = productAnalysis.toProductReviewState()
store.updateProductReviewState(productReviewState)
store.updateRecommendedProductState()
}
}
@ -125,6 +104,39 @@ class ReviewQualityCheckNetworkMiddleware(
}
}
private suspend fun Store<ReviewQualityCheckState, ReviewQualityCheckAction>.onReanalyze() {
val reanalysis = reviewQualityCheckService.reanalyzeProduct()
if (reanalysis == null) {
updateProductReviewState(ProductReviewState.Error.GenericError)
return
}
val status = pollForAnalysisStatus()
if (status == null ||
status == AnalysisStatusDto.PENDING ||
status == AnalysisStatusDto.IN_PROGRESS
) {
// poll failed, reset to previous state
val state = this.state
if (state is ReviewQualityCheckState.OptedIn) {
if (state.productReviewState is ProductReviewState.NoAnalysisPresent) {
updateProductReviewState(ProductReviewState.NoAnalysisPresent())
} else if (state.productReviewState is ProductReviewState.AnalysisPresent) {
updateProductReviewState(
state.productReviewState.copy(analysisStatus = AnalysisStatus.NEEDS_ANALYSIS),
)
}
}
} else {
// poll succeeded, update state
val productAnalysis = reviewQualityCheckService.fetchProductReview()
val productReviewState = productAnalysis.toProductReviewState()
updateProductReviewState(productReviewState)
}
}
private suspend fun pollForAnalysisStatus(): AnalysisStatusDto? =
retry(
predicate = { it.isPendingOrInProgress() },

View File

@ -68,6 +68,7 @@ class DefaultReviewQualityCheckService(
private val browserStore: BrowserStore,
) : ReviewQualityCheckService {
private val recommendationsCache: MutableMap<String, ProductRecommendation> = mutableMapOf()
private val logger = Logger("DefaultReviewQualityCheckService")
override suspend fun fetchProductReview(): ProductAnalysis? = withContext(Dispatchers.Main) {
@ -125,25 +126,34 @@ class DefaultReviewQualityCheckService(
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()
if (recommendationsCache.containsKey(tab.content.url)) {
continuation.resume(recommendationsCache[tab.content.url])
} else {
tab.engineState.engineSession?.requestProductRecommendations(
url = tab.content.url,
onResult = {
if (it.isEmpty()) {
if (shouldRecordAvailableTelemetry) {
Shopping.surfaceNoAdsAvailable.record()
}
} else {
Shopping.adsExposure.record()
}
} else {
Shopping.adsExposure.record()
}
// Return the first available recommendation since ui requires only
// one recommendation.
continuation.resume(it.firstOrNull())
},
onException = {
logger.error("Error fetching product recommendation", it)
continuation.resume(null)
},
)
// Return the first available recommendation since ui requires only
// one recommendation.
continuation.resume(
it.firstOrNull()?.also { recommendation ->
recommendationsCache[tab.content.url] = recommendation
},
)
},
onException = {
logger.error("Error fetching product recommendation", it)
continuation.resume(null)
},
)
}
}
}
}

View File

@ -56,7 +56,7 @@ sealed interface ReviewQualityCheckAction : Action {
/**
* Triggered when the user has enabled or disabled product recommendations.
*/
object ToggleProductRecommendation : PreferencesMiddlewareAction, UpdateAction, TelemetryAction
object ToggleProductRecommendation : PreferencesMiddlewareAction, UpdateAction, NetworkAction, TelemetryAction
/**
* Triggered as a result of a [OptIn] or [Init] whe user has opted in for shopping experience.

View File

@ -6,6 +6,7 @@ package org.mozilla.fenix.shopping.middleware
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.createTab
@ -239,4 +240,74 @@ class DefaultReviewQualityCheckServiceTest {
assertNull(actual)
}
@Test
fun `GIVEN product recommendations is called WHEN onResult is invoked with the result THEN recommendations returns the same result without re-fetching again`() =
runTest {
val engineSession = mockk<EngineSession>()
val expected = ProductRecommendationTestData.productRecommendation()
val productRecommendations = listOf(expected)
every {
engineSession.requestProductRecommendations(any(), any(), any())
}.answers {
secondArg<(List<ProductRecommendation>) -> 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))
tested.productRecommendation(false)
tested.productRecommendation(false)
val actual = tested.productRecommendation(false)
assertEquals(expected, actual)
verify(exactly = 1) {
engineSession.requestProductRecommendations(any(), any(), any())
}
}
@Test
fun `GIVEN product recommendations is called WHEN onResult is invoked with the empty result THEN recommendations fetches every time`() =
runTest {
val engineSession = mockk<EngineSession>()
every {
engineSession.requestProductRecommendations(any(), any(), any())
}.answers {
secondArg<(List<ProductRecommendation>) -> 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))
tested.productRecommendation(false)
tested.productRecommendation(false)
val actual = tested.productRecommendation(false)
assertNull(actual)
verify(exactly = 3) {
engineSession.requestProductRecommendations(any(), any(), any())
}
}
}

View File

@ -216,6 +216,135 @@ class ReviewQualityCheckStoreTest {
assertEquals(expected, tested.state)
}
@Test
fun `GIVEN product recommendations are available WHEN the user turns product recommendations off THEN state should not contain product recommendation`() =
runTest {
setAndResetLocale {
var productRecommendationsFetchCounter = 0
val tested = ReviewQualityCheckStore(
middleware = provideReviewQualityCheckMiddleware(
reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences(
isEnabled = true,
isProductRecommendationsEnabled = true,
),
reviewQualityCheckService = FakeReviewQualityCheckService(
productAnalysis = { ProductAnalysisTestData.productAnalysis() },
productRecommendation = {
productRecommendationsFetchCounter++
ProductRecommendationTestData.productRecommendation()
},
),
),
)
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
tested.dispatch(ReviewQualityCheckAction.FetchProductAnalysis).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
assertEquals(1, productRecommendationsFetchCounter)
tested.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
val expected = ReviewQualityCheckState.OptedIn(
productRecommendationsPreference = false,
productRecommendationsExposure = true,
productVendor = ProductVendor.BEST_BUY,
productReviewState = ProductAnalysisTestData.analysisPresent(),
)
assertEquals(expected, tested.state)
assertEquals(1, productRecommendationsFetchCounter)
}
}
@Test
fun `GIVEN product recommendations are available WHEN the user turns product recommendations off and then back on THEN state should contain product recommendation`() =
runTest {
setAndResetLocale {
val tested = ReviewQualityCheckStore(
middleware = provideReviewQualityCheckMiddleware(
reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences(
isEnabled = true,
isProductRecommendationsEnabled = true,
),
reviewQualityCheckService = FakeReviewQualityCheckService(
productAnalysis = { ProductAnalysisTestData.productAnalysis() },
productRecommendation = { ProductRecommendationTestData.productRecommendation() },
),
shoppingExperienceFeature = FakeShoppingExperienceFeature(
productRecommendationsExposureEnabled = true,
),
),
)
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
tested.dispatch(ReviewQualityCheckAction.FetchProductAnalysis).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
tested.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
tested.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
val expected = ReviewQualityCheckState.OptedIn(
productRecommendationsPreference = true,
productRecommendationsExposure = true,
productVendor = ProductVendor.BEST_BUY,
productReviewState = ProductAnalysisTestData.analysisPresent(
recommendedProductState = ProductRecommendationTestData.product(),
),
)
assertEquals(expected, tested.state)
}
}
@Test
fun `GIVEN product recommendations are available but analysis failed WHEN the user turns product recommendations on THEN recommendations should not be fetched`() =
runTest {
setAndResetLocale {
var productRecommendationsFetchCounter = 0
val tested = ReviewQualityCheckStore(
middleware = provideReviewQualityCheckMiddleware(
reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences(
isEnabled = true,
isProductRecommendationsEnabled = false,
),
reviewQualityCheckService = FakeReviewQualityCheckService(
productAnalysis = { null },
productRecommendation = {
productRecommendationsFetchCounter++
ProductRecommendationTestData.productRecommendation()
},
),
),
)
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
tested.dispatch(ReviewQualityCheckAction.FetchProductAnalysis).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
assertEquals(0, productRecommendationsFetchCounter)
tested.dispatch(ReviewQualityCheckAction.ToggleProductRecommendation).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
val expected = ReviewQualityCheckState.OptedIn(
productRecommendationsPreference = true,
productRecommendationsExposure = true,
productVendor = ProductVendor.BEST_BUY,
productReviewState = ReviewQualityCheckState.OptedIn.ProductReviewState.Error.GenericError,
)
assertEquals(expected, tested.state)
assertEquals(0, productRecommendationsFetchCounter)
}
}
@Test
fun `GIVEN the user has opted in the feature WHEN there is existing card state data for a pdp THEN it should be restored`() =
runTest {