diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt index 4c3b530c55..911790cb01 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/views/QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Google LLC + * Copyright 2023-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.extensions.DisplayItemControlType import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_SYSTEM import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_URL +import com.google.android.fhir.datacapture.extensions.EXTENSION_OPTION_EXCLUSIVE_URL import com.google.android.fhir.datacapture.extensions.ItemControlTypes import com.google.android.fhir.datacapture.test.TestActivity import com.google.android.fhir.datacapture.test.utilities.assertQuestionnaireResponseAtIndex @@ -52,6 +53,7 @@ import com.google.android.material.textfield.TextInputLayout import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertThat import org.hamcrest.Matchers.not +import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.CodeableConcept import org.hl7.fhir.r4.model.Coding import org.hl7.fhir.r4.model.Extension @@ -101,6 +103,136 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { assertQuestionnaireResponseAtIndex(answerHolder!!, "Coding 1", "Coding 3", "Coding 5") } + @Test + fun multipleChoice_selectMultiple_selectExclusive_clickSave_shouldSaveOnlyExclusiveOption() { + var answerHolder: List? = null + val questionnaireViewItem = + QuestionnaireViewItem( + answerOptions(true, "Coding 1", "Coding 2", "Coding 3") + .addAnswerOption( + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = Coding().apply { display = "Coding Exclusive" } + extension = listOf(Extension(EXTENSION_OPTION_EXCLUSIVE_URL, BooleanType(true))) + }, + ), + responseOptions(), + validationResult = NotValidated, + answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, + ) + + runOnUI { viewHolder.bind(questionnaireViewItem) } + + endIconClickInTextInputLayout(R.id.multi_select_summary_holder) + clickOnTextInDialog("Coding 1") + clickOnText("Coding 3") + clickOnText("Coding Exclusive") + clickOnText("Save") + + assertDisplayedText().isEqualTo("Coding Exclusive") + assertQuestionnaireResponseAtIndex(answerHolder!!, "Coding Exclusive") + } + + @Test + fun multipleChoice_selectExclusive_selectMultiple_clickSave_shouldSaveWithoutExclusiveOption() { + var answerHolder: List? = null + val questionnaireViewItem = + QuestionnaireViewItem( + answerOptions(true, "Coding 1", "Coding 2", "Coding 3") + .addAnswerOption( + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = Coding().apply { display = "Coding Exclusive" } + extension = listOf(Extension(EXTENSION_OPTION_EXCLUSIVE_URL, BooleanType(true))) + }, + ), + responseOptions(), + validationResult = NotValidated, + answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, + ) + + runOnUI { viewHolder.bind(questionnaireViewItem) } + + endIconClickInTextInputLayout(R.id.multi_select_summary_holder) + clickOnTextInDialog("Coding Exclusive") + clickOnText("Coding 1") + clickOnText("Coding 3") + clickOnText("Save") + + assertDisplayedText().isEqualTo("Coding 1, Coding 3") + assertQuestionnaireResponseAtIndex(answerHolder!!, "Coding 1", "Coding 3") + } + + @Test + fun multipleChoice_multipleOptionExclusive_selectMultiple_selectExclusive1_selectExclusive2_clickSave_shouldSaveOnlyLastSelectedExclusiveOption() { + var answerHolder: List? = null + val questionnaireViewItem = + QuestionnaireViewItem( + answerOptions(true, "Coding 1", "Coding 2", "Coding 3") + .addAnswerOption( + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = Coding().apply { display = "Coding Exclusive 1" } + extension = listOf(Extension(EXTENSION_OPTION_EXCLUSIVE_URL, BooleanType(true))) + }, + ) + .addAnswerOption( + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = Coding().apply { display = "Coding Exclusive 2" } + extension = listOf(Extension(EXTENSION_OPTION_EXCLUSIVE_URL, BooleanType(true))) + }, + ), + responseOptions(), + validationResult = NotValidated, + answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, + ) + + runOnUI { viewHolder.bind(questionnaireViewItem) } + + endIconClickInTextInputLayout(R.id.multi_select_summary_holder) + clickOnTextInDialog("Coding 1") + clickOnText("Coding 3") + clickOnText("Coding Exclusive 1") + clickOnText("Coding Exclusive 2") + clickOnText("Save") + + assertDisplayedText().isEqualTo("Coding Exclusive 2") + assertQuestionnaireResponseAtIndex(answerHolder!!, "Coding Exclusive 2") + } + + @Test + fun multipleChoice_multipleOptionExclusive_selectExclusive1_selectExclusive2_selectMultiple_clickSave_shouldSaveWithoutAnyExclusiveOption() { + var answerHolder: List? = null + val questionnaireViewItem = + QuestionnaireViewItem( + answerOptions(true, "Coding 1", "Coding 2", "Coding 3") + .addAnswerOption( + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = Coding().apply { display = "Coding Exclusive 1" } + extension = listOf(Extension(EXTENSION_OPTION_EXCLUSIVE_URL, BooleanType(true))) + }, + ) + .addAnswerOption( + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = Coding().apply { display = "Coding Exclusive 2" } + extension = listOf(Extension(EXTENSION_OPTION_EXCLUSIVE_URL, BooleanType(true))) + }, + ), + responseOptions(), + validationResult = NotValidated, + answersChangedCallback = { _, _, answers, _ -> answerHolder = answers }, + ) + + runOnUI { viewHolder.bind(questionnaireViewItem) } + + endIconClickInTextInputLayout(R.id.multi_select_summary_holder) + clickOnTextInDialog("Coding Exclusive 1") + clickOnTextInDialog("Coding Exclusive 2") + clickOnText("Coding 1") + clickOnText("Coding 3") + clickOnText("Save") + + assertDisplayedText().isEqualTo("Coding 1, Coding 3") + assertQuestionnaireResponseAtIndex(answerHolder!!, "Coding 1", "Coding 3") + } + @Test fun multipleChoice_SelectNothing_clickSave_shouldSaveNothing() { val questionnaireViewItem = @@ -364,6 +496,91 @@ class QuestionnaireItemDialogMultiSelectViewHolderFactoryEspressoTest { onView(withId(R.id.add_another)).check(matches(isDisplayed())) } + @Test + @SdkSuppress(minSdkVersion = 33) + fun selectOther_selectExclusive_shouldHideAddAnotherAnswer() { + val questionnaireItem = + answerOptions( + true, + "Coding 1", + "Coding 2", + "Coding 3", + "Coding 4", + "Coding 5", + "Coding 6", + "Coding 7", + "Coding 8", + ) + .addAnswerOption( + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = Coding().apply { display = "Coding Exclusive" } + extension = listOf(Extension(EXTENSION_OPTION_EXCLUSIVE_URL, BooleanType(true))) + }, + ) + + questionnaireItem.addExtension(openChoiceType) + val questionnaireViewItem = + QuestionnaireViewItem( + questionnaireItem, + responseOptions(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + ) + + runOnUI { viewHolder.bind(questionnaireViewItem) } + + endIconClickInTextInputLayout(R.id.multi_select_summary_holder) + onView(withId(R.id.recycler_view)) + .perform(RecyclerViewActions.scrollToPosition(9)) + clickOnTextInDialog("Other") + clickOnTextInDialog("Coding Exclusive") + onView(withId(R.id.add_another)).check(doesNotExist()) + } + + @Test + @SdkSuppress(minSdkVersion = 33) + fun selectOther_clickAddAnotherAnswer_selectExclusive_shouldHideAddAnotherAnswerWithEditText() { + val questionnaireItem = + answerOptions( + true, + "Coding 1", + "Coding 2", + "Coding 3", + "Coding 4", + "Coding 5", + "Coding 6", + "Coding 7", + "Coding 8", + ) + .addAnswerOption( + Questionnaire.QuestionnaireItemAnswerOptionComponent().apply { + value = Coding().apply { display = "Coding Exclusive" } + extension = listOf(Extension(EXTENSION_OPTION_EXCLUSIVE_URL, BooleanType(true))) + }, + ) + + questionnaireItem.addExtension(openChoiceType) + val questionnaireViewItem = + QuestionnaireViewItem( + questionnaireItem, + responseOptions(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + ) + + runOnUI { viewHolder.bind(questionnaireViewItem) } + + endIconClickInTextInputLayout(R.id.multi_select_summary_holder) + onView(withId(R.id.recycler_view)) + .perform(RecyclerViewActions.scrollToPosition(9)) + clickOnTextInDialog("Other") + onView(withId(R.id.add_another)).perform(delayMainThread()) + onView(withId(R.id.add_another)).perform(click()) + clickOnTextInDialog("Coding Exclusive") + onView(withId(R.id.add_another)).check(doesNotExist()) + onView(withId(R.id.edit_text)).check(doesNotExist()) + } + @Test fun shouldHideErrorTextviewInHeader() { val questionnaireItem = answerOptions(true, "Coding 1") diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/OptionSelectDialogFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/OptionSelectDialogFragment.kt index 6a6cd229e5..15ad260a81 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/OptionSelectDialogFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/OptionSelectDialogFragment.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.extensions.itemAnswerOptionImage +import com.google.android.fhir.datacapture.extensions.optionExclusive import com.google.android.fhir.datacapture.views.factories.OptionSelectOption import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemDialogSelectViewModel import com.google.android.fhir.datacapture.views.factories.SelectedOptions @@ -263,6 +264,8 @@ private class OptionSelectAdapter(val multiSelectEnabled: Boolean) : * if "Other" was just deselected, or adding them if "Other" was just selected). */ private fun submitSelectedChange(position: Int, selected: Boolean) { + val selectedItem = currentList[position] + val newList: List = currentList .mapIndexed { index, row -> @@ -272,8 +275,22 @@ private class OptionSelectAdapter(val multiSelectEnabled: Boolean) : } else { // This is some other row if (multiSelectEnabled) { - // In multi-select mode, the other rows don't need to change - row + // In multi-select mode, + if ( + selected && + ((selectedItem is OptionSelectRow.Option && + selectedItem.option.item.optionExclusive) || + (row is OptionSelectRow.Option && row.option.item.optionExclusive)) + ) { + // if the selected answer option has optionExclusive extension, then deselect other + // answer options. + // or if the selected answer option does not have optionExclusive extension, then + // deselect optionExclusive answer option. + row.withSelectedState(selected = false) ?: row + } else { + // the other rows don't need to change + row + } } else { // In single-select mode, we need to disable all of the other rows row.withSelectedState(selected = false) ?: row