diff --git a/app/metrics.yaml b/app/metrics.yaml index 4442b0ef9..58653aa4a 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -3704,6 +3704,91 @@ pocket: notification_emails: - android-probes@mozilla.com expires: "2022-02-01" + home_recs_shown: + type: event + description: | + The Pocket recommended stories are shown on the home screen. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/21593 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/21625#issuecomment-936745506 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: "2022-10-01" + home_recs_story_clicked: + type: event + description: | + User tapped a Pocket recommended story to be opened. + extra_keys: + times_shown: + description: | + How many times was this story shown, including current. + position: + description: | + Position of the clicked story in the list shown. + Uses the [row x column] matrix notation. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/21593 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/21625#issuecomment-936745506 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: "2022-10-01" + home_recs_category_clicked: + type: event + description: | + User tapped a Pocket stories category to filter stories. + extra_keys: + category_name: + description: | + Pocket set topic name representing the just clicked category. + selected_total: + description: | + How many categories were selected before this being tapped. + new_state: + description: | + Category's new state after being tapped. + Possible values: [selected], [deselected]. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/21593 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/21625#issuecomment-936745506 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: "2022-10-01" + home_recs_discover_clicked: + type: event + description: | + User tapped the "Discover more" tile to open a new tab + for more Pocket stories. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/21593 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/21625#issuecomment-936745506 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: "2022-10-01" + home_recs_learn_more_clicked: + type: event + description: | + User tapped "Learn more" to open a new tab for Pocket. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/21593 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/21625#issuecomment-936745506 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + expires: "2022-10-01" first_session: campaign: diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt index 6b1262f7e..7cac6214d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt @@ -19,6 +19,7 @@ import org.mozilla.fenix.GleanMetrics.ErrorPage import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.Onboarding +import org.mozilla.fenix.GleanMetrics.Pocket import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.TabsTray @@ -127,6 +128,35 @@ sealed class Event { object WhatsNewTapped : Event() object PocketTopSiteClicked : Event() object PocketTopSiteRemoved : Event() + object PocketHomeRecsShown : Event() + object PocketHomeRecsDiscoverMoreClicked : Event() + object PocketHomeRecsLearnMoreClicked : Event() + data class PocketHomeRecsStoryClicked( + val timesShown: Long, + val storyPosition: Pair, + ) : Event() { + override val extras: Map + get() = mapOf( + Pocket.homeRecsStoryClickedKeys.timesShown to timesShown.toString(), + Pocket.homeRecsStoryClickedKeys.position to "${storyPosition.first}x${storyPosition.second}" + ) + } + + data class PocketHomeRecsCategoryClicked( + val categoryname: String, + val previousSelectedCategoriesTotal: Int, + val isSelectedNextState: Boolean + ) : Event() { + override val extras: Map + get() = mapOf( + Pocket.homeRecsCategoryClickedKeys.categoryName to categoryname, + Pocket.homeRecsCategoryClickedKeys.selectedTotal to previousSelectedCategoriesTotal.toString(), + Pocket.homeRecsCategoryClickedKeys.newState to when (isSelectedNextState) { + true -> "selected" + false -> "deselected" + } + ) + } object FennecToFenixMigrated : Event() object AddonsOpenInSettings : Event() object VoiceSearchTapped : Event() diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index b13afb8d4..8b96a0728 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -493,6 +493,23 @@ private val Event.wrapper: EventWrapper<*>? is Event.PocketTopSiteRemoved -> EventWrapper( { Pocket.pocketTopSiteRemoved.record(it) } ) + is Event.PocketHomeRecsShown -> EventWrapper( + { Pocket.homeRecsShown.record(it) } + ) + is Event.PocketHomeRecsLearnMoreClicked -> EventWrapper( + { Pocket.homeRecsLearnMoreClicked.record(it) } + ) + is Event.PocketHomeRecsDiscoverMoreClicked -> EventWrapper( + { Pocket.homeRecsDiscoverClicked.record(it) } + ) + is Event.PocketHomeRecsStoryClicked -> EventWrapper( + { Pocket.homeRecsStoryClicked.record(it) }, + { Pocket.homeRecsStoryClickedKeys.valueOf(it) } + ) + is Event.PocketHomeRecsCategoryClicked -> EventWrapper( + { Pocket.homeRecsCategoryClicked.record(it) }, + { Pocket.homeRecsCategoryClickedKeys.valueOf(it) } + ) is Event.DarkThemeSelected -> EventWrapper( { AppTheme.darkThemeSelected.record(it) }, { AppTheme.darkThemeSelectedKeys.valueOf(it) } diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index b20528d8f..70d635b00 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -355,7 +355,8 @@ class HomeFragment : Fragment() { pocketStoriesController = DefaultPocketStoriesController( homeActivity = activity, homeStore = homeFragmentStore, - navController = findNavController() + navController = findNavController(), + metrics = requireComponents.analytics.metrics ) ) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index 2963da596..78aa0b037 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -391,15 +391,23 @@ class SessionControlInteractor( controller.handleCustomizeHomeTapped() } - override fun onCategoryClick(categoryClicked: PocketRecommendedStoriesCategory) { + override fun onStoriesShown(storiesShown: List) { + pocketStoriesController.handleStoriesShown(storiesShown) + } + + override fun onCategoryClicked(categoryClicked: PocketRecommendedStoriesCategory) { pocketStoriesController.handleCategoryClick(categoryClicked) } - override fun onStoriesShown(storiesShown: List) { - pocketStoriesController.handleStoriesShown(storiesShown) + override fun onStoryClicked(storyClicked: PocketRecommendedStory, storyPosition: Pair) { + pocketStoriesController.handleStoryClicked(storyClicked, storyPosition) + } + + override fun onLearnMoreClicked(link: String) { + pocketStoriesController.handleLearnMoreClicked(link) } - override fun onExternalLinkClicked(link: String) { - pocketStoriesController.handleExternalLinkClick(link) + override fun onDiscoverMoreClicked(link: String) { + pocketStoriesController.handleDiscoverMoreClicked(link) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesComposables.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesComposables.kt index 8860ffb9e..b059f6c8b 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesComposables.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesComposables.kt @@ -19,7 +19,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.Icon import androidx.compose.material.Text @@ -107,14 +107,15 @@ fun PocketStory( * @param stories The list of [PocketRecommendedStory]ies to be displayed. Expect a list with 8 items. * @param contentPadding Dimension for padding the content after it has been clipped. * This space will be used for shadows and also content rendering when the list is scrolled. - * @param onExternalLinkClicked Callback for when the user taps an element which contains an - * external link for where user can go for more recommendations. + * @param onStoryClicked Callback for when the user taps on a recommended story. + * @param onDiscoverMoreClicked Callback for when the user taps an element which contains an */ @Composable fun PocketStories( @PreviewParameter(PocketStoryProvider::class) stories: List, contentPadding: Dp, - onExternalLinkClicked: (String) -> Unit + onStoryClicked: (PocketRecommendedStory, Pair) -> Unit, + onDiscoverMoreClicked: (String) -> Unit ) { // Show stories in at most 3 rows but on any number of columns depending on the data received. val maxRowsNo = 3 @@ -129,20 +130,20 @@ fun PocketStories( flingBehavior = flingBehavior, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - items(storiesToShow) { columnItems -> + itemsIndexed(storiesToShow) { columnIndex, columnItems -> Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - columnItems.forEach { story -> + columnItems.forEachIndexed { rowIndex, story -> if (story == placeholderStory) { ListItemTabLargePlaceholder(stringResource(R.string.pocket_stories_placeholder_text)) { - onExternalLinkClicked("https://getpocket.com/explore?$POCKET_FEATURE_UTM_KEY_VALUE") + onDiscoverMoreClicked("https://getpocket.com/explore?$POCKET_FEATURE_UTM_KEY_VALUE") } } else { - val uri = Uri.parse(story.url) - .buildUpon() - .appendQueryParameter(URI_PARAM_UTM_KEY, POCKET_STORIES_UTM_VALUE) - .build().toString() PocketStory(story) { - onExternalLinkClicked(uri) + val uri = Uri.parse(story.url) + .buildUpon() + .appendQueryParameter(URI_PARAM_UTM_KEY, POCKET_STORIES_UTM_VALUE) + .build().toString() + onStoryClicked(it.copy(url = uri), rowIndex to columnIndex) } } } @@ -184,13 +185,13 @@ fun PocketStoriesCategories( * Pocket feature section title. * Shows a default text about Pocket and offers a external link to learn more. * - * @param onExternalLinkClicked Callback invoked when the user clicks the "Learn more" link. + * @param onLearnMoreClicked Callback invoked when the user clicks the "Learn more" link. * Contains the full URL for where the user should be navigated to. * @param modifier [Modifier] to be applied to the layout. */ @Composable fun PoweredByPocketHeader( - onExternalLinkClicked: (String) -> Unit, + onLearnMoreClicked: (String) -> Unit, modifier: Modifier = Modifier ) { val color = when (isSystemInDarkTheme()) { @@ -231,7 +232,7 @@ fun PoweredByPocketHeader( ) ClickableSubstringLink(text, color, linkStartIndex, linkEndIndex) { - onExternalLinkClicked("https://www.mozilla.org/en-US/firefox/pocket/?$POCKET_FEATURE_UTM_KEY_VALUE") + onLearnMoreClicked("https://www.mozilla.org/en-US/firefox/pocket/?$POCKET_FEATURE_UTM_KEY_VALUE") } } } @@ -247,7 +248,8 @@ private fun PocketStoriesComposablesPreview() { PocketStories( stories = getFakePocketStories(8), contentPadding = 0.dp, - onExternalLinkClicked = { } + onStoryClicked = { _, _ -> }, + onDiscoverMoreClicked = { } ) Spacer(Modifier.height(10.dp)) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesController.kt index 627415e8b..708f3136b 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesController.kt @@ -13,11 +13,20 @@ import mozilla.components.service.pocket.PocketRecommendedStory import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController /** * Contract for how all user interactions with the Pocket recommended stories feature are to be handled. */ interface PocketStoriesController { + /** + * Callback to decide what should happen as an effect of a new list of stories being shown. + * + * @param storiesShown the new list of [PocketRecommendedStory]es shown to the user. + */ + fun handleStoriesShown(storiesShown: List) + /** * Callback allowing to handle a specific [PocketRecommendedStoriesCategory] being clicked by the user. * @@ -26,18 +35,26 @@ interface PocketStoriesController { fun handleCategoryClick(categoryClicked: PocketRecommendedStoriesCategory): Unit /** - * Callback to decide what should happen as an effect of a new list of stories being shown. + * Callback for when the user clicks on a specific story. * - * @param storiesShown the new list of [PocketRecommendedStory]es shown to the user. + * @param storyClicked The just clicked [PocketRecommendedStory] URL. + * @param storyPosition `row x column` matrix representing the grid position of the clicked story. */ - fun handleStoriesShown(storiesShown: List) + fun handleStoryClicked(storyClicked: PocketRecommendedStory, storyPosition: Pair) /** - * Callback for when the an external link is clicked. + * Callback for when the "Learn more" link is clicked. * * @param link URL clicked. */ - fun handleExternalLinkClick(link: String) + fun handleLearnMoreClicked(link: String) + + /** + * Callback for when the "Discover more" link is clicked. + * + * @param link URL clicked. + */ + fun handleDiscoverMoreClicked(link: String) } /** @@ -50,14 +67,27 @@ interface PocketStoriesController { internal class DefaultPocketStoriesController( private val homeActivity: HomeActivity, private val homeStore: HomeFragmentStore, - private val navController: NavController + private val navController: NavController, + private val metrics: MetricController ) : PocketStoriesController { + override fun handleStoriesShown(storiesShown: List) { + homeStore.dispatch(HomeFragmentAction.PocketStoriesShown(storiesShown)) + metrics.track(Event.PocketHomeRecsShown) + } + override fun handleCategoryClick(categoryClicked: PocketRecommendedStoriesCategory) { val initialCategoriesSelections = homeStore.state.pocketStoriesCategoriesSelections // First check whether the category is clicked to be deselected. if (initialCategoriesSelections.map { it.name }.contains(categoryClicked.name)) { homeStore.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(categoryClicked.name)) + metrics.track( + Event.PocketHomeRecsCategoryClicked( + categoryClicked.name, + initialCategoriesSelections.size, + false + ) + ) return } @@ -75,19 +105,36 @@ internal class DefaultPocketStoriesController( // Finally update the selection. homeStore.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(categoryClicked.name)) + + metrics.track( + Event.PocketHomeRecsCategoryClicked( + categoryClicked.name, + initialCategoriesSelections.size, + true + ) + ) } - override fun handleStoriesShown(storiesShown: List) { - homeStore.dispatch(HomeFragmentAction.PocketStoriesShown(storiesShown)) + override fun handleStoryClicked(storyClicked: PocketRecommendedStory, storyPosition: Pair) { + dismissSearchDialogIfDisplayed() + homeActivity.openToBrowserAndLoad(storyClicked.url, true, BrowserDirection.FromHome) + metrics.track(Event.PocketHomeRecsStoryClicked(storyClicked.timesShown.inc(), storyPosition)) + } + + override fun handleLearnMoreClicked(link: String) { + dismissSearchDialogIfDisplayed() + homeActivity.openToBrowserAndLoad(link, true, BrowserDirection.FromHome) + metrics.track(Event.PocketHomeRecsLearnMoreClicked) } - override fun handleExternalLinkClick(link: String) { + override fun handleDiscoverMoreClicked(link: String) { dismissSearchDialogIfDisplayed() homeActivity.openToBrowserAndLoad(link, true, BrowserDirection.FromHome) + metrics.track(Event.PocketHomeRecsDiscoverMoreClicked) } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun dismissSearchDialogIfDisplayed() { + internal fun dismissSearchDialogIfDisplayed() { if (navController.currentDestination?.id == R.id.searchDialogFragment) { navController.navigateUp() } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesInteractor.kt index 67dab5137..db814fc84 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesInteractor.kt @@ -11,23 +11,38 @@ import mozilla.components.service.pocket.PocketRecommendedStory */ interface PocketStoriesInteractor { /** - * Callback for when the user clicked a specific category. + * Callback for then new stories are shown to the user. * - * @param categoryClicked the just clicked [PocketRecommendedStoriesCategory]. + * @param storiesShown The new list of [PocketRecommendedStory]es shown to the user. */ - fun onCategoryClick(categoryClicked: PocketRecommendedStoriesCategory) + fun onStoriesShown(storiesShown: List) /** - * Callback for then new stories are shown to the user. + * Callback for when the user clicks a specific category. * - * @param storiesShown the new list of [PocketRecommendedStory]es shown to the user. + * @param categoryClicked The just clicked [PocketRecommendedStoriesCategory]. */ - fun onStoriesShown(storiesShown: List) + fun onCategoryClicked(categoryClicked: PocketRecommendedStoriesCategory) + + /** + * Callback for when the user clicks on a specific story. + * + * @param storyClicked The just clicked [PocketRecommendedStory] URL. + * @param storyPosition `row x column` matrix representing the grid position of the clicked story. + */ + fun onStoryClicked(storyClicked: PocketRecommendedStory, storyPosition: Pair) + + /** + * Callback for when the user clicks the "Learn more" link. + * + * @param link URL clicked. + */ + fun onLearnMoreClicked(link: String) /** - * Callback for when the user clicks an external link. + * Callback for when the user clicks the "Discover more" link. * * @param link URL clicked. */ - fun onExternalLinkClicked(link: String) + fun onDiscoverMoreClicked(link: String) } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesViewHolder.kt index 116ff8ff8..358e269e7 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/PocketStoriesViewHolder.kt @@ -52,8 +52,10 @@ class PocketStoriesViewHolder( PocketStories( store, interactor::onStoriesShown, - interactor::onCategoryClick, - interactor::onExternalLinkClicked, + interactor::onStoryClicked, + interactor::onCategoryClicked, + interactor::onDiscoverMoreClicked, + interactor::onLearnMoreClicked, with(composeView.resources) { getDimensionPixelSize(R.dimen.home_item_horizontal_margin) / displayMetrics.density } @@ -68,11 +70,14 @@ class PocketStoriesViewHolder( } @Composable +@Suppress("LongParameterList") fun PocketStories( store: HomeFragmentStore, onStoriesShown: (List) -> Unit, - onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit, - onExternalLinkClicked: (String) -> Unit, + onStoryClicked: (PocketRecommendedStory, Pair) -> Unit, + onCategoryClicked: (PocketRecommendedStoriesCategory) -> Unit, + onDiscoverMoreClicked: (String) -> Unit, + onLearnMoreClicked: (String) -> Unit, @Dimension horizontalPadding: Float = 0f ) { val stories = store @@ -102,7 +107,7 @@ fun PocketStories( Spacer(Modifier.height(17.dp)) - PocketStories(stories ?: emptyList(), horizontalPadding.dp, onExternalLinkClicked) + PocketStories(stories ?: emptyList(), horizontalPadding.dp, onStoryClicked, onDiscoverMoreClicked) Spacer(Modifier.height(24.dp)) @@ -118,7 +123,7 @@ fun PocketStories( PocketStoriesCategories( categories = categories ?: emptyList(), selections = categoriesSelections ?: emptyList(), - onCategoryClick = onCategoryClick, + onCategoryClick = onCategoryClicked, modifier = Modifier .fillMaxWidth() .padding(horizontal = horizontalPadding.dp) @@ -127,7 +132,7 @@ fun PocketStories( Spacer(Modifier.height(24.dp)) PoweredByPocketHeader( - onExternalLinkClicked, + onLearnMoreClicked, modifier = Modifier .fillMaxWidth() .padding(horizontal = horizontalPadding.dp) diff --git a/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt index 7be805ce9..9bee29a0e 100644 --- a/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt @@ -10,6 +10,7 @@ import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.storage.BookmarkNodeType import mozilla.components.feature.tab.collections.Tab import mozilla.components.feature.tab.collections.TabCollection +import mozilla.components.service.pocket.PocketRecommendedStory import org.junit.Before import org.junit.Test import org.mozilla.fenix.browser.browsingmode.BrowsingMode @@ -213,21 +214,49 @@ class SessionControlInteractorTest { verify { controller.handlePrivateModeButtonClicked(newMode, hasBeenOnboarded) } } + @Test + fun `GIVEN a PocketStoriesInteractor WHEN stories are shown THEN handle it in a PocketStoriesController`() { + val shownStories: List = mockk() + + interactor.onStoriesShown(shownStories) + + verify { pocketStoriesController.handleStoriesShown(shownStories) } + } + @Test fun `GIVEN a PocketStoriesInteractor WHEN a category is clicked THEN handle it in a PocketStoriesController`() { val clickedCategory: PocketRecommendedStoriesCategory = mockk() - interactor.onCategoryClick(clickedCategory) + interactor.onCategoryClicked(clickedCategory) verify { pocketStoriesController.handleCategoryClick(clickedCategory) } } @Test - fun `GIVEN a PocketStoriesInteractor WHEN an external link is clicked THEN handle it in a PocketStoriesController`() { + fun `GIVEN a PocketStoriesInteractor WHEN a story is clicked THEN handle it in a PocketStoriesController`() { + val clickedStory: PocketRecommendedStory = mockk() + val storyGridLocation = 1 to 2 + + interactor.onStoryClicked(clickedStory, storyGridLocation) + + verify { pocketStoriesController.handleStoryClicked(clickedStory, storyGridLocation) } + } + + @Test + fun `GIVEN a PocketStoriesInteractor WHEN discover more clicked THEN handle it in a PocketStoriesController`() { + val link = "http://getpocket.com/explore" + + interactor.onDiscoverMoreClicked(link) + + verify { pocketStoriesController.handleDiscoverMoreClicked(link) } + } + + @Test + fun `GIVEN a PocketStoriesInteractor WHEN learn more clicked THEN handle it in a PocketStoriesController`() { val link = "https://www.mozilla.org/en-US/firefox/pocket/" - interactor.onExternalLinkClicked(link) + interactor.onLearnMoreClicked(link) - verify { pocketStoriesController.handleExternalLinkClick(link) } + verify { pocketStoriesController.handleLearnMoreClicked(link) } } } diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/DefaultPocketStoriesControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/DefaultPocketStoriesControllerTest.kt index ad4effd8f..9185ac21d 100644 --- a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/DefaultPocketStoriesControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/pocket/DefaultPocketStoriesControllerTest.kt @@ -9,18 +9,23 @@ import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify +import io.mockk.verifyOrder import mozilla.components.service.pocket.PocketRecommendedStory import org.junit.Test import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.home.HomeFragmentAction import org.mozilla.fenix.home.HomeFragmentState import org.mozilla.fenix.home.HomeFragmentStore class DefaultPocketStoriesControllerTest { + val metrics: MetricController = mockk(relaxed = true) + @Test - fun `GIVEN a category is selected WHEN that same category is clicked THEN deselect it`() { + fun `GIVEN a category is selected WHEN that same category is clicked THEN deselect it and record telemetry`() { val category1 = PocketRecommendedStoriesCategory("cat1", emptyList()) val category2 = PocketRecommendedStoriesCategory("cat2", emptyList()) val selections = listOf(PocketRecommendedStoriesSelectedCategory(category2.name)) @@ -32,17 +37,21 @@ class DefaultPocketStoriesControllerTest { ) ) ) - val controller = DefaultPocketStoriesController(mockk(), store, mockk()) + val controller = DefaultPocketStoriesController(mockk(), store, mockk(), metrics) controller.handleCategoryClick(category1) verify(exactly = 0) { store.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(category1.name)) } + verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(category1.name)) } + verify { metrics.track(Event.PocketHomeRecsCategoryClicked(category1.name, 1, true)) } controller.handleCategoryClick(category2) + verify(exactly = 0) { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(category2.name)) } verify { store.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(category2.name)) } + verify { metrics.track(Event.PocketHomeRecsCategoryClicked(category2.name, 2, false)) } } @Test - fun `GIVEN 8 categories are selected WHEN when a new one is clicked THEN the oldest selected is deselected before selecting the new one`() { + fun `GIVEN 8 categories are selected WHEN when a new one is clicked THEN the oldest selected is deselected before selecting the new one and record telemetry`() { val category1 = PocketRecommendedStoriesSelectedCategory(name = "cat1", selectionTimestamp = 111) val category2 = PocketRecommendedStoriesSelectedCategory(name = "cat2", selectionTimestamp = 222) val category3 = PocketRecommendedStoriesSelectedCategory(name = "cat3", selectionTimestamp = 333) @@ -61,16 +70,17 @@ class DefaultPocketStoriesControllerTest { ) ) ) - val controller = DefaultPocketStoriesController(mockk(), store, mockk()) + val controller = DefaultPocketStoriesController(mockk(), store, mockk(), metrics) controller.handleCategoryClick(PocketRecommendedStoriesCategory(newSelectedCategory.name)) verify { store.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(oldestSelectedCategory.name)) } verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(newSelectedCategory.name)) } + verify { metrics.track(Event.PocketHomeRecsCategoryClicked(newSelectedCategory.name, 8, true)) } } @Test - fun `GIVEN fewer than 8 categories are selected WHEN when a new one is clicked THEN don't deselect anything but select the newly clicked category`() { + fun `GIVEN fewer than 8 categories are selected WHEN when a new one is clicked THEN don't deselect anything but select the newly clicked category and record telemetry`() { val category1 = PocketRecommendedStoriesSelectedCategory(name = "cat1", selectionTimestamp = 111) val category2 = PocketRecommendedStoriesSelectedCategory(name = "cat2", selectionTimestamp = 222) val category3 = PocketRecommendedStoriesSelectedCategory(name = "cat3", selectionTimestamp = 333) @@ -87,50 +97,146 @@ class DefaultPocketStoriesControllerTest { ) ) ) - val controller = DefaultPocketStoriesController(mockk(), store, mockk()) + val newSelectedCategoryName = "newSelectedCategory" + val controller = DefaultPocketStoriesController(mockk(), store, mockk(), metrics) - controller.handleCategoryClick(PocketRecommendedStoriesCategory("newSelectedCategory")) + controller.handleCategoryClick(PocketRecommendedStoriesCategory(newSelectedCategoryName)) verify(exactly = 0) { store.dispatch(HomeFragmentAction.DeselectPocketStoriesCategory(oldestSelectedCategory.name)) } - verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory("newSelectedCategory")) } + verify { store.dispatch(HomeFragmentAction.SelectPocketStoriesCategory(newSelectedCategoryName)) } + verify { metrics.track(Event.PocketHomeRecsCategoryClicked(newSelectedCategoryName, 7, true)) } } @Test - fun `WHEN new stories are shown THEN update the State`() { + fun `WHEN new stories are shown THEN update the State and record telemetry`() { val store = spyk(HomeFragmentStore()) - val controller = DefaultPocketStoriesController(mockk(), store, mockk()) + val controller = DefaultPocketStoriesController(mockk(), store, mockk(), metrics) val storiesShown: List = mockk() controller.handleStoriesShown(storiesShown) verify { store.dispatch(HomeFragmentAction.PocketStoriesShown(storiesShown)) } + verify { metrics.track(Event.PocketHomeRecsShown) } } @Test - fun `WHEN an external link is clicked THEN link is opened`() { - val link = "https://www.mozilla.org/en-US/firefox/pocket/" + fun `WHEN a story is clicked then open that story's url using HomeActivity and record telemetry`() { + val story = PocketRecommendedStory( + title = "", + url = "testLink", + imageUrl = "", + publisher = "", + category = "", + timeToRead = 0, + timesShown = 123 + ) + val homeActivity: HomeActivity = mockk(relaxed = true) + val controller = DefaultPocketStoriesController(homeActivity, mockk(), mockk(relaxed = true), metrics) + + controller.handleStoryClicked(story, 1 to 2) + + verify { homeActivity.openToBrowserAndLoad(story.url, true, BrowserDirection.FromHome) } + metrics.track(Event.PocketHomeRecsStoryClicked(story.timesShown, 1 to 2)) + } + + @Test + fun `WHEN discover more is clicked then open that using HomeActivity and record telemetry`() { + val link = "http://getpocket.com/explore" val homeActivity: HomeActivity = mockk(relaxed = true) - val controller = DefaultPocketStoriesController(homeActivity, mockk(), mockk(relaxed = true)) + val controller = DefaultPocketStoriesController(homeActivity, mockk(), mockk(relaxed = true), metrics) - controller.handleExternalLinkClick(link) + controller.handleDiscoverMoreClicked(link) verify { homeActivity.openToBrowserAndLoad(link, true, BrowserDirection.FromHome) } + metrics.track(Event.PocketHomeRecsDiscoverMoreClicked) } @Test - fun `WHEN an external link is clicked THEN link is opened and search dismissed`() { + fun `WHEN learn more is clicked then open that using HomeActivity and record telemetry`() { val link = "https://www.mozilla.org/en-US/firefox/pocket/" val homeActivity: HomeActivity = mockk(relaxed = true) + val controller = DefaultPocketStoriesController(homeActivity, mockk(), mockk(relaxed = true), metrics) + + controller.handleLearnMoreClicked(link) + + verify { homeActivity.openToBrowserAndLoad(link, true, BrowserDirection.FromHome) } + metrics.track(Event.PocketHomeRecsLearnMoreClicked) + } + + @Test + fun `WHEN a story is clicked THEN search is dismissed and then its link opened`() { + val story = PocketRecommendedStory("", "url", "", "", "", 0, 0) + val homeActivity: HomeActivity = mockk(relaxed = true) val navController: NavController = mockk(relaxed = true) + every { navController.currentDestination } returns mockk { + every { id } returns R.id.searchDialogFragment + } + val controller = DefaultPocketStoriesController(homeActivity, mockk(), navController, metrics) + + controller.handleStoryClicked(story, 1 to 2) + verifyOrder { + navController.navigateUp() + homeActivity.openToBrowserAndLoad(story.url, true, BrowserDirection.FromHome) + } + } + + @Test + fun `WHEN discover more is clicked THEN search is dismissed and then its link opened`() { + val link = "https://discoverMore.link" + val homeActivity: HomeActivity = mockk(relaxed = true) + val navController: NavController = mockk(relaxed = true) every { navController.currentDestination } returns mockk { every { id } returns R.id.searchDialogFragment } + val controller = DefaultPocketStoriesController(homeActivity, mockk(), navController, metrics) - val controller = DefaultPocketStoriesController(homeActivity, mockk(), navController) - controller.handleExternalLinkClick(link) + controller.handleDiscoverMoreClicked(link) + + verifyOrder { + navController.navigateUp() + homeActivity.openToBrowserAndLoad(link, true, BrowserDirection.FromHome) + } + } + + @Test + fun `WHEN learn more link is clicked THEN search is dismissed and then that link is opened`() { + val link = "https://learnMore.link" + val homeActivity: HomeActivity = mockk(relaxed = true) + val navController: NavController = mockk(relaxed = true) + every { navController.currentDestination } returns mockk { + every { id } returns R.id.searchDialogFragment + } + val controller = DefaultPocketStoriesController(homeActivity, mockk(), navController, metrics) + + controller.handleLearnMoreClicked(link) + + verifyOrder { + navController.navigateUp() + homeActivity.openToBrowserAndLoad(link, true, BrowserDirection.FromHome) + } + } + + @Test + fun `GIVEN search dialog is currently focused WHEN dismissSearchDialogIfDisplayed is called THEN close the search dialog`() { + val navController: NavController = mockk(relaxed = true) + every { navController.currentDestination } returns mockk { + every { id } returns R.id.searchDialogFragment + } + val controller = DefaultPocketStoriesController(mockk(), mockk(), navController, mockk()) + + controller.dismissSearchDialogIfDisplayed() - verify { homeActivity.openToBrowserAndLoad(link, true, BrowserDirection.FromHome) } verify { navController.navigateUp() } } + + @Test + fun `GIVEN search dialog is not currently focused WHEN dismissSearchDialogIfDisplayed is called THEN do nothing`() { + val navController: NavController = mockk(relaxed = true) + val controller = DefaultPocketStoriesController(mockk(), mockk(), navController, mockk()) + + controller.dismissSearchDialogIfDisplayed() + + verify(exactly = 0) { navController.navigateUp() } + } }