Skip to content

Commit

Permalink
Bug 1867061 - Display and hide rc product recommendations on toggle
Browse files Browse the repository at this point in the history
  • Loading branch information
rahulsainani authored and mergify[bot] committed Dec 5, 2023
1 parent 75ffacb commit 69d5cd0
Show file tree
Hide file tree
Showing 5 changed files with 270 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -80,37 +80,16 @@ class ReviewQualityCheckNetworkMiddleware(
ReviewQualityCheckAction.AnalyzeProduct,
ReviewQualityCheckAction.RestoreReanalysis,
-> {
val reanalysis = reviewQualityCheckService.reanalyzeProduct()

if (reanalysis == null) {
store.updateProductReviewState(ProductReviewState.Error.GenericError)
return@launch
}

val status = pollForAnalysisStatus()
store.onReanalyze()
}

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()
}
}

Expand All @@ -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() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
},
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 69d5cd0

Please sign in to comment.