diff --git a/datacapture/sampledata/component_repeated_group.json b/datacapture/sampledata/component_repeated_group.json new file mode 100644 index 0000000000..e6af16282e --- /dev/null +++ b/datacapture/sampledata/component_repeated_group.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "1", + "type": "group", + "text": "Repeated Group", + "repeats": true, + "item": [ + { + "linkId": "1-1", + "text": "Sample date question", + "type": "date", + "extension": [ + { + "url": "http://hl7.org/fhir/StructureDefinition/entryFormat", + "valueString": "yyyy-mm-dd" + } + ] + } + ] + } + ] +} diff --git a/datacapture/sampledata/repeated_group_response.json b/datacapture/sampledata/repeated_group_response.json new file mode 100644 index 0000000000..9cd8f2828e --- /dev/null +++ b/datacapture/sampledata/repeated_group_response.json @@ -0,0 +1,20 @@ +{ + "resourceType": "QuestionnaireResponse", + "item": [ + { + "linkId": "1", + "text": "Repeated Group", + "item": [ + { + "linkId": "1-1", + "text": "Sample date question", + "answer": [ + { + "valueDate": "2024-06-03" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactoryInstrumentedTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactoryInstrumentedTest.kt index 92c8d1a9d4..be61690e64 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactoryInstrumentedTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/contrib/views/PhoneNumberViewHolderFactoryInstrumentedTest.kt @@ -65,7 +65,7 @@ class PhoneNumberViewHolderFactoryInstrumentedTest { } @Test - fun createViewHolder_shouldReturn_phoneNumberViewHolder() { + fun createViewHolder_phoneNumberViewHolderFactory_returnsViewHolder() { val viewHolderFromAdapter = questionnaireEditAdapter.createViewHolder( parent, @@ -74,8 +74,13 @@ class PhoneNumberViewHolderFactoryInstrumentedTest { subtype = QuestionnaireViewHolderType.PHONE_NUMBER.value, ) .viewType, + ) as QuestionnaireEditAdapter.ViewHolder.QuestionHolder + assertThat( + viewHolderFromAdapter.holder.itemView + .findViewById(R.id.text_input_edit_text) + .visibility, ) - assertThat(viewHolderFromAdapter).isInstanceOf(viewHolder::class.java) + .isEqualTo(View.VISIBLE) } @Test diff --git a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/QuestionnaireUiEspressoTest.kt b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/QuestionnaireUiEspressoTest.kt index e7bf0d3867..3ca8eb1b21 100644 --- a/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/QuestionnaireUiEspressoTest.kt +++ b/datacapture/src/androidTest/java/com/google/android/fhir/datacapture/test/QuestionnaireUiEspressoTest.kt @@ -20,12 +20,18 @@ import android.view.View import android.widget.FrameLayout import android.widget.TextView import androidx.fragment.app.commitNow +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.RootMatchers import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.rules.ActivityScenarioRule @@ -604,24 +610,109 @@ class QuestionnaireUiEspressoTest { } } + @Test + fun test_repeated_group_is_added() { + buildFragmentFromQuestionnaire("/component_repeated_group.json") + + onView(withId(R.id.questionnaire_edit_recycler_view)) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 0, + clickChildViewWithId(R.id.add_item), + ), + ) + + onView(ViewMatchers.withId(R.id.questionnaire_edit_recycler_view)).check { + view, + noViewFoundException, + -> + if (noViewFoundException != null) { + throw noViewFoundException + } + assertThat( + (view as RecyclerView).countChildViewOccurrences( + R.id.repeated_group_instance_header_title, + ), + ) + .isEqualTo(1) + } + } + + @Test + fun test_repeated_group_is_deleted() { + buildFragmentFromQuestionnaire( + "/component_repeated_group.json", + responseFileName = "/repeated_group_response.json", + ) + + onView(withId(R.id.questionnaire_edit_recycler_view)) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 1, + clickChildViewWithId(R.id.repeated_group_instance_header_delete_button), + ), + ) + + onView(ViewMatchers.withId(R.id.questionnaire_edit_recycler_view)).check { + view, + noViewFoundException, + -> + if (noViewFoundException != null) { + throw noViewFoundException + } + assertThat( + (view as RecyclerView).countChildViewOccurrences( + R.id.repeated_group_instance_header_title, + ), + ) + .isEqualTo(0) + } + } + + private fun RecyclerView.countChildViewOccurrences(viewId: Int): Int { + var count = 0 + for (i in 0 until this.adapter!!.itemCount) { + val holder = findViewHolderForAdapterPosition(i) + if (holder?.itemView?.findViewById(viewId) != null) { + count++ + } + } + return count + } + + private fun clickChildViewWithId(id: Int) = + object : ViewAction { + override fun getConstraints() = isAssignableFrom(View::class.java) + + override fun getDescription() = "Click on a child view with specified id." + + override fun perform(uiController: UiController?, view: View) { + view.findViewById(id)?.performClick() + } + } + private fun buildFragmentFromQuestionnaire( fileName: String, isReviewMode: Boolean = false, + responseFileName: String? = null, ): QuestionnaireFragment { val questionnaireJsonString = readFileFromAssets(fileName) - val questionnaireFragment = + val builder = QuestionnaireFragment.builder() .setQuestionnaire(questionnaireJsonString) .setShowCancelButton(true) .showReviewPageBeforeSubmit(isReviewMode) - .build() - activityScenarioRule.scenario.onActivity { activity -> - activity.supportFragmentManager.commitNow { - setReorderingAllowed(true) - add(R.id.container_holder, questionnaireFragment) + + responseFileName?.let { builder.setQuestionnaireResponse(readFileFromAssets(it)) } + + return builder.build().also { fragment -> + activityScenarioRule.scenario.onActivity { activity -> + activity.supportFragmentManager.commitNow { + setReorderingAllowed(true) + add(R.id.container_holder, fragment) + } } } - return questionnaireFragment } private fun buildFragmentFromQuestionnaire( diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt index 66d0a77da7..e725f7a136 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireAdapterItem.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 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. @@ -17,9 +17,20 @@ package com.google.android.fhir.datacapture import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import org.hl7.fhir.r4.model.QuestionnaireResponse /** Various types of rows that can be used in a Questionnaire RecyclerView. */ internal sealed interface QuestionnaireAdapterItem { /** A row for a question in a Questionnaire RecyclerView. */ data class Question(val item: QuestionnaireViewItem) : QuestionnaireAdapterItem + + /** A row for a repeated group response instance's header. */ + data class RepeatedGroupHeader( + /** The response index. This is 0-indexed, but should be 1-indexed when rendered in the UI. */ + val index: Int, + /** Callback that is invoked when the user clicks the delete button. */ + val onDeleteClicked: () -> Unit, + /** Responses nested under this header. */ + val responses: List, + ) : QuestionnaireAdapterItem } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt index c1df3c0f9f..17319760ab 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapter.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. @@ -16,10 +16,14 @@ package com.google.android.fhir.datacapture +import android.view.View import android.view.ViewGroup +import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView import com.google.android.fhir.datacapture.contrib.views.PhoneNumberViewHolderFactory +import com.google.android.fhir.datacapture.extensions.inflate import com.google.android.fhir.datacapture.extensions.itemControl import com.google.android.fhir.datacapture.views.QuestionnaireViewItem import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory @@ -46,16 +50,23 @@ internal class QuestionnaireEditAdapter( private val questionnaireItemViewHolderMatchers: List = emptyList(), -) : ListAdapter(DiffCallbacks.ITEMS) { +) : + ListAdapter(DiffCallbacks.ITEMS) { /** * @param viewType the integer value of the [QuestionnaireViewHolderType] used to render the * [QuestionnaireViewItem]. */ - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QuestionnaireItemViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val typedViewType = ViewType.parse(viewType) val subtype = typedViewType.subtype return when (typedViewType.type) { - ViewType.Type.QUESTION -> onCreateViewHolderQuestion(parent = parent, subtype = subtype) + ViewType.Type.QUESTION -> + ViewHolder.QuestionHolder(onCreateViewHolderQuestion(parent = parent, subtype = subtype)) + ViewType.Type.REPEATED_GROUP_HEADER -> { + ViewHolder.RepeatedGroupHeaderHolder( + parent.inflate(R.layout.repeated_group_instance_header_view), + ) + } } } @@ -99,10 +110,16 @@ internal class QuestionnaireEditAdapter( return viewHolderFactory.create(parent) } - override fun onBindViewHolder(holder: QuestionnaireItemViewHolder, position: Int) { + override fun onBindViewHolder(holder: ViewHolder, position: Int) { when (val item = getItem(position)) { is QuestionnaireAdapterItem.Question -> { - holder.bind(item.item) + holder as ViewHolder.QuestionHolder + holder.holder.bind(item.item) + } + is QuestionnaireAdapterItem.RepeatedGroupHeader -> { + holder as ViewHolder.RepeatedGroupHeaderHolder + holder.header.text = "Group ${item.index + 1}" + holder.delete.setOnClickListener { item.onDeleteClicked() } } } } @@ -120,6 +137,11 @@ internal class QuestionnaireEditAdapter( type = ViewType.Type.QUESTION subtype = getItemViewTypeForQuestion(item.item) } + is QuestionnaireAdapterItem.RepeatedGroupHeader -> { + type = ViewType.Type.REPEATED_GROUP_HEADER + // All of the repeated group headers will be rendered identically + subtype = 0 + } } return ViewType.from(type = type, subtype = subtype).viewType } @@ -150,6 +172,7 @@ internal class QuestionnaireEditAdapter( enum class Type { QUESTION, + REPEATED_GROUP_HEADER, } } @@ -240,6 +263,15 @@ internal class QuestionnaireEditAdapter( ?: QuestionnaireViewHolderType.EDIT_TEXT_SINGLE_LINE } + internal sealed class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + class QuestionHolder(val holder: QuestionnaireItemViewHolder) : ViewHolder(holder.itemView) + + class RepeatedGroupHeaderHolder(itemView: View) : ViewHolder(itemView) { + val header: TextView = itemView.findViewById(R.id.repeated_group_instance_header_title) + val delete: View = itemView.findViewById(R.id.repeated_group_instance_header_delete_button) + } + } + internal companion object { // Choice questions are rendered as dialogs if they have at least this many options const val MINIMUM_NUMBER_OF_ANSWER_OPTIONS_FOR_DIALOG = 10 @@ -261,6 +293,10 @@ internal object DiffCallbacks { newItem is QuestionnaireAdapterItem.Question && QUESTIONS.areItemsTheSame(oldItem, newItem) } + is QuestionnaireAdapterItem.RepeatedGroupHeader -> { + newItem is QuestionnaireAdapterItem.RepeatedGroupHeader && + oldItem.index == newItem.index + } } override fun areContentsTheSame( @@ -272,6 +308,10 @@ internal object DiffCallbacks { newItem is QuestionnaireAdapterItem.Question && QUESTIONS.areContentsTheSame(oldItem, newItem) } + is QuestionnaireAdapterItem.RepeatedGroupHeader -> { + newItem is QuestionnaireAdapterItem.RepeatedGroupHeader && + oldItem.responses == newItem.responses + } } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index bd02edaa64..da68320b85 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -40,6 +40,7 @@ import com.google.android.fhir.datacapture.extensions.isDisplayItem import com.google.android.fhir.datacapture.extensions.isHelpCode import com.google.android.fhir.datacapture.extensions.isHidden import com.google.android.fhir.datacapture.extensions.isPaginated +import com.google.android.fhir.datacapture.extensions.isRepeatedGroup import com.google.android.fhir.datacapture.extensions.localizedTextSpanned import com.google.android.fhir.datacapture.extensions.maxValue import com.google.android.fhir.datacapture.extensions.maxValueCqfCalculatedValueExpression @@ -769,7 +770,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat val isHelpCard = itemHelpCard != null val isHelpCardOpen = openedHelpCardSet.contains(questionnaireResponseItem) // Add an item for the question itself - add( + + val question = QuestionnaireAdapterItem.Question( QuestionnaireViewItem( questionnaireItem, @@ -813,8 +815,8 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat isHelpCardOpen = isHelpCard && isHelpCardOpen, helpCardStateChangedCallback = helpCardStateChangedCallback, ), - ), - ) + ) + add(question) // Add nested questions after the parent item. We need to get the questionnaire items and // (possibly multiple sets of) matching questionnaire response items and generate the adapter @@ -831,11 +833,23 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat // For background, see https://build.fhir.org/questionnaireresponse.html#link. buildList { // Case 1 - add(questionnaireResponseItem.item) + if (!questionnaireItem.isRepeatedGroup) { + add(questionnaireResponseItem.item) + } // Case 2 and 3 addAll(questionnaireResponseItem.answer.map { it.item }) } - .forEach { nestedResponseItemList -> + .forEachIndexed { index, nestedResponseItemList -> + if (questionnaireItem.isRepeatedGroup) { + // Case 3 + add( + QuestionnaireAdapterItem.RepeatedGroupHeader( + index = index, + onDeleteClicked = { viewModelScope.launch { question.item.removeAnswerAt(index) } }, + responses = nestedResponseItemList, + ), + ) + } addAll( getQuestionnaireAdapterItems( // If nested display item is identified as instructions or flyover, then do not create diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt index c84a958df4..5d73a71537 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponents.kt @@ -985,6 +985,9 @@ private fun List.flattenInto( } } +internal val Questionnaire.QuestionnaireItemComponent.isRepeatedGroup: Boolean + get() = type == Questionnaire.QuestionnaireItemType.GROUP && repeats + // TODO: Move this elsewhere. val Resource.logicalId: String get() { diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponses.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponses.kt index 0fdbe33b52..fa0f4c545d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponses.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireResponses.kt @@ -142,9 +142,7 @@ private fun unpackRepeatedGroups( questionnaireResponseItem.answer.forEach { it.item = unpackRepeatedGroups(questionnaireItem.item, it.item) } - return if ( - questionnaireItem.type == Questionnaire.QuestionnaireItemType.GROUP && questionnaireItem.repeats - ) { + return if (questionnaireItem.isRepeatedGroup) { questionnaireResponseItem.answer.map { QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { linkId = questionnaireItem.linkId diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreViewGroups.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreViewGroups.kt new file mode 100644 index 0000000000..92ce094e81 --- /dev/null +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/extensions/MoreViewGroups.kt @@ -0,0 +1,35 @@ +/* + * 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. + * 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.extensions + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes + +val Context.layoutInflater: LayoutInflater + get() = LayoutInflater.from(this) + +/** Inflates [layoutRes] and attaches it to [this]. */ +fun ViewGroup.include(@LayoutRes layoutRes: Int) { + context.layoutInflater.inflate(layoutRes, this, true) +} + +/** Inflates [layoutRes] and returns it without attaching it to [this]. */ +fun ViewGroup.inflate(@LayoutRes layoutRes: Int): View = + context.layoutInflater.inflate(layoutRes, this, false) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt index cd75191115..ea59151e5d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItem.kt @@ -167,6 +167,21 @@ data class QuestionnaireViewItem( ) } + internal suspend fun removeAnswerAt(index: Int) { + check(questionnaireItem.repeats) { + "Questionnaire item with linkId ${questionnaireItem.linkId} does not allow repeated answers" + } + require(index in answers.indices) { + "removeAnswerAt($index), but ${questionnaireItem.linkId} only has ${answers.size} answers" + } + answersChangedCallback( + questionnaireItem, + questionnaireResponseItem, + answers.filterIndexed { currentIndex, _ -> currentIndex != index }, + null, + ) + } + /** * Updates the draft answer stored in `QuestionnaireViewModel`. This clears any actual answer for * the question. diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/GroupViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/GroupViewHolderFactory.kt index d68c724c60..844ead25d4 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/GroupViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/GroupViewHolderFactory.kt @@ -80,7 +80,7 @@ internal object GroupViewHolderFactory : } override fun setReadOnly(isReadOnly: Boolean) { - // No user input + addItemButton.isEnabled = !isReadOnly } } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/QuestionnaireItemViewHolderFactory.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/QuestionnaireItemViewHolderFactory.kt index 52290eeef3..f7f26acec7 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/QuestionnaireItemViewHolderFactory.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/factories/QuestionnaireItemViewHolderFactory.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. @@ -50,7 +50,7 @@ abstract class QuestionnaireItemViewHolderFactory(@LayoutRes open val resId: Int * * This is used by [QuestionnaireItemAdapter] to initialize views and bind items in [RecyclerView]. */ -open class QuestionnaireItemViewHolder( +class QuestionnaireItemViewHolder( itemView: View, private val delegate: QuestionnaireItemViewHolderDelegate, ) : RecyclerView.ViewHolder(itemView) { @@ -62,7 +62,7 @@ open class QuestionnaireItemViewHolder( itemMediaView = itemView.findViewById(R.id.item_media) } - open fun bind(questionnaireViewItem: QuestionnaireViewItem) { + fun bind(questionnaireViewItem: QuestionnaireViewItem) { delegate.questionnaireViewItem = questionnaireViewItem delegate.bind(questionnaireViewItem) itemMediaView.bind(questionnaireViewItem.questionnaireItem) diff --git a/datacapture/src/main/res/layout/repeated_group_instance_header_view.xml b/datacapture/src/main/res/layout/repeated_group_instance_header_view.xml new file mode 100644 index 0000000000..b2357d3430 --- /dev/null +++ b/datacapture/src/main/res/layout/repeated_group_instance_header_view.xml @@ -0,0 +1,40 @@ + + + + + + diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapterTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapterTest.kt index 8b3b97bec3..6dd1a3e33d 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapterTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireEditAdapterTest.kt @@ -17,6 +17,8 @@ package com.google.android.fhir.datacapture import android.os.Build +import android.widget.FrameLayout +import androidx.test.core.app.ApplicationProvider import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_SYSTEM import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_SYSTEM_ANDROID_FHIR import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_URL @@ -24,7 +26,9 @@ import com.google.android.fhir.datacapture.extensions.EXTENSION_ITEM_CONTROL_URL import com.google.android.fhir.datacapture.extensions.ItemControlTypes import com.google.android.fhir.datacapture.validation.Invalid import com.google.android.fhir.datacapture.validation.NotValidated +import com.google.android.fhir.datacapture.views.MediaView import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolder import com.google.android.fhir.datacapture.views.factories.QuestionnaireItemViewHolderFactory import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest @@ -756,13 +760,10 @@ class QuestionnaireEditAdapterTest { fun onCreateViewHolder_customViewType_shouldReturnCorrectCustomViewHolder() { val viewFactoryMatchers = getQuestionnaireItemViewHolderFactoryMatchers() val questionnaireEditAdapter = QuestionnaireEditAdapter(viewFactoryMatchers) - assertThat( - questionnaireEditAdapter.onCreateViewHolder( - mock(), - QuestionnaireViewHolderType.values().size, - ), - ) - .isEqualTo(viewFactoryMatchers[0].factory.create(mock())) + val holder = + questionnaireEditAdapter.onCreateViewHolder(mock(), QuestionnaireViewHolderType.values().size) + holder as QuestionnaireEditAdapter.ViewHolder.QuestionHolder + assertThat(holder.holder).isEqualTo(fakeHolder) } @Test @@ -803,11 +804,20 @@ class QuestionnaireEditAdapterTest { return listOf( QuestionnaireFragment.QuestionnaireItemViewHolderFactoryMatcher( mock().apply { - whenever(create(any())).thenReturn(mock()) + whenever(create(any())).thenReturn(fakeHolder) }, ) { questionnaireItem -> questionnaireItem.type == Questionnaire.QuestionnaireItemType.DATE }, ) } + + private val fakeHolder = + QuestionnaireItemViewHolder( + itemView = + FrameLayout(ApplicationProvider.getApplicationContext()).apply { + addView(MediaView(context, null).apply { id = R.id.item_media }) + }, + delegate = mock(), + ) } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt index b5ae423aad..4c8def1287 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/QuestionnaireViewModelTest.kt @@ -4118,36 +4118,36 @@ class QuestionnaireViewModelTest { } val viewModel = createQuestionnaireViewModel(questionnaire) - fun repeatedGroupA() = + fun getQuestionnaireAdapterItemListA() = viewModel.getQuestionnaireItemViewItemList().single { - it.asQuestion().questionnaireItem.linkId == "repeated-group-a" + it.asQuestionOrNull()?.questionnaireItem?.linkId == "repeated-group-a" } - fun repeatedGroupB() = + fun getQuestionnaireAdapterItemListB() = viewModel.getQuestionnaireItemViewItemList().single { - it.asQuestion().questionnaireItem.linkId == "repeated-group-b" + it.asQuestionOrNull()?.questionnaireItem?.linkId == "repeated-group-b" } viewModel.runViewModelBlocking { // Calling addAnswer out of order should not result in the answers in the response being out // of order; all of the answers to repeated-group-a should come before repeated-group-b. repeat(times = 2) { - repeatedGroupA() + getQuestionnaireAdapterItemListA() .asQuestion() .addAnswer( QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { item = - repeatedGroupA() + getQuestionnaireAdapterItemListA() .asQuestion() .questionnaireItem .getNestedQuestionnaireResponseItems() }, ) - repeatedGroupB() + getQuestionnaireAdapterItemListB() .asQuestion() .addAnswer( QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { item = - repeatedGroupB() + getQuestionnaireAdapterItemListB() .asQuestion() .questionnaireItem .getNestedQuestionnaireResponseItems() @@ -4157,18 +4157,25 @@ class QuestionnaireViewModelTest { assertThat( viewModel.getQuestionnaireItemViewItemList().map { - it.asQuestion().questionnaireItem.linkId + when (it) { + is QuestionnaireAdapterItem.Question -> it.item.questionnaireItem.linkId + is QuestionnaireAdapterItem.RepeatedGroupHeader -> "RepeatedGroupHeader:${it.index}" + } }, ) .containsExactly( "repeated-group-a", + "RepeatedGroupHeader:0", "nested-item-a", "another-nested-item-a", + "RepeatedGroupHeader:1", "nested-item-a", "another-nested-item-a", "repeated-group-b", + "RepeatedGroupHeader:0", "nested-item-b", "another-nested-item-b", + "RepeatedGroupHeader:1", "nested-item-b", "another-nested-item-b", ) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt index 32c60eec04..43c75f665e 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/extensions/MoreQuestionnaireItemComponentsTest.kt @@ -2478,6 +2478,30 @@ class MoreQuestionnaireItemComponentsTest { assertThat(zipList.size).isEqualTo(3) } + @Test + fun `test questionnaireItemComponent is repeatedGroup`() { + val question = + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "1" + type = Questionnaire.QuestionnaireItemType.GROUP + repeats = true + } + + assertThat(question.isRepeatedGroup).isTrue() + } + + @Test + fun `test questionnaireItemComponent is not RepeatedGroup`() { + val question = + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "1" + type = Questionnaire.QuestionnaireItemType.GROUP + repeats = false + } + + assertThat(question.isRepeatedGroup).isFalse() + } + private val displayCategoryExtensionWithInstructionsCode = Extension().apply { url = EXTENSION_DISPLAY_CATEGORY_URL diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItemTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItemTest.kt index 4beaca8ecc..5b5c678a04 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItemTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/QuestionnaireViewItemTest.kt @@ -163,6 +163,91 @@ class QuestionnaireViewItemTest { assertThat(answers).hasSize(1) } + @Test + fun `remove answer at given index`() = runTest { + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "group-1" + type = Questionnaire.QuestionnaireItemType.GROUP + repeats = true + item = + listOf( + Questionnaire.QuestionnaireItemComponent().apply { + linkId = "nested-item-1" + type = Questionnaire.QuestionnaireItemType.STRING + }, + ) + } + + val responseItem1 = + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "1" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = StringType("Answer 1") + }, + ) + } + + val responseItem2 = + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "2" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = StringType("Answer 2") + }, + ) + } + + val responseItem3 = + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "3" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = StringType("Answer 3") + }, + ) + } + + val questionnaireResponseItem = + QuestionnaireResponse.QuestionnaireResponseItemComponent().apply { + linkId = "group-1" + answer = + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + item = listOf(responseItem1) + }, + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + item = listOf(responseItem2) + }, + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + item = listOf(responseItem3) + }, + ) + } + + var updatedAnswers: List = + listOf() + val questionnaireViewItem = + QuestionnaireViewItem( + questionnaireItem = questionnaireItem, + questionnaireResponseItem = questionnaireResponseItem, + validationResult = NotValidated, + answersChangedCallback = { _, responseItem, result, _ -> updatedAnswers = result }, + ) + + questionnaireViewItem.removeAnswerAt(1) + + assertThat(updatedAnswers.size).isEqualTo(2) + assertThat(updatedAnswers.first().item.first().answer.first().valueStringType.value) + .isEqualTo("Answer 1") + assertThat(updatedAnswers.last().item.first().answer.first().valueStringType.value) + .isEqualTo("Answer 3") + } + @Test fun hasAnswerOption_questionnaireItemRepeats_shouldReturnTrue() { val questionnaireViewItem = diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/GroupViewHolderFactoryTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/GroupViewHolderFactoryTest.kt index 538536cfee..d66e7768a1 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/GroupViewHolderFactoryTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/factories/GroupViewHolderFactoryTest.kt @@ -17,6 +17,7 @@ package com.google.android.fhir.datacapture.views.factories import android.view.View +import android.widget.Button import android.widget.FrameLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity @@ -196,4 +197,40 @@ class GroupViewHolderFactoryTest { assertThat(viewHolder.itemView.findViewById(R.id.add_item).visibility) .isEqualTo(View.GONE) } + + @Test + fun `test repeated group is read only view`() { + viewHolder.bind( + QuestionnaireViewItem( + Questionnaire.QuestionnaireItemComponent().apply { + text = "Question?" + type = Questionnaire.QuestionnaireItemType.GROUP + repeats = true + readOnly = true + }, + QuestionnaireResponse.QuestionnaireResponseItemComponent(), + validationResult = NotValidated, + answersChangedCallback = { _, _, _, _ -> }, + ), + ) + assertThat((viewHolder.itemView.findViewById