diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/DropDownViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/DropDownViewHolderFactoryEspressoTest.kt index 569ada097b..77fad9292c 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/DropDownViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/DropDownViewHolderFactoryEspressoTest.kt @@ -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 @@ -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 @@ -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? = 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(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() } @@ -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 + } } ) } diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactoryEspressoTest.kt new file mode 100644 index 0000000000..08ab7607c0 --- /dev/null +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactoryEspressoTest.kt @@ -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://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 = + 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(R.id.autoCompleteTextView) + .adapter.count + ) + .isEqualTo(1) + } + + @Test + fun shouldAddDropDownValueSelectedForMultipleAnswersAutoCompleteTextView() { + var answerHolder: List? = 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(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(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 + } + } + ) + } + } +} diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt index f6ea81c8c2..116d72135c 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreTypes.kt @@ -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 @@ -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. */ diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt index 929988033b..8fee63df06 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/mapping/ResourceMapper.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022-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. @@ -18,6 +18,7 @@ package com.google.android.fhir.datacapture.mapping import com.google.android.fhir.datacapture.DataCapture import com.google.android.fhir.datacapture.extensions.createQuestionnaireResponseItem +import com.google.android.fhir.datacapture.extensions.logicalId import com.google.android.fhir.datacapture.extensions.targetStructureMap import com.google.android.fhir.datacapture.extensions.toCodeType import com.google.android.fhir.datacapture.extensions.toCoding @@ -28,6 +29,7 @@ import java.lang.reflect.Field import java.lang.reflect.Method import java.lang.reflect.ParameterizedType import java.util.Locale +import org.hl7.fhir.exceptions.FHIRException import org.hl7.fhir.r4.context.IWorkerContext import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.Bundle @@ -45,6 +47,7 @@ import org.hl7.fhir.r4.model.IntegerType import org.hl7.fhir.r4.model.Parameters 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.Resource import org.hl7.fhir.r4.model.StringType import org.hl7.fhir.r4.model.StructureDefinition @@ -252,10 +255,9 @@ object ResourceMapper { ?.let { // Set initial value for the questionnaire item. Questionnaire items should not have both // initial value and initial expression. + val value = it.asExpectedType(questionnaireItem.type) questionnaireItem.initial = - mutableListOf( - Questionnaire.QuestionnaireItemInitialComponent().setValue(it.asExpectedType()) - ) + mutableListOf(Questionnaire.QuestionnaireItemInitialComponent().setValue(value)) } populateInitialValues(questionnaireItem.item, *resources) @@ -758,14 +760,36 @@ private fun Questionnaire.createResource(): Resource? = * objects and throws exception otherwise. This extension function takes care of the conversion * based on the input and expected [Type]. */ -private fun Base.asExpectedType(): Type { - return when (this) { - is Enumeration<*> -> toCoding() - is IdType -> StringType(idPart) +private fun Base.asExpectedType( + questionnaireItemType: Questionnaire.QuestionnaireItemType? = null +): Type { + return when { + questionnaireItemType == Questionnaire.QuestionnaireItemType.REFERENCE -> + asExpectedReferenceType() + this is Enumeration<*> -> toCoding() + this is IdType -> StringType(idPart) else -> this as Type } } +private fun Base.asExpectedReferenceType(): Type { + return when { + this.isResource -> { + this@asExpectedReferenceType as Resource + Reference().apply { + reference = + "${this@asExpectedReferenceType.resourceType}/${this@asExpectedReferenceType.logicalId}" + } + } + this is IdType -> + Reference().apply { + reference = + "${this@asExpectedReferenceType.resourceType}/${this@asExpectedReferenceType.idPart}" + } + else -> throw FHIRException("Expression supplied does not evaluate to IdType.") + } +} + /** * Returns a newly created [Resource] from the item extraction context extension if one and only one * such extension exists in the questionnaire item, or null otherwise. diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactory.kt index cf6da03da5..d71bc7b8b5 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactory.kt @@ -25,6 +25,7 @@ import androidx.core.view.get import androidx.core.view.isEmpty import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.extensions.displayString +import com.google.android.fhir.datacapture.extensions.identifierString import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.NotValidated import com.google.android.fhir.datacapture.validation.Valid @@ -64,8 +65,10 @@ internal object AutoCompleteViewHolderFactory : value = questionnaireViewItem.enabledAnswerOptions .first { - it.value.displayString(header.context) == - autoCompleteTextView.adapter.getItem(position) as String + it.value.identifierString(header.context) == + (autoCompleteTextView.adapter.getItem(position) + as AutoCompleteViewAnswerOption) + .answerId } .valueCoding } @@ -78,9 +81,20 @@ internal object AutoCompleteViewHolderFactory : override fun bind(questionnaireViewItem: QuestionnaireViewItem) { header.bind(questionnaireViewItem) header.showRequiredOrOptionalTextInHeaderView(questionnaireViewItem) - val answerOptionString = - questionnaireViewItem.enabledAnswerOptions.map { it.value.displayString(header.context) } - val adapter = ArrayAdapter(header.context, R.layout.drop_down_list_item, answerOptionString) + val answerOptionValues = + questionnaireViewItem.enabledAnswerOptions.map { + AutoCompleteViewAnswerOption( + answerId = it.value.identifierString(header.context), + answerDisplay = it.value.displayString(header.context) + ) + } + val adapter = + ArrayAdapter( + header.context, + R.layout.drop_down_list_item, + R.id.answer_option_textview, + answerOptionValues + ) autoCompleteTextView.setAdapter(adapter) // Remove chips if any from the last bindView call on this VH. chipContainer.removeAllViews() @@ -204,3 +218,13 @@ internal object AutoCompleteViewHolderFactory : } } } + +/** + * An answer option that would show up as a dropdown item in an [AutoCompleteViewHolderFactory] + * textview + */ +internal data class AutoCompleteViewAnswerOption(val answerId: String, val answerDisplay: String) { + override fun toString(): String { + return this.answerDisplay + } +} diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt index 52dccd9ab2..fafdb09a64 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactory.kt @@ -28,6 +28,7 @@ import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.extensions.displayString import com.google.android.fhir.datacapture.extensions.getRequiredOrOptionalText import com.google.android.fhir.datacapture.extensions.getValidationErrorMessage +import com.google.android.fhir.datacapture.extensions.identifierString import com.google.android.fhir.datacapture.extensions.itemAnswerOptionImage import com.google.android.fhir.datacapture.extensions.localizedFlyoverSpanned import com.google.android.fhir.datacapture.validation.ValidationResult @@ -67,19 +68,26 @@ internal object DropDownViewHolderFactory : this.questionnaireViewItem.enabledAnswerOptions .map { DropDownAnswerOption( + it.value.identifierString(context), it.value.displayString(context), it.itemAnswerOptionImage(context) ) } .toMutableList() - answerOptionList.add(0, DropDownAnswerOption(context.getString(R.string.hyphen), null)) + answerOptionList.add( + 0, + DropDownAnswerOption( + context.getString(R.string.hyphen), + context.getString(R.string.hyphen), + null + ) + ) val adapter = AnswerOptionDropDownArrayAdapter(context, R.layout.drop_down_list_item, answerOptionList) - val selectedAnswer = - questionnaireViewItem.answers.singleOrNull()?.value?.displayString(header.context) + val selectedAnswerIdentifier = + questionnaireViewItem.answers.singleOrNull()?.value?.identifierString(header.context) answerOptionList - .filter { it.answerOptionString == selectedAnswer } - .singleOrNull() + .firstOrNull { it.answerId == selectedAnswerIdentifier } ?.let { autoCompleteTextView.setText(it.answerOptionString) autoCompleteTextView.setSelection(it.answerOptionString.length) @@ -103,7 +111,7 @@ internal object DropDownViewHolderFactory : ) val selectedAnswer = questionnaireViewItem.enabledAnswerOptions - .firstOrNull { it.value.displayString(context) == selectedItem?.answerOptionString } + .firstOrNull { it.value.identifierString(context) == selectedItem?.answerId } ?.value if (selectedAnswer == null) { @@ -167,6 +175,7 @@ internal class AnswerOptionDropDownArrayAdapter( } internal data class DropDownAnswerOption( + val answerId: String, val answerOptionString: String, val answerOptionImage: Drawable? ) { diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt index 9c3af08f8d..a159417a98 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/mapping/ResourceMapperTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Google LLC + * Copyright 2022-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. @@ -48,6 +48,7 @@ import org.hl7.fhir.r4.model.Observation import org.hl7.fhir.r4.model.Patient 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.RelatedPerson import org.hl7.fhir.r4.model.ResourceFactory import org.hl7.fhir.r4.model.StringType @@ -1522,6 +1523,96 @@ class ResourceMapperTest { .isEqualTo(patientId) } + @Test + fun `populate() should correctly populate Reference value in QuestionnaireResponse`() = + runBlocking { + val questionnaire = + Questionnaire() + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "patient-id" + type = Questionnaire.QuestionnaireItemType.REFERENCE + extension = + listOf( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "Patient.id" + } + ) + ) + } + ) + + val patientId = UUID.randomUUID().toString() + val patient = Patient().apply { id = "Patient/$patientId" } + val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + + assertThat((questionnaireResponse.item[0].answer[0].value as Reference).reference) + .isEqualTo(patient.id) + } + + @Test + fun `populate() should throw error when Reference value in QuestionnaireResponse but FhirExpression `() = + runBlocking { + val questionnaire = + Questionnaire() + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "patient-id" + type = Questionnaire.QuestionnaireItemType.REFERENCE + extension = + listOf( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "Patient.gender" + } + ) + ) + } + ) + + val patientId = UUID.randomUUID().toString() + val patient = + Patient().apply { + id = "Patient/$patientId" + gender = Enumerations.AdministrativeGender.MALE + } + + val errorMessage = + assertFailsWith { ResourceMapper.populate(questionnaire, patient) } + .localizedMessage + assertThat(errorMessage).isEqualTo("Expression supplied does not evaluate to IdType.") + } + + @Test + fun `populate() should correctly populate Reference value in QuestionnaireResponse when expression resolves to type Resource`() = + runBlocking { + val questionnaire = + Questionnaire() + .addItem( + Questionnaire.QuestionnaireItemComponent().apply { + type = Questionnaire.QuestionnaireItemType.REFERENCE + addExtension( + Extension( + ITEM_INITIAL_EXPRESSION_URL, + Expression().apply { + language = "text/fhirpath" + expression = "Patient" + } + ) + ) + } + ) + val patient = Patient().apply { id = UUID.randomUUID().toString() } + val questionnaireResponse = ResourceMapper.populate(questionnaire, patient) + + assertThat(questionnaireResponse.itemFirstRep.answerFirstRep.valueReference.reference) + .isEqualTo("Patient/${patient.id}") + } @Test fun `populate() should correctly populate IdType value with history in QuestionnaireResponse`() = runBlocking { diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactoryTest.kt index db990a9a83..1c9c7a527c 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/AutoCompleteViewHolderFactoryTest.kt @@ -22,6 +22,7 @@ import android.widget.TextView import androidx.core.view.get import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.extensions.displayString +import com.google.android.fhir.datacapture.extensions.identifierString import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.NotValidated import com.google.android.fhir.datacapture.validation.Valid @@ -150,6 +151,59 @@ class AutoCompleteViewHolderFactoryTest { .isEqualTo(2) } + @Test + fun shouldHaveTwoAnswerChipWithAnswerOptionsHavingSameDisplayStringDifferentId() { + val answers = + listOf( + Questionnaire.QuestionnaireItemAnswerOptionComponent() + .setValue( + Coding().setCode("test1-code").setDisplay("Test Code").setId("test1-code") as Coding + ), + Questionnaire.QuestionnaireItemAnswerOptionComponent() + .setValue(Coding().setCode("test2-code").setDisplay("Test Code") as Coding) + ) + + val fakeAnswerValueSetResolver = { uri: String -> + if (uri == "http://answwer-value-set-url") { + answers + } else { + emptyList() + } + } + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + repeats = true + answerValueSet = "http://answwer-value-set-url" + } + viewHolder.bind( + QuestionnaireViewItem( + questionnaireItem, + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = answers.first { it.value.id == "test1-code" }.valueCoding + } + ) + + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = + answers + .first { it.value.identifierString(parent.context) == "test2-code" } + .valueCoding + } + ) + }, + enabledAnswerOptions = fakeAnswerValueSetResolver.invoke(questionnaireItem.answerValueSet), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + ) + ) + + assertThat(viewHolder.itemView.findViewById(R.id.chipContainer).childCount) + .isEqualTo(2) + } + @Test fun shouldHaveSingleAnswerChipWithContainedAnswerValueSet() { val answers = diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactoryTest.kt index 8b00ffad13..ed8e7485d9 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/DropDownViewHolderFactoryTest.kt @@ -217,6 +217,50 @@ class DropDownViewHolderFactoryTest { .isEqualTo(answerOption.value.displayString(parent.context)) } + @Test + fun shouldAutoCompleteTextViewToDisplayIfAnswerNotNullAndDisplayMatchesMoreThanOneOption() { + val answerOption1 = + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = + Reference().apply { + reference = "Patient/1234" + display = "John" + } + } + + val answerOption2 = + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = + Reference().apply { + reference = "Patient/6789" + display = "John" + } + } + + viewHolder.bind( + QuestionnaireViewItem( + Questionnaire.QuestionnaireItemComponent().apply { + addAnswerOption(answerOption1) + addAnswerOption(answerOption2) + }, + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + addAnswer( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = answerOption2.value + } + ) + }, + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> } + ) + ) + + assertThat( + viewHolder.itemView.findViewById(R.id.auto_complete).text.toString() + ) + .isEqualTo(answerOption2.value.displayString(parent.context)) + } + @Test fun displayValidationResult_error_shouldShowErrorMessage() { viewHolder.bind(