From 9ea7c268f5ff85e083645a9cb1fec208ec946887 Mon Sep 17 00:00:00 2001 From: Maximilian Keppeler Date: Sun, 4 Feb 2024 18:25:40 +0800 Subject: [PATCH] (Calendar) Setting the initial week #80 --- .../calendar/functional/CalendarViewTests.kt | 144 ++++++++++++++++-- .../sheets/calendar/CalendarState.kt | 5 +- .../sheets/calendar/models/CalendarConfig.kt | 4 +- .../sheets/calendar/utils/Utils.kt | 49 +++--- 4 files changed, 171 insertions(+), 31 deletions(-) diff --git a/calendar/src/androidTest/java/com/maxkeppeler/sheets/calendar/functional/CalendarViewTests.kt b/calendar/src/androidTest/java/com/maxkeppeler/sheets/calendar/functional/CalendarViewTests.kt index 1393ee10..3055b2f7 100644 --- a/calendar/src/androidTest/java/com/maxkeppeler/sheets/calendar/functional/CalendarViewTests.kt +++ b/calendar/src/androidTest/java/com/maxkeppeler/sheets/calendar/functional/CalendarViewTests.kt @@ -19,6 +19,7 @@ package com.maxkeppeler.sheets.calendar.functional import android.util.Range import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.performClick @@ -45,7 +46,7 @@ class CalendarViewTests { val rule = createComposeRule() @Test - fun calendarViewDateSelectionSuccess() { + fun givenCalendarView_whenDateSelected_thenDateSelectionSuccess() { val testDate = LocalDate.now() .withDayOfMonth(12) @@ -65,7 +66,7 @@ class CalendarViewTests { } @Test - fun calendarViewDateSelectionInvalid() { + fun givenCalendarView_whenNoDateSelected_thenDateSelectionInvalid() { rule.setContentAndWaitForIdle { CalendarView( useCaseState = UseCaseState(visible = true), @@ -76,7 +77,7 @@ class CalendarViewTests { } @Test - fun calendarViewDatesSelectionSuccess() { + fun givenCalendarView_whenMultipleDatesSelected_thenDatesSelectionSuccess() { val testDates = listOf( LocalDate.now().withDayOfMonth(2), LocalDate.now().withDayOfMonth(8), @@ -104,7 +105,7 @@ class CalendarViewTests { } @Test - fun calendarViewDatesSelectionInvalid() { + fun givenCalendarView_whenNoDatesSelected_thenDatesSelectionInvalid() { rule.setContentAndWaitForIdle { CalendarView( useCaseState = UseCaseState(visible = true), @@ -115,7 +116,7 @@ class CalendarViewTests { } @Test - fun calendarViewPeriodSelectionSuccess() { + fun givenCalendarView_whenDateSelectedWithStyleMonthAndDisabledDates_thenDateSelectionStyleMonthConfigDatesDisabled() { val testStartDate = LocalDate.now().withDayOfMonth(2) val testEndDate = LocalDate.now().withDayOfMonth(12) @@ -147,7 +148,7 @@ class CalendarViewTests { } @Test - fun calendarViewDateSelectionStyleMonthConfigDatesDisabled() { + fun givenCalendarView_whenMultipleDatesSelectedWithStyleMonthAndDisabledDates_thenDatesSelectionStyleMonthConfigDatesDisabled() { val testDate = LocalDate.now().withDayOfMonth(15) val newDates = listOf( testDate.plusDays(2), @@ -183,7 +184,7 @@ class CalendarViewTests { } @Test - fun calendarViewDatesSelectionStyleMonthConfigDatesDisabled() { + fun givenCalendarView_whenPeriodSelectedWithStyleMonthAndDisabledDates_thenPeriodSelectionStyleMonthConfigDatesDisabled() { val testDate = LocalDate.now().withDayOfMonth(15) val defaultDates = listOf( testDate.minusDays(10), @@ -224,7 +225,7 @@ class CalendarViewTests { @Test - fun calendarViewPeriodSelectionStyleMonthConfigDatesDisabled() { + fun givenCalendarView_whenPeriodSelectedWithStyleMonthAndDisabledDatesAlt_thenPeriodSelectionStyleMonthConfigDatesDisabled() { val testDate = LocalDate.now().withDayOfMonth(15) val disabledDates = listOf( testDate.minusDays(1), @@ -262,7 +263,7 @@ class CalendarViewTests { } @Test - fun calendarViewPeriodSelectionInvalid() { + fun givenCalendarView_whenNoPeriodSelected_thenPeriodSelectionInvalid() { rule.setContentAndWaitForIdle { CalendarView( useCaseState = UseCaseState(visible = true), @@ -273,7 +274,7 @@ class CalendarViewTests { } @Test - fun calendarViewPeriodSelectionInvalidSelectEndDateBeforeStartDate() { + fun givenCalendarView_whenEndDateSelectedBeforeStartDate_thenPeriodSelectionInvalidSelectEndDateBeforeStartDate() { val testStartDate = LocalDate.now().withDayOfMonth(12) val testEndDate = LocalDate.now().withDayOfMonth(2) rule.setContentAndWaitForIdle { @@ -297,7 +298,7 @@ class CalendarViewTests { } @Test - fun calendarViewDisplaysCalendarStyleWeek() { + fun givenCalendarView_whenCalendarStyleWeek_thenCalendarViewDisplaysCalendarStyleWeek() { rule.setContentAndWaitForIdle { CalendarView( useCaseState = UseCaseState(visible = true), @@ -309,7 +310,7 @@ class CalendarViewTests { } @Test - fun calendarViewDisplaysCalendarStyleMonth() { + fun givenCalendarView_whenCalendarStyleMonth_thenCalendarViewDisplaysCalendarStyleMonth() { rule.setContentAndWaitForIdle { CalendarView( useCaseState = UseCaseState(visible = true), @@ -320,4 +321,123 @@ class CalendarViewTests { rule.onPositiveButton().assertIsNotEnabled() } + + @Test + fun givenCalendarView_whenDateSelectedWithCameraDate_thenDisplayCorrectTime() { + val testDate = LocalDate.now().withDayOfMonth(15) + val testCameraDate = LocalDate.now().minusMonths(2) + rule.setContentAndWaitForIdle { + CalendarView( + useCaseState = UseCaseState(visible = true), + selection = CalendarSelection.Date( + selectedDate = testDate, + onSelectDate = { date -> } + ), + config = CalendarConfig( + style = CalendarStyle.MONTH, + cameraDate = testCameraDate + ) + ) + } + + rule.onNodeWithTags( + TestTags.CALENDAR_DATE_SELECTION, + testCameraDate.format(DateTimeFormatter.ISO_DATE) + ).apply { + assertExists() + assertIsDisplayed() + } + } + + @Test + fun givenCalendarView_whenDateSelectedWithCameraDateOutsideBoundary_thenDisplaySelectedTime() { + val testDate = LocalDate.now().withDayOfMonth(15) + val testBoundary = testDate.minusYears(2)..testDate.plusYears(2) + val testCameraDate = LocalDate.now().minusYears(4) + rule.setContentAndWaitForIdle { + CalendarView( + useCaseState = UseCaseState(visible = true), + selection = CalendarSelection.Date( + selectedDate = testDate, + onSelectDate = { date -> } + ), + config = CalendarConfig( + boundary = testBoundary, + cameraDate = testCameraDate, + style = CalendarStyle.MONTH + ) + ) + } + + rule.onNodeWithTags( + TestTags.CALENDAR_DATE_SELECTION, + testCameraDate.format(DateTimeFormatter.ISO_DATE) + ).assertDoesNotExist() + + rule.onNodeWithTags( + TestTags.CALENDAR_DATE_SELECTION, + testDate.format(DateTimeFormatter.ISO_DATE) + ).apply { + assertExists() + assertIsDisplayed() + } + } + + + @Test + fun givenCalendarView_whenCameraDateOutsideBoundaryCurrentTimeInsideBoundary_thenDisplayCurrentTime() { + val testDate = LocalDate.now() + val testBoundary = testDate.minusYears(2)..testDate.plusYears(2) + val testCameraDate = LocalDate.now().minusYears(4) + rule.setContentAndWaitForIdle { + CalendarView( + useCaseState = UseCaseState(visible = true), + selection = CalendarSelection.Date( + onSelectDate = { date -> } + ), + config = CalendarConfig( + boundary = testBoundary, + cameraDate = testCameraDate, + style = CalendarStyle.MONTH + ) + ) + } + + rule.onNodeWithTags( + TestTags.CALENDAR_DATE_SELECTION, + testDate.format(DateTimeFormatter.ISO_DATE) + ).apply { + assertExists() + assertIsDisplayed() + } + } + + @Test + fun givenCalendarView_whenCameraDateOutsideBoundaryCurrentTimeOutsideBoundary_thenDisplayCurrentTime() { + val testDate = LocalDate.now() + val testBoundary = testDate.plusYears(2)..testDate.plusYears(4) + val testCameraDate = LocalDate.now().minusYears(4) + rule.setContentAndWaitForIdle { + CalendarView( + useCaseState = UseCaseState(visible = true), + selection = CalendarSelection.Date( + onSelectDate = { date -> } + ), + config = CalendarConfig( + boundary = testBoundary, + cameraDate = testCameraDate, + style = CalendarStyle.MONTH + ) + ) + } + + rule.onNodeWithTags( + TestTags.CALENDAR_DATE_SELECTION, + testBoundary.start.format(DateTimeFormatter.ISO_DATE) + ).apply { + assertExists() + assertIsDisplayed() + } + } + } \ No newline at end of file diff --git a/calendar/src/main/java/com/maxkeppeler/sheets/calendar/CalendarState.kt b/calendar/src/main/java/com/maxkeppeler/sheets/calendar/CalendarState.kt index 9fcabb45..7f2b71b1 100644 --- a/calendar/src/main/java/com/maxkeppeler/sheets/calendar/CalendarState.kt +++ b/calendar/src/main/java/com/maxkeppeler/sheets/calendar/CalendarState.kt @@ -38,6 +38,7 @@ import com.maxkeppeler.sheets.calendar.utils.endOfMonth import com.maxkeppeler.sheets.calendar.utils.endOfWeek import com.maxkeppeler.sheets.calendar.utils.endValue import com.maxkeppeler.sheets.calendar.utils.getInitialCameraDate +import com.maxkeppeler.sheets.calendar.utils.getInitialCustomCameraDate import com.maxkeppeler.sheets.calendar.utils.jumpNext import com.maxkeppeler.sheets.calendar.utils.jumpPrev import com.maxkeppeler.sheets.calendar.utils.rangeValue @@ -65,7 +66,9 @@ internal class CalendarState( val today by mutableStateOf(LocalDate.now()) var mode by mutableStateOf(stateData?.mode ?: CalendarDisplayMode.CALENDAR) var cameraDate by mutableStateOf( - stateData?.cameraDate ?: selection.getInitialCameraDate(config.boundary) + stateData?.cameraDate + ?: getInitialCustomCameraDate(config.cameraDate, config.boundary) + ?: getInitialCameraDate(selection, config.boundary) ) var date = mutableStateOf(stateData?.date ?: selection.dateValue) var dates = mutableStateListOf(*(stateData?.dates ?: selection.datesValue)) diff --git a/calendar/src/main/java/com/maxkeppeler/sheets/calendar/models/CalendarConfig.kt b/calendar/src/main/java/com/maxkeppeler/sheets/calendar/models/CalendarConfig.kt index 5d61e8a6..f8fe699d 100644 --- a/calendar/src/main/java/com/maxkeppeler/sheets/calendar/models/CalendarConfig.kt +++ b/calendar/src/main/java/com/maxkeppeler/sheets/calendar/models/CalendarConfig.kt @@ -20,12 +20,13 @@ import com.maxkeppeker.sheets.core.models.base.BaseConfigs import com.maxkeppeker.sheets.core.utils.BaseConstants.DEFAULT_ICON_STYLE import com.maxkeppeler.sheets.calendar.utils.Constants import java.time.LocalDate -import java.util.* +import java.util.Locale /** * The general configuration for the calendar dialog. * @param locale The locale of the calendar. * @param style The style of the calendar. + * @param cameraDate The date that is initially displayed when the calendar is opened. * @param monthSelection Allow the direct selection of a month. * @param yearSelection Allow the direct selection of a year. * @param boundary The range of dates that are displayed. @@ -35,6 +36,7 @@ import java.util.* class CalendarConfig( val locale: Locale = Locale.getDefault(), val style: CalendarStyle = CalendarStyle.MONTH, + val cameraDate: LocalDate? = null, val monthSelection: Boolean = Constants.DEFAULT_MONTH_SELECTION, val yearSelection: Boolean = Constants.DEFAULT_YEAR_SELECTION, val boundary: ClosedRange = Constants.DEFAULT_RANGE, diff --git a/calendar/src/main/java/com/maxkeppeler/sheets/calendar/utils/Utils.kt b/calendar/src/main/java/com/maxkeppeler/sheets/calendar/utils/Utils.kt index 212a7577..6e0b63c1 100644 --- a/calendar/src/main/java/com/maxkeppeler/sheets/calendar/utils/Utils.kt +++ b/calendar/src/main/java/com/maxkeppeler/sheets/calendar/utils/Utils.kt @@ -16,13 +16,18 @@ package com.maxkeppeler.sheets.calendar.utils import androidx.annotation.RestrictTo -import com.maxkeppeler.sheets.calendar.models.* +import com.maxkeppeler.sheets.calendar.models.CalendarConfig +import com.maxkeppeler.sheets.calendar.models.CalendarData +import com.maxkeppeler.sheets.calendar.models.CalendarDateData +import com.maxkeppeler.sheets.calendar.models.CalendarMonthData +import com.maxkeppeler.sheets.calendar.models.CalendarSelection +import com.maxkeppeler.sheets.calendar.models.CalendarStyle import java.time.DayOfWeek import java.time.LocalDate import java.time.Month import java.time.temporal.TemporalAdjusters import java.time.temporal.WeekFields -import java.util.* +import java.util.Locale /** * Returns the week of the week-based-year for this [LocalDate]. @@ -95,9 +100,11 @@ internal val LocalDate.previousWeek: LocalDate get() = when { dayOfMonth == Constants.FIRST_DAY_IN_MONTH && dayOfWeek != DayOfWeek.MONDAY -> with(DayOfWeek.MONDAY) + dayOfMonth >= Constants.DAYS_IN_WEEK || dayOfMonth == Constants.FIRST_DAY_IN_MONTH && dayOfWeek == DayOfWeek.MONDAY -> minusWeeks(1) + else -> withDayOfMonth(Constants.FIRST_DAY_IN_MONTH) } @@ -150,27 +157,34 @@ fun LocalDate.jumpNext(config: CalendarConfig): LocalDate = when (config.style) /** * Returns the initial date to be displayed on the CalendarView based on the selection mode. - * - * The initial camera date is calculated based on the selected mode. If the mode is [CalendarSelection.Date], - * the selected date is returned. If the mode is [CalendarSelection.Dates], the first selected date is returned. - * If the mode is [CalendarSelection.Period], the lower range of the selected period is returned. - * - * If the selected mode doesn't have a date, the current date will be returned as the initial camera date. - * - * @return The initial camera date. + * @param selection The selection mode. + * @param boundary The boundary of the calendar. + * @return The initial date to be displayed on the CalendarView. */ -internal fun CalendarSelection.getInitialCameraDate(boundary: ClosedRange): LocalDate { - val cameraDateBasedOnMode = when (this) { - is CalendarSelection.Date -> selectedDate - is CalendarSelection.Dates -> selectedDates?.firstOrNull() - is CalendarSelection.Period -> selectedRange?.lower - } ?: kotlin.run { +internal fun getInitialCameraDate(selection: CalendarSelection, boundary: ClosedRange): LocalDate { + val cameraDateBasedOnMode = when (selection) { + is CalendarSelection.Date -> selection.selectedDate + is CalendarSelection.Dates -> selection.selectedDates?.firstOrNull() + is CalendarSelection.Period -> selection.selectedRange?.lower + } ?: run { val now = LocalDate.now() - if (now in boundary) now else boundary.endInclusive + if (now in boundary) now else boundary.start } return cameraDateBasedOnMode.startOfWeekOrMonth } +/** + * Returns the custom initial date in case the camera date is within the boundary. Otherwise, it returns null. + * + * @param cameraDate The initial camera date. + * @param boundary The boundary of the calendar. + * @return The initial camera date if it's within the boundary, otherwise null. + */ +internal fun getInitialCustomCameraDate( + cameraDate: LocalDate?, + boundary: ClosedRange +): LocalDate? = cameraDate?.takeIf { it in boundary }?.startOfWeekOrMonth + /** * Get selection value of date. */ @@ -300,6 +314,7 @@ internal fun calcCalendarDateData( is CalendarSelection.Dates -> { selectedDates?.contains(date) ?: false } + is CalendarSelection.Period -> { val selectedStart = selectedRange.first == date selectedStartInit = selectedStart && selectedRange.second != null