diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 561d8d5d94db..d54ae42c2943 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -6,6 +6,7 @@ ----- - [*] Woo POS: Remote Tap to Pay failures now explain what went wrong - phone not eligible for Tap to Pay, NFC turned off, or a payment service error - instead of a generic message or raw error text [https://github.com/woocommerce/woocommerce-android/pull/16384] - [*] QR login now asks you to choose a store when your WordPress.com account has multiple sites instead of preselecting one [https://github.com/woocommerce/woocommerce-android/pull/16400] +- [*] Analytics date ranges now follow the date order of the language the app is displayed in [https://github.com/woocommerce/woocommerce-android/pull/16416] - [Internal] Close analytics gaps in the Remote Tap to Pay flow: transport on the reader-ready event, cause chains in error descriptions, no stale card reader model, and a real session ended reason [https://github.com/woocommerce/woocommerce-android/pull/16368] - [*] Tap to Pay no longer disappears from the Payments screen shortly after it appears [https://github.com/woocommerce/woocommerce-android/pull/16370] - [*] Fixed a crash when pressing Back on the "You're already signed in" warning shown after opening a QR login link while signed in [https://github.com/woocommerce/woocommerce-android/pull/16347] diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/DateExt.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/DateExt.kt index d405d05179a4..13bc495e5a87 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/DateExt.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/extensions/DateExt.kt @@ -1,6 +1,8 @@ package com.woocommerce.android.extensions import android.content.Context +import android.icu.text.DateIntervalFormat +import android.icu.util.DateInterval import android.text.format.DateFormat import java.text.DateFormatSymbols import java.text.SimpleDateFormat @@ -161,39 +163,13 @@ fun Date.oneYearAgo(): Date = add(Calendar.YEAR, -1) }.time -fun Date.isInSameYearAs(other: Date, baseCalendar: Calendar): Boolean { - val calendar = baseCalendar.clone() as Calendar - calendar.time = this - val thisYear = calendar.get(Calendar.YEAR) - calendar.time = other - val otherYear = calendar.get(Calendar.YEAR) - return thisYear == otherYear -} - -fun Date.isInSameMonthAs(other: Date, baseCalendar: Calendar): Boolean { - val calendar = baseCalendar.clone() as Calendar - calendar.time = this - val thisMonth = calendar.get(Calendar.MONTH) - calendar.time = other - val otherMonth = calendar.get(Calendar.MONTH) - return thisMonth == otherMonth && isInSameYearAs(other, calendar) -} - -fun Date.formatAsRangeWith(other: Date, locale: Locale, calendar: Calendar): String { - val formattedStartDate = if (this.isInSameYearAs(other, calendar)) { - SimpleDateFormat("MMM d", locale).format(this) - } else { - SimpleDateFormat("MMM d, yyyy", locale).format(this) - } - - val formattedEndDate = if (this.isInSameMonthAs(other, calendar)) { - SimpleDateFormat("d, yyyy", locale).format(other) - } else { - SimpleDateFormat("MMM d, yyyy", locale).format(other) - } - - return "$formattedStartDate – $formattedEndDate" -} +/** + * Formats the two dates as a range in the locale's own interval format, e.g. "Jul 1 – 31, 2022" in English + * and "1.–31. Juli 2022" in German. + */ +fun Date.formatAsRangeWith(other: Date, locale: Locale): String = DateIntervalFormat + .getInstance("yMMMd", locale) + .format(DateInterval(this.time, other.time)) private const val THREE_MONTHS = 3 private const val SEVEN_DAYS = 7 diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/aiassistant/AiAssistantStatsCardRenderer.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/aiassistant/AiAssistantStatsCardRenderer.kt index 973b56d91fdf..6de67e8fb839 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/aiassistant/AiAssistantStatsCardRenderer.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/aiassistant/AiAssistantStatsCardRenderer.kt @@ -8,8 +8,12 @@ import com.woocommerce.android.aiassistant.ui.cards.AiAssistantStatsCard import com.woocommerce.android.aiassistant.ui.cards.AiAssistantStatsCardState import com.woocommerce.android.aiassistant.ui.cards.AssistantCard import com.woocommerce.android.aiassistant.ui.cards.AssistantCardAction +import com.woocommerce.android.extensions.formatAsRangeWith +import com.woocommerce.android.extensions.formatToLocalizedMonthDayYear import java.time.LocalDate +import java.time.ZoneId import java.time.format.DateTimeFormatter +import java.util.Date import java.util.Locale internal class AiAssistantStatsCardRenderer( @@ -53,21 +57,16 @@ private fun formatStatsPeriod( .filter { it.isNotBlank() } .joinToString(ASSISTANT_STATS_PERIOD_SEPARATOR) .ifBlank { unavailableValue } - start == end -> start.format(DateTimeFormatter.ofPattern("MMM d, yyyy", locale)) - start.year == end.year -> listOf( - start.format(DateTimeFormatter.ofPattern("MMM d", locale)), - end.format(DateTimeFormatter.ofPattern("MMM d, yyyy", locale)), - ).joinToString(ASSISTANT_STATS_PERIOD_SEPARATOR) - else -> listOf( - start.format(DateTimeFormatter.ofPattern("MMM d, yyyy", locale)), - end.format(DateTimeFormatter.ofPattern("MMM d, yyyy", locale)), - ).joinToString(ASSISTANT_STATS_PERIOD_SEPARATOR) + start == end -> start.toDate().formatToLocalizedMonthDayYear(locale) + else -> start.toDate().formatAsRangeWith(end.toDate(), locale) } } private fun String.toStatsLocalDate(): LocalDate? = runCatching { LocalDate.parse(this, DateTimeFormatter.ISO_LOCAL_DATE) }.getOrNull() +private fun LocalDate.toDate(): Date = Date.from(atStartOfDay(ZoneId.systemDefault()).toInstant()) + private fun formatStatsMoney( value: String, currency: String, diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/CustomRangeData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/CustomRangeData.kt index b4c61096dc37..0bb6ac79f62d 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/CustomRangeData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/CustomRangeData.kt @@ -39,7 +39,7 @@ class CustomRangeData( start = currentStart, end = currentEnd ) - formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale, calendar) + formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale) val dayDifference = selectedEndDate.time - selectedStartDate.time @@ -53,6 +53,6 @@ class CustomRangeData( start = previousStart, end = previousEnd ) - formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale, calendar) + formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastMonthRangeData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastMonthRangeData.kt index 3c032fa56249..90682ca5645b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastMonthRangeData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastMonthRangeData.kt @@ -38,7 +38,7 @@ class LastMonthRangeData( start = currentStart, end = currentEnd ) - formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale, calendar) + formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale) val twoMonthsAgo = oneMonthAgo.oneMonthAgo() calendar.time = twoMonthsAgo @@ -48,6 +48,6 @@ class LastMonthRangeData( start = previousStart, end = previousEnd ) - formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale, calendar) + formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastQuarterRangeData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastQuarterRangeData.kt index f54801310398..ccb908dd0b2c 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastQuarterRangeData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastQuarterRangeData.kt @@ -38,7 +38,7 @@ class LastQuarterRangeData( start = currentStart, end = currentEnd ) - formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale, calendar) + formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale) val twoQuartersAgo = oneQuarterAgo.oneQuarterAgo() calendar.time = twoQuartersAgo @@ -48,6 +48,6 @@ class LastQuarterRangeData( start = previousStart, end = previousEnd ) - formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale, calendar) + formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastWeekRangeData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastWeekRangeData.kt index 964e81ae8402..851f2f0a9fdf 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastWeekRangeData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastWeekRangeData.kt @@ -38,7 +38,7 @@ class LastWeekRangeData( start = currentStart, end = currentEnd ) - formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale, calendar) + formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale) val twoWeeksAgo = oneWeekAgo.oneWeekAgo() calendar.time = twoWeeksAgo @@ -48,6 +48,6 @@ class LastWeekRangeData( start = previousStart, end = previousEnd ) - formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale, calendar) + formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastYearRangeData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastYearRangeData.kt index 8dce94bff2d7..1bc99da20f1b 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastYearRangeData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/LastYearRangeData.kt @@ -38,7 +38,7 @@ class LastYearRangeData( start = currentStart, end = currentEnd ) - formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale, calendar) + formattedCurrentRange = currentStart.formatAsRangeWith(currentEnd, locale) val twoYearsAgo = oneYearAgo.oneYearAgo() calendar.time = twoYearsAgo @@ -48,6 +48,6 @@ class LastYearRangeData( start = previousStart, end = previousEnd ) - formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale, calendar) + formattedPreviousRange = previousStart.formatAsRangeWith(previousEnd, locale) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/MonthToDateRangeData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/MonthToDateRangeData.kt index 213044d7625d..9ea20ab26b7e 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/MonthToDateRangeData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/MonthToDateRangeData.kt @@ -37,7 +37,7 @@ class MonthToDateRangeData( start = currentStart, end = currentEnd ) - formattedCurrentRange = currentStart.formatAsRangeWith(referenceDate, locale, calendar) + formattedCurrentRange = currentStart.formatAsRangeWith(referenceDate, locale) val oneMonthAgo = referenceDate.oneMonthAgo() calendar.time = oneMonthAgo @@ -46,6 +46,6 @@ class MonthToDateRangeData( start = previousStart, end = oneMonthAgo ) - formattedPreviousRange = previousStart.formatAsRangeWith(oneMonthAgo, locale, calendar) + formattedPreviousRange = previousStart.formatAsRangeWith(oneMonthAgo, locale) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/QuarterToDateRangeData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/QuarterToDateRangeData.kt index c829ee7b8c55..f889cf6c20f7 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/QuarterToDateRangeData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/QuarterToDateRangeData.kt @@ -37,7 +37,7 @@ class QuarterToDateRangeData( start = currentStart, end = currentEnd ) - formattedCurrentRange = currentStart.formatAsRangeWith(referenceDate, locale, calendar) + formattedCurrentRange = currentStart.formatAsRangeWith(referenceDate, locale) val oneQuarterAgo = referenceDate.oneQuarterAgo() calendar.time = oneQuarterAgo @@ -46,6 +46,6 @@ class QuarterToDateRangeData( start = previousStart, end = oneQuarterAgo ) - formattedPreviousRange = previousStart.formatAsRangeWith(oneQuarterAgo, locale, calendar) + formattedPreviousRange = previousStart.formatAsRangeWith(oneQuarterAgo, locale) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/WeekToDateRangeData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/WeekToDateRangeData.kt index dfb54c2fa2e2..903f4808a8ab 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/WeekToDateRangeData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/WeekToDateRangeData.kt @@ -37,7 +37,7 @@ class WeekToDateRangeData( start = currentStart, end = currentEnd ) - formattedCurrentRange = currentStart.formatAsRangeWith(referenceDate, locale, calendar) + formattedCurrentRange = currentStart.formatAsRangeWith(referenceDate, locale) val oneWeekAgo = referenceDate.oneWeekAgo() calendar.time = oneWeekAgo @@ -46,6 +46,6 @@ class WeekToDateRangeData( start = startOfPreviousWeek, end = oneWeekAgo ) - formattedPreviousRange = startOfPreviousWeek.formatAsRangeWith(oneWeekAgo, locale, calendar) + formattedPreviousRange = startOfPreviousWeek.formatAsRangeWith(oneWeekAgo, locale) } } diff --git a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/YearToDateRangeData.kt b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/YearToDateRangeData.kt index dd5fbdfeb1fd..b64f3d404eca 100644 --- a/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/YearToDateRangeData.kt +++ b/WooCommerce/src/main/kotlin/com/woocommerce/android/ui/analytics/ranges/data/YearToDateRangeData.kt @@ -37,7 +37,7 @@ class YearToDateRangeData( start = currentStart, end = currentEnd ) - formattedCurrentRange = currentStart.formatAsRangeWith(referenceDate, locale, calendar) + formattedCurrentRange = currentStart.formatAsRangeWith(referenceDate, locale) val oneYearAgo = referenceDate.oneYearAgo() calendar.time = oneYearAgo @@ -46,6 +46,6 @@ class YearToDateRangeData( start = previousStart, end = oneYearAgo ) - formattedPreviousRange = previousStart.formatAsRangeWith(oneYearAgo, locale, calendar) + formattedPreviousRange = previousStart.formatAsRangeWith(oneYearAgo, locale) } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/background/BackgroundUpdateAnalyticsRepositoryTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/background/BackgroundUpdateAnalyticsRepositoryTest.kt index 548515811f8a..fe642b7b0b86 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/background/BackgroundUpdateAnalyticsRepositoryTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/background/BackgroundUpdateAnalyticsRepositoryTest.kt @@ -6,10 +6,12 @@ import com.woocommerce.android.tools.SelectedSite import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection.SelectionType.CUSTOM import com.woocommerce.android.ui.dashboard.data.StatsRepository +import com.woocommerce.android.util.LocalizedDatePatternsTestRule import com.woocommerce.android.viewmodel.BaseUnitTest import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Rule import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock @@ -37,6 +39,8 @@ import kotlin.test.Test @OptIn(ExperimentalCoroutinesApi::class) class BackgroundUpdateAnalyticsRepositoryTest : BaseUnitTest() { + @get:Rule + val localizedDatePatterns = LocalizedDatePatternsTestRule() private val statsRepository: StatsRepository = mock() private val googleAdsStore: WCGoogleStore = mock() @@ -46,12 +50,14 @@ class BackgroundUpdateAnalyticsRepositoryTest : BaseUnitTest() { private val testTimeZone = TimeZone.getDefault() private val testCalendar = Calendar.getInstance(testLocale) - private val testSelectionData: StatsTimeRangeSelection = CUSTOM.generateSelectionData( - referenceStartDate = "2024-10-02".dayStartFrom(), - referenceEndDate = "2024-10-02".dayEndFrom(), - calendar = testCalendar, - locale = testLocale - ) + private val testSelectionData: StatsTimeRangeSelection by lazy { + CUSTOM.generateSelectionData( + referenceStartDate = "2024-10-02".dayStartFrom(), + referenceEndDate = "2024-10-02".dayEndFrom(), + calendar = testCalendar, + locale = testLocale + ) + } private val backgroundUpdateAnalyticsRepository = BackgroundUpdateAnalyticsRepository( statsRepository, diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/aiassistant/AiAssistantStatsCardRendererTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/aiassistant/AiAssistantStatsCardRendererTest.kt index 8b76479d2d77..71652623fa22 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/aiassistant/AiAssistantStatsCardRendererTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/aiassistant/AiAssistantStatsCardRendererTest.kt @@ -3,7 +3,9 @@ package com.woocommerce.android.ui.aiassistant import com.woocommerce.android.aiassistant.ui.cards.AiAssistantStatsCardState import com.woocommerce.android.aiassistant.ui.cards.AssistantCard import com.woocommerce.android.aiassistant.ui.cards.AssistantCardAction +import com.woocommerce.android.util.LocalizedDatePatternsTestRule import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -12,6 +14,9 @@ import org.mockito.kotlin.whenever import java.util.Locale class AiAssistantStatsCardRendererTest { + @get:Rule + val localizedDatePatterns = LocalizedDatePatternsTestRule() + private val currencyFormatter: AiAssistantCurrencyFormatter = mock() @Test @@ -30,7 +35,7 @@ class AiAssistantStatsCardRendererTest { locale = Locale.US, ) - assertThat(model.period).isEqualTo("May 1 - May 7, 2026") + assertThat(model.period).isEqualTo("May 1, 2026 – May 7, 2026") assertThat(model.metrics).containsExactly( AiAssistantStatsCardState.Metric( type = AssistantCard.Stats.MetricType.TotalSales, @@ -68,7 +73,7 @@ class AiAssistantStatsCardRendererTest { assertThat(model).isEqualTo( AiAssistantStatsCardState( - period = "May 1 - May 7, 2026", + period = "May 1, 2026 – May 7, 2026", metrics = listOf( AiAssistantStatsCardState.Metric( type = AssistantCard.Stats.MetricType.TotalSales, diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/aiassistant/WooAssistantCardActionNavigatorTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/aiassistant/WooAssistantCardActionNavigatorTest.kt index 1d81d9768ac3..61af0aaed20b 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/aiassistant/WooAssistantCardActionNavigatorTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/aiassistant/WooAssistantCardActionNavigatorTest.kt @@ -8,8 +8,10 @@ import com.woocommerce.android.model.CustomerWithAnalytics import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection import com.woocommerce.android.ui.moremenu.customer.GetCustomerWithStats import com.woocommerce.android.ui.products.details.ProductDetailFragment +import com.woocommerce.android.util.LocalizedDatePatternsTestRule import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule import org.junit.Test import org.mockito.kotlin.mock import org.mockito.kotlin.verify @@ -19,6 +21,9 @@ import java.util.Locale import java.util.TimeZone class WooAssistantCardActionNavigatorTest { + @get:Rule + val localizedDatePatterns = LocalizedDatePatternsTestRule() + private val getCustomerWithStats: GetCustomerWithStats = mock() private val navigator = WooAssistantCardActionNavigator(getCustomerWithStats) diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/analytics/hub/sync/AnalyticsUpdateDataStoreTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/analytics/hub/sync/AnalyticsUpdateDataStoreTest.kt index d762cf0bc76f..38b576398b3c 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/analytics/hub/sync/AnalyticsUpdateDataStoreTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/analytics/hub/sync/AnalyticsUpdateDataStoreTest.kt @@ -37,12 +37,14 @@ class AnalyticsUpdateDataStoreTest : BaseUnitTest() { private lateinit var sut: AnalyticsUpdateDataStore - private val defaultSelectionData = LAST_MONTH.generateSelectionData( - calendar = Calendar.getInstance(), - locale = Locale.getDefault(), - referenceStartDate = Date(), - referenceEndDate = Date() - ) + private val defaultSelectionData by lazy { + LAST_MONTH.generateSelectionData( + calendar = Calendar.getInstance(), + locale = Locale.getDefault(), + referenceStartDate = Date(), + referenceEndDate = Date() + ) + } @Test fun `given shouldUpdateAnalytics is called, when time elapsed is enough, then return true`() = testBlocking { diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/analytics/ranges/StatsTimeRangeSelectionTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/analytics/ranges/StatsTimeRangeSelectionTest.kt index a6b95d2f979c..6323c6bc846d 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/analytics/ranges/StatsTimeRangeSelectionTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/analytics/ranges/StatsTimeRangeSelectionTest.kt @@ -346,226 +346,6 @@ internal class StatsTimeRangeSelectionTest { assertThat(sut.previousRange).isEqualTo(expectedPreviousRange) } - @Test - fun `when selection type is year to date, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-07-01") - val sut = YEAR_TO_DATE.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jan 1 – Jul 1, 2022") - assertThat(previousRangeDescription).isEqualTo("Jan 1 – Jul 1, 2021") - } - - @Test - fun `when selection type is last year, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-07-01") - val sut = LAST_YEAR.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jan 1 – Dec 31, 2021") - assertThat(previousRangeDescription).isEqualTo("Jan 1 – Dec 31, 2020") - } - - @Test - fun `when selection type is quarter to date, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-02-15") - val sut = QUARTER_TO_DATE.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jan 1 – Feb 15, 2022") - assertThat(previousRangeDescription).isEqualTo("Oct 1 – Nov 15, 2021") - } - - @Test - fun `when selection type is last quarter, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-05-15") - val sut = LAST_QUARTER.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jan 1 – Mar 31, 2022") - assertThat(previousRangeDescription).isEqualTo("Oct 1 – Dec 31, 2021") - } - - @Test - fun `when selection type is month to date, and today is last day of month, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-07-31") - val sut = MONTH_TO_DATE.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jul 1 – 31, 2022") - assertThat(previousRangeDescription).isEqualTo("Jun 1 – 30, 2022") - } - - @Test - fun `when selection type is month to date, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-07-20") - val sut = MONTH_TO_DATE.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jul 1 – 20, 2022") - assertThat(previousRangeDescription).isEqualTo("Jun 1 – 20, 2022") - } - - @Test - fun `when selection type is last month, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-07-31") - val sut = LAST_MONTH.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jun 1 – 30, 2022") - assertThat(previousRangeDescription).isEqualTo("May 1 – 31, 2022") - } - - @Test - fun `when selection type is week to date, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-07-29") - val sut = WEEK_TO_DATE.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jul 25 – 29, 2022") - assertThat(previousRangeDescription).isEqualTo("Jul 18 – 22, 2022") - } - - @Test - fun `when selection type is week to date with different months, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-07-02") - val sut = WEEK_TO_DATE.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jun 27 – Jul 2, 2022") - assertThat(previousRangeDescription).isEqualTo("Jun 20 – 25, 2022") - } - - @Test - fun `when selection type is last week, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-07-29") - val sut = LAST_WEEK.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jul 18 – 24, 2022") - assertThat(previousRangeDescription).isEqualTo("Jul 11 – 17, 2022") - } - - @Test - fun `when selection type is last week with different months, then generate expected descriptions`() { - // Given - val today = midDayFrom("2022-07-05") - val sut = LAST_WEEK.generateSelectionData( - referenceStartDate = today, - calendar = testCalendar, - locale = testLocale, - referenceEndDate = Date() - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Jun 27 – Jul 3, 2022") - assertThat(previousRangeDescription).isEqualTo("Jun 20 – 26, 2022") - } - @Test fun `when selection type is today, then generate expected descriptions`() { // Given @@ -606,27 +386,6 @@ internal class StatsTimeRangeSelectionTest { assertThat(previousRangeDescription).isEqualTo("Jun 30, 2022") } - @Test - fun `when selection type is custom, then generate expected descriptions`() { - // Given - val start = dayStartFrom("2022-12-05") - val end = dayEndFrom("2022-12-07") - val sut = CUSTOM.generateSelectionData( - referenceStartDate = start, - referenceEndDate = end, - calendar = testCalendar, - locale = testLocale - ) - - // When - val currentRangeDescription = sut.currentRangeDescription - val previousRangeDescription = sut.previousRangeDescription - - // Then - assertThat(currentRangeDescription).isEqualTo("Dec 5 – 7, 2022") - assertThat(previousRangeDescription).isEqualTo("Dec 2 – 4, 2022") - } - private fun midDayFrom(date: String): Date { val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") formatter.timeZone = testTimeZone diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/stats/GetStatsTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/stats/GetStatsTest.kt index dedee51a58e9..f0d80af9681d 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/stats/GetStatsTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/stats/GetStatsTest.kt @@ -7,6 +7,7 @@ import com.woocommerce.android.ui.analytics.hub.sync.AnalyticsUpdateDataStore import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection import com.woocommerce.android.ui.dashboard.data.StatsRepository import com.woocommerce.android.ui.dashboard.data.StatsRepository.StatsException +import com.woocommerce.android.util.LocalizedDatePatternsTestRule import com.woocommerce.android.viewmodel.BaseUnitTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.collect @@ -17,6 +18,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.last import org.assertj.core.api.Assertions.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -39,6 +41,9 @@ import java.util.Locale @ExperimentalCoroutinesApi class GetStatsTest : BaseUnitTest() { + @get:Rule + val localizedDatePatterns = LocalizedDatePatternsTestRule() + private val siteModel: SiteModel = mock() private val selectedSite: SelectedSite = mock { on { get() }.thenReturn(siteModel) @@ -421,12 +426,14 @@ class GetStatsTest : BaseUnitTest() { val GENERIC_ORDER_STATS_ERROR = OrderStatsError(GENERIC_ERROR, ANY_ERROR_MESSAGE) val PLUGIN_NOT_ACTIVE_ORDER_STATS_ERROR = OrderStatsError(PLUGIN_NOT_ACTIVE, ANY_ERROR_MESSAGE) val ANY_SELECTION_TYPE = StatsTimeRangeSelection.SelectionType.WEEK_TO_DATE - val ANY_STATS_RANGE_SELECTION = StatsTimeRangeSelection.build( - selectionType = ANY_SELECTION_TYPE, - referenceDate = Date(), - calendar = Calendar.getInstance(), - locale = Locale.getDefault() - ) + val ANY_STATS_RANGE_SELECTION by lazy { + StatsTimeRangeSelection.build( + selectionType = ANY_SELECTION_TYPE, + referenceDate = Date(), + calendar = Calendar.getInstance(), + locale = Locale.getDefault() + ) + } val ANY_REVENUE_STATS = WCRevenueStatsModel(LocalId(1), "", "", "", "", "", "") val ANY_VISITOR_STATS = mapOf( "2020-10-01" to 1, diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/topperformers/GetTopPerformersTest.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/topperformers/GetTopPerformersTest.kt index f2704022c75c..388508579517 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/topperformers/GetTopPerformersTest.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/ui/dashboard/topperformers/GetTopPerformersTest.kt @@ -4,12 +4,14 @@ import com.woocommerce.android.ui.analytics.hub.sync.AnalyticsUpdateDataStore import com.woocommerce.android.ui.analytics.ranges.StatsTimeRangeSelection import com.woocommerce.android.ui.dashboard.data.StatsRepository import com.woocommerce.android.ui.dashboard.domain.GetTopPerformers +import com.woocommerce.android.util.LocalizedDatePatternsTestRule import com.woocommerce.android.util.ResultWithOutdatedFlag import com.woocommerce.android.viewmodel.BaseUnitTest import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import org.assertj.core.api.Assertions.assertThat +import org.junit.Rule import org.junit.Test import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt @@ -27,6 +29,9 @@ import java.util.Locale @ExperimentalCoroutinesApi class GetTopPerformersTest : BaseUnitTest() { + @get:Rule + val localizedDatePatterns = LocalizedDatePatternsTestRule() + private val statsRepository: StatsRepository = mock() private val analyticsUpdateDataStore: AnalyticsUpdateDataStore = mock() @@ -174,11 +179,13 @@ class GetTopPerformersTest : BaseUnitTest() { ) ) val ANY_SELECTION_TYPE = StatsTimeRangeSelection.SelectionType.WEEK_TO_DATE - val ANY_STATS_RANGE_SELECTION = StatsTimeRangeSelection.build( - selectionType = ANY_SELECTION_TYPE, - referenceDate = Date(), - calendar = Calendar.getInstance(), - locale = Locale.getDefault() - ) + val ANY_STATS_RANGE_SELECTION by lazy { + StatsTimeRangeSelection.build( + selectionType = ANY_SELECTION_TYPE, + referenceDate = Date(), + calendar = Calendar.getInstance(), + locale = Locale.getDefault() + ) + } } } diff --git a/WooCommerce/src/test/kotlin/com/woocommerce/android/util/LocalizedDatePatternsTestRule.kt b/WooCommerce/src/test/kotlin/com/woocommerce/android/util/LocalizedDatePatternsTestRule.kt index 17d3ba1787dd..69ecbee2c857 100644 --- a/WooCommerce/src/test/kotlin/com/woocommerce/android/util/LocalizedDatePatternsTestRule.kt +++ b/WooCommerce/src/test/kotlin/com/woocommerce/android/util/LocalizedDatePatternsTestRule.kt @@ -1,29 +1,58 @@ package com.woocommerce.android.util +import android.icu.text.DateIntervalFormat +import android.icu.util.DateInterval import android.text.format.DateFormat import org.junit.rules.TestWatcher import org.junit.runner.Description +import org.mockito.MockedConstruction import org.mockito.MockedStatic import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale /** - * Stubs [DateFormat.getBestDateTimePattern] with the patterns CLDR returns for a month-first locale, for tests - * that format dates only as a side effect of what they actually cover. + * Stubs the ICU date APIs with the values CLDR returns for en-US, for tests that format dates only as a side + * effect of what they actually cover. The interval stub joins both full dates instead of replicating ICU's elision. */ class LocalizedDatePatternsTestRule : TestWatcher() { private var dateFormat: MockedStatic? = null + private var intervalFormat: MockedStatic? = null + private var dateInterval: MockedConstruction? = null override fun starting(description: Description?) { - dateFormat = Mockito.mockStatic(DateFormat::class.java) + val lenient = Mockito.withSettings().strictness(Strictness.LENIENT) + + dateFormat = Mockito.mockStatic(DateFormat::class.java, lenient) whenever(DateFormat.getBestDateTimePattern(any(), eq("MMMd"))).thenReturn("MMM d") whenever(DateFormat.getBestDateTimePattern(any(), eq("yMMMd"))).thenReturn("MMM d, y") whenever(DateFormat.getBestDateTimePattern(any(), eq("EEEEMMMd"))).thenReturn("EEEE, MMM d") + + dateInterval = Mockito.mockConstruction(DateInterval::class.java, lenient) { mock, context -> + whenever(mock.fromDate).thenReturn(context.arguments()[0] as Long) + whenever(mock.toDate).thenReturn(context.arguments()[1] as Long) + } + + val enUsIntervalFormat = Mockito.mock(DateIntervalFormat::class.java, lenient) + whenever(enUsIntervalFormat.format(any())).thenAnswer { invocation -> + val interval = invocation.getArgument(0) + val format = SimpleDateFormat("MMM d, yyyy", Locale.US) + "${format.format(Date(interval.fromDate))} – ${format.format(Date(interval.toDate))}" + } + intervalFormat = Mockito.mockStatic(DateIntervalFormat::class.java, lenient) + whenever(DateIntervalFormat.getInstance(eq("yMMMd"), any())).thenReturn(enUsIntervalFormat) } override fun finished(description: Description?) { + dateInterval?.close() + dateInterval = null + intervalFormat?.close() + intervalFormat = null dateFormat?.close() dateFormat = null }