From 635a0133444ad954a6e93df24f374540f9d1230e Mon Sep 17 00:00:00 2001 From: Arturo Mejia Date: Sun, 21 Jul 2019 18:22:54 -0400 Subject: [PATCH] Closes #3843: Ported the Month picker widget from Fennec --- .../gecko/prompt/GeckoPromptDelegate.kt | 1 + .../concept/engine/prompt/PromptRequest.kt | 2 +- .../feature/prompts/PromptFeature.kt | 1 + .../prompts/TimePickerDialogFragment.kt | 46 +++-- .../feature/prompts/ext/Calendar.kt | 39 ++++ .../prompts/widget/MonthAndYearPicker.kt | 179 ++++++++++++++++ ...zac_feature_promps_widget_month_picker.xml | 40 ++++ .../res/values/strings-no-translatable.xml | 21 ++ .../prompts/src/main/res/values/strings.xml | 26 +++ .../feature/prompts/PromptFeatureTest.kt | 7 +- .../prompts/TimePickerDialogFragmentTest.kt | 55 ++++- .../prompts/widget/MonthAndYearPickerTest.kt | 194 ++++++++++++++++++ docs/changelog.md | 3 + 13 files changed, 590 insertions(+), 24 deletions(-) create mode 100644 components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/Calendar.kt create mode 100644 components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/MonthAndYearPicker.kt create mode 100644 components/feature/prompts/src/main/res/layout/mozac_feature_promps_widget_month_picker.xml create mode 100644 components/feature/prompts/src/main/res/values/strings-no-translatable.xml create mode 100644 components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/MonthAndYearPickerTest.kt diff --git a/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt b/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt index be8a8449f2e..073ad31eada 100644 --- a/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt +++ b/components/browser/engine-gecko-nightly/src/main/java/mozilla/components/browser/engine/gecko/prompt/GeckoPromptDelegate.kt @@ -389,6 +389,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe val selectionType = when (format) { "HH:mm" -> TimeSelection.Type.TIME + "yyyy-MM" -> TimeSelection.Type.MONTH "yyyy-MM-dd'T'HH:mm" -> TimeSelection.Type.DATE_AND_TIME else -> TimeSelection.Type.DATE } diff --git a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt index 72f23097e91..5bc28ee8952 100644 --- a/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt +++ b/components/concept/engine/src/main/java/mozilla/components/concept/engine/prompt/PromptRequest.kt @@ -90,7 +90,7 @@ sealed class PromptRequest { val onClear: () -> Unit ) : PromptRequest() { enum class Type { - DATE, DATE_AND_TIME, TIME + DATE, DATE_AND_TIME, TIME, MONTH } } diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt index e3549c48a8d..313177320ea 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/PromptFeature.kt @@ -305,6 +305,7 @@ class PromptFeature( TimeSelection.Type.DATE -> TimePickerDialogFragment.SELECTION_TYPE_DATE TimeSelection.Type.DATE_AND_TIME -> TimePickerDialogFragment.SELECTION_TYPE_DATE_AND_TIME TimeSelection.Type.TIME -> TimePickerDialogFragment.SELECTION_TYPE_TIME + TimeSelection.Type.MONTH -> TimePickerDialogFragment.SELECTION_TYPE_MONTH } with(promptRequest) { diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/TimePickerDialogFragment.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/TimePickerDialogFragment.kt index ec8c88743bf..c8af9e81103 100644 --- a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/TimePickerDialogFragment.kt +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/TimePickerDialogFragment.kt @@ -22,6 +22,13 @@ import android.widget.DatePicker import android.widget.TimePicker import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PRIVATE +import mozilla.components.feature.prompts.ext.day +import mozilla.components.feature.prompts.ext.hour +import mozilla.components.feature.prompts.ext.minute +import mozilla.components.feature.prompts.ext.month +import mozilla.components.feature.prompts.ext.toCalendar +import mozilla.components.feature.prompts.ext.year +import mozilla.components.feature.prompts.widget.MonthAndYearPicker import java.util.Calendar import java.util.Date @@ -37,7 +44,8 @@ private const val KEY_SELECTION_TYPE = "KEY_SELECTION_TYPE" @Suppress("TooManyFunctions") internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnDateChangedListener, TimePicker.OnTimeChangedListener, TimePickerDialog.OnTimeSetListener, - DatePickerDialog.OnDateSetListener, DialogInterface.OnClickListener { + DatePickerDialog.OnDateSetListener, DialogInterface.OnClickListener, + MonthAndYearPicker.OnDateSetListener { private val initialDate: Date by lazy { safeArguments.getSerializable(KEY_INITIAL_DATE) as Date } private val minimumDate: Date? by lazy { safeArguments.getSerializable((KEY_MIN_DATE)) as? Date } private val maximumDate: Date? by lazy { safeArguments.getSerializable(KEY_MAX_DATE) as? Date } @@ -78,6 +86,14 @@ internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnD it.setButton(BUTTON_POSITIVE, context.getString(R.string.mozac_feature_prompts_set_date), this) it.setButton(BUTTON_NEGATIVE, context.getString(R.string.mozac_feature_prompts_cancel), this) } + SELECTION_TYPE_MONTH -> AlertDialog.Builder(context) + .setTitle(R.string.mozac_feature_prompts_set_month) + .setView(inflateDateMonthPicker()) + .create() + .also { + it.setButton(BUTTON_POSITIVE, context.getString(R.string.mozac_feature_prompts_set_date), this) + it.setButton(BUTTON_NEGATIVE, context.getString(R.string.mozac_feature_prompts_cancel), this) + } else -> throw IllegalArgumentException() } @@ -112,6 +128,16 @@ internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnD return view } + private fun inflateDateMonthPicker(): View { + return MonthAndYearPicker( + context = requireContext(), + selectedDate = initialDate.toCalendar(), + maxDate = maximumDate?.toCalendar() ?: MonthAndYearPicker.getDefaultMaxDate(), + minDate = minimumDate?.toCalendar() ?: MonthAndYearPicker.getDefaultMinDate(), + dateSetListener = this + ) + } + @Suppress("DEPRECATION") private fun initTimePicker(picker: TimePicker, cal: Calendar) { if (Build.VERSION.SDK_INT >= M) { @@ -145,6 +171,10 @@ internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnD onClick(null, BUTTON_POSITIVE) } + override fun onDateSet(picker: MonthAndYearPicker, month: Int, year: Int) { + onDateChanged(null, year, month, 0) + } + override fun onTimeChanged(picker: TimePicker?, hourOfDay: Int, minute: Int) { val calendar = selectedDate.toCalendar() calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) @@ -203,18 +233,6 @@ internal class TimePickerDialogFragment : PromptDialogFragment(), DatePicker.OnD const val SELECTION_TYPE_DATE = 1 const val SELECTION_TYPE_DATE_AND_TIME = 2 const val SELECTION_TYPE_TIME = 3 + const val SELECTION_TYPE_MONTH = 4 } } - -internal fun Date.toCalendar() = Calendar.getInstance().also { it.time = this } - -internal val Calendar.minute: Int - get() = get(Calendar.MINUTE) -internal val Calendar.hour: Int - get() = get(Calendar.HOUR_OF_DAY) -internal val Calendar.day: Int - get() = get(Calendar.DAY_OF_MONTH) -internal val Calendar.year: Int - get() = get(Calendar.YEAR) -internal val Calendar.month: Int - get() = get(Calendar.MONTH) diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/Calendar.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/Calendar.kt new file mode 100644 index 00000000000..609b6546942 --- /dev/null +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/ext/Calendar.kt @@ -0,0 +1,39 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.prompts.ext + +import java.util.Calendar +import java.util.Date + +internal fun Date.toCalendar() = Calendar.getInstance().also { it.time = this } + +internal val Calendar.minute: Int + get() = get(Calendar.MINUTE) +internal val Calendar.hour: Int + get() = get(Calendar.HOUR_OF_DAY) +internal var Calendar.day: Int + get() = get(Calendar.DAY_OF_MONTH) + set(value) { + set(Calendar.DAY_OF_MONTH, value) + } +internal var Calendar.year: Int + get() = get(Calendar.YEAR) + set(value) { + set(Calendar.YEAR, value) + } + +internal var Calendar.month: Int + get() = get(Calendar.MONTH) + set(value) { + set(Calendar.MONTH, value) + } + +internal fun Calendar.minMonth(): Int = getMinimum(Calendar.MONTH) +internal fun Calendar.maxMonth(): Int = getActualMaximum(Calendar.MONTH) +internal fun Calendar.minDay(): Int = getMinimum(Calendar.DAY_OF_MONTH) +internal fun Calendar.maxDay(): Int = getActualMaximum(Calendar.DAY_OF_MONTH) +internal fun Calendar.minYear(): Int = getMinimum(Calendar.YEAR) +internal fun Calendar.maxYear(): Int = getActualMaximum(Calendar.YEAR) +internal fun now() = Calendar.getInstance() diff --git a/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/MonthAndYearPicker.kt b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/MonthAndYearPicker.kt new file mode 100644 index 00000000000..3f273b2f6ef --- /dev/null +++ b/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/widget/MonthAndYearPicker.kt @@ -0,0 +1,179 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package mozilla.components.feature.prompts.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.widget.NumberPicker +import android.widget.ScrollView +import androidx.annotation.VisibleForTesting +import mozilla.components.feature.prompts.R +import mozilla.components.feature.prompts.ext.month +import mozilla.components.feature.prompts.ext.now +import mozilla.components.feature.prompts.ext.year +import java.util.Calendar + +/** + * UI widget that allows to select a month and a year. + */ +@SuppressLint("ViewConstructor") // This view is only instantiated in code +internal class MonthAndYearPicker @JvmOverloads constructor( + context: Context, + private val selectedDate: Calendar = now(), + private val maxDate: Calendar = getDefaultMaxDate(), + private val minDate: Calendar = getDefaultMinDate(), + internal var dateSetListener: OnDateSetListener? = null +) : ScrollView(context), NumberPicker.OnValueChangeListener { + + @VisibleForTesting + internal val monthView: NumberPicker + @VisibleForTesting + internal val yearView: NumberPicker + private val monthsLabels: Array + + init { + inflate(context, R.layout.mozac_feature_promps_widget_month_picker, this) + + adjustMinMaxDateIfAreInIllogicalRange() + adjustIfSelectedDateIsInIllogicalRange() + + monthsLabels = context.resources.getStringArray(R.array.mozac_feature_prompts_months) + + monthView = findViewById(R.id.month_chooser) + yearView = findViewById(R.id.year_chooser) + + iniMonthView() + iniYearView() + } + + @Suppress("LongMethod") + override fun onValueChange(view: NumberPicker, oldVal: Int, newVal: Int) { + var month = 0 + var year = 0 + when (view.id) { + R.id.month_chooser -> { + month = newVal + // Wrapping months to update greater fields + if (oldVal == view.maxValue && newVal == view.minValue) { + yearView.value += 1 + if (!yearView.value.isMinYear()) { + month = Calendar.JANUARY + } + } else if (oldVal == view.minValue && newVal == view.maxValue) { + yearView.value -= 1 + if (!yearView.value.isMaxYear()) { + month = Calendar.DECEMBER + } + } + year = yearView.value + } + R.id.year_chooser -> { + month = monthView.value + year = newVal + } + } + + selectedDate.month = month + selectedDate.year = year + updateMonthView(month) + dateSetListener?.onDateSet(this, month + 1, year) // Month is zero based + } + + private fun Int.isMinYear() = minDate.year == this + private fun Int.isMaxYear() = maxDate.year == this + + private fun iniMonthView() { + monthView.setOnValueChangedListener(this) + monthView.setOnLongPressUpdateInterval(SPEED_MONTH_SPINNER) + updateMonthView(selectedDate.month) + } + + private fun iniYearView() { + val year = selectedDate.year + val max = maxDate.year + val min = minDate.year + + yearView.init(year, min, max) + yearView.wrapSelectorWheel = false + yearView.setOnLongPressUpdateInterval(SPEED_YEAR_SPINNER) + } + + private fun updateMonthView(month: Int) { + var min = Calendar.JANUARY + var max = Calendar.DECEMBER + + if (selectedDate.year.isMinYear()) { + min = minDate.month + } + + if (selectedDate.year.isMaxYear()) { + max = maxDate.month + } + + monthView.apply { + displayedValues = null + minValue = min + maxValue = max + displayedValues = monthsLabels.copyOfRange(monthView.minValue, monthView.maxValue + 1) + value = month + wrapSelectorWheel = true + } + } + + private fun adjustMinMaxDateIfAreInIllogicalRange() { + // If the input date range is illogical/garbage, we should not restrict the input range (i.e. allow the + // user to select any date). If we try to make any assumptions based on the illogical min/max date we could + // potentially prevent the user from selecting dates that are in the developers intended range, so it's best + // to allow anything. + if (maxDate.before(minDate)) { + minDate.timeInMillis = getDefaultMinDate().timeInMillis + maxDate.timeInMillis = getDefaultMaxDate().timeInMillis + } + } + + private fun adjustIfSelectedDateIsInIllogicalRange() { + if (selectedDate.before(minDate) || selectedDate.after(maxDate)) { + selectedDate.timeInMillis = minDate.timeInMillis + } + } + + private fun NumberPicker.init(currentValue: Int, min: Int, max: Int) { + minValue = min + maxValue = max + value = currentValue + setOnValueChangedListener(this@MonthAndYearPicker) + } + + interface OnDateSetListener { + fun onDateSet(picker: MonthAndYearPicker, month: Int, year: Int) + } + + companion object { + + private const val SPEED_MONTH_SPINNER = 200L + private const val SPEED_YEAR_SPINNER = 100L + private const val DEFAULT_VALUE = -1 + + @VisibleForTesting + internal const val DEFAULT_MAX_YEAR = 9999 + + @VisibleForTesting + internal const val DEFAULT_MIN_YEAR = 1 + + internal fun getDefaultMinDate(): Calendar { + return now().apply { + month = Calendar.JANUARY + year = DEFAULT_MIN_YEAR + } + } + + internal fun getDefaultMaxDate(): Calendar { + return now().apply { + month = Calendar.DECEMBER + year = DEFAULT_MAX_YEAR + } + } + } +} diff --git a/components/feature/prompts/src/main/res/layout/mozac_feature_promps_widget_month_picker.xml b/components/feature/prompts/src/main/res/layout/mozac_feature_promps_widget_month_picker.xml new file mode 100644 index 00000000000..4d004923511 --- /dev/null +++ b/components/feature/prompts/src/main/res/layout/mozac_feature_promps_widget_month_picker.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + diff --git a/components/feature/prompts/src/main/res/values/strings-no-translatable.xml b/components/feature/prompts/src/main/res/values/strings-no-translatable.xml new file mode 100644 index 00000000000..70151e45cc2 --- /dev/null +++ b/components/feature/prompts/src/main/res/values/strings-no-translatable.xml @@ -0,0 +1,21 @@ + + + + + + @string/mozac_feature_prompts_jan + @string/mozac_feature_prompts_feb + @string/mozac_feature_prompts_mar + @string/mozac_feature_prompts_apr + @string/mozac_feature_prompts_may + @string/mozac_feature_prompts_jun + @string/mozac_feature_prompts_jul + @string/mozac_feature_prompts_aug + @string/mozac_feature_prompts_sep + @string/mozac_feature_prompts_oct + @string/mozac_feature_prompts_nov + @string/mozac_feature_prompts_dec + + diff --git a/components/feature/prompts/src/main/res/values/strings.xml b/components/feature/prompts/src/main/res/values/strings.xml index a65701bc6dd..37f6b2f8938 100644 --- a/components/feature/prompts/src/main/res/values/strings.xml +++ b/components/feature/prompts/src/main/res/values/strings.xml @@ -28,4 +28,30 @@ Allow Deny + + Pick a month + + Jan + + Feb + + Mar + + Apr + + May + + Jun + + Jul + + Aug + + Sep + + Oct + + Nov + + Dec diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt index 2d179c2a013..efd8771bc04 100644 --- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/PromptFeatureTest.kt @@ -326,10 +326,11 @@ class PromptFeatureTest { val timeSelectionTypes = listOf( PromptRequest.TimeSelection.Type.DATE, PromptRequest.TimeSelection.Type.DATE_AND_TIME, - PromptRequest.TimeSelection.Type.TIME + PromptRequest.TimeSelection.Type.TIME, + PromptRequest.TimeSelection.Type.MONTH ) - timeSelectionTypes.forEach { _ -> + timeSelectionTypes.forEach { type -> val session = getSelectedSession() var onClearWasCalled = false var selectedDate: Date? = null @@ -337,7 +338,7 @@ class PromptFeatureTest { "title", Date(0), null, null, - PromptRequest.TimeSelection.Type.DATE, + type, { date -> selectedDate = date }) { onClearWasCalled = true } diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/TimePickerDialogFragmentTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/TimePickerDialogFragmentTest.kt index 2cf5153e9d5..82e7927b69d 100644 --- a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/TimePickerDialogFragmentTest.kt +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/TimePickerDialogFragmentTest.kt @@ -11,10 +11,15 @@ import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.DialogInterface.BUTTON_POSITIVE import android.os.Build.VERSION_CODES.LOLLIPOP import android.widget.DatePicker +import android.widget.NumberPicker import android.widget.TimePicker import androidx.test.ext.junit.runners.AndroidJUnit4 import mozilla.components.feature.prompts.TimePickerDialogFragment.Companion.SELECTION_TYPE_DATE_AND_TIME +import mozilla.components.feature.prompts.TimePickerDialogFragment.Companion.SELECTION_TYPE_MONTH import mozilla.components.feature.prompts.TimePickerDialogFragment.Companion.SELECTION_TYPE_TIME +import mozilla.components.feature.prompts.ext.month +import mozilla.components.feature.prompts.ext.toCalendar +import mozilla.components.feature.prompts.ext.year import mozilla.components.support.ktx.kotlin.toDate import mozilla.components.support.test.any import mozilla.components.support.test.eq @@ -141,6 +146,50 @@ class TimePickerDialogFragmentTest { assertEquals(30, timePicker.minute) } + @Test + fun `building a month picker`() { + val initialDate = "2018-06".toDate("yyyy-MM") + val minDate = "2018-04".toDate("yyyy-MM") + val maxDate = "2018-09".toDate("yyyy-MM") + + val initialDateCal = initialDate.toCalendar() + val minCal = minDate.toCalendar() + val maxCal = maxDate.toCalendar() + + val fragment = spy( + TimePickerDialogFragment.newInstance( + "sessionId", + initialDate, + minDate, + maxDate, + SELECTION_TYPE_MONTH + ) + ) + + doReturn(appCompatContext).`when`(fragment).requireContext() + + val dialog = fragment.onCreateDialog(null) + dialog.show() + + val monthPicker = dialog.findViewById(R.id.month_chooser) + val yearPicker = dialog.findViewById(R.id.year_chooser) + + assertEquals(initialDateCal.year, yearPicker.value) + assertEquals(initialDateCal.month, monthPicker.value) + + assertEquals(minCal.year, yearPicker.minValue) + assertEquals(minCal.month, monthPicker.minValue) + + assertEquals(maxCal.year, yearPicker.maxValue) + assertEquals(maxCal.month, monthPicker.maxValue) + + fragment.onDateSet(mock(), 8, 2019) + val selectedDate = fragment.selectedDate.toCalendar() + + assertEquals(2019, selectedDate.year) + assertEquals(7, selectedDate.month) + } + @Test @Config(sdk = [LOLLIPOP]) @Suppress("DEPRECATION") @@ -207,12 +256,6 @@ class TimePickerDialogFragmentTest { dialog.show() } - private fun Date.toCalendar(): Calendar { - val calendar = Calendar.getInstance() - calendar.time = this - return calendar - } - private val Calendar.minutes: Int get() = get(Calendar.MINUTE) private val Calendar.hour: Int diff --git a/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/MonthAndYearPickerTest.kt b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/MonthAndYearPickerTest.kt new file mode 100644 index 00000000000..b8262f58fa6 --- /dev/null +++ b/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/widget/MonthAndYearPickerTest.kt @@ -0,0 +1,194 @@ +package mozilla.components.feature.prompts.widget + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import mozilla.components.feature.prompts.ext.month +import mozilla.components.feature.prompts.ext.now +import mozilla.components.feature.prompts.ext.toCalendar +import mozilla.components.feature.prompts.ext.year +import mozilla.components.feature.prompts.widget.MonthAndYearPicker.Companion.DEFAULT_MAX_YEAR +import mozilla.components.feature.prompts.widget.MonthAndYearPicker.Companion.DEFAULT_MIN_YEAR +import mozilla.components.support.ktx.kotlin.toDate +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import java.util.Calendar.DECEMBER +import java.util.Calendar.FEBRUARY +import java.util.Calendar.JANUARY + +@RunWith(AndroidJUnit4::class) +class MonthAndYearPickerTest { + + @Test + fun `WHEN picker widget THEN initial values must be displayed`() { + + val initialDate = "2018-06".toDate("yyyy-MM").toCalendar() + val minDate = "2018-04".toDate("yyyy-MM").toCalendar() + val maxDate = "2018-09".toDate("yyyy-MM").toCalendar() + + val monthAndYearPicker = MonthAndYearPicker( + context = testContext, + selectedDate = initialDate, + minDate = minDate, + maxDate = maxDate + ) + + with(monthAndYearPicker.monthView) { + assertEquals(initialDate.month, value) + assertEquals(minDate.month, minValue) + assertEquals(maxDate.month, maxValue) + } + + with(monthAndYearPicker.yearView) { + assertEquals(initialDate.year, value) + assertEquals(minDate.year, minValue) + assertEquals(maxDate.year, maxValue) + } + } + + @Test + fun `WHEN selectedDate is a year less than maxDate THEN month picker MUST allow selecting until the last month of the year`() { + + val initialDate = "2018-06".toDate("yyyy-MM").toCalendar() + val minDate = "2018-04".toDate("yyyy-MM").toCalendar() + val maxDate = "2019-09".toDate("yyyy-MM").toCalendar() + + val monthAndYearPicker = MonthAndYearPicker( + context = testContext, + selectedDate = initialDate, + minDate = minDate, + maxDate = maxDate + ) + + with(monthAndYearPicker.monthView) { + assertEquals(initialDate.month, value) + assertEquals(minDate.month, minValue) + assertEquals(DECEMBER, maxValue) + } + + with(monthAndYearPicker.yearView) { + assertEquals(initialDate.year, value) + assertEquals(minDate.year, minValue) + assertEquals(maxDate.year, maxValue) + } + } + + @Test + fun `WHEN changing month picker from DEC to JAN THEN year picker MUST be increased by 1`() { + val initialDate = "2018-06".toDate("yyyy-MM") + val initialCal = "2018-06".toDate("yyyy-MM").toCalendar() + + val monthAndYearPicker = MonthAndYearPicker( + context = testContext, + selectedDate = initialDate.toCalendar() + ) + + val yearView = monthAndYearPicker.yearView + assertEquals(initialCal.year, yearView.value) + + monthAndYearPicker.onValueChange(monthAndYearPicker.monthView, DECEMBER, JANUARY) + + assertEquals(initialCal.year + 1, yearView.value) + } + + @Test + fun `WHEN changing month picker from JAN to DEC THEN year picker MUST be decreased by 1`() { + val initialDate = "2018-06".toDate("yyyy-MM") + val initialCal = "2018-06".toDate("yyyy-MM").toCalendar() + + val monthAndYearPicker = MonthAndYearPicker( + context = testContext, + selectedDate = initialDate.toCalendar() + ) + + val yearView = monthAndYearPicker.yearView + assertEquals(initialCal.year, yearView.value) + + monthAndYearPicker.onValueChange(monthAndYearPicker.monthView, JANUARY, DECEMBER) + + assertEquals(initialCal.year - 1, yearView.value) + } + + @Test + fun `WHEN selecting a month or a year THEN dateSetListener MUST be notified`() { + val initialDate = "2018-06".toDate("yyyy-MM") + val initialCal = "2018-06".toDate("yyyy-MM").toCalendar() + + val monthAndYearPicker = MonthAndYearPicker( + context = testContext, + selectedDate = initialDate.toCalendar() + ) + + var newMonth = 0 + var newYear = 0 + + monthAndYearPicker.dateSetListener = object : MonthAndYearPicker.OnDateSetListener { + override fun onDateSet(picker: MonthAndYearPicker, month: Int, year: Int) { + newMonth = month + newYear = year + } + } + + assertEquals(0, newMonth) + assertEquals(0, newYear) + + val yearView = monthAndYearPicker.yearView + val monthView = monthAndYearPicker.monthView + + monthAndYearPicker.onValueChange(yearView, initialCal.year - 1, initialCal.year + 1) + + assertEquals(initialCal.year + 1, newYear) + + monthAndYearPicker.onValueChange(monthView, JANUARY, FEBRUARY) + + assertEquals(FEBRUARY + 1, newMonth) // Month is zero based + } + + @Test + fun `WHEN max or min date are in a illogical range THEN picker must allow to select the default values for max and min`() { + val initialDate = "2018-06".toDate("yyyy-MM").toCalendar() + val minDate = "2019-04".toDate("yyyy-MM").toCalendar() + val maxDate = "2018-09".toDate("yyyy-MM").toCalendar() + + val monthAndYearPicker = MonthAndYearPicker( + context = testContext, + selectedDate = initialDate, + minDate = minDate, + maxDate = maxDate + ) + + with(monthAndYearPicker.monthView) { + assertEquals(JANUARY, minValue) + assertEquals(DECEMBER, maxValue) + } + + with(monthAndYearPicker.yearView) { + assertEquals(DEFAULT_MIN_YEAR, minValue) + assertEquals(DEFAULT_MAX_YEAR, maxValue) + } + } + + @Test + fun `WHEN selecting a date that is before or after min or max date THEN selectDate will be set to min date`() { + val minDate = "2018-04".toDate("yyyy-MM").toCalendar() + val maxDate = "2018-09".toDate("yyyy-MM").toCalendar() + val initialDate = now() + + initialDate.year = minDate.year - 1 + + val monthAndYearPicker = MonthAndYearPicker( + context = testContext, + selectedDate = initialDate, + minDate = minDate, + maxDate = maxDate + ) + + with(monthAndYearPicker.monthView) { + assertEquals(minDate.month, value) + } + + with(monthAndYearPicker.yearView) { + assertEquals(minDate.year, value) + } + } +} \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 1b11117b751..cd96648b5bc 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -21,6 +21,9 @@ permalink: /changelog/ * **service-location** * 🆕 A new component for accessing Mozilla's and other location services. +* **feature-prompts** + * Improved month picker UI, now we have the same widget as Fennec. + # 5.0.0 * [Commits](https://github.com/mozilla-mobile/android-components/compare/v4.0.0...v5.0.0)