From 4774ea0eab66dbfae5112451ae8bb59e193e00c0 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 4 Aug 2026 10:26:17 -0400 Subject: [PATCH 1/3] Open new stats from the My Site stats card The new stats gate lived only in ListItemActionHandler, so the Today's Stats card always opened legacy stats. Extract the gate into NewStatsFeatureUtils and apply it to the card and the post-login path. --- .../android/ui/mysite/MySiteFragment.kt | 3 ++ .../android/ui/mysite/MySiteViewModel.kt | 8 ++- .../android/ui/mysite/SiteNavigationAction.kt | 1 + .../ui/mysite/cards/ListItemActionHandler.kt | 9 ++-- .../todaysstats/TodaysStatsViewModelSlice.kt | 17 ++++--- .../android/ui/newstats/NewStatsActivity.kt | 17 ++++++- .../ui/newstats/NewStatsFeatureUtils.kt | 19 +++++++ .../viewsstats/ViewsStatsViewModel.kt | 14 ++++-- .../refresh/lists/widget/utils/WidgetUtils.kt | 15 ++---- .../android/ui/mysite/MySiteViewModelTest.kt | 5 ++ .../mysite/cards/ListItemActionHandlerTest.kt | 35 ++++--------- .../TodaysStatsViewModelSliceTest.kt | 47 ++++++++++++++++- .../ui/newstats/NewStatsFeatureUtilsTest.kt | 50 +++++++++++++++++++ 13 files changed, 184 insertions(+), 56 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtils.kt create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtilsTest.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt index 8be71e0f32f1..0ce7ee9421ca 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt @@ -55,6 +55,7 @@ import org.wordpress.android.ui.posts.PostUtils import org.wordpress.android.ui.reader.ReaderActivityLauncher import org.wordpress.android.ui.reader.tracker.ReaderTracker import org.wordpress.android.ui.newstats.NewStatsActivity +import org.wordpress.android.ui.newstats.StatsPeriod import org.wordpress.android.ui.stats.StatsTimeframe import org.wordpress.android.ui.stats.refresh.utils.StatsLaunchedFrom import org.wordpress.android.ui.uploads.UploadService @@ -648,6 +649,8 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment), StatsLaunchedFrom.TODAY_STATS_CARD ) + is SiteNavigationAction.OpenNewStatsForToday -> NewStatsActivity.start(requireContext(), StatsPeriod.Today) + is SiteNavigationAction.OpenExternalUrl -> ActivityLauncher.openUrlExternal(requireActivity(), action.url) is SiteNavigationAction.OpenUrlInWebView -> diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt index 7fe427ddd878..e9c4d3e4b65f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt @@ -28,6 +28,7 @@ import org.wordpress.android.ui.mysite.MySiteViewModel.State.SiteSelected import org.wordpress.android.ui.mysite.cards.DashboardCardsViewModelSlice import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardViewModelSlice import org.wordpress.android.ui.mysite.items.DashboardItemsViewModelSlice +import org.wordpress.android.ui.newstats.NewStatsFeatureUtils import org.wordpress.android.ui.pages.SnackbarMessageHolder import org.wordpress.android.ui.mediapicker.MediaPickerActivity import org.wordpress.android.ui.posts.BasicDialogViewModel @@ -70,6 +71,7 @@ class MySiteViewModel @Inject constructor( private val gutenbergEditorPreloader: GutenbergEditorPreloader, private val siteConnectivityBannerViewModelSlice: SiteConnectivityBannerViewModelSlice, private val gutenbergKitAnnouncementController: GutenbergKitAnnouncementController, + private val newStatsFeatureUtils: NewStatsFeatureUtils, ) : ScopedViewModel(mainDispatcher) { private val _onSnackbarMessage = MutableLiveData>() private val _onNavigation = MutableLiveData>() @@ -243,7 +245,11 @@ class MySiteViewModel @Inject constructor( fun handleSuccessfulLoginResult() { selectedSiteRepository.getSelectedSite()?.let { site -> _onNavigation.value = Event( - SiteNavigationAction.OpenStats(site) + if (newStatsFeatureUtils.isNewStatsEnabled()) { + SiteNavigationAction.OpenNewStats + } else { + SiteNavigationAction.OpenStats(site) + } ) } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt index 37dcedf13f3a..4bc60d2c37f6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt @@ -52,6 +52,7 @@ sealed class SiteNavigationAction { data class EditDraftPost(val site: SiteModel, val postId: Int) : SiteNavigationAction() data class EditScheduledPost(val site: SiteModel, val postId: Int) : SiteNavigationAction() data class OpenStatsByDay(val site: SiteModel) : SiteNavigationAction() + object OpenNewStatsForToday : SiteNavigationAction() data class OpenExternalUrl(val url: String) : SiteNavigationAction() data class OpenUrlInWebView(val url: String) : SiteNavigationAction() data class OpenDeepLink(val url: String) : SiteNavigationAction() diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt index f6ea59acb767..25f4993bac0b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandler.kt @@ -8,16 +8,14 @@ import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.CampaignLis import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper import org.wordpress.android.ui.mysite.SiteNavigationAction import org.wordpress.android.ui.mysite.items.listitem.ListItemAction -import org.wordpress.android.ui.prefs.AppPrefsWrapper -import org.wordpress.android.util.config.NewStatsFeatureConfig +import org.wordpress.android.ui.newstats.NewStatsFeatureUtils import javax.inject.Inject class ListItemActionHandler @Inject constructor( private val accountStore: AccountStore, private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper, private val blazeFeatureUtils: BlazeFeatureUtils, - private val newStatsFeatureConfig: NewStatsFeatureConfig, - private val appPrefsWrapper: AppPrefsWrapper + private val newStatsFeatureUtils: NewStatsFeatureUtils ) { fun handleAction( action: ListItemAction, @@ -61,8 +59,7 @@ class ListItemActionHandler @Inject constructor( // If it's a WordPress.com or Jetpack site, show the Stats screen. site.isWPCom || site.isJetpackInstalled && site.isJetpackConnected -> { - // Show New Stats when the remote flag is on (rollout) or the user has opted in locally. - if (newStatsFeatureConfig.isEnabled() || appPrefsWrapper.getNewStatsUserOptedIn()) { + if (newStatsFeatureUtils.isNewStatsEnabled()) { SiteNavigationAction.OpenNewStats } else { SiteNavigationAction.OpenStats(site) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt index 068f35d7160b..fe2d21212783 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSlice.kt @@ -9,6 +9,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItemBuilderParams.TodaysStat import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.mysite.SiteNavigationAction import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker +import org.wordpress.android.ui.newstats.NewStatsFeatureUtils import org.wordpress.android.ui.prefs.AppPrefsWrapper import org.wordpress.android.viewmodel.Event import javax.inject.Inject @@ -18,7 +19,8 @@ class TodaysStatsViewModelSlice @Inject constructor( private val selectedSiteRepository: SelectedSiteRepository, private val jetpackFeatureRemovalPhaseHelper: JetpackFeatureRemovalPhaseHelper, private val appPrefsWrapper: AppPrefsWrapper, - private val todaysStatsCardBuilder: TodaysStatsCardBuilder + private val todaysStatsCardBuilder: TodaysStatsCardBuilder, + private val newStatsFeatureUtils: NewStatsFeatureUtils ) { private val _uiModel = MutableLiveData() val uiModel = _uiModel as LiveData @@ -86,12 +88,15 @@ class TodaysStatsViewModelSlice @Inject constructor( } private fun navigateToTodaysStats() { - val selectedSite = requireNotNull(selectedSiteRepository.getSelectedSite()) - if (jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()) { - _onNavigation.value = Event(SiteNavigationAction.ShowJetpackRemovalStaticPostersView) - } else { - _onNavigation.value = Event(SiteNavigationAction.OpenStatsByDay(selectedSite)) + val navigationAction = when { + jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage() -> + SiteNavigationAction.ShowJetpackRemovalStaticPostersView + + newStatsFeatureUtils.isNewStatsEnabled() -> SiteNavigationAction.OpenNewStatsForToday + + else -> SiteNavigationAction.OpenStatsByDay(requireNotNull(selectedSiteRepository.getSelectedSite())) } + _onNavigation.value = Event(navigationAction) } fun clearValue() { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt index 28897043ab6c..7b56fa11e623 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsActivity.kt @@ -199,8 +199,21 @@ class NewStatsActivity : BaseAppCompatActivity() { } companion object { - fun start(context: Context) { - context.startActivity(Intent(context, NewStatsActivity::class.java)) + /** + * Opens New Stats, optionally on a specific [period]. The period is passed as an Intent + * extra that seeds [ViewsStatsViewModel]'s SavedStateHandle, so it takes precedence over the + * persisted period without overwriting it. + */ + fun start(context: Context, period: StatsPeriod? = null) { + val intent = Intent(context, NewStatsActivity::class.java) + period?.let { + intent.putExtra(ViewsStatsViewModel.KEY_PERIOD_TYPE, it.toTypeString()) + if (it is StatsPeriod.Custom) { + intent.putExtra(ViewsStatsViewModel.KEY_CUSTOM_START_DATE, it.startDate.toEpochDay()) + intent.putExtra(ViewsStatsViewModel.KEY_CUSTOM_END_DATE, it.endDate.toEpochDay()) + } + } + context.startActivity(intent) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtils.kt new file mode 100644 index 000000000000..d49f004259c3 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtils.kt @@ -0,0 +1,19 @@ +package org.wordpress.android.ui.newstats + +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.config.NewStatsFeatureConfig +import javax.inject.Inject + +/** + * Single source of truth for deciding whether Stats entry points should open New Stats. + */ +class NewStatsFeatureUtils @Inject constructor( + private val newStatsFeatureConfig: NewStatsFeatureConfig, + private val appPrefsWrapper: AppPrefsWrapper +) { + /** + * Returns true when the remote flag is on (rollout) or the user has opted in locally. + */ + fun isNewStatsEnabled(): Boolean = + newStatsFeatureConfig.isEnabled() || appPrefsWrapper.getNewStatsUserOptedIn() +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/viewsstats/ViewsStatsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/viewsstats/ViewsStatsViewModel.kt index b9323bdd1617..70935fc6e6f4 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/viewsstats/ViewsStatsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/viewsstats/ViewsStatsViewModel.kt @@ -42,9 +42,6 @@ private val HOURLY_FORMAT_REGEX = Regex("""\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}"" private val DAILY_FORMAT_REGEX = Regex("""\d{4}-\d{2}-\d{2}""") private val MONTHLY_FORMAT_REGEX = Regex("""\d{4}-\d{2}""") -private const val KEY_PERIOD_TYPE = "period_type" -private const val KEY_CUSTOM_START_DATE = "custom_start_date" -private const val KEY_CUSTOM_END_DATE = "custom_end_date" private const val KEY_CHART_TYPE = "chart_type" private const val PERIOD_TODAY = "today" @@ -761,4 +758,15 @@ class ViewsStatsViewModel @Inject constructor( fun onRetry() { loadData() } + + companion object { + /** + * Keys used to restore the selected period. They double as Intent extras: Hilt seeds the + * [SavedStateHandle] from the launching Intent, so an entry point can open New Stats on a + * specific period without persisting it as the user's preference. + */ + const val KEY_PERIOD_TYPE = "period_type" + const val KEY_CUSTOM_START_DATE = "custom_start_date" + const val KEY_CUSTOM_END_DATE = "custom_end_date" + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetUtils.kt b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetUtils.kt index 8233a2fb6e7b..1a4587c24f0c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetUtils.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/stats/refresh/lists/widget/utils/WidgetUtils.kt @@ -21,7 +21,7 @@ import org.wordpress.android.fluxc.model.stats.time.VisitsAndViewsModel.PeriodDa import org.wordpress.android.fluxc.network.utils.StatsGranularity import org.wordpress.android.modules.UI_THREAD import org.wordpress.android.ui.newstats.NewStatsActivity -import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.ui.newstats.NewStatsFeatureUtils import org.wordpress.android.ui.stats.StatsTimeframe import org.wordpress.android.ui.stats.refresh.StatsActivity import org.wordpress.android.ui.stats.refresh.lists.widget.IS_WIDE_VIEW_KEY @@ -32,7 +32,6 @@ import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsCo import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsColorSelectionViewModel.Color.LIGHT import org.wordpress.android.ui.stats.refresh.lists.widget.configuration.StatsWidgetConfigureFragment.WidgetType import org.wordpress.android.ui.stats.refresh.utils.StatsLaunchedFrom -import org.wordpress.android.util.config.NewStatsFeatureConfig import org.wordpress.android.util.image.ImageManager import org.wordpress.android.util.image.ImageType.ICON import org.wordpress.android.viewmodel.ResourceProvider @@ -51,8 +50,7 @@ class WidgetUtils @Inject constructor( @Named(UI_THREAD) private val mainDispatcher: CoroutineDispatcher, val imageManager: ImageManager, - private val newStatsFeatureConfig: NewStatsFeatureConfig, - private val appPrefsWrapper: AppPrefsWrapper + private val newStatsFeatureUtils: NewStatsFeatureUtils ) { private val coroutineScope = CoroutineScope(mainDispatcher) fun isWidgetWiderThanLimit( @@ -181,7 +179,7 @@ class WidgetUtils statsTimeframe: StatsTimeframe, granularity: StatsGranularity? = null ): PendingIntent { - val intent = if (isNewStatsEnabled()) { + val intent = if (newStatsFeatureUtils.isNewStatsEnabled()) { Intent(context, NewStatsActivity::class.java).apply { putExtra(WordPress.LOCAL_SITE_ID, localSiteId) // Mirror the row-tap (template) path: CLEAR_TOP forces an already-running @@ -210,7 +208,7 @@ class WidgetUtils private fun getPendingTemplate(context: Context): PendingIntent { // The per-row fill-in intents already carry the LOCAL_SITE_ID extra, which is merged into // whichever component this template targets, so New Stats receives the tapped row's site. - val targetActivity = if (isNewStatsEnabled()) { + val targetActivity = if (newStatsFeatureUtils.isNewStatsEnabled()) { NewStatsActivity::class.java } else { StatsActivity::class.java @@ -235,11 +233,6 @@ class WidgetUtils return Random(Date().time).nextInt() } - // Mirrors the routing used by the My Site menu (ListItemActionHandler): open New Stats when the - // remote flag is on (rollout) or the user has opted in locally. - private fun isNewStatsEnabled(): Boolean = - newStatsFeatureConfig.isEnabled() || appPrefsWrapper.getNewStatsUserOptedIn() - fun getLastWeekPeriodData(visitsAndViewsModel: VisitsAndViewsModel): PeriodData? { val currentDateForSite = WeeklyRoundupUtils.parseStandardDate(visitsAndViewsModel.period) ?: return null val lastWeekStartDate = currentDateForSite.minusWeeks(1).with(TemporalAdjusters.previousOrSame(MONDAY)) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt index 92a9a5aa90ab..aeefa49c0e16 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/MySiteViewModelTest.kt @@ -38,6 +38,7 @@ import org.wordpress.android.ui.mysite.cards.DashboardCardsViewModelSlice import org.wordpress.android.ui.mysite.cards.applicationpassword.ApplicationPasswordViewModelSlice import org.wordpress.android.ui.mysite.cards.siteinfo.SiteInfoHeaderCardViewModelSlice import org.wordpress.android.ui.mysite.items.DashboardItemsViewModelSlice +import org.wordpress.android.ui.newstats.NewStatsFeatureUtils import org.wordpress.android.ui.mysite.items.listitem.SiteCapabilityChecker import org.wordpress.android.ui.mysite.cards.connectivity.SiteConnectivityBannerViewModelSlice import org.wordpress.android.ui.pages.SnackbarMessageHolder @@ -111,6 +112,9 @@ class MySiteViewModelTest : BaseUnitTest() { lateinit var gutenbergKitAnnouncementController: org.wordpress.android.ui.posts.GutenbergKitAnnouncementController + @Mock + lateinit var newStatsFeatureUtils: NewStatsFeatureUtils + private lateinit var viewModel: MySiteViewModel private lateinit var uiModels: MutableList private lateinit var snackbars: MutableList @@ -169,6 +173,7 @@ class MySiteViewModelTest : BaseUnitTest() { gutenbergEditorPreloader, siteConnectivityBannerViewModelSlice, gutenbergKitAnnouncementController, + newStatsFeatureUtils, ) uiModels = mutableListOf() snackbars = mutableListOf() diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandlerTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandlerTest.kt index f65bb4adeb8e..e32a1b4c3b75 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandlerTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/ListItemActionHandlerTest.kt @@ -16,8 +16,7 @@ import org.wordpress.android.ui.blaze.blazecampaigns.campaignlisting.CampaignLis import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper import org.wordpress.android.ui.mysite.SiteNavigationAction import org.wordpress.android.ui.mysite.items.listitem.ListItemAction -import org.wordpress.android.ui.prefs.AppPrefsWrapper -import org.wordpress.android.util.config.NewStatsFeatureConfig +import org.wordpress.android.ui.newstats.NewStatsFeatureUtils import kotlin.test.assertEquals @ExperimentalCoroutinesApi @@ -33,10 +32,7 @@ class ListItemActionHandlerTest: BaseUnitTest() { lateinit var blazeFeatureUtils: BlazeFeatureUtils @Mock - lateinit var newStatsFeatureConfig: NewStatsFeatureConfig - - @Mock - lateinit var appPrefsWrapper: AppPrefsWrapper + lateinit var newStatsFeatureUtils: NewStatsFeatureUtils private val site = SiteModel() @@ -48,8 +44,7 @@ class ListItemActionHandlerTest: BaseUnitTest() { accountStore, jetpackFeatureRemovalPhaseHelper, blazeFeatureUtils, - newStatsFeatureConfig, - appPrefsWrapper + newStatsFeatureUtils ) } @@ -167,9 +162,9 @@ class ListItemActionHandlerTest: BaseUnitTest() { } @Test - fun `stats item click emits OpenNewStats when NEW_STATS feature is enabled and site is WPCom`() { + fun `stats item click emits OpenNewStats when new stats is enabled and site is WPCom`() { site.setIsWPCom(true) - whenever(newStatsFeatureConfig.isEnabled()).thenReturn(true) + whenever(newStatsFeatureUtils.isNewStatsEnabled()).thenReturn(true) val navigationAction = invokeItemClickAction(action = ListItemAction.STATS) @@ -177,22 +172,11 @@ class ListItemActionHandlerTest: BaseUnitTest() { } @Test - fun `stats item click emits OpenNewStats when NEW_STATS feature is enabled and site is Jetpack`() { + fun `stats item click emits OpenNewStats when new stats is enabled and site is Jetpack`() { site.setIsJetpackConnected(true) site.setIsWPCom(true) whenever(accountStore.hasAccessToken()).thenReturn(true) - whenever(newStatsFeatureConfig.isEnabled()).thenReturn(true) - - val navigationAction = invokeItemClickAction(action = ListItemAction.STATS) - - assertEquals(SiteNavigationAction.OpenNewStats, navigationAction) - } - - @Test - fun `stats item click emits OpenNewStats when user opted in even if NEW_STATS feature is disabled`() { - site.setIsWPCom(true) - whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false) - whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(true) + whenever(newStatsFeatureUtils.isNewStatsEnabled()).thenReturn(true) val navigationAction = invokeItemClickAction(action = ListItemAction.STATS) @@ -200,10 +184,9 @@ class ListItemActionHandlerTest: BaseUnitTest() { } @Test - fun `stats item click emits OpenStats when NEW_STATS feature is disabled and user has not opted in`() { + fun `stats item click emits OpenStats when new stats is disabled`() { site.setIsWPCom(true) - whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false) - whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(false) + whenever(newStatsFeatureUtils.isNewStatsEnabled()).thenReturn(false) val navigationAction = invokeItemClickAction(action = ListItemAction.STATS) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt index f6a18d0043cf..d3cb6bf55586 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt @@ -17,6 +17,7 @@ import org.wordpress.android.ui.mysite.MySiteCardAndItem import org.wordpress.android.ui.mysite.SelectedSiteRepository import org.wordpress.android.ui.mysite.SiteNavigationAction import org.wordpress.android.ui.mysite.cards.dashboard.CardsTracker +import org.wordpress.android.ui.newstats.NewStatsFeatureUtils import org.wordpress.android.ui.prefs.AppPrefsWrapper @ExperimentalCoroutinesApi @@ -37,6 +38,9 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() { @Mock lateinit var todaysStatsCardBuilder: TodaysStatsCardBuilder + @Mock + lateinit var newStatsFeatureUtils: NewStatsFeatureUtils + private lateinit var todaysStatsViewModelSlice: TodaysStatsViewModelSlice private lateinit var navigationActions: MutableList @@ -52,7 +56,8 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() { selectedSiteRepository, jetpackFeatureRemovalPhaseHelper, appPrefsWrapper, - todaysStatsCardBuilder + todaysStatsCardBuilder, + newStatsFeatureUtils ) navigationActions = mutableListOf() todaysStatsViewModelSlice.onNavigation.observeForever { event -> @@ -81,6 +86,46 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() { ) } + @Test + fun `given new stats enabled, when card item is clicked, then new stats is opened for today`() = + test { + whenever(newStatsFeatureUtils.isNewStatsEnabled()).thenReturn(true) + + val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(mock()) + + params.onTodaysStatsCardClick() + + assertThat(navigationActions).containsOnly(SiteNavigationAction.OpenNewStatsForToday) + verify(cardsTracker).trackCardItemClicked( + CardsTracker.Type.STATS.label, + CardsTracker.StatsSubtype.TODAYS_STATS.label + ) + } + + @Test + fun `given new stats enabled, when more menu item view stats is clicked, then new stats is opened for today`() = + test { + whenever(newStatsFeatureUtils.isNewStatsEnabled()).thenReturn(true) + + val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(mock()) + + params.moreMenuClickParams.onViewStatsMenuItemClick.invoke() + + assertThat(navigationActions).containsOnly(SiteNavigationAction.OpenNewStatsForToday) + } + + @Test + fun `given static posters phase, when card item is clicked, then static posters view is shown`() = + test { + whenever(jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()).thenReturn(true) + + val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(mock()) + + params.onTodaysStatsCardClick() + + assertThat(navigationActions).containsOnly(SiteNavigationAction.ShowJetpackRemovalStaticPostersView) + } + @Test fun `given today's stat card, when get more views url is clicked, then external link is opened`() = test { diff --git a/WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtilsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtilsTest.kt new file mode 100644 index 000000000000..649710646673 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtilsTest.kt @@ -0,0 +1,50 @@ +package org.wordpress.android.ui.newstats + +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.whenever +import org.wordpress.android.ui.prefs.AppPrefsWrapper +import org.wordpress.android.util.config.NewStatsFeatureConfig + +@RunWith(MockitoJUnitRunner::class) +class NewStatsFeatureUtilsTest { + @Mock + lateinit var newStatsFeatureConfig: NewStatsFeatureConfig + + @Mock + lateinit var appPrefsWrapper: AppPrefsWrapper + + private lateinit var newStatsFeatureUtils: NewStatsFeatureUtils + + @Before + fun setUp() { + newStatsFeatureUtils = NewStatsFeatureUtils(newStatsFeatureConfig, appPrefsWrapper) + } + + @Test + fun `given remote flag is on, then new stats is enabled`() { + whenever(newStatsFeatureConfig.isEnabled()).thenReturn(true) + + assertThat(newStatsFeatureUtils.isNewStatsEnabled()).isTrue() + } + + @Test + fun `given remote flag is off and user opted in, then new stats is enabled`() { + whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false) + whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(true) + + assertThat(newStatsFeatureUtils.isNewStatsEnabled()).isTrue() + } + + @Test + fun `given remote flag is off and user has not opted in, then new stats is disabled`() { + whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false) + whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(false) + + assertThat(newStatsFeatureUtils.isNewStatsEnabled()).isFalse() + } +} From da601fb65e843538abc3012810dceac71936a050 Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 4 Aug 2026 10:32:13 -0400 Subject: [PATCH 2/3] Remove NewStatsFeatureUtilsTest The class under test is a single boolean OR with no branching logic, and the routing it feeds is already covered at both call sites. --- .../ui/newstats/NewStatsFeatureUtilsTest.kt | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtilsTest.kt diff --git a/WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtilsTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtilsTest.kt deleted file mode 100644 index 649710646673..000000000000 --- a/WordPress/src/test/java/org/wordpress/android/ui/newstats/NewStatsFeatureUtilsTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.wordpress.android.ui.newstats - -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.whenever -import org.wordpress.android.ui.prefs.AppPrefsWrapper -import org.wordpress.android.util.config.NewStatsFeatureConfig - -@RunWith(MockitoJUnitRunner::class) -class NewStatsFeatureUtilsTest { - @Mock - lateinit var newStatsFeatureConfig: NewStatsFeatureConfig - - @Mock - lateinit var appPrefsWrapper: AppPrefsWrapper - - private lateinit var newStatsFeatureUtils: NewStatsFeatureUtils - - @Before - fun setUp() { - newStatsFeatureUtils = NewStatsFeatureUtils(newStatsFeatureConfig, appPrefsWrapper) - } - - @Test - fun `given remote flag is on, then new stats is enabled`() { - whenever(newStatsFeatureConfig.isEnabled()).thenReturn(true) - - assertThat(newStatsFeatureUtils.isNewStatsEnabled()).isTrue() - } - - @Test - fun `given remote flag is off and user opted in, then new stats is enabled`() { - whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false) - whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(true) - - assertThat(newStatsFeatureUtils.isNewStatsEnabled()).isTrue() - } - - @Test - fun `given remote flag is off and user has not opted in, then new stats is disabled`() { - whenever(newStatsFeatureConfig.isEnabled()).thenReturn(false) - whenever(appPrefsWrapper.getNewStatsUserOptedIn()).thenReturn(false) - - assertThat(newStatsFeatureUtils.isNewStatsEnabled()).isFalse() - } -} From b85d95fa8ddf8746ae240f8cf2470a8630d51a3a Mon Sep 17 00:00:00 2001 From: Nick Bradbury Date: Tue, 4 Aug 2026 11:52:57 -0400 Subject: [PATCH 3/3] Stop mocking TodaysStatsCardModel in the new tests The lint baseline covers exactly five DoNotMockDataClass hits in this file, so the three added mocks failed lint. These tests only exercise the click callbacks, so the nullable model can just be null. --- .../dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt index d3cb6bf55586..9e0b03957812 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/dashboard/todaysstats/TodaysStatsViewModelSliceTest.kt @@ -91,7 +91,7 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() { test { whenever(newStatsFeatureUtils.isNewStatsEnabled()).thenReturn(true) - val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(mock()) + val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(null) params.onTodaysStatsCardClick() @@ -107,7 +107,7 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() { test { whenever(newStatsFeatureUtils.isNewStatsEnabled()).thenReturn(true) - val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(mock()) + val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(null) params.moreMenuClickParams.onViewStatsMenuItemClick.invoke() @@ -119,7 +119,7 @@ class TodaysStatsViewModelSliceTest : BaseUnitTest() { test { whenever(jetpackFeatureRemovalPhaseHelper.shouldShowStaticPage()).thenReturn(true) - val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(mock()) + val params = todaysStatsViewModelSlice.getTodaysStatsBuilderParams(null) params.onTodaysStatsCardClick()