Bug 1858018 - Persist review checker reanalysis state

fenix/120.0
rahulsainani 8 months ago committed by mergify[bot]
parent be5fb77680
commit 692e1e6853

@ -268,7 +268,9 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
context.getString(R.string.review_quality_check_close_handle_content_description),
visible = { reviewQualityCheckAvailable },
listener = { _ ->
requireComponents.appStore.dispatch(AppAction.ShoppingSheetStateUpdated(expanded = true))
requireComponents.appStore.dispatch(
AppAction.ShoppingAction.ShoppingSheetStateUpdated(expanded = true),
)
findNavController().navigate(
BrowserFragmentDirections.actionBrowserFragmentToReviewQualityCheckDialogFragment(),

@ -218,7 +218,24 @@ sealed class AppAction : Action {
) : AppAction()
/**
* [AppAction] used to update the expansion state of the shopping sheet.
* [AppAction]s related to shopping sheet state.
*/
data class ShoppingSheetStateUpdated(val expanded: Boolean) : AppAction()
sealed class ShoppingAction : AppAction() {
/**
* [ShoppingAction] used to update the expansion state of the shopping sheet.
*/
data class ShoppingSheetStateUpdated(val expanded: Boolean) : ShoppingAction()
/**
* [ShoppingAction] used to add a product to a set of products that are being analysed.
*/
data class AddToProductAnalysed(val productPageUrl: String) : ShoppingAction()
/**
* [ShoppingAction] used to remove a product from the set of products that are being
* analysed.
*/
data class RemoveFromProductAnalysed(val productPageUrl: String) : ShoppingAction()
}
}

@ -13,6 +13,7 @@ import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import org.mozilla.fenix.browser.StandardSnackbarError
import org.mozilla.fenix.components.appstate.shopping.ShoppingState
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
@ -53,7 +54,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState
* Also serves as an in memory cache of all stories mapped by category allowing for quick stories filtering.
* @property wallpaperState The [WallpaperState] to display in the [HomeFragment].
* @property standardSnackbarError A snackbar error message to display.
* @property shoppingSheetExpanded Boolean indicating if the shopping sheet is expanded and visible.
* @property shoppingState Holds state for shopping feature that's required to live the lifetime of a session.
*/
data class AppState(
val isForeground: Boolean = true,
@ -77,5 +78,5 @@ data class AppState(
val pendingDeletionHistoryItems: Set<PendingDeletionHistory> = emptySet(),
val wallpaperState: WallpaperState = WallpaperState.default,
val standardSnackbarError: StandardSnackbarError? = null,
val shoppingSheetExpanded: Boolean? = null,
val shoppingState: ShoppingState = ShoppingState(),
) : State

@ -9,6 +9,7 @@ import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.ext.recordNewImpression
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.shopping.ShoppingStateReducer
import org.mozilla.fenix.ext.filterOutTab
import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
@ -232,7 +233,7 @@ internal object AppStoreReducer {
standardSnackbarError = action.standardSnackbarError,
)
is AppAction.ShoppingSheetStateUpdated -> state.copy(shoppingSheetExpanded = action.expanded)
is AppAction.ShoppingAction -> ShoppingStateReducer.reduce(state, action)
}
}

@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.appstate.shopping
/**
* State for shopping feature that's required to live the lifetime of a session.
*
* @property productsInAnalysis Set of product ids that are currently being analysed or were being
* analysed when the sheet was closed.
* @property shoppingSheetExpanded Boolean indicating if the shopping sheet is expanded and visible.
*/
data class ShoppingState(
val productsInAnalysis: Set<String> = emptySet(),
val shoppingSheetExpanded: Boolean? = null,
)

@ -0,0 +1,43 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.appstate.shopping
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction
import org.mozilla.fenix.components.appstate.AppState
/**
* Reducer for the shopping state that handles [ShoppingAction]s.
*/
internal object ShoppingStateReducer {
private val logger = Logger("ReviewQualityCheckShoppingStateReducer")
/**
* Reduces the given [ShoppingAction] into a new [AppState].
*/
fun reduce(state: AppState, action: ShoppingAction): AppState =
when (action) {
is ShoppingAction.ShoppingSheetStateUpdated -> state.copy(
shoppingState = state.shoppingState.copy(
shoppingSheetExpanded = action.expanded,
),
)
is ShoppingAction.AddToProductAnalysed -> state.copy(
shoppingState = state.shoppingState.copy(
productsInAnalysis = state.shoppingState.productsInAnalysis + action.productPageUrl,
),
)
is ShoppingAction.RemoveFromProductAnalysed -> state.copy(
shoppingState = state.shoppingState.copy(
productsInAnalysis = state.shoppingState.productsInAnalysis - action.productPageUrl,
),
)
}.also {
logger.debug("Action processed: $action, updated shopping state: ${it.shoppingState}")
}
}

@ -50,7 +50,7 @@ class ReviewQualityCheckFeature(
}
appStoreScope = appStore.flowScoped { flow ->
flow.mapNotNull { it.shoppingSheetExpanded }
flow.mapNotNull { it.shoppingState.shoppingSheetExpanded }
.distinctUntilChanged()
.collect(onBottomSheetStateChange)
}

@ -40,6 +40,7 @@ class ReviewQualityCheckFragment : BottomSheetDialogFragment() {
middleware = ReviewQualityCheckMiddlewareProvider.provideMiddleware(
settings = requireComponents.settings,
browserStore = requireComponents.core.store,
appStore = requireComponents.appStore,
context = requireContext().applicationContext,
scope = lifecycleScope,
),
@ -96,7 +97,7 @@ class ReviewQualityCheckFragment : BottomSheetDialogFragment() {
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
requireComponents.appStore.dispatch(AppAction.ShoppingSheetStateUpdated(expanded = false))
requireComponents.appStore.dispatch(AppAction.ShoppingAction.ShoppingSheetStateUpdated(expanded = false))
}
private fun BottomSheetBehavior<View>.setPeekHeightToHalfScreenHeight() {

@ -8,6 +8,7 @@ import android.content.Context
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.middleware.DefaultNetworkChecker
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckPreferences
import org.mozilla.fenix.shopping.middleware.DefaultReviewQualityCheckService
@ -30,18 +31,20 @@ object ReviewQualityCheckMiddlewareProvider {
*
* @param settings The [Settings] instance to use.
* @param browserStore The [BrowserStore] instance to access state.
* @param appStore The [AppStore] instance to access state.
* @param context The [Context] instance to use.
* @param scope The [CoroutineScope] to use for launching coroutines.
*/
fun provideMiddleware(
settings: Settings,
browserStore: BrowserStore,
appStore: AppStore,
context: Context,
scope: CoroutineScope,
): List<ReviewQualityCheckMiddleware> =
listOf(
providePreferencesMiddleware(settings, browserStore, scope),
provideNetworkMiddleware(browserStore, context, scope),
provideNetworkMiddleware(browserStore, appStore, context, scope),
provideNavigationMiddleware(TabsUseCases.SelectOrAddUseCase(browserStore), context),
provideTelemetryMiddleware(),
)
@ -58,11 +61,13 @@ object ReviewQualityCheckMiddlewareProvider {
private fun provideNetworkMiddleware(
browserStore: BrowserStore,
appStore: AppStore,
context: Context,
scope: CoroutineScope,
) = ReviewQualityCheckNetworkMiddleware(
reviewQualityCheckService = DefaultReviewQualityCheckService(browserStore),
networkChecker = DefaultNetworkChecker(context),
appStore = appStore,
scope = scope,
)

@ -15,15 +15,8 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductR
/**
* Maps [ProductAnalysis] to [ProductReviewState].
*/
fun ProductAnalysis?.toProductReviewState(): ProductReviewState =
if (this == null) {
ProductReviewState.Error.GenericError
} else {
when (this) {
is GeckoProductAnalysis -> toProductReview()
else -> ProductReviewState.Error.GenericError
}
}
fun GeckoProductAnalysis?.toProductReviewState(): ProductReviewState =
this?.toProductReview() ?: ProductReviewState.Error.GenericError
private fun GeckoProductAnalysis.toProductReview(): ProductReviewState =
if (productId == null) {

@ -6,8 +6,12 @@ package org.mozilla.fenix.shopping.middleware
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.browser.engine.gecko.shopping.GeckoProductAnalysis
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.lib.state.Store
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction.FetchProductAnalysis
import org.mozilla.fenix.shopping.store.ReviewQualityCheckAction.RetryProductAnalysis
@ -21,14 +25,18 @@ import org.mozilla.fenix.shopping.store.ReviewQualityCheckState.OptedIn.ProductR
*
* @property reviewQualityCheckService The service that handles the network requests.
* @property networkChecker The [NetworkChecker] instance to check the network status.
* @property appStore The [AppStore] instance to access state and dispatch [ShoppingAction]s.
* @property scope The [CoroutineScope] that will be used to launch coroutines.
*/
class ReviewQualityCheckNetworkMiddleware(
private val reviewQualityCheckService: ReviewQualityCheckService,
private val networkChecker: NetworkChecker,
private val appStore: AppStore,
private val scope: CoroutineScope,
) : ReviewQualityCheckMiddleware {
private val logger = Logger("ReviewQualityCheckNetworkMiddleware")
override fun invoke(
context: MiddlewareContext<ReviewQualityCheckState, ReviewQualityCheckAction>,
next: (ReviewQualityCheckAction) -> Unit,
@ -55,8 +63,18 @@ class ReviewQualityCheckNetworkMiddleware(
scope.launch {
when (action) {
FetchProductAnalysis, RetryProductAnalysis -> {
val productReviewState = fetchAnalysis()
val productPageUrl = reviewQualityCheckService.selectedTabUrl()
val productAnalysis = reviewQualityCheckService.fetchProductReview()
val productReviewState = productAnalysis.toProductReviewState()
store.updateProductReviewState(productReviewState)
productPageUrl?.let {
store.restoreAnalysingStateIfRequired(
productPageUrl = productPageUrl,
productReviewState = productReviewState,
productAnalysis = productAnalysis,
)
}
}
ReviewQualityCheckAction.ReanalyzeProduct, ReviewQualityCheckAction.AnalyzeProduct -> {
@ -67,6 +85,12 @@ class ReviewQualityCheckNetworkMiddleware(
return@launch
}
// add product to the set of products that are being analysed
val productPageUrl = reviewQualityCheckService.selectedTabUrl()
productPageUrl?.let {
appStore.dispatch(ShoppingAction.AddToProductAnalysed(it))
}
val status = pollForAnalysisStatus()
if (status == null ||
@ -90,23 +114,30 @@ class ReviewQualityCheckNetworkMiddleware(
// poll succeeded, update state
store.updateProductReviewState(status.toProductReviewState())
}
// remove product from the set of products that are being analysed
productPageUrl?.let {
appStore.dispatch(ShoppingAction.RemoveFromProductAnalysed(it))
}
}
}
}
}
private suspend fun fetchAnalysis(): ProductReviewState =
reviewQualityCheckService.fetchProductReview().toProductReviewState()
private suspend fun pollForAnalysisStatus(): AnalysisStatusDto? =
retry(
predicate = { it == AnalysisStatusDto.PENDING || it == AnalysisStatusDto.IN_PROGRESS },
block = { reviewQualityCheckService.analysisStatus() },
block = {
logger.debug("Retrying")
reviewQualityCheckService.analysisStatus()
},
)
private suspend fun AnalysisStatusDto.toProductReviewState(): ProductReviewState =
when (this) {
AnalysisStatusDto.COMPLETED -> fetchAnalysis()
AnalysisStatusDto.COMPLETED ->
reviewQualityCheckService.fetchProductReview().toProductReviewState()
AnalysisStatusDto.NOT_ANALYZABLE -> ProductReviewState.Error.UnsupportedProductTypeError
else -> ProductReviewState.Error.GenericError
}
@ -116,4 +147,21 @@ class ReviewQualityCheckNetworkMiddleware(
) {
dispatch(ReviewQualityCheckAction.UpdateProductReview(productReviewState))
}
private fun Store<ReviewQualityCheckState, ReviewQualityCheckAction>.restoreAnalysingStateIfRequired(
productPageUrl: String,
productReviewState: ProductReviewState,
productAnalysis: GeckoProductAnalysis?,
) {
if (productReviewState.isAnalysisPresentOrNoAnalysisPresent() &&
productAnalysis?.needsAnalysis == true &&
appStore.state.shoppingState.productsInAnalysis.contains(productPageUrl)
) {
logger.debug("Found product in the set: $productPageUrl")
dispatch(ReviewQualityCheckAction.ReanalyzeProduct)
}
}
private fun ProductReviewState.isAnalysisPresentOrNoAnalysisPresent() =
this is ProductReviewState.AnalysisPresent || this is ProductReviewState.NoAnalysisPresent
}

@ -6,6 +6,7 @@ package org.mozilla.fenix.shopping.middleware
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import mozilla.components.browser.engine.gecko.shopping.GeckoProductAnalysis
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.shopping.ProductAnalysis
@ -23,7 +24,7 @@ interface ReviewQualityCheckService {
*
* @return [ProductAnalysis] if the request succeeds, null otherwise.
*/
suspend fun fetchProductReview(): ProductAnalysis?
suspend fun fetchProductReview(): GeckoProductAnalysis?
/**
* Triggers a reanalysis of the product review for the current tab.
@ -38,6 +39,11 @@ interface ReviewQualityCheckService {
* @return [AnalysisStatusDto] if the request succeeds, null otherwise.
*/
suspend fun analysisStatus(): AnalysisStatusDto?
/**
* Returns the selected tab url.
*/
fun selectedTabUrl(): String?
}
/**
@ -51,13 +57,16 @@ class DefaultReviewQualityCheckService(
private val logger = Logger("DefaultReviewQualityCheckService")
override suspend fun fetchProductReview(): ProductAnalysis? = withContext(Dispatchers.Main) {
override suspend fun fetchProductReview(): GeckoProductAnalysis? = withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
browserStore.state.selectedTab?.let { tab ->
tab.engineState.engineSession?.requestProductAnalysis(
url = tab.content.url,
onResult = {
continuation.resume(it)
when (it) {
is GeckoProductAnalysis -> continuation.resume(it)
else -> continuation.resume(null)
}
},
onException = {
logger.error("Error fetching product review", it)
@ -102,6 +111,9 @@ class DefaultReviewQualityCheckService(
}
}
override fun selectedTabUrl(): String? =
browserStore.state.selectedTab?.content?.url
private inline fun <reified T : Enum<T>> String.asEnumOrDefault(defaultValue: T? = null): T? =
enumValues<T>().firstOrNull { it.name.equals(this, ignoreCase = true) } ?: defaultValue
}

@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.appstate
import mozilla.components.support.test.ext.joinBlocking
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.shopping.ShoppingState
class ShoppingActionTest {
@Test
fun `WHEN shopping sheet is collapsed THEN state should reflect that`() {
val store = AppStore(
initialState = AppState(
shoppingState = ShoppingState(
shoppingSheetExpanded = true,
),
),
)
store.dispatch(AppAction.ShoppingAction.ShoppingSheetStateUpdated(false)).joinBlocking()
val expected = ShoppingState(
shoppingSheetExpanded = false,
)
assertEquals(expected, store.state.shoppingState)
}
@Test
fun `WHEN shopping sheet is expanded THEN state should reflect that`() {
val store = AppStore()
store.dispatch(AppAction.ShoppingAction.ShoppingSheetStateUpdated(true)).joinBlocking()
val expected = ShoppingState(
shoppingSheetExpanded = true,
)
assertEquals(expected, store.state.shoppingState)
}
@Test
fun `WHEN product analysed is added THEN state should reflect that`() {
val store = AppStore()
store.dispatch(AppAction.ShoppingAction.AddToProductAnalysed("pdp")).joinBlocking()
val expected = ShoppingState(
productsInAnalysis = setOf("pdp"),
)
assertEquals(expected, store.state.shoppingState)
}
@Test
fun `WHEN product analysed is removed THEN state should reflect that`() {
val store = AppStore(
initialState = AppState(
shoppingState = ShoppingState(
productsInAnalysis = setOf("pdp"),
),
),
)
store.dispatch(AppAction.ShoppingAction.RemoveFromProductAnalysed("pdp")).joinBlocking()
val expected = ShoppingState(
productsInAnalysis = emptySet(),
)
assertEquals(expected, store.state.shoppingState)
}
}

@ -17,8 +17,9 @@ import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppAction.ShoppingAction
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.components.appstate.shopping.ShoppingState
import org.mozilla.fenix.shopping.fake.FakeShoppingExperienceFeature
class ReviewQualityCheckFeatureTest {
@ -226,7 +227,7 @@ class ReviewQualityCheckFeatureTest {
fun `WHEN the shopping sheet is collapsed THEN the callback is called with false`() {
val appStore = AppStore(
initialState = AppState(
shoppingSheetExpanded = true,
shoppingState = ShoppingState(shoppingSheetExpanded = true),
),
)
var isExpanded: Boolean? = null
@ -242,7 +243,7 @@ class ReviewQualityCheckFeatureTest {
tested.start()
appStore.dispatch(AppAction.ShoppingSheetStateUpdated(expanded = false)).joinBlocking()
appStore.dispatch(ShoppingAction.ShoppingSheetStateUpdated(expanded = false)).joinBlocking()
assertFalse(isExpanded!!)
}
@ -251,7 +252,7 @@ class ReviewQualityCheckFeatureTest {
fun `WHEN the shopping sheet is expanded THEN the collapsed callback is called with true`() {
val appStore = AppStore(
initialState = AppState(
shoppingSheetExpanded = false,
shoppingState = ShoppingState(shoppingSheetExpanded = false),
),
)
var isExpanded: Boolean? = null
@ -267,7 +268,7 @@ class ReviewQualityCheckFeatureTest {
tested.start()
appStore.dispatch(AppAction.ShoppingSheetStateUpdated(expanded = true)).joinBlocking()
appStore.dispatch(ShoppingAction.ShoppingSheetStateUpdated(expanded = true)).joinBlocking()
assertTrue(isExpanded!!)
}
@ -276,7 +277,7 @@ class ReviewQualityCheckFeatureTest {
fun `WHEN the feature is restarted THEN first emission is collected to set the tint`() {
val appStore = AppStore(
initialState = AppState(
shoppingSheetExpanded = false,
shoppingState = ShoppingState(shoppingSheetExpanded = false),
),
)
var isExpanded: Boolean? = null
@ -294,7 +295,7 @@ class ReviewQualityCheckFeatureTest {
tested.stop()
// emulate emission
appStore.dispatch(AppAction.ShoppingSheetStateUpdated(expanded = false)).joinBlocking()
appStore.dispatch(ShoppingAction.ShoppingSheetStateUpdated(expanded = false)).joinBlocking()
tested.start()
assertFalse(isExpanded!!)

@ -4,19 +4,20 @@
package org.mozilla.fenix.shopping.fake
import mozilla.components.concept.engine.shopping.ProductAnalysis
import mozilla.components.browser.engine.gecko.shopping.GeckoProductAnalysis
import org.mozilla.fenix.shopping.middleware.AnalysisStatusDto
import org.mozilla.fenix.shopping.middleware.ReviewQualityCheckService
class FakeReviewQualityCheckService(
private val productAnalysis: (Int) -> ProductAnalysis? = { null },
private val productAnalysis: (Int) -> GeckoProductAnalysis? = { null },
private val reanalysis: AnalysisStatusDto? = null,
private val status: AnalysisStatusDto? = null,
private val selectedTabUrl: String? = null,
) : ReviewQualityCheckService {
private var analysisCount = 0
override suspend fun fetchProductReview(): ProductAnalysis? {
override suspend fun fetchProductReview(): GeckoProductAnalysis? {
return productAnalysis(analysisCount).also {
analysisCount++
}
@ -25,4 +26,6 @@ class FakeReviewQualityCheckService(
override suspend fun reanalyzeProduct(): AnalysisStatusDto? = reanalysis
override suspend fun analysisStatus(): AnalysisStatusDto? = status
override fun selectedTabUrl(): String? = selectedTabUrl
}

@ -25,7 +25,7 @@ class DefaultReviewQualityCheckServiceTest {
val coroutinesTestRule = MainCoroutineRule()
@Test
fun `GIVEN fetch is called WHEN onResult is invoked THEN product analysis returns the same data`() =
fun `GIVEN fetch is called WHEN onResult is invoked with the expected type THEN product analysis returns the same data`() =
runTest {
val engineSession = mockk<EngineSession>()
val expected = ProductAnalysisTestData.productAnalysis()
@ -110,4 +110,33 @@ class DefaultReviewQualityCheckServiceTest {
assertEquals(expected, actual)
}
@Test
fun `GIVEN fetch is called WHEN onResult is invoked with an unexpected type THEN product analysis returns null`() =
runTest {
val engineSession = mockk<EngineSession>()
val randomAnalysis = object : ProductAnalysis {
override val productId: String = "id1"
}
every {
engineSession.requestProductAnalysis(any(), any(), any())
}.answers {
secondArg<(ProductAnalysis) -> Unit>().invoke(randomAnalysis)
}
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))
assertNull(tested.fetchProductReview())
}
}

@ -5,7 +5,6 @@
package org.mozilla.fenix.shopping.middleware
import mozilla.components.browser.engine.gecko.shopping.Highlight
import mozilla.components.concept.engine.shopping.ProductAnalysis
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.shopping.ProductAnalysisTestData
@ -148,16 +147,4 @@ class ProductAnalysisMapperTest {
assertEquals(expected, actual)
}
@Test
fun `WHEN ProductAnalysis is not GeckoProductAnalysis THEN it is mapped to Error`() {
val randomAnalysis = object : ProductAnalysis {
override val productId: String = "id1"
}
val actual = randomAnalysis.toProductReviewState()
val expected = ReviewQualityCheckState.OptedIn.ProductReviewState.Error.GenericError
assertEquals(expected, actual)
}
}

@ -5,12 +5,20 @@
package org.mozilla.fenix.shopping.store
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import kotlinx.coroutines.test.runTest
import mozilla.components.lib.state.ext.observeForever
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.middleware.CaptureActionsMiddleware
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertFalse
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
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.fake.FakeNetworkChecker
import org.mozilla.fenix.shopping.fake.FakeReviewQualityCheckPreferences
@ -363,11 +371,145 @@ class ReviewQualityCheckStoreTest {
assertEquals(expected, tested.state)
}
@Test
fun `GIVEN that the product was being analysed earlier WHEN needsAnalysis is true THEN state should be restored to reanalysing`() =
runTest {
val tested = ReviewQualityCheckStore(
middleware = provideReviewQualityCheckMiddleware(
reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences(isEnabled = true),
reviewQualityCheckService = FakeReviewQualityCheckService(
productAnalysis = {
ProductAnalysisTestData.productAnalysis(needsAnalysis = true)
},
reanalysis = AnalysisStatusDto.PENDING,
status = AnalysisStatusDto.COMPLETED,
selectedTabUrl = "pdp",
),
networkChecker = FakeNetworkChecker(isConnected = true),
appStore = AppStore(
AppState(shoppingState = ShoppingState(productsInAnalysis = setOf("pdp"))),
),
),
)
val observedState = mutableListOf<ReviewQualityCheckState>()
tested.observeForever {
observedState.add(it)
}
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
tested.waitUntilIdle()
tested.dispatch(ReviewQualityCheckAction.FetchProductAnalysis).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
val expected = ReviewQualityCheckState.OptedIn(
productReviewState = ProductAnalysisTestData.analysisPresent(
analysisStatus = AnalysisStatus.REANALYZING,
),
productRecommendationsPreference = false,
productVendor = ProductVendor.BEST_BUY,
)
// Since reanalyzing is an intermediate state and the tests completes to get to the final
// state, this checks if the intermediate state is present in the observed state.
assertTrue(observedState.contains(expected))
}
@Test
fun `GIVEN that the product was not being analysed earlier WHEN needsAnalysis is true THEN state should display needs analysis as usual`() =
runTest {
val tested = ReviewQualityCheckStore(
middleware = provideReviewQualityCheckMiddleware(
reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences(isEnabled = true),
reviewQualityCheckService = FakeReviewQualityCheckService(
productAnalysis = {
ProductAnalysisTestData.productAnalysis(needsAnalysis = true)
},
reanalysis = AnalysisStatusDto.PENDING,
status = AnalysisStatusDto.COMPLETED,
selectedTabUrl = "pdp",
),
networkChecker = FakeNetworkChecker(isConnected = true),
appStore = AppStore(
AppState(
shoppingState = ShoppingState(
productsInAnalysis = setOf("test", "another", "product"),
),
),
),
),
)
val observedState = mutableListOf<ReviewQualityCheckState>()
tested.observeForever {
observedState.add(it)
}
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
tested.waitUntilIdle()
tested.dispatch(ReviewQualityCheckAction.FetchProductAnalysis).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
val expected = ReviewQualityCheckState.OptedIn(
productReviewState = ProductAnalysisTestData.analysisPresent(
analysisStatus = AnalysisStatus.NEEDS_ANALYSIS,
),
productRecommendationsPreference = false,
productVendor = ProductVendor.BEST_BUY,
)
assertEquals(expected, tested.state)
val notExpected = ReviewQualityCheckState.OptedIn(
productReviewState = ProductAnalysisTestData.analysisPresent(
analysisStatus = AnalysisStatus.REANALYZING,
),
productRecommendationsPreference = false,
productVendor = ProductVendor.BEST_BUY,
)
assertFalse(observedState.contains(notExpected))
}
@Test
fun `WHEN reanalysis is triggered THEN shopping state should contain the url of the product being analyzed`() =
runTest {
val captureActionsMiddleware = CaptureActionsMiddleware<AppState, AppAction>()
val appStore = AppStore(middlewares = listOf(captureActionsMiddleware))
val tested = ReviewQualityCheckStore(
middleware = provideReviewQualityCheckMiddleware(
reviewQualityCheckPreferences = FakeReviewQualityCheckPreferences(isEnabled = true),
reviewQualityCheckService = FakeReviewQualityCheckService(
productAnalysis = { ProductAnalysisTestData.productAnalysis() },
reanalysis = AnalysisStatusDto.PENDING,
status = AnalysisStatusDto.COMPLETED,
selectedTabUrl = "pdp",
),
networkChecker = FakeNetworkChecker(isConnected = true),
appStore = appStore,
),
)
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
tested.waitUntilIdle()
tested.dispatch(ReviewQualityCheckAction.ReanalyzeProduct).joinBlocking()
tested.waitUntilIdle()
dispatcher.scheduler.advanceUntilIdle()
captureActionsMiddleware.assertFirstAction(AppAction.ShoppingAction.AddToProductAnalysed::class) {
assertEquals("pdp", it.productPageUrl)
}
}
private fun provideReviewQualityCheckMiddleware(
reviewQualityCheckPreferences: ReviewQualityCheckPreferences = FakeReviewQualityCheckPreferences(),
reviewQualityCheckVendorsService: FakeReviewQualityCheckVendorsService = FakeReviewQualityCheckVendorsService(),
reviewQualityCheckService: ReviewQualityCheckService = FakeReviewQualityCheckService(),
networkChecker: NetworkChecker = FakeNetworkChecker(),
appStore: AppStore = AppStore(),
): List<ReviewQualityCheckMiddleware> {
return listOf(
ReviewQualityCheckPreferencesMiddleware(
@ -378,6 +520,7 @@ class ReviewQualityCheckStoreTest {
ReviewQualityCheckNetworkMiddleware(
reviewQualityCheckService = reviewQualityCheckService,
networkChecker = networkChecker,
appStore = appStore,
scope = this.scope,
),
)

Loading…
Cancel
Save