From 1cd5ee7e5a92730f77c0984b59f0c649bd7279cc Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Tue, 12 Mar 2024 19:14:51 +0100 Subject: [PATCH 1/5] feat: [ANDROAPP-5952] Update design system and adapt InputAge to new implementation, add tests for component --- .../ui/provider/inputfield/AgeProviderTest.kt | 191 ++++++++++++++++++ .../ui/provider/inputfield/AgeProvider.kt | 134 ++++++------ .../ui/provider/inputfield/FieldProvider.kt | 1 - 3 files changed, 254 insertions(+), 72 deletions(-) create mode 100644 form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt diff --git a/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt b/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt new file mode 100644 index 0000000000..8bea2a5b5a --- /dev/null +++ b/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt @@ -0,0 +1,191 @@ +package org.dhis2.form.ui.provider.inputfield + +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.assertTextEquals +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performTextInput +import androidx.test.platform.app.InstrumentationRegistry +import org.dhis2.form.di.Injector +import org.hisp.dhis.android.core.common.ValueType +import org.hisp.dhis.mobile.ui.designsystem.component.InputStyle +import org.junit.Rule +import org.junit.Test + + +class AgeProviderTest { + val resourceManager = Injector.provideResourcesManager(InstrumentationRegistry.getInstrumentation().getContext()) + companion object { + + const val AGE_VALUE = "2023-01-19" + const val INPUT_AGE_RESET_BUTTON = "INPUT_AGE_RESET_BUTTON" + const val INPUT_AGE = "INPUT_AGE" + const val INPUT_AGE_MODE_SELECTOR = "INPUT_AGE_MODE_SELECTOR" + const val DATE_OF_BIRTH = "DATE OF BIRTH" + const val AGE_BUTTON_TEXT = "AGE" + const val INPUT_AGE_OPEN_CALENDAR_BUTTON = "INPUT_AGE_OPEN_CALENDAR_BUTTON" + const val INPUT_AGE_TIME_UNIT_SELECTOR = "INPUT_AGE_TIME_UNIT_SELECTOR" + const val INPUT_AGE_TEXT_FIELD = "INPUT_AGE_TEXT_FIELD" + const val RADIO_BUTTON_months = "RADIO_BUTTON_months" + const val RADIO_BUTTON_days = "RADIO_BUTTON_days" + const val RADIO_BUTTON_years = "RADIO_BUTTON_years" + const val AGE_SELECTOR_TEXT = "6 Years" + const val INPUT_AGE_TEST_TAG = "INPUT_AGE" + const val FIELD_UI_MODEL_UID = "FieldUIModelUid" + + } + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun shouldDisplayInputAgeCorrectlyWhenModelHasValue() { + + val dateValueTypeFieldUiModel = + generateFieldUiModel(FIELD_UI_MODEL_UID, AGE_VALUE, AGE_VALUE, ValueType.DATE) + composeTestRule.setContent { + ProvideInputAge( + inputStyle = InputStyle.DataInputStyle(), + modifier = Modifier.testTag(INPUT_AGE_TEST_TAG), + fieldUiModel = dateValueTypeFieldUiModel, + intentHandler = {}, + resources = resourceManager, + ) + } + composeTestRule.onNodeWithTag(INPUT_AGE_TEST_TAG).assertIsDisplayed() + + } + + @Test + fun shouldDisplayTextButtonSelectorWhenValueIsEmptyString() { + + val dateValueTypeFieldUiModel = + generateFieldUiModel(FIELD_UI_MODEL_UID, "", AGE_VALUE, ValueType.DATE) + composeTestRule.setContent { + ProvideInputAge( + inputStyle = InputStyle.DataInputStyle(), + modifier = Modifier.testTag(INPUT_AGE_TEST_TAG), + fieldUiModel = dateValueTypeFieldUiModel, + intentHandler = {}, + resources = resourceManager, + ) + } + composeTestRule.onNodeWithTag(INPUT_AGE) + composeTestRule.onNodeWithTag(INPUT_AGE_MODE_SELECTOR) + + } + + @Test + fun shouldDisplayInputDateWhenClickingOnDateButton() { + + val dateValueTypeFieldUiModel = + generateFieldUiModel(FIELD_UI_MODEL_UID, "", AGE_VALUE, ValueType.DATE) + composeTestRule.setContent { + ProvideInputAge( + inputStyle = InputStyle.DataInputStyle(), + + modifier = Modifier.testTag(INPUT_AGE_TEST_TAG), + fieldUiModel = dateValueTypeFieldUiModel, + intentHandler = {}, + resources = resourceManager, + ) + } + composeTestRule.onNodeWithTag(INPUT_AGE) + composeTestRule.onNodeWithTag(INPUT_AGE_MODE_SELECTOR) + composeTestRule.onNodeWithText(DATE_OF_BIRTH).performClick() + composeTestRule.onNodeWithTag(INPUT_AGE_OPEN_CALENDAR_BUTTON).assertIsDisplayed() + + } + + + @Test + fun shouldDisplayYearMonthDaySelector() { + + val dateValueTypeFieldUiModel = + generateFieldUiModel(FIELD_UI_MODEL_UID, "", AGE_VALUE, ValueType.DATE) + composeTestRule.setContent { + ProvideInputAge( + inputStyle = InputStyle.DataInputStyle(), + modifier = Modifier.testTag(INPUT_AGE_TEST_TAG), + fieldUiModel = dateValueTypeFieldUiModel, + intentHandler = {}, + resources = resourceManager, + ) + + } + composeTestRule.onNodeWithTag(INPUT_AGE) + composeTestRule.onNodeWithTag(INPUT_AGE_MODE_SELECTOR) + composeTestRule.onNodeWithText(AGE_BUTTON_TEXT).performClick() + composeTestRule.onNodeWithTag(INPUT_AGE_TIME_UNIT_SELECTOR).assertIsDisplayed() + } + + @Test + fun shouldNotChangeValueWhenUsingAgeSelectorRadioButtons() { + + val dateValueTypeFieldUiModel = + generateFieldUiModel(FIELD_UI_MODEL_UID, "", AGE_VALUE, ValueType.DATE) + composeTestRule.setContent { + ProvideInputAge( + inputStyle = InputStyle.DataInputStyle(), + modifier = Modifier.testTag(INPUT_AGE_TEST_TAG), + fieldUiModel = dateValueTypeFieldUiModel, + intentHandler = {}, + resources = resourceManager, + ) + } + composeTestRule.onNodeWithTag(INPUT_AGE) + composeTestRule.onNodeWithTag(INPUT_AGE_MODE_SELECTOR) + composeTestRule.onNodeWithText(AGE_BUTTON_TEXT).performClick() + composeTestRule.onNodeWithTag(INPUT_AGE_TIME_UNIT_SELECTOR).assertIsDisplayed() + composeTestRule.onNodeWithTag(INPUT_AGE_TEXT_FIELD).performTextInput("6") + composeTestRule.onNodeWithTag(RADIO_BUTTON_months).performClick() + composeTestRule.onNodeWithTag(RADIO_BUTTON_days).performClick() + composeTestRule.onNodeWithTag(RADIO_BUTTON_years).performClick() + composeTestRule.onNodeWithTag(INPUT_AGE_TEXT_FIELD).assertTextEquals(AGE_SELECTOR_TEXT) + + } + @Test + fun shouldDisplayTextButtonSelectorWhenTappingResetButton() { + + val dateValueTypeFieldUiModel = + generateFieldUiModel(FIELD_UI_MODEL_UID, "", AGE_VALUE, ValueType.DATE) + composeTestRule.setContent { + ProvideInputAge( + inputStyle = InputStyle.DataInputStyle(), + modifier = Modifier.testTag(INPUT_AGE_TEST_TAG), + fieldUiModel = dateValueTypeFieldUiModel, + intentHandler = {}, + resources = resourceManager, + ) + } + composeTestRule.onNodeWithText(AGE_BUTTON_TEXT).performClick() + composeTestRule.onNodeWithTag(INPUT_AGE_RESET_BUTTON).assertIsDisplayed().performClick() + composeTestRule.onNodeWithTag(INPUT_AGE_MODE_SELECTOR).assertIsDisplayed() + + } + + @Test + fun shouldDisplayDatePickerWhenTappingOnCalendarButton() { + + val dateValueTypeFieldUiModel = + generateFieldUiModel(FIELD_UI_MODEL_UID, AGE_VALUE, AGE_VALUE, ValueType.DATE) + composeTestRule.setContent { + ProvideInputAge( + inputStyle = InputStyle.DataInputStyle(), + modifier = Modifier.testTag(INPUT_AGE_TEST_TAG), + fieldUiModel = dateValueTypeFieldUiModel, + intentHandler = {}, + resources = resourceManager, + ) + } + composeTestRule.onNodeWithTag(INPUT_AGE_OPEN_CALENDAR_BUTTON).assertIsDisplayed().performClick() + composeTestRule.waitForIdle() + composeTestRule.onNodeWithTag("DATE_PICKER").assertIsDisplayed() + + } + +} diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt index a1b9b6a232..fb3ea2ff92 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/AgeProvider.kt @@ -7,17 +7,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import org.dhis2.commons.extensions.toDate import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.R import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.supportingText import org.dhis2.form.model.FieldUiModel -import org.dhis2.form.ui.event.RecyclerViewUiEvents import org.dhis2.form.ui.intent.FormIntent import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.mobile.ui.designsystem.component.AgeInputType import org.hisp.dhis.mobile.ui.designsystem.component.InputAge +import org.hisp.dhis.mobile.ui.designsystem.component.InputAgeModel import org.hisp.dhis.mobile.ui.designsystem.component.InputStyle import org.hisp.dhis.mobile.ui.designsystem.component.TimeUnitValues import java.text.SimpleDateFormat @@ -31,7 +30,6 @@ fun ProvideInputAge( inputStyle: InputStyle, fieldUiModel: FieldUiModel, intentHandler: (FormIntent) -> Unit, - uiEventHandler: (RecyclerViewUiEvents) -> Unit, resources: ResourceManager, ) { var inputType by remember { @@ -47,86 +45,80 @@ fun ProvideInputAge( } DisposableEffect(fieldUiModel.value) { - inputType = if (fieldUiModel.value.isNullOrEmpty()) { - AgeInputType.None - } else { - when (inputType) { - is AgeInputType.Age -> - calculateAgeFromDate( - fieldUiModel.value!!, - (inputType as AgeInputType.Age).unit, - )?.let { - (inputType as AgeInputType.Age).copy(value = it) - } ?: AgeInputType.None - - is AgeInputType.DateOfBirth -> - formatStoredDateToUI(fieldUiModel.value!!).let { - (inputType as AgeInputType.DateOfBirth).copy(value = it) - } + when (inputType) { + is AgeInputType.Age -> + calculateAgeFromDate( + fieldUiModel.value!!, + (inputType as AgeInputType.Age).unit, + )?.let { + (inputType as AgeInputType.Age).copy(value = it) + } ?: AgeInputType.None + + is AgeInputType.DateOfBirth -> + formatStoredDateToUI(fieldUiModel.value!!).let { + (inputType as AgeInputType.DateOfBirth).copy(value = it) + } - AgeInputType.None -> inputType + AgeInputType.None -> { + // no-óp } } + onDispose { } } InputAge( - title = fieldUiModel.label, - inputType = inputType, - inputStyle = inputStyle, - onCalendarActionClicked = { - uiEventHandler.invoke( - RecyclerViewUiEvents.OpenCustomCalendar( - uid = fieldUiModel.uid, - label = fieldUiModel.label, - date = fieldUiModel.value?.toDate(), - allowFutureDates = fieldUiModel.allowFutureDates ?: false, - ), - ) - }, - modifier = modifier, - state = fieldUiModel.inputState(), - supportingText = fieldUiModel.supportingText(), - isRequired = fieldUiModel.mandatory, - dateOfBirthLabel = resources.getString(R.string.date_birth), - orLabel = resources.getString(R.string.or), - ageLabel = resources.getString(R.string.age), - onValueChanged = { ageInputType -> - inputType = ageInputType - when (val type = inputType) { - is AgeInputType.Age -> { - calculateDateFromAge(type)?.let { calculatedDate -> - intentHandler.invoke( - FormIntent.OnTextChange( - fieldUiModel.uid, - calculatedDate, - fieldUiModel.valueType, - ), + InputAgeModel( + title = fieldUiModel.label, + inputType = inputType, + inputStyle = inputStyle, + state = fieldUiModel.inputState(), + supportingText = fieldUiModel.supportingText(), + isRequired = fieldUiModel.mandatory, + dateOfBirthLabel = resources.getString(R.string.date_birth), + orLabel = resources.getString(R.string.or), + ageLabel = resources.getString(R.string.age), + cancelText = resources.getString(R.string.cancel), + acceptText = resources.getString(R.string.ok), + onValueChanged = { ageInputType -> + inputType = ageInputType + when (val type = inputType) { + is AgeInputType.Age -> { + calculateDateFromAge(type)?.let { calculatedDate -> + intentHandler.invoke( + FormIntent.OnTextChange( + fieldUiModel.uid, + calculatedDate, + fieldUiModel.valueType, + ), + ) + } + } + + is AgeInputType.DateOfBirth -> { + saveValue( + intentHandler, + fieldUiModel.uid, + formatUIDateToStored(type.value), + fieldUiModel.valueType, + fieldUiModel.allowFutureDates, ) } - } - is AgeInputType.DateOfBirth -> { - saveValue( - intentHandler, - fieldUiModel.uid, - formatUIDateToStored(type.value), - fieldUiModel.valueType, - fieldUiModel.allowFutureDates, - ) + AgeInputType.None -> { + saveValue( + intentHandler, + fieldUiModel.uid, + null, + fieldUiModel.valueType, + fieldUiModel.allowFutureDates, + ) + } } + }, + ), + modifier = modifier, - AgeInputType.None -> { - saveValue( - intentHandler, - fieldUiModel.uid, - null, - fieldUiModel.valueType, - fieldUiModel.allowFutureDates, - ) - } - } - }, ) } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt index 4a03971071..6d1f476ea5 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt @@ -393,7 +393,6 @@ fun FieldProvider( inputStyle = inputStyle, fieldUiModel = fieldUiModel, intentHandler = intentHandler, - uiEventHandler = uiEventHandler, resources = resources, ) } From 6bf84455e28c96410f38805cb7493c6b56f4b3f4 Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Tue, 12 Mar 2024 19:48:00 +0100 Subject: [PATCH 2/5] fix: [ANDROAPP-5952] take timezone into account for date setting --- .../eventDetails/ui/EventDetailsViewModel.kt | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt index 8041d77ddb..891921941c 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt @@ -32,10 +32,13 @@ import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.Geometry import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates +import java.security.Timestamp import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date +import java.util.GregorianCalendar import java.util.Locale +import java.util.TimeZone class EventDetailsViewModel( private val configureEventDetails: ConfigureEventDetails, @@ -266,7 +269,25 @@ class EventDetailsViewModel( val calendar = Calendar.getInstance() calendar[year, month, day, 0, 0] = 0 calendar[Calendar.MILLISECOND] = 0 + + val currentTimeZone: TimeZone = calendar.getTimeZone() + val currentDt: Calendar = GregorianCalendar(currentTimeZone, Locale.getDefault()) + + var gmtOffset: Int = currentTimeZone.getOffset( + currentDt[Calendar.ERA], + currentDt[Calendar.YEAR], + currentDt[Calendar.MONTH], + currentDt[Calendar.DAY_OF_MONTH], + currentDt[Calendar.DAY_OF_WEEK], + currentDt[Calendar.MILLISECOND] + ) +// convert to hours +// convert to hours + gmtOffset /= (60 * 60 * 1000) + + calendar.add(Calendar.HOUR_OF_DAY, +gmtOffset) val selectedDate = calendar.time + setUpEventReportDate(selectedDate) } From bd5dda6369cfee40ad5d4135addcbd872bfa98e1 Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Wed, 13 Mar 2024 08:57:15 +0100 Subject: [PATCH 3/5] fix: [ANDROAPP-5952] code clean up --- .../eventDetails/ui/EventDetailsViewModel.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt index 891921941c..9251366774 100644 --- a/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt +++ b/app/src/main/java/org/dhis2/usescases/eventsWithoutRegistration/eventDetails/ui/EventDetailsViewModel.kt @@ -32,7 +32,6 @@ import org.hisp.dhis.android.core.common.FeatureType import org.hisp.dhis.android.core.common.Geometry import org.hisp.dhis.android.core.period.PeriodType import org.hisp.dhis.mobile.ui.designsystem.component.SelectableDates -import java.security.Timestamp import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -279,12 +278,9 @@ class EventDetailsViewModel( currentDt[Calendar.MONTH], currentDt[Calendar.DAY_OF_MONTH], currentDt[Calendar.DAY_OF_WEEK], - currentDt[Calendar.MILLISECOND] + currentDt[Calendar.MILLISECOND], ) -// convert to hours -// convert to hours gmtOffset /= (60 * 60 * 1000) - calendar.add(Calendar.HOUR_OF_DAY, +gmtOffset) val selectedDate = calendar.time From b0566a2e1eeeda861baad8849f4ce7e27ebb4099 Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Wed, 13 Mar 2024 11:03:58 +0100 Subject: [PATCH 4/5] fix: [ANDROAPP-5952] update design system --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3d0732c8f5..e1bbfc4302 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,7 +10,7 @@ kotlin = '1.9.21' hilt = '2.47' hiltCompiler = '1.0.0' jacoco = '0.8.10' -designSystem = "0.2-20240312.081502-39" +designSystem = "0.2-20240313.090548-40" dhis2sdk = "1.10.0-20240307.130705-33" ruleEngine = "3.0.0-20240119.134348-12" expressionParser = "1.1.0-20240219.115041-14" From 21813ae8eda4a56ff7cf85d31525e6bab2246d0b Mon Sep 17 00:00:00 2001 From: Xavier Molloy Date: Wed, 13 Mar 2024 12:03:16 +0100 Subject: [PATCH 5/5] fix: [ANDROAPP-5952] fix test --- .../org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt b/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt index 8bea2a5b5a..30584915fb 100644 --- a/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt +++ b/form/src/androidTest/kotlin/org/dhis2/form/ui/provider/inputfield/AgeProviderTest.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTextInput +import androidx.compose.ui.test.printToLog import androidx.test.platform.app.InstrumentationRegistry import org.dhis2.form.di.Injector import org.hisp.dhis.android.core.common.ValueType @@ -33,7 +34,7 @@ class AgeProviderTest { const val RADIO_BUTTON_months = "RADIO_BUTTON_months" const val RADIO_BUTTON_days = "RADIO_BUTTON_days" const val RADIO_BUTTON_years = "RADIO_BUTTON_years" - const val AGE_SELECTOR_TEXT = "6 Years" + const val AGE_SELECTOR_TEXT = "6 years" const val INPUT_AGE_TEST_TAG = "INPUT_AGE" const val FIELD_UI_MODEL_UID = "FieldUIModelUid" @@ -145,6 +146,8 @@ class AgeProviderTest { composeTestRule.onNodeWithTag(RADIO_BUTTON_months).performClick() composeTestRule.onNodeWithTag(RADIO_BUTTON_days).performClick() composeTestRule.onNodeWithTag(RADIO_BUTTON_years).performClick() + composeTestRule.onNodeWithTag(INPUT_AGE_TEXT_FIELD).printToLog("AGE_SELECTOR_TEXT") + composeTestRule.onNodeWithTag(INPUT_AGE_TEXT_FIELD).assertTextEquals(AGE_SELECTOR_TEXT) }