From 880f5ebf38306dbde35102ce3325dca375491353 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 22 Jan 2026 10:44:39 +0100 Subject: [PATCH 1/5] Add StatsDataSource and refactor StatsRepository - Create StatsDataSource interface and implementation to abstract data fetching - Add StatsModule for dependency injection - Refactor StatsRepository to use the new data source - Rename todaysstat -> todaystats package in newstats - Add StatsRepositoryTest with comprehensive test coverage --- .../wordpress/android/modules/StatsModule.kt | 15 + .../android/ui/newstats/NewStatsActivity.kt | 4 +- .../ui/newstats/datasource/StatsDataSource.kt | 96 ++++ .../datasource/StatsDataSourceImpl.kt | 114 +++++ .../ui/newstats/repository/StatsRepository.kt | 255 +++-------- .../TodaysStatsCard.kt | 2 +- .../TodaysStatsCardUiState.kt | 2 +- .../TodaysStatsViewModel.kt | 2 +- .../repository/StatsRepositoryTest.kt | 425 ++++++++++++++++++ .../TodaysStatsViewModelTest.kt | 2 +- 10 files changed, 727 insertions(+), 190 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/modules/StatsModule.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSource.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt rename WordPress/src/main/java/org/wordpress/android/ui/newstats/{todaysstat => todaystats}/TodaysStatsCard.kt (99%) rename WordPress/src/main/java/org/wordpress/android/ui/newstats/{todaysstat => todaystats}/TodaysStatsCardUiState.kt (94%) rename WordPress/src/main/java/org/wordpress/android/ui/newstats/{todaysstat => todaystats}/TodaysStatsViewModel.kt (99%) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/newstats/repository/StatsRepositoryTest.kt rename WordPress/src/test/java/org/wordpress/android/ui/newstats/{todaysstat => todaystats}/TodaysStatsViewModelTest.kt (99%) diff --git a/WordPress/src/main/java/org/wordpress/android/modules/StatsModule.kt b/WordPress/src/main/java/org/wordpress/android/modules/StatsModule.kt new file mode 100644 index 000000000000..8216a860e935 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/modules/StatsModule.kt @@ -0,0 +1,15 @@ +package org.wordpress.android.modules + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.wordpress.android.ui.newstats.datasource.StatsDataSource +import org.wordpress.android.ui.newstats.datasource.StatsDataSourceImpl + +@InstallIn(SingletonComponent::class) +@Module +abstract class StatsModule { + @Binds + abstract fun bindStatsDataSource(impl: StatsDataSourceImpl): StatsDataSource +} 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 1b7c45f12c3b..eb87e12e0ca5 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 @@ -40,8 +40,8 @@ import kotlinx.coroutines.launch import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppThemeM3 import org.wordpress.android.ui.main.BaseAppCompatActivity -import org.wordpress.android.ui.newstats.todaysstat.TodaysStatsCard -import org.wordpress.android.ui.newstats.todaysstat.TodaysStatsViewModel +import org.wordpress.android.ui.newstats.todaystats.TodaysStatsCard +import org.wordpress.android.ui.newstats.todaystats.TodaysStatsViewModel import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsCard import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsViewModel diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSource.kt new file mode 100644 index 000000000000..d8c0b0edf81f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSource.kt @@ -0,0 +1,96 @@ +package org.wordpress.android.ui.newstats.datasource + +/** + * Data source interface for fetching stats data. + * This abstraction allows mocking the data layer in tests without needing access to uniffi objects. + */ +interface StatsDataSource { + /** + * Initializes the data source with the access token. + */ + fun init(accessToken: String) + + /** + * Fetches stats data for a specific site. + * + * @param siteId The WordPress.com site ID + * @param unit The time unit for the stats (HOUR, DAY, etc.) + * @param quantity The number of data points to fetch + * @param endDate The end date for the stats period (format: yyyy-MM-dd) + * @return Result containing the stats data or an error + */ + suspend fun fetchStatsVisits( + siteId: Long, + unit: StatsUnit, + quantity: Int, + endDate: String + ): StatsVisitsDataResult +} + +/** + * Time unit for stats data. + */ +enum class StatsUnit { + HOUR, + DAY +} + +/** + * Result wrapper for stats visits fetch operation. + */ +sealed class StatsVisitsDataResult { + data class Success(val data: StatsVisitsData) : StatsVisitsDataResult() + data class Error(val message: String) : StatsVisitsDataResult() +} + +/** + * Stats visits data from the API. + * Contains all the data points for views, visitors, likes, comments, and posts. + */ +data class StatsVisitsData( + val visits: List, + val visitors: List, + val likes: List, + val comments: List, + val posts: List +) + +/** + * Data point for visits/views. + */ +data class VisitsDataPoint( + val period: String, + val visits: Long +) + +/** + * Data point for visitors. + */ +data class VisitorsDataPoint( + val period: String, + val visitors: Long +) + +/** + * Data point for likes. + */ +data class LikesDataPoint( + val period: String, + val likes: Long +) + +/** + * Data point for comments. + */ +data class CommentsDataPoint( + val period: String, + val comments: Long +) + +/** + * Data point for posts. + */ +data class PostsDataPoint( + val period: String, + val posts: Long +) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt new file mode 100644 index 000000000000..a24c07edadcc --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt @@ -0,0 +1,114 @@ +package org.wordpress.android.ui.newstats.datasource + +import org.wordpress.android.networking.restapi.WpComApiClientProvider +import org.wordpress.android.ui.newstats.extension.statsCommentsData +import org.wordpress.android.ui.newstats.extension.statsLikesData +import org.wordpress.android.ui.newstats.extension.statsPostsData +import org.wordpress.android.ui.newstats.extension.statsVisitorsData +import org.wordpress.android.ui.newstats.extension.statsVisitsData +import rs.wordpress.api.kotlin.WpComApiClient +import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.StatsVisitsParams +import uniffi.wp_api.StatsVisitsUnit +import javax.inject.Inject + +/** + * Implementation of [StatsDataSource] that fetches stats data from the WordPress.com API + * using the wordpress-rs library. + */ +class StatsDataSourceImpl @Inject constructor( + private val wpComApiClientProvider: WpComApiClientProvider +) : StatsDataSource { + /** + * Access token for API authentication. + * Marked as @Volatile to ensure visibility across threads since this data source is accessed + * from multiple coroutine contexts. + */ + @Volatile + private var accessToken: String? = null + + private val wpComApiClient: WpComApiClient by lazy { + check(accessToken != null) { "DataSource not initialized" } + wpComApiClientProvider.getWpComApiClient(accessToken!!) + } + + override fun init(accessToken: String) { + this.accessToken = accessToken + } + + override suspend fun fetchStatsVisits( + siteId: Long, + unit: StatsUnit, + quantity: Int, + endDate: String + ): StatsVisitsDataResult { + if (accessToken == null) { + return StatsVisitsDataResult.Error("DataSource not initialized") + } + + val params = StatsVisitsParams( + unit = unit.toApiUnit(), + quantity = quantity.toUInt(), + endDate = endDate + ) + + val result = wpComApiClient.request { requestBuilder -> + requestBuilder.statsVisits().getStatsVisits( + wpComSiteId = siteId.toULong(), + params = params + ) + } + + return when (result) { + is WpRequestResult.Success -> { + val response = result.response.data + val statsData = StatsVisitsData( + visits = response.statsVisitsData().map { dataPoint -> + VisitsDataPoint( + period = dataPoint.period, + visits = dataPoint.visits.toLong() + ) + }, + visitors = response.statsVisitorsData().map { dataPoint -> + VisitorsDataPoint( + period = dataPoint.period, + visitors = dataPoint.visitors.toLong() + ) + }, + likes = response.statsLikesData().map { dataPoint -> + LikesDataPoint( + period = dataPoint.period, + likes = dataPoint.likes.toLong() + ) + }, + comments = response.statsCommentsData().map { dataPoint -> + CommentsDataPoint( + period = dataPoint.period, + comments = dataPoint.comments.toLong() + ) + }, + posts = response.statsPostsData().map { dataPoint -> + PostsDataPoint( + period = dataPoint.period, + posts = dataPoint.posts.toLong() + ) + } + ) + StatsVisitsDataResult.Success(statsData) + } + + is WpRequestResult.WpError -> { + StatsVisitsDataResult.Error(result.errorMessage) + } + + else -> { + StatsVisitsDataResult.Error("Unknown error") + } + } + } + + private fun StatsUnit.toApiUnit(): StatsVisitsUnit = when (this) { + StatsUnit.HOUR -> StatsVisitsUnit.HOUR + StatsUnit.DAY -> StatsVisitsUnit.DAY + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsRepository.kt index 24da50d9278b..dfe74fcc85ea 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsRepository.kt @@ -1,29 +1,22 @@ package org.wordpress.android.ui.newstats.repository import kotlinx.coroutines.CoroutineDispatcher +import org.wordpress.android.ui.newstats.datasource.StatsDataSource +import org.wordpress.android.ui.newstats.datasource.StatsUnit +import org.wordpress.android.ui.newstats.datasource.StatsVisitsDataResult import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.utils.AppLogWrapper import org.wordpress.android.modules.IO_THREAD -import org.wordpress.android.networking.restapi.WpComApiClientProvider import org.wordpress.android.util.AppLog -import org.wordpress.android.ui.newstats.extension.statsCommentsData -import org.wordpress.android.ui.newstats.extension.statsLikesData -import org.wordpress.android.ui.newstats.extension.statsPostsData -import org.wordpress.android.ui.newstats.extension.statsVisitorsData -import org.wordpress.android.ui.newstats.extension.statsVisitsData -import rs.wordpress.api.kotlin.WpComApiClient -import rs.wordpress.api.kotlin.WpRequestResult -import uniffi.wp_api.StatsVisitsParams -import uniffi.wp_api.StatsVisitsUnit import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale import javax.inject.Inject import javax.inject.Named -private const val HOURLY_QUANTITY = 24u -private const val DAILY_QUANTITY = 1u -private const val WEEKLY_QUANTITY = 7u +private const val HOURLY_QUANTITY = 24 +private const val DAILY_QUANTITY = 1 +private const val WEEKLY_QUANTITY = 7 private const val DAYS_BEFORE_END_DATE = -6 /** @@ -31,23 +24,10 @@ private const val DAYS_BEFORE_END_DATE = -6 * Handles hourly visits/views data for the Today's Stats card chart. */ class StatsRepository @Inject constructor( - private val wpComApiClientProvider: WpComApiClientProvider, + private val statsDataSource: StatsDataSource, private val appLogWrapper: AppLogWrapper, @Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher, ) { - /** - * Access token for API authentication. - * Marked as @Volatile to ensure visibility across threads since this repository is accessed - * from multiple coroutine contexts (main thread initialization, IO dispatcher for API calls). - */ - @Volatile - private var accessToken: String? = null - - private val wpComApiClient: WpComApiClient by lazy { - check(accessToken != null) { "Repository not initialized" } - wpComApiClientProvider.getWpComApiClient(accessToken!!) - } - /** * Thread-local date formatter for thread-safe date formatting. * SimpleDateFormat is NOT thread-safe, so we use ThreadLocal to provide each thread @@ -60,7 +40,7 @@ class StatsRepository @Inject constructor( private fun getDateFormat(): SimpleDateFormat = dateFormat.get()!! fun init(accessToken: String) { - this.accessToken = accessToken + statsDataSource.init(accessToken) } /** @@ -70,34 +50,23 @@ class StatsRepository @Inject constructor( * @return Today's aggregated stats or error */ suspend fun fetchTodayAggregates(siteId: Long): TodayAggregatesResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext TodayAggregatesResult.Error("Repository not initialized") - } - val calendar = Calendar.getInstance() val dateString = getDateFormat().format(calendar.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.DAY, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.DAY, quantity = DAILY_QUANTITY, - endDate = dateString, + endDate = dateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val views = response.statsVisitsData().firstOrNull()?.visits?.toLong() ?: 0L - val visitors = response.statsVisitorsData().firstOrNull()?.visitors?.toLong() ?: 0L - val likes = response.statsLikesData().firstOrNull()?.likes?.toLong() ?: 0L - val comments = response.statsCommentsData().firstOrNull()?.comments?.toLong() ?: 0L + is StatsVisitsDataResult.Success -> { + val data = result.data + val views = data.visits.firstOrNull()?.visits ?: 0L + val visitors = data.visitors.firstOrNull()?.visitors ?: 0L + val likes = data.likes.firstOrNull()?.likes ?: 0L + val comments = data.comments.firstOrNull()?.comments ?: 0L val aggregates = TodayAggregates( views = views, @@ -108,14 +77,9 @@ class StatsRepository @Inject constructor( TodayAggregatesResult.Success(aggregates) } - is WpRequestResult.WpError -> { - appLogWrapper.e(AppLog.T.STATS, "API Error fetching today aggregates: ${result.errorMessage}") - TodayAggregatesResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching today aggregates") - TodayAggregatesResult.Error("Unknown error") + is StatsVisitsDataResult.Error -> { + appLogWrapper.e(AppLog.T.STATS, "API Error fetching today aggregates: ${result.message}") + TodayAggregatesResult.Error(result.message) } } } @@ -131,11 +95,6 @@ class StatsRepository @Inject constructor( siteId: Long, offsetDays: Int = 0 ): HourlyViewsResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext HourlyViewsResult.Error("Repository not initialized") - } - val calendar = Calendar.getInstance() // The API's endDate is exclusive for hourly queries, so we need to add 1 day to get // the target day's hours. Formula: 1 (for exclusive end) - offsetDays (0=today, 1=yesterday) @@ -144,36 +103,24 @@ class StatsRepository @Inject constructor( calendar.add(Calendar.DAY_OF_YEAR, 1 - offsetDays) val dateString = getDateFormat().format(calendar.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.HOUR, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.HOUR, quantity = HOURLY_QUANTITY, - endDate = dateString, + endDate = dateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val dataPoints = response.statsVisitsData().map { dataPoint -> - HourlyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits.toLong()) + is StatsVisitsDataResult.Success -> { + val dataPoints = result.data.visits.map { dataPoint -> + HourlyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits) } HourlyViewsResult.Success(dataPoints) } - is WpRequestResult.WpError -> { - appLogWrapper.e(AppLog.T.STATS, "API Error fetching hourly views: ${result.errorMessage}") - HourlyViewsResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching hourly views") - HourlyViewsResult.Error("Unknown error") + is StatsVisitsDataResult.Error -> { + appLogWrapper.e(AppLog.T.STATS, "API Error fetching hourly views: ${result.message}") + HourlyViewsResult.Error(result.message) } } } @@ -187,41 +134,24 @@ class StatsRepository @Inject constructor( */ suspend fun fetchWeeklyStats(siteId: Long, weeksAgo: Int = 0): WeeklyStatsResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext WeeklyStatsResult.Error("Repository not initialized") - } - val (startDate, endDate) = calculateWeekDateRange(weeksAgo) val endDateString = getDateFormat().format(endDate.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.DAY, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.DAY, quantity = WEEKLY_QUANTITY, - endDate = endDateString, + endDate = endDateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val visitsData = response.statsVisitsData() - val visitorsData = response.statsVisitorsData() - val likesData = response.statsLikesData() - val commentsData = response.statsCommentsData() - val postsData = response.statsPostsData() - - val totalViews = visitsData.sumOf { it.visits.toLong() } - val totalVisitors = visitorsData.sumOf { it.visitors.toLong() } - val totalLikes = likesData.sumOf { it.likes.toLong() } - val totalComments = commentsData.sumOf { it.comments.toLong() } - val totalPosts = postsData.sumOf { it.posts.toLong() } + is StatsVisitsDataResult.Success -> { + val data = result.data + val totalViews = data.visits.sumOf { it.visits } + val totalVisitors = data.visitors.sumOf { it.visitors } + val totalLikes = data.likes.sumOf { it.likes } + val totalComments = data.comments.sumOf { it.comments } + val totalPosts = data.posts.sumOf { it.posts } val startDateFormatted = getDateFormat().format(startDate.time) @@ -237,14 +167,9 @@ class StatsRepository @Inject constructor( WeeklyStatsResult.Success(aggregates) } - is WpRequestResult.WpError -> { - appLogWrapper.e(AppLog.T.STATS, "API Error fetching weekly stats: ${result.errorMessage}") - WeeklyStatsResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching weekly stats") - WeeklyStatsResult.Error("Unknown error") + is StatsVisitsDataResult.Error -> { + appLogWrapper.e(AppLog.T.STATS, "API Error fetching weekly stats: ${result.message}") + WeeklyStatsResult.Error(result.message) } } } @@ -258,44 +183,27 @@ class StatsRepository @Inject constructor( */ suspend fun fetchDailyViewsForWeek(siteId: Long, weeksAgo: Int = 0): DailyViewsResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext DailyViewsResult.Error("Repository not initialized") - } - val (_, endDate) = calculateWeekDateRange(weeksAgo) val endDateString = getDateFormat().format(endDate.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.DAY, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.DAY, quantity = WEEKLY_QUANTITY, - endDate = endDateString, + endDate = endDateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val dataPoints = response.statsVisitsData().map { dataPoint -> - DailyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits.toLong()) + is StatsVisitsDataResult.Success -> { + val dataPoints = result.data.visits.map { dataPoint -> + DailyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits) } DailyViewsResult.Success(dataPoints) } - is WpRequestResult.WpError -> { - appLogWrapper.e(AppLog.T.STATS, "API Error fetching daily views: ${result.errorMessage}") - DailyViewsResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching daily views") - DailyViewsResult.Error("Unknown error") + is StatsVisitsDataResult.Error -> { + appLogWrapper.e(AppLog.T.STATS, "API Error fetching daily views: ${result.message}") + DailyViewsResult.Error(result.message) } } } @@ -312,42 +220,26 @@ class StatsRepository @Inject constructor( siteId: Long, weeksAgo: Int = 0 ): WeeklyStatsWithDailyDataResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext WeeklyStatsWithDailyDataResult.Error("Repository not initialized") - } - val (startDate, endDate) = calculateWeekDateRange(weeksAgo) val endDateString = getDateFormat().format(endDate.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.DAY, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.DAY, quantity = WEEKLY_QUANTITY, - endDate = endDateString, + endDate = endDateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val visitsData = response.statsVisitsData() - val visitorsData = response.statsVisitorsData() - val likesData = response.statsLikesData() - val commentsData = response.statsCommentsData() - val postsData = response.statsPostsData() + is StatsVisitsDataResult.Success -> { + val data = result.data // Build aggregates - val totalViews = visitsData.sumOf { it.visits.toLong() } - val totalVisitors = visitorsData.sumOf { it.visitors.toLong() } - val totalLikes = likesData.sumOf { it.likes.toLong() } - val totalComments = commentsData.sumOf { it.comments.toLong() } - val totalPosts = postsData.sumOf { it.posts.toLong() } + val totalViews = data.visits.sumOf { it.visits } + val totalVisitors = data.visitors.sumOf { it.visitors } + val totalLikes = data.likes.sumOf { it.likes } + val totalComments = data.comments.sumOf { it.comments } + val totalPosts = data.posts.sumOf { it.posts } val startDateFormatted = getDateFormat().format(startDate.time) val aggregates = WeeklyAggregates( @@ -361,24 +253,19 @@ class StatsRepository @Inject constructor( ) // Build daily data points - val dailyDataPoints = visitsData.map { dataPoint -> - DailyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits.toLong()) + val dailyDataPoints = data.visits.map { dataPoint -> + DailyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits) } WeeklyStatsWithDailyDataResult.Success(aggregates, dailyDataPoints) } - is WpRequestResult.WpError -> { + is StatsVisitsDataResult.Error -> { appLogWrapper.e( AppLog.T.STATS, - "API Error fetching weekly stats with daily data: ${result.errorMessage}" + "API Error fetching weekly stats with daily data: ${result.message}" ) - WeeklyStatsWithDailyDataResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching weekly stats with daily data") - WeeklyStatsWithDailyDataResult.Error("Unknown error") + WeeklyStatsWithDailyDataResult.Error(result.message) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCard.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCard.kt rename to WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCard.kt index 9875394edb0b..17963b45328f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCard.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaysstat +package org.wordpress.android.ui.newstats.todaystats import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCardUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCardUiState.kt similarity index 94% rename from WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCardUiState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCardUiState.kt index 06fb7d1dd293..725e70968dcc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCardUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCardUiState.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaysstat +package org.wordpress.android.ui.newstats.todaystats /** * UI State for the Today's Stats card in the new stats screen. diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModel.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModel.kt index 55739425a0f5..a18cf844d2ef 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModel.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaysstat +package org.wordpress.android.ui.newstats.todaystats import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/WordPress/src/test/java/org/wordpress/android/ui/newstats/repository/StatsRepositoryTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/newstats/repository/StatsRepositoryTest.kt new file mode 100644 index 000000000000..3076798dc9f6 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/newstats/repository/StatsRepositoryTest.kt @@ -0,0 +1,425 @@ +package org.wordpress.android.ui.newstats.repository + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.wordpress.android.ui.newstats.datasource.CommentsDataPoint +import org.wordpress.android.ui.newstats.datasource.LikesDataPoint +import org.wordpress.android.ui.newstats.datasource.PostsDataPoint +import org.wordpress.android.ui.newstats.datasource.StatsDataSource +import org.wordpress.android.ui.newstats.datasource.StatsUnit +import org.wordpress.android.ui.newstats.datasource.StatsVisitsData +import org.wordpress.android.ui.newstats.datasource.StatsVisitsDataResult +import org.wordpress.android.ui.newstats.datasource.VisitorsDataPoint +import org.wordpress.android.ui.newstats.datasource.VisitsDataPoint +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.utils.AppLogWrapper + +@ExperimentalCoroutinesApi +class StatsRepositoryTest : BaseUnitTest() { + @Mock + private lateinit var statsDataSource: StatsDataSource + + @Mock + private lateinit var appLogWrapper: AppLogWrapper + + private lateinit var repository: StatsRepository + + @Before + fun setUp() { + repository = StatsRepository( + statsDataSource = statsDataSource, + appLogWrapper = appLogWrapper, + ioDispatcher = testDispatcher() + ) + } + + // region init + @Test + fun `when init is called, then data source is initialized with access token`() { + repository.init(TEST_ACCESS_TOKEN) + + verify(statsDataSource).init(eq(TEST_ACCESS_TOKEN)) + } + // endregion + + // region fetchTodayAggregates + @Test + fun `given successful response, when fetchTodayAggregates is called, then success result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createStatsVisitsData())) + + val result = repository.fetchTodayAggregates(TEST_SITE_ID) + + assertThat(result).isInstanceOf(TodayAggregatesResult.Success::class.java) + val success = result as TodayAggregatesResult.Success + assertThat(success.aggregates.views).isEqualTo(TEST_VIEWS) + assertThat(success.aggregates.visitors).isEqualTo(TEST_VISITORS) + assertThat(success.aggregates.likes).isEqualTo(TEST_LIKES) + assertThat(success.aggregates.comments).isEqualTo(TEST_COMMENTS) + } + + @Test + fun `given successful response, when fetchTodayAggregates is called, then data source is called with DAY unit`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createStatsVisitsData())) + + repository.fetchTodayAggregates(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(1), + endDate = any() + ) + } + + @Test + fun `given empty data, when fetchTodayAggregates is called, then zeros are returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createEmptyStatsVisitsData())) + + val result = repository.fetchTodayAggregates(TEST_SITE_ID) + + assertThat(result).isInstanceOf(TodayAggregatesResult.Success::class.java) + val success = result as TodayAggregatesResult.Success + assertThat(success.aggregates.views).isEqualTo(0L) + assertThat(success.aggregates.visitors).isEqualTo(0L) + assertThat(success.aggregates.likes).isEqualTo(0L) + assertThat(success.aggregates.comments).isEqualTo(0L) + } + + @Test + fun `given error response, when fetchTodayAggregates is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchTodayAggregates(TEST_SITE_ID) + + assertThat(result).isInstanceOf(TodayAggregatesResult.Error::class.java) + assertThat((result as TodayAggregatesResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + // endregion + + // region fetchHourlyViews + @Test + fun `given successful response, when fetchHourlyViews is called, then success result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createHourlyStatsVisitsData())) + + val result = repository.fetchHourlyViews(TEST_SITE_ID) + + assertThat(result).isInstanceOf(HourlyViewsResult.Success::class.java) + val success = result as HourlyViewsResult.Success + assertThat(success.dataPoints).hasSize(2) + assertThat(success.dataPoints[0].period).isEqualTo(TEST_PERIOD_1) + assertThat(success.dataPoints[0].views).isEqualTo(TEST_VIEWS_1) + assertThat(success.dataPoints[1].period).isEqualTo(TEST_PERIOD_2) + assertThat(success.dataPoints[1].views).isEqualTo(TEST_VIEWS_2) + } + + @Test + fun `given successful response, when fetchHourlyViews is called, then data source is called with HOUR unit`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createHourlyStatsVisitsData())) + + repository.fetchHourlyViews(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.HOUR), + quantity = eq(24), + endDate = any() + ) + } + + @Test + fun `given error response, when fetchHourlyViews is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchHourlyViews(TEST_SITE_ID) + + assertThat(result).isInstanceOf(HourlyViewsResult.Error::class.java) + assertThat((result as HourlyViewsResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + + @Test + fun `given offset days, when fetchHourlyViews is called, then data source is called`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createHourlyStatsVisitsData())) + + repository.fetchHourlyViews(TEST_SITE_ID, offsetDays = 1) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.HOUR), + quantity = eq(24), + endDate = any() + ) + } + // endregion + + // region fetchWeeklyStats + @Test + fun `given successful response, when fetchWeeklyStats is called, then success result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + val result = repository.fetchWeeklyStats(TEST_SITE_ID) + + assertThat(result).isInstanceOf(WeeklyStatsResult.Success::class.java) + val success = result as WeeklyStatsResult.Success + assertThat(success.aggregates.views).isEqualTo(TEST_VIEWS_1 + TEST_VIEWS_2) + assertThat(success.aggregates.visitors).isEqualTo(TEST_VISITORS_1 + TEST_VISITORS_2) + assertThat(success.aggregates.likes).isEqualTo(TEST_LIKES_1 + TEST_LIKES_2) + assertThat(success.aggregates.comments).isEqualTo(TEST_COMMENTS_1 + TEST_COMMENTS_2) + assertThat(success.aggregates.posts).isEqualTo(TEST_POSTS_1 + TEST_POSTS_2) + } + + @Test + fun `given successful response, when fetchWeeklyStats is called, then data source is called with DAY unit`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchWeeklyStats(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + + @Test + fun `given error response, when fetchWeeklyStats is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchWeeklyStats(TEST_SITE_ID) + + assertThat(result).isInstanceOf(WeeklyStatsResult.Error::class.java) + assertThat((result as WeeklyStatsResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + + @Test + fun `given weeks ago parameter, when fetchWeeklyStats is called, then data source is called`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchWeeklyStats(TEST_SITE_ID, weeksAgo = 1) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + // endregion + + // region fetchDailyViewsForWeek + @Test + fun `given successful response, when fetchDailyViewsForWeek is called, then success result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + val result = repository.fetchDailyViewsForWeek(TEST_SITE_ID) + + assertThat(result).isInstanceOf(DailyViewsResult.Success::class.java) + val success = result as DailyViewsResult.Success + assertThat(success.dataPoints).hasSize(2) + assertThat(success.dataPoints[0].period).isEqualTo(TEST_PERIOD_1) + assertThat(success.dataPoints[0].views).isEqualTo(TEST_VIEWS_1) + assertThat(success.dataPoints[1].period).isEqualTo(TEST_PERIOD_2) + assertThat(success.dataPoints[1].views).isEqualTo(TEST_VIEWS_2) + } + + @Test + fun `given successful response, when fetchDailyViewsForWeek is called, then data source is called with DAY unit`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchDailyViewsForWeek(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + + @Test + fun `given error response, when fetchDailyViewsForWeek is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchDailyViewsForWeek(TEST_SITE_ID) + + assertThat(result).isInstanceOf(DailyViewsResult.Error::class.java) + assertThat((result as DailyViewsResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + // endregion + + // region fetchWeeklyStatsWithDailyData + @Test + fun `given successful response, when fetchWeeklyStatsWithDailyData is called, then success result is returned`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + val result = repository.fetchWeeklyStatsWithDailyData(TEST_SITE_ID) + + assertThat(result).isInstanceOf(WeeklyStatsWithDailyDataResult.Success::class.java) + val success = result as WeeklyStatsWithDailyDataResult.Success + + // Verify aggregates + assertThat(success.aggregates.views).isEqualTo(TEST_VIEWS_1 + TEST_VIEWS_2) + assertThat(success.aggregates.visitors).isEqualTo(TEST_VISITORS_1 + TEST_VISITORS_2) + assertThat(success.aggregates.likes).isEqualTo(TEST_LIKES_1 + TEST_LIKES_2) + assertThat(success.aggregates.comments).isEqualTo(TEST_COMMENTS_1 + TEST_COMMENTS_2) + assertThat(success.aggregates.posts).isEqualTo(TEST_POSTS_1 + TEST_POSTS_2) + + // Verify daily data points + assertThat(success.dailyDataPoints).hasSize(2) + assertThat(success.dailyDataPoints[0].period).isEqualTo(TEST_PERIOD_1) + assertThat(success.dailyDataPoints[0].views).isEqualTo(TEST_VIEWS_1) + } + + @Test + fun `given successful response, when fetchWeeklyStatsWithDailyData is called, data source is called correctly`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchWeeklyStatsWithDailyData(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + + @Test + fun `given error response, when fetchWeeklyStatsWithDailyData is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchWeeklyStatsWithDailyData(TEST_SITE_ID) + + assertThat(result).isInstanceOf(WeeklyStatsWithDailyDataResult.Error::class.java) + assertThat((result as WeeklyStatsWithDailyDataResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + + @Test + fun `given weeks ago parameter, when fetchWeeklyStatsWithDailyData is called, then data source is called`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchWeeklyStatsWithDailyData(TEST_SITE_ID, weeksAgo = 2) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + // endregion + + // region Helper functions + private fun createStatsVisitsData() = StatsVisitsData( + visits = listOf(VisitsDataPoint(TEST_PERIOD_1, TEST_VIEWS)), + visitors = listOf(VisitorsDataPoint(TEST_PERIOD_1, TEST_VISITORS)), + likes = listOf(LikesDataPoint(TEST_PERIOD_1, TEST_LIKES)), + comments = listOf(CommentsDataPoint(TEST_PERIOD_1, TEST_COMMENTS)), + posts = listOf(PostsDataPoint(TEST_PERIOD_1, TEST_POSTS)) + ) + + private fun createEmptyStatsVisitsData() = StatsVisitsData( + visits = emptyList(), + visitors = emptyList(), + likes = emptyList(), + comments = emptyList(), + posts = emptyList() + ) + + private fun createHourlyStatsVisitsData() = StatsVisitsData( + visits = listOf( + VisitsDataPoint(TEST_PERIOD_1, TEST_VIEWS_1), + VisitsDataPoint(TEST_PERIOD_2, TEST_VIEWS_2) + ), + visitors = listOf( + VisitorsDataPoint(TEST_PERIOD_1, TEST_VISITORS_1), + VisitorsDataPoint(TEST_PERIOD_2, TEST_VISITORS_2) + ), + likes = emptyList(), + comments = emptyList(), + posts = emptyList() + ) + + private fun createWeeklyStatsVisitsData() = StatsVisitsData( + visits = listOf( + VisitsDataPoint(TEST_PERIOD_1, TEST_VIEWS_1), + VisitsDataPoint(TEST_PERIOD_2, TEST_VIEWS_2) + ), + visitors = listOf( + VisitorsDataPoint(TEST_PERIOD_1, TEST_VISITORS_1), + VisitorsDataPoint(TEST_PERIOD_2, TEST_VISITORS_2) + ), + likes = listOf( + LikesDataPoint(TEST_PERIOD_1, TEST_LIKES_1), + LikesDataPoint(TEST_PERIOD_2, TEST_LIKES_2) + ), + comments = listOf( + CommentsDataPoint(TEST_PERIOD_1, TEST_COMMENTS_1), + CommentsDataPoint(TEST_PERIOD_2, TEST_COMMENTS_2) + ), + posts = listOf( + PostsDataPoint(TEST_PERIOD_1, TEST_POSTS_1), + PostsDataPoint(TEST_PERIOD_2, TEST_POSTS_2) + ) + ) + // endregion + + companion object { + private const val TEST_SITE_ID = 123L + private const val TEST_ACCESS_TOKEN = "test_access_token" + private const val ERROR_MESSAGE = "Test error message" + + private const val TEST_PERIOD_1 = "2024-01-15" + private const val TEST_PERIOD_2 = "2024-01-16" + + private const val TEST_VIEWS = 500L + private const val TEST_VISITORS = 100L + private const val TEST_LIKES = 50L + private const val TEST_COMMENTS = 25L + private const val TEST_POSTS = 5L + + private const val TEST_VIEWS_1 = 100L + private const val TEST_VIEWS_2 = 150L + private const val TEST_VISITORS_1 = 50L + private const val TEST_VISITORS_2 = 75L + private const val TEST_LIKES_1 = 10L + private const val TEST_LIKES_2 = 15L + private const val TEST_COMMENTS_1 = 5L + private const val TEST_COMMENTS_2 = 8L + private const val TEST_POSTS_1 = 2L + private const val TEST_POSTS_2 = 3L + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModelTest.kt similarity index 99% rename from WordPress/src/test/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModelTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModelTest.kt index 93254e765c0d..68cd6d3c11a8 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModelTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaysstat +package org.wordpress.android.ui.newstats.todaystats import kotlinx.coroutines.ExperimentalCoroutinesApi import org.assertj.core.api.Assertions.assertThat From b3693223f2ab4305201500645c592654ec0f7e36 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 22 Jan 2026 10:44:39 +0100 Subject: [PATCH 2/5] Add StatsDataSource and refactor StatsRepository - Create StatsDataSource interface and implementation to abstract data fetching - Add StatsModule for dependency injection - Refactor StatsRepository to use the new data source - Rename todaysstat -> todaystats package in newstats - Add StatsRepositoryTest with comprehensive test coverage --- .../wordpress/android/modules/StatsModule.kt | 15 + .../android/ui/newstats/NewStatsActivity.kt | 4 +- .../ui/newstats/datasource/StatsDataSource.kt | 96 ++++ .../datasource/StatsDataSourceImpl.kt | 85 ++++ .../ui/newstats/repository/StatsRepository.kt | 255 +++-------- .../TodaysStatsCard.kt | 2 +- .../TodaysStatsCardUiState.kt | 2 +- .../TodaysStatsViewModel.kt | 2 +- .../repository/StatsRepositoryTest.kt | 425 ++++++++++++++++++ .../TodaysStatsViewModelTest.kt | 2 +- 10 files changed, 698 insertions(+), 190 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/modules/StatsModule.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSource.kt create mode 100644 WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt rename WordPress/src/main/java/org/wordpress/android/ui/newstats/{todaysstat => todaystats}/TodaysStatsCard.kt (99%) rename WordPress/src/main/java/org/wordpress/android/ui/newstats/{todaysstat => todaystats}/TodaysStatsCardUiState.kt (94%) rename WordPress/src/main/java/org/wordpress/android/ui/newstats/{todaysstat => todaystats}/TodaysStatsViewModel.kt (99%) create mode 100644 WordPress/src/test/java/org/wordpress/android/ui/newstats/repository/StatsRepositoryTest.kt rename WordPress/src/test/java/org/wordpress/android/ui/newstats/{todaysstat => todaystats}/TodaysStatsViewModelTest.kt (99%) diff --git a/WordPress/src/main/java/org/wordpress/android/modules/StatsModule.kt b/WordPress/src/main/java/org/wordpress/android/modules/StatsModule.kt new file mode 100644 index 000000000000..8216a860e935 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/modules/StatsModule.kt @@ -0,0 +1,15 @@ +package org.wordpress.android.modules + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.wordpress.android.ui.newstats.datasource.StatsDataSource +import org.wordpress.android.ui.newstats.datasource.StatsDataSourceImpl + +@InstallIn(SingletonComponent::class) +@Module +abstract class StatsModule { + @Binds + abstract fun bindStatsDataSource(impl: StatsDataSourceImpl): StatsDataSource +} 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 1b7c45f12c3b..eb87e12e0ca5 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 @@ -40,8 +40,8 @@ import kotlinx.coroutines.launch import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppThemeM3 import org.wordpress.android.ui.main.BaseAppCompatActivity -import org.wordpress.android.ui.newstats.todaysstat.TodaysStatsCard -import org.wordpress.android.ui.newstats.todaysstat.TodaysStatsViewModel +import org.wordpress.android.ui.newstats.todaystats.TodaysStatsCard +import org.wordpress.android.ui.newstats.todaystats.TodaysStatsViewModel import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsCard import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsViewModel diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSource.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSource.kt new file mode 100644 index 000000000000..d8c0b0edf81f --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSource.kt @@ -0,0 +1,96 @@ +package org.wordpress.android.ui.newstats.datasource + +/** + * Data source interface for fetching stats data. + * This abstraction allows mocking the data layer in tests without needing access to uniffi objects. + */ +interface StatsDataSource { + /** + * Initializes the data source with the access token. + */ + fun init(accessToken: String) + + /** + * Fetches stats data for a specific site. + * + * @param siteId The WordPress.com site ID + * @param unit The time unit for the stats (HOUR, DAY, etc.) + * @param quantity The number of data points to fetch + * @param endDate The end date for the stats period (format: yyyy-MM-dd) + * @return Result containing the stats data or an error + */ + suspend fun fetchStatsVisits( + siteId: Long, + unit: StatsUnit, + quantity: Int, + endDate: String + ): StatsVisitsDataResult +} + +/** + * Time unit for stats data. + */ +enum class StatsUnit { + HOUR, + DAY +} + +/** + * Result wrapper for stats visits fetch operation. + */ +sealed class StatsVisitsDataResult { + data class Success(val data: StatsVisitsData) : StatsVisitsDataResult() + data class Error(val message: String) : StatsVisitsDataResult() +} + +/** + * Stats visits data from the API. + * Contains all the data points for views, visitors, likes, comments, and posts. + */ +data class StatsVisitsData( + val visits: List, + val visitors: List, + val likes: List, + val comments: List, + val posts: List +) + +/** + * Data point for visits/views. + */ +data class VisitsDataPoint( + val period: String, + val visits: Long +) + +/** + * Data point for visitors. + */ +data class VisitorsDataPoint( + val period: String, + val visitors: Long +) + +/** + * Data point for likes. + */ +data class LikesDataPoint( + val period: String, + val likes: Long +) + +/** + * Data point for comments. + */ +data class CommentsDataPoint( + val period: String, + val comments: Long +) + +/** + * Data point for posts. + */ +data class PostsDataPoint( + val period: String, + val posts: Long +) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt new file mode 100644 index 000000000000..d93eda1e5847 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt @@ -0,0 +1,85 @@ +package org.wordpress.android.ui.newstats.datasource + +import org.wordpress.android.networking.restapi.WpComApiClientProvider +import org.wordpress.android.ui.newstats.extension.statsCommentsData +import org.wordpress.android.ui.newstats.extension.statsLikesData +import org.wordpress.android.ui.newstats.extension.statsPostsData +import org.wordpress.android.ui.newstats.extension.statsVisitorsData +import org.wordpress.android.ui.newstats.extension.statsVisitsData +import rs.wordpress.api.kotlin.WpComApiClient +import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.StatsVisitsParams +import uniffi.wp_api.StatsVisitsUnit +import javax.inject.Inject + +/** + * Implementation of [StatsDataSource] that fetches stats data from the WordPress.com API + * using the wordpress-rs library. + */ +class StatsDataSourceImpl @Inject constructor( + private val wpComApiClientProvider: WpComApiClientProvider +) : StatsDataSource { + /** + * Access token for API authentication. + * Marked as @Volatile to ensure visibility across threads since this data source is accessed + * from multiple coroutine contexts. + */ + @Volatile + private var accessToken: String? = null + + private val wpComApiClient: WpComApiClient by lazy { + check(accessToken != null) { "DataSource not initialized" } + wpComApiClientProvider.getWpComApiClient(accessToken!!) + } + + override fun init(accessToken: String) { + this.accessToken = accessToken + } + + override suspend fun fetchStatsVisits( + siteId: Long, + unit: StatsUnit, + quantity: Int, + endDate: String + ): StatsVisitsDataResult { + if (accessToken == null) { + return StatsVisitsDataResult.Error("DataSource not initialized") + } + + val params = StatsVisitsParams( + unit = unit.toApiUnit(), + quantity = quantity.toUInt(), + endDate = endDate + ) + + val result = wpComApiClient.request { requestBuilder -> + requestBuilder.statsVisits().getStatsVisits( + wpComSiteId = siteId.toULong(), + params = params + ) + } + + return when (result) { + is WpRequestResult.Success -> { + StatsVisitsDataResult.Success(mapResponseToStatsVisitsData(result.response.data)) + } + is WpRequestResult.WpError -> StatsVisitsDataResult.Error(result.errorMessage) + else -> StatsVisitsDataResult.Error("Unknown error") + } + } + + private fun mapResponseToStatsVisitsData( + response: uniffi.wp_api.StatsVisitsResponse + ): StatsVisitsData = StatsVisitsData( + visits = response.statsVisitsData().map { VisitsDataPoint(it.period, it.visits.toLong()) }, + visitors = response.statsVisitorsData().map { VisitorsDataPoint(it.period, it.visitors.toLong()) }, + likes = response.statsLikesData().map { LikesDataPoint(it.period, it.likes.toLong()) }, + comments = response.statsCommentsData().map { CommentsDataPoint(it.period, it.comments.toLong()) }, + posts = response.statsPostsData().map { PostsDataPoint(it.period, it.posts.toLong()) } + ) + + private fun StatsUnit.toApiUnit(): StatsVisitsUnit = when (this) { + StatsUnit.HOUR -> StatsVisitsUnit.HOUR + StatsUnit.DAY -> StatsVisitsUnit.DAY + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsRepository.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsRepository.kt index 24da50d9278b..dfe74fcc85ea 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsRepository.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/repository/StatsRepository.kt @@ -1,29 +1,22 @@ package org.wordpress.android.ui.newstats.repository import kotlinx.coroutines.CoroutineDispatcher +import org.wordpress.android.ui.newstats.datasource.StatsDataSource +import org.wordpress.android.ui.newstats.datasource.StatsUnit +import org.wordpress.android.ui.newstats.datasource.StatsVisitsDataResult import kotlinx.coroutines.withContext import org.wordpress.android.fluxc.utils.AppLogWrapper import org.wordpress.android.modules.IO_THREAD -import org.wordpress.android.networking.restapi.WpComApiClientProvider import org.wordpress.android.util.AppLog -import org.wordpress.android.ui.newstats.extension.statsCommentsData -import org.wordpress.android.ui.newstats.extension.statsLikesData -import org.wordpress.android.ui.newstats.extension.statsPostsData -import org.wordpress.android.ui.newstats.extension.statsVisitorsData -import org.wordpress.android.ui.newstats.extension.statsVisitsData -import rs.wordpress.api.kotlin.WpComApiClient -import rs.wordpress.api.kotlin.WpRequestResult -import uniffi.wp_api.StatsVisitsParams -import uniffi.wp_api.StatsVisitsUnit import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale import javax.inject.Inject import javax.inject.Named -private const val HOURLY_QUANTITY = 24u -private const val DAILY_QUANTITY = 1u -private const val WEEKLY_QUANTITY = 7u +private const val HOURLY_QUANTITY = 24 +private const val DAILY_QUANTITY = 1 +private const val WEEKLY_QUANTITY = 7 private const val DAYS_BEFORE_END_DATE = -6 /** @@ -31,23 +24,10 @@ private const val DAYS_BEFORE_END_DATE = -6 * Handles hourly visits/views data for the Today's Stats card chart. */ class StatsRepository @Inject constructor( - private val wpComApiClientProvider: WpComApiClientProvider, + private val statsDataSource: StatsDataSource, private val appLogWrapper: AppLogWrapper, @Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher, ) { - /** - * Access token for API authentication. - * Marked as @Volatile to ensure visibility across threads since this repository is accessed - * from multiple coroutine contexts (main thread initialization, IO dispatcher for API calls). - */ - @Volatile - private var accessToken: String? = null - - private val wpComApiClient: WpComApiClient by lazy { - check(accessToken != null) { "Repository not initialized" } - wpComApiClientProvider.getWpComApiClient(accessToken!!) - } - /** * Thread-local date formatter for thread-safe date formatting. * SimpleDateFormat is NOT thread-safe, so we use ThreadLocal to provide each thread @@ -60,7 +40,7 @@ class StatsRepository @Inject constructor( private fun getDateFormat(): SimpleDateFormat = dateFormat.get()!! fun init(accessToken: String) { - this.accessToken = accessToken + statsDataSource.init(accessToken) } /** @@ -70,34 +50,23 @@ class StatsRepository @Inject constructor( * @return Today's aggregated stats or error */ suspend fun fetchTodayAggregates(siteId: Long): TodayAggregatesResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext TodayAggregatesResult.Error("Repository not initialized") - } - val calendar = Calendar.getInstance() val dateString = getDateFormat().format(calendar.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.DAY, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.DAY, quantity = DAILY_QUANTITY, - endDate = dateString, + endDate = dateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val views = response.statsVisitsData().firstOrNull()?.visits?.toLong() ?: 0L - val visitors = response.statsVisitorsData().firstOrNull()?.visitors?.toLong() ?: 0L - val likes = response.statsLikesData().firstOrNull()?.likes?.toLong() ?: 0L - val comments = response.statsCommentsData().firstOrNull()?.comments?.toLong() ?: 0L + is StatsVisitsDataResult.Success -> { + val data = result.data + val views = data.visits.firstOrNull()?.visits ?: 0L + val visitors = data.visitors.firstOrNull()?.visitors ?: 0L + val likes = data.likes.firstOrNull()?.likes ?: 0L + val comments = data.comments.firstOrNull()?.comments ?: 0L val aggregates = TodayAggregates( views = views, @@ -108,14 +77,9 @@ class StatsRepository @Inject constructor( TodayAggregatesResult.Success(aggregates) } - is WpRequestResult.WpError -> { - appLogWrapper.e(AppLog.T.STATS, "API Error fetching today aggregates: ${result.errorMessage}") - TodayAggregatesResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching today aggregates") - TodayAggregatesResult.Error("Unknown error") + is StatsVisitsDataResult.Error -> { + appLogWrapper.e(AppLog.T.STATS, "API Error fetching today aggregates: ${result.message}") + TodayAggregatesResult.Error(result.message) } } } @@ -131,11 +95,6 @@ class StatsRepository @Inject constructor( siteId: Long, offsetDays: Int = 0 ): HourlyViewsResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext HourlyViewsResult.Error("Repository not initialized") - } - val calendar = Calendar.getInstance() // The API's endDate is exclusive for hourly queries, so we need to add 1 day to get // the target day's hours. Formula: 1 (for exclusive end) - offsetDays (0=today, 1=yesterday) @@ -144,36 +103,24 @@ class StatsRepository @Inject constructor( calendar.add(Calendar.DAY_OF_YEAR, 1 - offsetDays) val dateString = getDateFormat().format(calendar.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.HOUR, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.HOUR, quantity = HOURLY_QUANTITY, - endDate = dateString, + endDate = dateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val dataPoints = response.statsVisitsData().map { dataPoint -> - HourlyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits.toLong()) + is StatsVisitsDataResult.Success -> { + val dataPoints = result.data.visits.map { dataPoint -> + HourlyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits) } HourlyViewsResult.Success(dataPoints) } - is WpRequestResult.WpError -> { - appLogWrapper.e(AppLog.T.STATS, "API Error fetching hourly views: ${result.errorMessage}") - HourlyViewsResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching hourly views") - HourlyViewsResult.Error("Unknown error") + is StatsVisitsDataResult.Error -> { + appLogWrapper.e(AppLog.T.STATS, "API Error fetching hourly views: ${result.message}") + HourlyViewsResult.Error(result.message) } } } @@ -187,41 +134,24 @@ class StatsRepository @Inject constructor( */ suspend fun fetchWeeklyStats(siteId: Long, weeksAgo: Int = 0): WeeklyStatsResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext WeeklyStatsResult.Error("Repository not initialized") - } - val (startDate, endDate) = calculateWeekDateRange(weeksAgo) val endDateString = getDateFormat().format(endDate.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.DAY, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.DAY, quantity = WEEKLY_QUANTITY, - endDate = endDateString, + endDate = endDateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val visitsData = response.statsVisitsData() - val visitorsData = response.statsVisitorsData() - val likesData = response.statsLikesData() - val commentsData = response.statsCommentsData() - val postsData = response.statsPostsData() - - val totalViews = visitsData.sumOf { it.visits.toLong() } - val totalVisitors = visitorsData.sumOf { it.visitors.toLong() } - val totalLikes = likesData.sumOf { it.likes.toLong() } - val totalComments = commentsData.sumOf { it.comments.toLong() } - val totalPosts = postsData.sumOf { it.posts.toLong() } + is StatsVisitsDataResult.Success -> { + val data = result.data + val totalViews = data.visits.sumOf { it.visits } + val totalVisitors = data.visitors.sumOf { it.visitors } + val totalLikes = data.likes.sumOf { it.likes } + val totalComments = data.comments.sumOf { it.comments } + val totalPosts = data.posts.sumOf { it.posts } val startDateFormatted = getDateFormat().format(startDate.time) @@ -237,14 +167,9 @@ class StatsRepository @Inject constructor( WeeklyStatsResult.Success(aggregates) } - is WpRequestResult.WpError -> { - appLogWrapper.e(AppLog.T.STATS, "API Error fetching weekly stats: ${result.errorMessage}") - WeeklyStatsResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching weekly stats") - WeeklyStatsResult.Error("Unknown error") + is StatsVisitsDataResult.Error -> { + appLogWrapper.e(AppLog.T.STATS, "API Error fetching weekly stats: ${result.message}") + WeeklyStatsResult.Error(result.message) } } } @@ -258,44 +183,27 @@ class StatsRepository @Inject constructor( */ suspend fun fetchDailyViewsForWeek(siteId: Long, weeksAgo: Int = 0): DailyViewsResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext DailyViewsResult.Error("Repository not initialized") - } - val (_, endDate) = calculateWeekDateRange(weeksAgo) val endDateString = getDateFormat().format(endDate.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.DAY, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.DAY, quantity = WEEKLY_QUANTITY, - endDate = endDateString, + endDate = endDateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val dataPoints = response.statsVisitsData().map { dataPoint -> - DailyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits.toLong()) + is StatsVisitsDataResult.Success -> { + val dataPoints = result.data.visits.map { dataPoint -> + DailyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits) } DailyViewsResult.Success(dataPoints) } - is WpRequestResult.WpError -> { - appLogWrapper.e(AppLog.T.STATS, "API Error fetching daily views: ${result.errorMessage}") - DailyViewsResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching daily views") - DailyViewsResult.Error("Unknown error") + is StatsVisitsDataResult.Error -> { + appLogWrapper.e(AppLog.T.STATS, "API Error fetching daily views: ${result.message}") + DailyViewsResult.Error(result.message) } } } @@ -312,42 +220,26 @@ class StatsRepository @Inject constructor( siteId: Long, weeksAgo: Int = 0 ): WeeklyStatsWithDailyDataResult = withContext(ioDispatcher) { - if (accessToken == null) { - appLogWrapper.e(AppLog.T.STATS, "Cannot fetch stats: repository not initialized") - return@withContext WeeklyStatsWithDailyDataResult.Error("Repository not initialized") - } - val (startDate, endDate) = calculateWeekDateRange(weeksAgo) val endDateString = getDateFormat().format(endDate.time) - val params = StatsVisitsParams( - unit = StatsVisitsUnit.DAY, + val result = statsDataSource.fetchStatsVisits( + siteId = siteId, + unit = StatsUnit.DAY, quantity = WEEKLY_QUANTITY, - endDate = endDateString, + endDate = endDateString ) - val result = wpComApiClient.request { requestBuilder -> - requestBuilder.statsVisits().getStatsVisits( - wpComSiteId = siteId.toULong(), - params = params - ) - } - when (result) { - is WpRequestResult.Success -> { - val response = result.response.data - val visitsData = response.statsVisitsData() - val visitorsData = response.statsVisitorsData() - val likesData = response.statsLikesData() - val commentsData = response.statsCommentsData() - val postsData = response.statsPostsData() + is StatsVisitsDataResult.Success -> { + val data = result.data // Build aggregates - val totalViews = visitsData.sumOf { it.visits.toLong() } - val totalVisitors = visitorsData.sumOf { it.visitors.toLong() } - val totalLikes = likesData.sumOf { it.likes.toLong() } - val totalComments = commentsData.sumOf { it.comments.toLong() } - val totalPosts = postsData.sumOf { it.posts.toLong() } + val totalViews = data.visits.sumOf { it.visits } + val totalVisitors = data.visitors.sumOf { it.visitors } + val totalLikes = data.likes.sumOf { it.likes } + val totalComments = data.comments.sumOf { it.comments } + val totalPosts = data.posts.sumOf { it.posts } val startDateFormatted = getDateFormat().format(startDate.time) val aggregates = WeeklyAggregates( @@ -361,24 +253,19 @@ class StatsRepository @Inject constructor( ) // Build daily data points - val dailyDataPoints = visitsData.map { dataPoint -> - DailyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits.toLong()) + val dailyDataPoints = data.visits.map { dataPoint -> + DailyViewsDataPoint(period = dataPoint.period, views = dataPoint.visits) } WeeklyStatsWithDailyDataResult.Success(aggregates, dailyDataPoints) } - is WpRequestResult.WpError -> { + is StatsVisitsDataResult.Error -> { appLogWrapper.e( AppLog.T.STATS, - "API Error fetching weekly stats with daily data: ${result.errorMessage}" + "API Error fetching weekly stats with daily data: ${result.message}" ) - WeeklyStatsWithDailyDataResult.Error(result.errorMessage) - } - - else -> { - appLogWrapper.e(AppLog.T.STATS, "Unknown error fetching weekly stats with daily data") - WeeklyStatsWithDailyDataResult.Error("Unknown error") + WeeklyStatsWithDailyDataResult.Error(result.message) } } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCard.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCard.kt rename to WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCard.kt index 9875394edb0b..17963b45328f 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCard.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaysstat +package org.wordpress.android.ui.newstats.todaystats import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCardUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCardUiState.kt similarity index 94% rename from WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCardUiState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCardUiState.kt index 06fb7d1dd293..725e70968dcc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsCardUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCardUiState.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaysstat +package org.wordpress.android.ui.newstats.todaystats /** * UI State for the Today's Stats card in the new stats screen. diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModel.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModel.kt index 55739425a0f5..a18cf844d2ef 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModel.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaysstat +package org.wordpress.android.ui.newstats.todaystats import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/WordPress/src/test/java/org/wordpress/android/ui/newstats/repository/StatsRepositoryTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/newstats/repository/StatsRepositoryTest.kt new file mode 100644 index 000000000000..3076798dc9f6 --- /dev/null +++ b/WordPress/src/test/java/org/wordpress/android/ui/newstats/repository/StatsRepositoryTest.kt @@ -0,0 +1,425 @@ +package org.wordpress.android.ui.newstats.repository + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.wordpress.android.ui.newstats.datasource.CommentsDataPoint +import org.wordpress.android.ui.newstats.datasource.LikesDataPoint +import org.wordpress.android.ui.newstats.datasource.PostsDataPoint +import org.wordpress.android.ui.newstats.datasource.StatsDataSource +import org.wordpress.android.ui.newstats.datasource.StatsUnit +import org.wordpress.android.ui.newstats.datasource.StatsVisitsData +import org.wordpress.android.ui.newstats.datasource.StatsVisitsDataResult +import org.wordpress.android.ui.newstats.datasource.VisitorsDataPoint +import org.wordpress.android.ui.newstats.datasource.VisitsDataPoint +import org.assertj.core.api.Assertions.assertThat +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.wordpress.android.BaseUnitTest +import org.wordpress.android.fluxc.utils.AppLogWrapper + +@ExperimentalCoroutinesApi +class StatsRepositoryTest : BaseUnitTest() { + @Mock + private lateinit var statsDataSource: StatsDataSource + + @Mock + private lateinit var appLogWrapper: AppLogWrapper + + private lateinit var repository: StatsRepository + + @Before + fun setUp() { + repository = StatsRepository( + statsDataSource = statsDataSource, + appLogWrapper = appLogWrapper, + ioDispatcher = testDispatcher() + ) + } + + // region init + @Test + fun `when init is called, then data source is initialized with access token`() { + repository.init(TEST_ACCESS_TOKEN) + + verify(statsDataSource).init(eq(TEST_ACCESS_TOKEN)) + } + // endregion + + // region fetchTodayAggregates + @Test + fun `given successful response, when fetchTodayAggregates is called, then success result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createStatsVisitsData())) + + val result = repository.fetchTodayAggregates(TEST_SITE_ID) + + assertThat(result).isInstanceOf(TodayAggregatesResult.Success::class.java) + val success = result as TodayAggregatesResult.Success + assertThat(success.aggregates.views).isEqualTo(TEST_VIEWS) + assertThat(success.aggregates.visitors).isEqualTo(TEST_VISITORS) + assertThat(success.aggregates.likes).isEqualTo(TEST_LIKES) + assertThat(success.aggregates.comments).isEqualTo(TEST_COMMENTS) + } + + @Test + fun `given successful response, when fetchTodayAggregates is called, then data source is called with DAY unit`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createStatsVisitsData())) + + repository.fetchTodayAggregates(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(1), + endDate = any() + ) + } + + @Test + fun `given empty data, when fetchTodayAggregates is called, then zeros are returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createEmptyStatsVisitsData())) + + val result = repository.fetchTodayAggregates(TEST_SITE_ID) + + assertThat(result).isInstanceOf(TodayAggregatesResult.Success::class.java) + val success = result as TodayAggregatesResult.Success + assertThat(success.aggregates.views).isEqualTo(0L) + assertThat(success.aggregates.visitors).isEqualTo(0L) + assertThat(success.aggregates.likes).isEqualTo(0L) + assertThat(success.aggregates.comments).isEqualTo(0L) + } + + @Test + fun `given error response, when fetchTodayAggregates is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchTodayAggregates(TEST_SITE_ID) + + assertThat(result).isInstanceOf(TodayAggregatesResult.Error::class.java) + assertThat((result as TodayAggregatesResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + // endregion + + // region fetchHourlyViews + @Test + fun `given successful response, when fetchHourlyViews is called, then success result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createHourlyStatsVisitsData())) + + val result = repository.fetchHourlyViews(TEST_SITE_ID) + + assertThat(result).isInstanceOf(HourlyViewsResult.Success::class.java) + val success = result as HourlyViewsResult.Success + assertThat(success.dataPoints).hasSize(2) + assertThat(success.dataPoints[0].period).isEqualTo(TEST_PERIOD_1) + assertThat(success.dataPoints[0].views).isEqualTo(TEST_VIEWS_1) + assertThat(success.dataPoints[1].period).isEqualTo(TEST_PERIOD_2) + assertThat(success.dataPoints[1].views).isEqualTo(TEST_VIEWS_2) + } + + @Test + fun `given successful response, when fetchHourlyViews is called, then data source is called with HOUR unit`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createHourlyStatsVisitsData())) + + repository.fetchHourlyViews(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.HOUR), + quantity = eq(24), + endDate = any() + ) + } + + @Test + fun `given error response, when fetchHourlyViews is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchHourlyViews(TEST_SITE_ID) + + assertThat(result).isInstanceOf(HourlyViewsResult.Error::class.java) + assertThat((result as HourlyViewsResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + + @Test + fun `given offset days, when fetchHourlyViews is called, then data source is called`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createHourlyStatsVisitsData())) + + repository.fetchHourlyViews(TEST_SITE_ID, offsetDays = 1) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.HOUR), + quantity = eq(24), + endDate = any() + ) + } + // endregion + + // region fetchWeeklyStats + @Test + fun `given successful response, when fetchWeeklyStats is called, then success result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + val result = repository.fetchWeeklyStats(TEST_SITE_ID) + + assertThat(result).isInstanceOf(WeeklyStatsResult.Success::class.java) + val success = result as WeeklyStatsResult.Success + assertThat(success.aggregates.views).isEqualTo(TEST_VIEWS_1 + TEST_VIEWS_2) + assertThat(success.aggregates.visitors).isEqualTo(TEST_VISITORS_1 + TEST_VISITORS_2) + assertThat(success.aggregates.likes).isEqualTo(TEST_LIKES_1 + TEST_LIKES_2) + assertThat(success.aggregates.comments).isEqualTo(TEST_COMMENTS_1 + TEST_COMMENTS_2) + assertThat(success.aggregates.posts).isEqualTo(TEST_POSTS_1 + TEST_POSTS_2) + } + + @Test + fun `given successful response, when fetchWeeklyStats is called, then data source is called with DAY unit`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchWeeklyStats(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + + @Test + fun `given error response, when fetchWeeklyStats is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchWeeklyStats(TEST_SITE_ID) + + assertThat(result).isInstanceOf(WeeklyStatsResult.Error::class.java) + assertThat((result as WeeklyStatsResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + + @Test + fun `given weeks ago parameter, when fetchWeeklyStats is called, then data source is called`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchWeeklyStats(TEST_SITE_ID, weeksAgo = 1) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + // endregion + + // region fetchDailyViewsForWeek + @Test + fun `given successful response, when fetchDailyViewsForWeek is called, then success result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + val result = repository.fetchDailyViewsForWeek(TEST_SITE_ID) + + assertThat(result).isInstanceOf(DailyViewsResult.Success::class.java) + val success = result as DailyViewsResult.Success + assertThat(success.dataPoints).hasSize(2) + assertThat(success.dataPoints[0].period).isEqualTo(TEST_PERIOD_1) + assertThat(success.dataPoints[0].views).isEqualTo(TEST_VIEWS_1) + assertThat(success.dataPoints[1].period).isEqualTo(TEST_PERIOD_2) + assertThat(success.dataPoints[1].views).isEqualTo(TEST_VIEWS_2) + } + + @Test + fun `given successful response, when fetchDailyViewsForWeek is called, then data source is called with DAY unit`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchDailyViewsForWeek(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + + @Test + fun `given error response, when fetchDailyViewsForWeek is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchDailyViewsForWeek(TEST_SITE_ID) + + assertThat(result).isInstanceOf(DailyViewsResult.Error::class.java) + assertThat((result as DailyViewsResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + // endregion + + // region fetchWeeklyStatsWithDailyData + @Test + fun `given successful response, when fetchWeeklyStatsWithDailyData is called, then success result is returned`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + val result = repository.fetchWeeklyStatsWithDailyData(TEST_SITE_ID) + + assertThat(result).isInstanceOf(WeeklyStatsWithDailyDataResult.Success::class.java) + val success = result as WeeklyStatsWithDailyDataResult.Success + + // Verify aggregates + assertThat(success.aggregates.views).isEqualTo(TEST_VIEWS_1 + TEST_VIEWS_2) + assertThat(success.aggregates.visitors).isEqualTo(TEST_VISITORS_1 + TEST_VISITORS_2) + assertThat(success.aggregates.likes).isEqualTo(TEST_LIKES_1 + TEST_LIKES_2) + assertThat(success.aggregates.comments).isEqualTo(TEST_COMMENTS_1 + TEST_COMMENTS_2) + assertThat(success.aggregates.posts).isEqualTo(TEST_POSTS_1 + TEST_POSTS_2) + + // Verify daily data points + assertThat(success.dailyDataPoints).hasSize(2) + assertThat(success.dailyDataPoints[0].period).isEqualTo(TEST_PERIOD_1) + assertThat(success.dailyDataPoints[0].views).isEqualTo(TEST_VIEWS_1) + } + + @Test + fun `given successful response, when fetchWeeklyStatsWithDailyData is called, data source is called correctly`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchWeeklyStatsWithDailyData(TEST_SITE_ID) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + + @Test + fun `given error response, when fetchWeeklyStatsWithDailyData is called, then error result is returned`() = test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Error(ERROR_MESSAGE)) + + val result = repository.fetchWeeklyStatsWithDailyData(TEST_SITE_ID) + + assertThat(result).isInstanceOf(WeeklyStatsWithDailyDataResult.Error::class.java) + assertThat((result as WeeklyStatsWithDailyDataResult.Error).message).isEqualTo(ERROR_MESSAGE) + } + + @Test + fun `given weeks ago parameter, when fetchWeeklyStatsWithDailyData is called, then data source is called`() = + test { + whenever(statsDataSource.fetchStatsVisits(any(), any(), any(), any())) + .thenReturn(StatsVisitsDataResult.Success(createWeeklyStatsVisitsData())) + + repository.fetchWeeklyStatsWithDailyData(TEST_SITE_ID, weeksAgo = 2) + + verify(statsDataSource).fetchStatsVisits( + siteId = eq(TEST_SITE_ID), + unit = eq(StatsUnit.DAY), + quantity = eq(7), + endDate = any() + ) + } + // endregion + + // region Helper functions + private fun createStatsVisitsData() = StatsVisitsData( + visits = listOf(VisitsDataPoint(TEST_PERIOD_1, TEST_VIEWS)), + visitors = listOf(VisitorsDataPoint(TEST_PERIOD_1, TEST_VISITORS)), + likes = listOf(LikesDataPoint(TEST_PERIOD_1, TEST_LIKES)), + comments = listOf(CommentsDataPoint(TEST_PERIOD_1, TEST_COMMENTS)), + posts = listOf(PostsDataPoint(TEST_PERIOD_1, TEST_POSTS)) + ) + + private fun createEmptyStatsVisitsData() = StatsVisitsData( + visits = emptyList(), + visitors = emptyList(), + likes = emptyList(), + comments = emptyList(), + posts = emptyList() + ) + + private fun createHourlyStatsVisitsData() = StatsVisitsData( + visits = listOf( + VisitsDataPoint(TEST_PERIOD_1, TEST_VIEWS_1), + VisitsDataPoint(TEST_PERIOD_2, TEST_VIEWS_2) + ), + visitors = listOf( + VisitorsDataPoint(TEST_PERIOD_1, TEST_VISITORS_1), + VisitorsDataPoint(TEST_PERIOD_2, TEST_VISITORS_2) + ), + likes = emptyList(), + comments = emptyList(), + posts = emptyList() + ) + + private fun createWeeklyStatsVisitsData() = StatsVisitsData( + visits = listOf( + VisitsDataPoint(TEST_PERIOD_1, TEST_VIEWS_1), + VisitsDataPoint(TEST_PERIOD_2, TEST_VIEWS_2) + ), + visitors = listOf( + VisitorsDataPoint(TEST_PERIOD_1, TEST_VISITORS_1), + VisitorsDataPoint(TEST_PERIOD_2, TEST_VISITORS_2) + ), + likes = listOf( + LikesDataPoint(TEST_PERIOD_1, TEST_LIKES_1), + LikesDataPoint(TEST_PERIOD_2, TEST_LIKES_2) + ), + comments = listOf( + CommentsDataPoint(TEST_PERIOD_1, TEST_COMMENTS_1), + CommentsDataPoint(TEST_PERIOD_2, TEST_COMMENTS_2) + ), + posts = listOf( + PostsDataPoint(TEST_PERIOD_1, TEST_POSTS_1), + PostsDataPoint(TEST_PERIOD_2, TEST_POSTS_2) + ) + ) + // endregion + + companion object { + private const val TEST_SITE_ID = 123L + private const val TEST_ACCESS_TOKEN = "test_access_token" + private const val ERROR_MESSAGE = "Test error message" + + private const val TEST_PERIOD_1 = "2024-01-15" + private const val TEST_PERIOD_2 = "2024-01-16" + + private const val TEST_VIEWS = 500L + private const val TEST_VISITORS = 100L + private const val TEST_LIKES = 50L + private const val TEST_COMMENTS = 25L + private const val TEST_POSTS = 5L + + private const val TEST_VIEWS_1 = 100L + private const val TEST_VIEWS_2 = 150L + private const val TEST_VISITORS_1 = 50L + private const val TEST_VISITORS_2 = 75L + private const val TEST_LIKES_1 = 10L + private const val TEST_LIKES_2 = 15L + private const val TEST_COMMENTS_1 = 5L + private const val TEST_COMMENTS_2 = 8L + private const val TEST_POSTS_1 = 2L + private const val TEST_POSTS_2 = 3L + } +} diff --git a/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModelTest.kt similarity index 99% rename from WordPress/src/test/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModelTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModelTest.kt index 93254e765c0d..68cd6d3c11a8 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaysstat/TodaysStatsViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModelTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaysstat +package org.wordpress.android.ui.newstats.todaystats import kotlinx.coroutines.ExperimentalCoroutinesApi import org.assertj.core.api.Assertions.assertThat From 45890d2cc146d3ffb37f6eb3a571061a06fc4e37 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 22 Jan 2026 10:56:13 +0100 Subject: [PATCH 3/5] detekt --- .../datasource/StatsDataSourceImpl.kt | 57 +++++++------------ 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt index a24c07edadcc..92bc311557cd 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt @@ -61,52 +61,37 @@ class StatsDataSourceImpl @Inject constructor( return when (result) { is WpRequestResult.Success -> { - val response = result.response.data - val statsData = StatsVisitsData( - visits = response.statsVisitsData().map { dataPoint -> - VisitsDataPoint( - period = dataPoint.period, - visits = dataPoint.visits.toLong() - ) - }, - visitors = response.statsVisitorsData().map { dataPoint -> - VisitorsDataPoint( - period = dataPoint.period, - visitors = dataPoint.visitors.toLong() - ) - }, - likes = response.statsLikesData().map { dataPoint -> - LikesDataPoint( - period = dataPoint.period, - likes = dataPoint.likes.toLong() - ) - }, - comments = response.statsCommentsData().map { dataPoint -> - CommentsDataPoint( - period = dataPoint.period, - comments = dataPoint.comments.toLong() - ) - }, - posts = response.statsPostsData().map { dataPoint -> - PostsDataPoint( - period = dataPoint.period, - posts = dataPoint.posts.toLong() - ) - } - ) - StatsVisitsDataResult.Success(statsData) + StatsVisitsDataResult.Success(mapToStatsVisitsData(result.response.data)) } - is WpRequestResult.WpError -> { StatsVisitsDataResult.Error(result.errorMessage) } - else -> { StatsVisitsDataResult.Error("Unknown error") } } } + private fun mapToStatsVisitsData(response: uniffi.wp_api.StatsVisitsResponse): StatsVisitsData { + return StatsVisitsData( + visits = response.statsVisitsData().map { dataPoint -> + VisitsDataPoint(period = dataPoint.period, visits = dataPoint.visits.toLong()) + }, + visitors = response.statsVisitorsData().map { dataPoint -> + VisitorsDataPoint(period = dataPoint.period, visitors = dataPoint.visitors.toLong()) + }, + likes = response.statsLikesData().map { dataPoint -> + LikesDataPoint(period = dataPoint.period, likes = dataPoint.likes.toLong()) + }, + comments = response.statsCommentsData().map { dataPoint -> + CommentsDataPoint(period = dataPoint.period, comments = dataPoint.comments.toLong()) + }, + posts = response.statsPostsData().map { dataPoint -> + PostsDataPoint(period = dataPoint.period, posts = dataPoint.posts.toLong()) + } + ) + } + private fun StatsUnit.toApiUnit(): StatsVisitsUnit = when (this) { StatsUnit.HOUR -> StatsVisitsUnit.HOUR StatsUnit.DAY -> StatsVisitsUnit.DAY From 6b038cae050f992c2c32191d1157d067c99a629f Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 22 Jan 2026 10:57:34 +0100 Subject: [PATCH 4/5] rename --- .../org/wordpress/android/ui/newstats/NewStatsActivity.kt | 4 ++-- .../newstats/{todaystats => todaysstats}/TodaysStatsCard.kt | 2 +- .../{todaystats => todaysstats}/TodaysStatsCardUiState.kt | 2 +- .../{todaystats => todaysstats}/TodaysStatsViewModel.kt | 2 +- .../{todaystats => todaysstats}/TodaysStatsViewModelTest.kt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename WordPress/src/main/java/org/wordpress/android/ui/newstats/{todaystats => todaysstats}/TodaysStatsCard.kt (99%) rename WordPress/src/main/java/org/wordpress/android/ui/newstats/{todaystats => todaysstats}/TodaysStatsCardUiState.kt (94%) rename WordPress/src/main/java/org/wordpress/android/ui/newstats/{todaystats => todaysstats}/TodaysStatsViewModel.kt (99%) rename WordPress/src/test/java/org/wordpress/android/ui/newstats/{todaystats => todaysstats}/TodaysStatsViewModelTest.kt (99%) 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 eb87e12e0ca5..ff50f2ea4254 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 @@ -40,8 +40,8 @@ import kotlinx.coroutines.launch import org.wordpress.android.R import org.wordpress.android.ui.compose.theme.AppThemeM3 import org.wordpress.android.ui.main.BaseAppCompatActivity -import org.wordpress.android.ui.newstats.todaystats.TodaysStatsCard -import org.wordpress.android.ui.newstats.todaystats.TodaysStatsViewModel +import org.wordpress.android.ui.newstats.todaysstats.TodaysStatsCard +import org.wordpress.android.ui.newstats.todaysstats.TodaysStatsViewModel import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsCard import org.wordpress.android.ui.newstats.viewsstats.ViewsStatsViewModel diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCard.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsCard.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCard.kt rename to WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsCard.kt index 17963b45328f..55732e3f07c7 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCard.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsCard.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaystats +package org.wordpress.android.ui.newstats.todaysstats import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCardUiState.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsCardUiState.kt similarity index 94% rename from WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCardUiState.kt rename to WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsCardUiState.kt index 725e70968dcc..4072c9f802b2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsCardUiState.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsCardUiState.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaystats +package org.wordpress.android.ui.newstats.todaysstats /** * UI State for the Today's Stats card in the new stats screen. diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsViewModel.kt similarity index 99% rename from WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModel.kt rename to WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsViewModel.kt index a18cf844d2ef..7007d639b978 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsViewModel.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaystats +package org.wordpress.android.ui.newstats.todaysstats import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsViewModelTest.kt similarity index 99% rename from WordPress/src/test/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModelTest.kt rename to WordPress/src/test/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsViewModelTest.kt index 68cd6d3c11a8..6f2f4c1a91bb 100644 --- a/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaystats/TodaysStatsViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/ui/newstats/todaysstats/TodaysStatsViewModelTest.kt @@ -1,4 +1,4 @@ -package org.wordpress.android.ui.newstats.todaystats +package org.wordpress.android.ui.newstats.todaysstats import kotlinx.coroutines.ExperimentalCoroutinesApi import org.assertj.core.api.Assertions.assertThat From d93b3f769f8451f0d01d3c76d01f3ef56d888255 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 22 Jan 2026 11:38:50 +0100 Subject: [PATCH 5/5] PR suggestions --- .../android/ui/newstats/datasource/StatsDataSourceImpl.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt index 92bc311557cd..1e0cccfa4fdc 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/newstats/datasource/StatsDataSourceImpl.kt @@ -27,7 +27,7 @@ class StatsDataSourceImpl @Inject constructor( @Volatile private var accessToken: String? = null - private val wpComApiClient: WpComApiClient by lazy { + private val wpComApiClient: WpComApiClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { check(accessToken != null) { "DataSource not initialized" } wpComApiClientProvider.getWpComApiClient(accessToken!!) } @@ -42,10 +42,6 @@ class StatsDataSourceImpl @Inject constructor( quantity: Int, endDate: String ): StatsVisitsDataResult { - if (accessToken == null) { - return StatsVisitsDataResult.Error("DataSource not initialized") - } - val params = StatsVisitsParams( unit = unit.toApiUnit(), quantity = quantity.toUInt(),