Skip to content

Commit

Permalink
Refactor the code for the DatePickerView (google#1880)
Browse files Browse the repository at this point in the history
* Refactor the code for the date picker view.

* clean up.

* clean up.

* refactor to resemble DateTimePicer

* Add tests

* parse propah

* clean up UiEspresso

* fix tests

* make function name more accurate

* address comments

* spotless apply duh

* forgot to change names to match new names

* add test case based on comment
'

---------

Co-authored-by: Santosh Pingle <[email protected]>
Co-authored-by: omarismail <[email protected]>
  • Loading branch information
3 people committed Feb 24, 2023
1 parent 2363c11 commit 409b45f
Show file tree
Hide file tree
Showing 10 changed files with 389 additions and 610 deletions.
36 changes: 36 additions & 0 deletions datacapture/sampledata/component_date_picker.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"resourceType": "Questionnaire",
"item": [
{
"linkId": "1",
"text": "Enter a date",
"type": "date",
"extension": [
{
"url": "http:https://hl7.org/fhir/StructureDefinition/entryFormat",
"valueString": "yyyy-mm-dd"
}
],
"item": [
{
"extension": [
{
"url": "http:https://hl7.org/fhir/StructureDefinition/questionnaire-displayCategory",
"valueCodeableConcept": {
"coding": [
{
"system": "http:https://hl7.org/fhir/questionnaire-display-category",
"code": "instructions"
}
]
}
}
],
"linkId": "1-most-recent",
"text": "Use keyboard entry or date picker",
"type": "display"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,40 @@
package com.google.android.fhir.datacapture

import android.widget.FrameLayout
import androidx.core.os.bundleOf
import androidx.fragment.app.add
import androidx.fragment.app.commitNow
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.android.fhir.datacapture.TestQuestionnaireFragment.Companion.QUESTIONNAIRE_FILE_PATH_KEY
import androidx.test.platform.app.InstrumentationRegistry
import ca.uhn.fhir.context.FhirContext
import ca.uhn.fhir.context.FhirVersionEnum
import ca.uhn.fhir.parser.IParser
import com.google.android.fhir.datacapture.test.R
import com.google.android.fhir.datacapture.utilities.clickIcon
import com.google.android.fhir.datacapture.utilities.clickOnText
import com.google.android.fhir.datacapture.validation.Invalid
import com.google.android.fhir.datacapture.validation.QuestionnaireResponseValidator
import com.google.android.fhir.datacapture.validation.Valid
import com.google.android.fhir.datacapture.views.factories.localDate
import com.google.android.fhir.datacapture.views.factories.localDateTime
import com.google.android.material.textfield.TextInputLayout
import com.google.common.truth.Truth.assertThat
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.Calendar
import java.util.Date
import org.hamcrest.CoreMatchers
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
Expand All @@ -51,6 +65,8 @@ class QuestionnaireUiEspressoTest {
ActivityScenarioRule(TestActivity::class.java)

private lateinit var parent: FrameLayout
private val parser: IParser = FhirContext.forCached(FhirVersionEnum.R4).newJsonParser()
private val context = InstrumentationRegistry.getInstrumentation().context

@Before
fun setup() {
Expand All @@ -59,7 +75,7 @@ class QuestionnaireUiEspressoTest {

@Test
fun shouldDisplayReviewButtonWhenNoMorePagesToDisplay() {
buildFragmentFromQuestionnaire("/paginated_questionnaire_with_dependent_answer.json")
buildFragmentFromQuestionnaire("/paginated_questionnaire_with_dependent_answer.json", true)

onView(withId(R.id.review_mode_button))
.check(
Expand Down Expand Up @@ -148,22 +164,243 @@ class QuestionnaireUiEspressoTest {
assertThat(answer.localDateTime).isEqualTo(LocalDateTime.of(2005, 1, 5, 6, 10))
}

private fun buildFragmentFromQuestionnaire(fileName: String) {
val bundle = bundleOf(QUESTIONNAIRE_FILE_PATH_KEY to fileName)
@Test
fun datePicker_shouldShowErrorForWrongDate() {
buildFragmentFromQuestionnaire("/component_date_picker.json")

// Add month and day. No need to add slashes as they are added automatically
onView(withId(R.id.text_input_edit_text))
.perform(ViewActions.click())
.perform(ViewActions.typeTextIntoFocusedView("0105"))

onView(withId(R.id.text_input_layout)).check { view, _ ->
val actualError = (view as TextInputLayout).error
assertThat(actualError).isEqualTo("Date format needs to be MM/dd/yyyy (e.g. 01/31/2023)")
}
}

@Test
fun datePicker_shouldSaveInQuestionnaireResponseWhenCorrectDateEntered() {
buildFragmentFromQuestionnaire("/component_date_picker.json")

onView(withId(R.id.text_input_edit_text))
.perform(ViewActions.click())
.perform(ViewActions.typeTextIntoFocusedView("01052005"))

onView(withId(R.id.text_input_layout)).check { view, _ ->
val actualError = (view as TextInputLayout).error
assertThat(actualError).isEqualTo(null)
}

val answer = getQuestionnaireResponse().item.first().answer.first().valueDateType
assertThat(answer.localDate).isEqualTo(LocalDate.of(2005, 1, 5))
}

@Test
fun datePicker_shouldSetDateInput_withinRange() {
val questionnaire =
Questionnaire().apply {
id = "a-questionnaire"
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
type = Questionnaire.QuestionnaireItemType.DATE
linkId = "link-1"
addExtension().apply {
url = "http:https://hl7.org/fhir/StructureDefinition/minValue"
val minDate = DateType(Date()).apply { add(Calendar.YEAR, -1) }
setValue(minDate)
}
addExtension().apply {
url = "http:https://hl7.org/fhir/StructureDefinition/maxValue"
val maxDate = DateType(Date()).apply { add(Calendar.YEAR, 4) }
setValue(maxDate)
}
}
)
}

buildFragmentFromQuestionnaire(questionnaire)
onView(withId(R.id.text_input_layout)).perform(clickIcon(true))
onView(CoreMatchers.allOf(ViewMatchers.withText("OK")))
.inRoot(RootMatchers.isDialog())
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())

val today = DateTimeType.today().valueAsString
val answer = getQuestionnaireResponse().item.first().answer.first().valueDateType.valueAsString

assertThat(answer).isEqualTo(today)
val validationResult =
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
getQuestionnaireResponse(),
context
)

assertThat(validationResult["link-1"]?.first()).isEqualTo(Valid)
}

@Test
fun datePicker_shouldNotSetDateInput_outsideMaxRange() {
val maxDate = DateType(Date()).apply { add(Calendar.YEAR, -2) }
val questionnaire =
Questionnaire().apply {
id = "a-questionnaire"
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
type = Questionnaire.QuestionnaireItemType.DATE
linkId = "link-1"
addExtension().apply {
url = "http:https://hl7.org/fhir/StructureDefinition/minValue"
val minDate = DateType(Date()).apply { add(Calendar.YEAR, -4) }
setValue(minDate)
}
addExtension().apply {
url = "http:https://hl7.org/fhir/StructureDefinition/maxValue"
setValue(maxDate)
}
}
)
}

buildFragmentFromQuestionnaire(questionnaire)
onView(withId(R.id.text_input_layout)).perform(clickIcon(true))
onView(CoreMatchers.allOf(ViewMatchers.withText("OK")))
.inRoot(RootMatchers.isDialog())
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())

val maxDateAllowed = maxDate.valueAsString
val validationResult =
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
getQuestionnaireResponse(),
context
)

assertThat((validationResult["link-1"]?.first() as Invalid).getSingleStringValidationMessage())
.isEqualTo("Maximum value allowed is:$maxDateAllowed")
}

@Test
fun datePicker_shouldNotSetDateInput_outsideMinRange() {
val minDate = DateType(Date()).apply { add(Calendar.YEAR, 1) }
val questionnaire =
Questionnaire().apply {
id = "a-questionnaire"
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
type = Questionnaire.QuestionnaireItemType.DATE
linkId = "link-1"
addExtension().apply {
url = "http:https://hl7.org/fhir/StructureDefinition/minValue"
setValue(minDate)
}
addExtension().apply {
url = "http:https://hl7.org/fhir/StructureDefinition/maxValue"
val maxDate = DateType(Date()).apply { add(Calendar.YEAR, 2) }
setValue(maxDate)
}
}
)
}

buildFragmentFromQuestionnaire(questionnaire)
onView(withId(R.id.text_input_layout)).perform(clickIcon(true))
onView(CoreMatchers.allOf(ViewMatchers.withText("OK")))
.inRoot(RootMatchers.isDialog())
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())

val minDateAllowed = minDate.valueAsString
val validationResult =
QuestionnaireResponseValidator.validateQuestionnaireResponse(
questionnaire,
getQuestionnaireResponse(),
context
)

assertThat((validationResult["link-1"]?.first() as Invalid).getSingleStringValidationMessage())
.isEqualTo("Minimum value allowed is:$minDateAllowed")
}

@Test
fun datePicker_shouldThrowException_whenMinValueRangeIsGreaterThanMaxValueRange() {
val questionnaire =
Questionnaire().apply {
id = "a-questionnaire"
addItem(
Questionnaire.QuestionnaireItemComponent().apply {
type = Questionnaire.QuestionnaireItemType.DATE
linkId = "link-1"
addExtension().apply {
url = "http:https://hl7.org/fhir/StructureDefinition/minValue"
val minDate = DateType(Date()).apply { add(Calendar.YEAR, 1) }

setValue(minDate)
}
addExtension().apply {
url = "http:https://hl7.org/fhir/StructureDefinition/maxValue"
val maxDate = DateType(Date()).apply { add(Calendar.YEAR, -1) }
setValue(maxDate)
}
}
)
}

buildFragmentFromQuestionnaire(questionnaire)
val exception =
Assert.assertThrows(IllegalArgumentException::class.java) {
onView(withId(com.google.android.fhir.datacapture.R.id.text_input_layout))
.perform(clickIcon(true))
onView(CoreMatchers.allOf(ViewMatchers.withText("OK")))
.inRoot(RootMatchers.isDialog())
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
}
assertThat(exception.message).isEqualTo("minValue cannot be greater than maxValue")
}

private fun buildFragmentFromQuestionnaire(fileName: String, isReviewMode: Boolean = false) {
val questionnaireJsonString = readFileFromAssets(fileName)
val questionnaireFragment =
QuestionnaireFragment.builder()
.setQuestionnaire(questionnaireJsonString)
.showReviewPageBeforeSubmit(isReviewMode)
.build()
activityScenarioRule.scenario.onActivity { activity ->
activity.supportFragmentManager.commitNow {
setReorderingAllowed(true)
add<TestQuestionnaireFragment>(R.id.container_holder, args = bundle)
add(R.id.container_holder, questionnaireFragment)
}
}
}

private fun buildFragmentFromQuestionnaire(
questionnaire: Questionnaire,
isReviewMode: Boolean = false
) {
val questionnaireFragment =
QuestionnaireFragment.builder()
.setQuestionnaire(parser.encodeResourceToString(questionnaire))
.showReviewPageBeforeSubmit(isReviewMode)
.build()
activityScenarioRule.scenario.onActivity { activity ->
activity.supportFragmentManager.commitNow {
setReorderingAllowed(true)
add(R.id.container_holder, questionnaireFragment)
}
}
}

private fun readFileFromAssets(filename: String) =
javaClass.getResourceAsStream(filename)!!.bufferedReader().use { it.readText() }
private fun getQuestionnaireResponse(): QuestionnaireResponse {
var testQuestionnaireFragment: QuestionnaireFragment? = null
activityScenarioRule.scenario.onActivity { activity ->
testQuestionnaireFragment =
activity.supportFragmentManager
.findFragmentById(R.id.container_holder)
?.childFragmentManager?.findFragmentById(R.id.container) as QuestionnaireFragment
activity.supportFragmentManager.findFragmentById(R.id.container_holder)
as QuestionnaireFragment
}
return testQuestionnaireFragment!!.getQuestionnaireResponse()
}
Expand Down
Loading

0 comments on commit 409b45f

Please sign in to comment.