Skip to content

Commit

Permalink
Populate initial value from dropdown list for questionnaire items wit…
Browse files Browse the repository at this point in the history
…h type reference (#2065)

* set the initial value using the initial expression extension for reference type

* Allow developers to add custom search parameter to the engine. (#1778)

* Added custom search paramter.

* Review changes : Injecting ResourceIndexer into ResourceDao

* Show submit button in review mode. (#1826)

* Show submit button in review mode

* Added tests

* Review comments: Updated the fragment result listener to check explicit result values

* Fix for removing old indexes when resource is updated (#1840)

* Fix for removing old indexes when resource is updated

* Added comments

* add datacapture config in demo app

* Fix dropdown initial selection for answerOptions of type Reference

Sometimes choiceColumn evaluated to similar 'display' text across many options, and the autocompletetextview text would get set to null

* Support expression evaluating to ResourceType for items of type Reference

* Update autocompleteTextView to check answerId

* Add AutoCompleteViewHolderFactory ui tests

* Fix failing tests for AutoCompleteViewHolderFactory

* Refactor MoreTypes.kt to remove duplications

Co-authored-by: Omar Ismail <[email protected]>

* Refactor evaluation of questionnaireItem initial value

* Fix AutoCompleteTextViewFactory espresso test failing in api 24

---------

Co-authored-by: omarismail <[email protected]>
Co-authored-by: aditya-07 <[email protected]>
Co-authored-by: Omar Ismail <[email protected]>
  • Loading branch information
4 people committed Aug 16, 2023
1 parent 6660f64 commit 4b2ae40
Show file tree
Hide file tree
Showing 9 changed files with 517 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package com.google.android.fhir.datacapture.test.views
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.test.espresso.Espresso.onData
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
Expand All @@ -35,15 +36,19 @@ import com.google.android.fhir.datacapture.test.TestActivity
import com.google.android.fhir.datacapture.test.utilities.showDropDown
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.fhir.datacapture.views.factories.DropDownAnswerOption
import com.google.android.fhir.datacapture.views.factories.DropDownViewHolderFactory
import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.common.truth.Truth.assertThat
import org.hamcrest.Matchers.instanceOf
import org.hamcrest.Matchers.`is`
import org.hl7.fhir.r4.model.Attachment
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Extension
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.StringType
import org.junit.Before
import org.junit.Rule
Expand Down Expand Up @@ -264,6 +269,53 @@ class DropDownViewHolderFactoryEspressoTest {
.isEqualTo(0)
}

@Test
fun shouldSetCorrectDropDownValueToAutoCompleteTextViewForDifferentAnswerOptionsWithSimilarDisplayString() {
val questionnaireItem =
Questionnaire.QuestionnaireItemComponent().apply {
addAnswerOption(
Questionnaire.QuestionnaireItemAnswerOptionComponent().apply {
value =
Reference().apply {
id = "ref_1"
display = "Reference"
}
}
)
addAnswerOption(
Questionnaire.QuestionnaireItemAnswerOptionComponent().apply {
value =
Reference().apply {
id = "ref_2"
display = "Reference"
}
}
)
}

var answerHolder: List<QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>? = null
val questionnaireViewItem =
QuestionnaireViewItem(
questionnaireItem,
responseValueStringOptions(),
validationResult = NotValidated,
answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }
)

runOnUI { viewHolder.bind(questionnaireViewItem) }

onView(withId(R.id.auto_complete)).perform(showDropDown())
onData(`is`(instanceOf(DropDownAnswerOption::class.java)))
.atPosition(2)
.inRoot(isPlatformPopup())
.perform(click())

assertThat(viewHolder.itemView.findViewById<TextView>(R.id.auto_complete).text.toString())
.isEqualTo("Reference")
assertThat((answerHolder!!.single().value as Reference).display).isEqualTo("Reference")
assertThat((answerHolder!!.single().value as Reference).id).isEqualTo("ref_2")
}

/** Method to run code snippet on UI/main thread */
private fun runOnUI(action: () -> Unit) {
activityScenarioRule.scenario.onActivity { action() }
Expand Down Expand Up @@ -295,7 +347,11 @@ class DropDownViewHolderFactoryEspressoTest {
responses.forEach { response ->
addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value = Coding().apply { display = response }
value =
Coding().apply {
code = response.replace(" ", "_")
display = response
}
}
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.android.fhir.datacapture.views.factories

import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.platform.app.InstrumentationRegistry
import com.google.android.fhir.datacapture.R
import com.google.android.fhir.datacapture.test.TestActivity
import com.google.android.fhir.datacapture.test.utilities.showDropDown
import com.google.android.fhir.datacapture.validation.NotValidated
import com.google.android.fhir.datacapture.views.QuestionnaireViewItem
import com.google.android.material.chip.ChipGroup
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.common.truth.Truth.assertThat
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.junit.Before
import org.junit.Rule
import org.junit.Test

class AutoCompleteViewHolderFactoryEspressoTest {
@Rule
@JvmField
var activityScenarioRule: ActivityScenarioRule<TestActivity> =
ActivityScenarioRule(TestActivity::class.java)

private lateinit var parent: FrameLayout
private lateinit var viewHolder: QuestionnaireItemViewHolder

@Before
fun setup() {
activityScenarioRule.scenario.onActivity { activity -> parent = FrameLayout(activity) }
viewHolder = AutoCompleteViewHolderFactory.create(parent)
setTestLayout(viewHolder.itemView)
}

@Test
fun shouldReturnFilteredDropDownMenuItems() {
val questionnaireViewItem =
QuestionnaireViewItem(
answerOptions(false, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"),
responseOptions(),
validationResult = NotValidated,
answersChangedCallback = { _, _, _, _ -> },
)
runOnUI { viewHolder.bind(questionnaireViewItem) }

onView(ViewMatchers.withId(R.id.autoCompleteTextView)).perform(ViewActions.typeText("Coding 1"))
assertThat(
viewHolder.itemView
.findViewById<MaterialAutoCompleteTextView>(R.id.autoCompleteTextView)
.adapter.count
)
.isEqualTo(1)
}

@Test
fun shouldAddDropDownValueSelectedForMultipleAnswersAutoCompleteTextView() {
var answerHolder: List<QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent>? = null
val questionnaireViewItem =
QuestionnaireViewItem(
answerOptions(true, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"),
responseOptions("Coding 1", "Coding 5"),
validationResult = NotValidated,
answersChangedCallback = { _, _, answers, _ -> answerHolder = answers },
)
runOnUI { viewHolder.bind(questionnaireViewItem) }

onView(ViewMatchers.withId(R.id.autoCompleteTextView)).perform(ViewActions.typeText("Coding 3"))
onView(ViewMatchers.withId(R.id.autoCompleteTextView)).perform(showDropDown())
onView(ViewMatchers.withText("Coding 3"))
.inRoot(RootMatchers.isPlatformPopup())
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
.perform(ViewActions.click())
assertThat(
viewHolder.itemView.findViewById<TextView>(R.id.autoCompleteTextView).text.toString()
)
.isEmpty()
assertThat(answerHolder!!.map { it.valueCoding.display })
.containsExactly("Coding 1", "Coding 5", "Coding 3")
}

@Test
fun shouldSetCorrectNumberOfChipsForSelectedAnswers() {
val questionnaireViewItem =
QuestionnaireViewItem(
answerOptions(true, "Coding 1", "Coding 2", "Coding 3", "Coding 4", "Coding 5"),
responseOptions("Coding 1", "Coding 5"),
validationResult = NotValidated,
answersChangedCallback = { _, _, _, _ -> },
)
runOnUI { viewHolder.bind(questionnaireViewItem) }

assertThat(viewHolder.itemView.findViewById<ChipGroup>(R.id.chipContainer).childCount)
.isEqualTo(2)
}

/** Method to run code snippet on UI/main thread */
private fun runOnUI(action: () -> Unit) {
activityScenarioRule.scenario.onActivity { action() }
}

/** Method to set content view for test activity */
private fun setTestLayout(view: View) {
activityScenarioRule.scenario.onActivity { activity -> activity.setContentView(view) }
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
}

private fun answerOptions(repeats: Boolean, vararg options: String) =
Questionnaire.QuestionnaireItemComponent().apply {
this.repeats = repeats
options.forEach { option ->
addAnswerOption(
Questionnaire.QuestionnaireItemAnswerOptionComponent().apply {
value =
Coding().apply {
code = option.replace(" ", "_")
display = option
}
}
)
}
}

private fun responseOptions(vararg options: String) =
QuestionnaireResponse.QuestionnaireResponseItemComponent().apply {
options.forEach { option ->
addAnswer(
QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply {
value =
Coding().apply {
code = option.replace(" ", "_")
display = option
}
}
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,14 @@ import org.hl7.fhir.r4.model.CodeType
import org.hl7.fhir.r4.model.Coding
import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.DateType
import org.hl7.fhir.r4.model.DecimalType
import org.hl7.fhir.r4.model.Expression
import org.hl7.fhir.r4.model.IdType
import org.hl7.fhir.r4.model.IntegerType
import org.hl7.fhir.r4.model.PrimitiveType
import org.hl7.fhir.r4.model.Quantity
import org.hl7.fhir.r4.model.Questionnaire
import org.hl7.fhir.r4.model.QuestionnaireResponse
import org.hl7.fhir.r4.model.Reference
import org.hl7.fhir.r4.model.StringType
import org.hl7.fhir.r4.model.TimeType
import org.hl7.fhir.r4.model.Type
import org.hl7.fhir.r4.model.UriType

Expand All @@ -59,54 +56,45 @@ fun Type.asStringValue(): String {
* [Questionnaire.QuestionnaireItemAnswerOptionComponent].
*/
fun Type.displayString(context: Context): String =
when (this) {
is Attachment -> this.url ?: context.getString(R.string.not_answered)
getDisplayString(this, context) ?: context.getString(R.string.not_answered)

/** Returns value as string depending on the [Type] of element. */
fun Type.getValueAsString(context: Context): String =
getValueString(this) ?: context.getString(R.string.not_answered)

/*
* Returns the unique identifier of a [Type]. Used to differentiate between item answer options that
* may have similar display strings
*/
fun Type.identifierString(context: Context): String =
id ?: (this as? Coding)?.code ?: (this as? Reference)?.reference ?: displayString(context)

private fun getDisplayString(type: Type, context: Context): String? =
when (type) {
is Coding -> type.displayElement.getLocalizedText() ?: type.display ?: type.code
is StringType -> type.getLocalizedText() ?: type.asStringValue()
is DateType -> type.localDate?.format()
is DateTimeType -> "${type.localDate.format()} ${type.localTime.toLocalizedString(context)}"
is Reference -> type.display ?: type.reference
is Attachment -> type.url
is BooleanType -> {
when (this.value) {
when (type.value) {
true -> context.getString(R.string.yes)
false -> context.getString(R.string.no)
null -> context.getString(R.string.not_answered)
null -> null
}
}
is Coding -> {
val display = this.displayElement.getLocalizedText() ?: this.display
if (display.isNullOrEmpty()) {
this.code ?: context.getString(R.string.not_answered)
} else display
}
is DateType -> this.localDate?.format() ?: context.getString(R.string.not_answered)
is DateTimeType -> "${this.localDate.format()} ${this.localTime.toLocalizedString(context)}"
is DecimalType,
is IntegerType -> (this as PrimitiveType<*>).valueAsString
?: context.getString(R.string.not_answered)
is Quantity -> this.value.toString()
is Reference -> {
val display = this.display
if (display.isNullOrEmpty()) {
this.reference ?: context.getString(R.string.not_answered)
} else display
}
is StringType -> this.getLocalizedText()
?: this.valueAsString ?: context.getString(R.string.not_answered)
is TimeType,
is UriType -> (this as PrimitiveType<*>).valueAsString
?: context.getString(R.string.not_answered)
else -> context.getString(R.string.not_answered)
is Quantity -> type.value.toString()
else -> (type as? PrimitiveType<*>)?.valueAsString
}

/** Returns value as string depending on the [Type] of element. */
fun Type.getValueAsString(context: Context): String =
when (this) {
is DateType -> this.valueAsString ?: context.getString(R.string.not_answered)
is DateTimeType -> this.valueAsString ?: context.getString(R.string.not_answered)
is Quantity -> this.value.toString()
is StringType -> this.getLocalizedText()
?: this.valueAsString ?: context.getString(R.string.not_answered)
is DecimalType,
is IntegerType,
is TimeType -> (this as PrimitiveType<*>).valueAsString
?: context.getString(R.string.not_answered)
else -> context.getString(R.string.not_answered)
private fun getValueString(type: Type): String? =
when (type) {
is DateType,
is DateTimeType,
is StringType -> type.asStringValue()
is Quantity -> type.value.toString()
else -> (type as? PrimitiveType<*>)?.valueAsString
}

/** Converts StringType to toUriType. */
Expand Down
Loading

0 comments on commit 4b2ae40

Please sign in to comment.