diff --git a/datacapture/sampledata/component_multiple_repeated_group.json b/datacapture/sampledata/component_multiple_repeated_group.json new file mode 100644 index 0000000000..6df96b41ca --- /dev/null +++ b/datacapture/sampledata/component_multiple_repeated_group.json @@ -0,0 +1,37 @@ +{ + "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" + } + ] + } + ] + }, + { + "linkId": "2", + "type": "group", + "text": "Decimal Repeated Group", + "repeats": true, + "item": [ + { + "linkId": "2-1", + "text": "Sample decimal question", + "type": "decimal" + } + ] + } + ] +} diff --git a/datacapture/sampledata/component_non_repeated_group.json b/datacapture/sampledata/component_non_repeated_group.json new file mode 100644 index 0000000000..01268f4df1 --- /dev/null +++ b/datacapture/sampledata/component_non_repeated_group.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Questionnaire", + "item": [ + { + "linkId": "1", + "type": "group", + "text": "Group", + "repeats": false, + "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/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 3ca8eb1b21..560d377a50 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ 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.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.RootMatchers import androidx.test.espresso.matcher.ViewMatchers @@ -610,6 +611,12 @@ class QuestionnaireUiEspressoTest { } } + @Test + fun test_add_item_button_does_not_exist_for_non_repeated_groups() { + buildFragmentFromQuestionnaire("/component_non_repeated_group.json") + onView(withId(R.id.add_item_to_repeated_group)).check(doesNotExist()) + } + @Test fun test_repeated_group_is_added() { buildFragmentFromQuestionnaire("/component_repeated_group.json") @@ -617,8 +624,9 @@ class QuestionnaireUiEspressoTest { onView(withId(R.id.questionnaire_edit_recycler_view)) .perform( RecyclerViewActions.actionOnItemAtPosition( - 0, - clickChildViewWithId(R.id.add_item), + 1, // 'Add item' is in the second row of the recyclerview with group header as the first + // item + clickChildViewWithId(R.id.add_item_to_repeated_group), ), ) @@ -638,6 +646,75 @@ class QuestionnaireUiEspressoTest { } } + @Test + fun test_repeated_group_adds_multiple_items() { + buildFragmentFromQuestionnaire("/component_multiple_repeated_group.json") + onView(withId(R.id.questionnaire_edit_recycler_view)) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 1, // The add button position is 1 (zero-indexed) after the group's header + clickChildViewWithId(R.id.add_item_to_repeated_group), + ), + ) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 3, // The add button new position becomes 3 (zero-indexed) after the group's header, + // repeated item's header and the one item added + clickChildViewWithId(R.id.add_item_to_repeated_group), + ), + ) + + 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(2) + } + } + + @Test + fun test_repeated_group_adds_items_for_subsequent() { + buildFragmentFromQuestionnaire("/component_multiple_repeated_group.json") + onView(withId(R.id.questionnaire_edit_recycler_view)) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 3, // The add button for the second repeated group is at position 3 (zero-indexed), after + // the first group's header (0), the first group's add button (1), and the second + // group's header (2) + clickChildViewWithId(R.id.add_item_to_repeated_group), + ), + ) + .perform( + RecyclerViewActions.actionOnItemAtPosition( + 5, // The add button for the second group is now at position 5 after adding one item + clickChildViewWithId(R.id.add_item_to_repeated_group), + ), + ) + + 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(2) + } + } + @Test fun test_repeated_group_is_deleted() { 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 5b97c29b62..9d26010034 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-2024 Google LLC + * Copyright 2022-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,8 @@ 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 + data class Question(val item: QuestionnaireViewItem) : + QuestionnaireAdapterItem, ReviewAdapterItem /** A row for a repeated group response instance's header. */ data class RepeatedGroupHeader( @@ -35,6 +36,12 @@ internal sealed interface QuestionnaireAdapterItem { val title: String, ) : QuestionnaireAdapterItem + data class RepeatedGroupAddButton( + val item: QuestionnaireViewItem, + ) : QuestionnaireAdapterItem + data class Navigation(val questionnaireNavigationUIState: QuestionnaireNavigationUIState) : - QuestionnaireAdapterItem + QuestionnaireAdapterItem, ReviewAdapterItem } + +internal sealed interface ReviewAdapterItem 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 6fc427eae2..d5243482c6 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-2024 Google LLC + * Copyright 2022-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import com.google.android.fhir.datacapture.extensions.itemControl import com.google.android.fhir.datacapture.extensions.shouldUseDialog import com.google.android.fhir.datacapture.views.NavigationViewHolder import com.google.android.fhir.datacapture.views.QuestionnaireViewItem +import com.google.android.fhir.datacapture.views.RepeatedGroupAddItemViewHolder import com.google.android.fhir.datacapture.views.factories.AttachmentViewHolderFactory import com.google.android.fhir.datacapture.views.factories.AutoCompleteViewHolderFactory import com.google.android.fhir.datacapture.views.factories.BooleanChoiceViewHolderFactory @@ -80,6 +81,11 @@ internal class QuestionnaireEditAdapter( ), ) } + ViewType.Type.REPEATED_GROUP_ADD_BUTTON -> { + ViewHolder.RepeatedGroupAddButtonViewHolder( + RepeatedGroupAddItemViewHolder.create(parent), + ) + } } } @@ -138,6 +144,10 @@ internal class QuestionnaireEditAdapter( holder as ViewHolder.NavigationHolder holder.viewHolder.bind(item.questionnaireNavigationUIState) } + is QuestionnaireAdapterItem.RepeatedGroupAddButton -> { + holder as ViewHolder.RepeatedGroupAddButtonViewHolder + holder.viewHolder.bind(item.item) + } } } @@ -163,6 +173,10 @@ internal class QuestionnaireEditAdapter( type = ViewType.Type.NAVIGATION subtype = 0xFFFFFF } + is QuestionnaireAdapterItem.RepeatedGroupAddButton -> { + type = ViewType.Type.REPEATED_GROUP_ADD_BUTTON + subtype = 0 + } } return ViewType.from(type = type, subtype = subtype).viewType } @@ -194,6 +208,7 @@ internal class QuestionnaireEditAdapter( enum class Type { QUESTION, REPEATED_GROUP_HEADER, + REPEATED_GROUP_ADD_BUTTON, NAVIGATION, } } @@ -296,6 +311,9 @@ internal class QuestionnaireEditAdapter( ViewHolder(viewHolder.itemView) class NavigationHolder(val viewHolder: NavigationViewHolder) : ViewHolder(viewHolder.itemView) + + class RepeatedGroupAddButtonViewHolder(val viewHolder: RepeatedGroupAddItemViewHolder) : + ViewHolder(viewHolder.itemView) } internal companion object { @@ -324,6 +342,10 @@ internal object DiffCallbacks { oldItem.index == newItem.index } is QuestionnaireAdapterItem.Navigation -> newItem is QuestionnaireAdapterItem.Navigation + is QuestionnaireAdapterItem.RepeatedGroupAddButton -> { + newItem is QuestionnaireAdapterItem.RepeatedGroupAddButton && + oldItem.item.hasTheSameItem(newItem.item) + } } override fun areContentsTheSame( @@ -363,6 +385,12 @@ internal object DiffCallbacks { newItem is QuestionnaireAdapterItem.Navigation && oldItem.questionnaireNavigationUIState == newItem.questionnaireNavigationUIState } + is QuestionnaireAdapterItem.RepeatedGroupAddButton -> { + newItem is QuestionnaireAdapterItem.RepeatedGroupAddButton && + oldItem.item.hasTheSameItem(newItem.item) && + oldItem.item.hasTheSameResponse(newItem.item) && + oldItem.item.hasTheSameValidationResult(newItem.item) + } } } @@ -390,4 +418,25 @@ internal object DiffCallbacks { oldItem.item.hasTheSameValidationResult(newItem.item) } } + + val REVIEW_ITEMS = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ReviewAdapterItem, + newItem: ReviewAdapterItem, + ): Boolean = + ITEMS.areItemsTheSame( + oldItem as QuestionnaireAdapterItem, + newItem as QuestionnaireAdapterItem, + ) + + override fun areContentsTheSame( + oldItem: ReviewAdapterItem, + newItem: ReviewAdapterItem, + ): Boolean = + ITEMS.areContentsTheSame( + oldItem as QuestionnaireAdapterItem, + newItem as QuestionnaireAdapterItem, + ) + } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt index 590339b5e7..f303da57ea 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireFragment.kt @@ -163,7 +163,7 @@ class QuestionnaireFragment : Fragment() { // Set items questionnaireEditRecyclerView.visibility = View.GONE questionnaireReviewAdapter.submitList( - state.items, + state.items.filterIsInstance(), ) questionnaireReviewRecyclerView.visibility = View.VISIBLE reviewModeEditButton.visibility = diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireReviewAdapter.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireReviewAdapter.kt index 7f323562c2..97506b4a72 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireReviewAdapter.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireReviewAdapter.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 Google LLC + * Copyright 2022-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,19 +26,18 @@ import com.google.android.fhir.datacapture.views.factories.ReviewViewHolderFacto /** List Adapter used to bind answers to [QuestionnaireItemViewHolder] in review mode. */ internal class QuestionnaireReviewAdapter : - ListAdapter( - DiffCallbacks.ITEMS, + ListAdapter( + DiffCallbacks.REVIEW_ITEMS, ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val typedViewType = QuestionnaireEditAdapter.ViewType.parse(viewType) - return when (typedViewType.type) { - QuestionnaireEditAdapter.ViewType.Type.QUESTION -> ReviewViewHolderFactory.create(parent) - QuestionnaireEditAdapter.ViewType.Type.NAVIGATION -> + return when (viewType) { + VIEW_TYPE_QUESTION -> ReviewViewHolderFactory.create(parent) + VIEW_TYPE_NAVIGATION -> NavigationViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.pagination_navigation_view, parent, false), ) - QuestionnaireEditAdapter.ViewType.Type.REPEATED_GROUP_HEADER -> TODO() + else -> throw IllegalStateException("Invalid view type: $viewType") } } @@ -52,29 +51,18 @@ internal class QuestionnaireReviewAdapter : holder as NavigationViewHolder holder.bind(item.questionnaireNavigationUIState) } - is QuestionnaireAdapterItem.RepeatedGroupHeader -> TODO() } } - override fun getItemViewType(position: Int): Int { - // Because we have multiple Item subtypes, we will pack two ints into the item view type. - - // The first 8 bits will be represented by this type, which is unique for each Item subclass. - val type: QuestionnaireEditAdapter.ViewType.Type - // The last 24 bits will be represented by this subtype, which will further divide each Item - // subclass into more view types. - val subtype: Int + override fun getItemViewType(position: Int): Int = when (getItem(position)) { - is QuestionnaireAdapterItem.Question -> { - type = QuestionnaireEditAdapter.ViewType.Type.QUESTION - subtype = 0xFFFFFF - } - is QuestionnaireAdapterItem.Navigation -> { - type = QuestionnaireEditAdapter.ViewType.Type.NAVIGATION - subtype = 0xFFFFFF - } - is QuestionnaireAdapterItem.RepeatedGroupHeader -> TODO() + is QuestionnaireAdapterItem.Question -> VIEW_TYPE_QUESTION + is QuestionnaireAdapterItem.Navigation -> VIEW_TYPE_NAVIGATION + else -> super.getItemViewType(position) } - return QuestionnaireEditAdapter.ViewType.from(type = type, subtype = subtype).viewType + + companion object { + const val VIEW_TYPE_QUESTION = 1 + const val VIEW_TYPE_NAVIGATION = 2 } } 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 e96bd28a50..4f76294732 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 @@ -1024,6 +1024,10 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat ), ) } + + if (questionnaireItem.isRepeatedGroup) { + add(QuestionnaireAdapterItem.RepeatedGroupAddButton(question.item)) + } } currentPageItems = items return items diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/views/RepeatedGroupAddItemViewHolder.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/RepeatedGroupAddItemViewHolder.kt new file mode 100644 index 0000000000..7bef18b944 --- /dev/null +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/views/RepeatedGroupAddItemViewHolder.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 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 + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import com.google.android.fhir.datacapture.R +import com.google.android.fhir.datacapture.extensions.tryUnwrapContext +import kotlinx.coroutines.launch +import org.hl7.fhir.r4.model.QuestionnaireResponse + +class RepeatedGroupAddItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + + private var context: AppCompatActivity = itemView.context.tryUnwrapContext()!! + + fun bind(questionnaireViewItem: QuestionnaireViewItem) { + val addItemButton: Button = itemView.findViewById(R.id.add_item_to_repeated_group) + + addItemButton.text = + itemView.context.getString( + R.string.add_repeated_group_item, + questionnaireViewItem.questionText ?: "", + ) + addItemButton.visibility = + if (questionnaireViewItem.questionnaireItem.repeats) View.VISIBLE else View.GONE + addItemButton.setOnClickListener { + context.lifecycleScope.launch { + questionnaireViewItem.addAnswer( + // Nested items will be added in answerChangedCallback in the QuestionnaireViewModel + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(), + ) + } + } + + addItemButton.isEnabled = !questionnaireViewItem.questionnaireItem.readOnly + } + + companion object { + val layoutRes = R.layout.add_repeated_item + + fun create(parent: ViewGroup): RepeatedGroupAddItemViewHolder { + return RepeatedGroupAddItemViewHolder( + LayoutInflater.from(parent.context).inflate(layoutRes, parent, false), + ) + } + } +} 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 24f3cc67ab..aedb716d1a 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 @@ -17,10 +17,8 @@ package com.google.android.fhir.datacapture.views.factories import android.view.View -import android.widget.Button import android.widget.TextView import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope import com.google.android.fhir.datacapture.R import com.google.android.fhir.datacapture.extensions.tryUnwrapContext import com.google.android.fhir.datacapture.validation.Invalid @@ -29,8 +27,6 @@ import com.google.android.fhir.datacapture.validation.Valid import com.google.android.fhir.datacapture.validation.ValidationResult import com.google.android.fhir.datacapture.views.GroupHeaderView import com.google.android.fhir.datacapture.views.QuestionnaireViewItem -import kotlinx.coroutines.launch -import org.hl7.fhir.r4.model.QuestionnaireResponse internal object GroupViewHolderFactory : QuestionnaireItemAndroidViewHolderFactory(R.layout.group_header_view) { @@ -39,33 +35,16 @@ internal object GroupViewHolderFactory : private lateinit var context: AppCompatActivity private lateinit var header: GroupHeaderView private lateinit var error: TextView - private lateinit var addItemButton: Button override lateinit var questionnaireViewItem: QuestionnaireViewItem override fun init(itemView: View) { context = itemView.context.tryUnwrapContext()!! header = itemView.findViewById(R.id.header) error = itemView.findViewById(R.id.error) - addItemButton = itemView.findViewById(R.id.add_item) } override fun bind(questionnaireViewItem: QuestionnaireViewItem) { header.bind(questionnaireViewItem) - addItemButton.text = - context.getString( - R.string.add_repeated_group_item, - questionnaireViewItem.questionText ?: "", - ) - addItemButton.visibility = - if (questionnaireViewItem.questionnaireItem.repeats) View.VISIBLE else View.GONE - addItemButton.setOnClickListener { - context.lifecycleScope.launch { - questionnaireViewItem.addAnswer( - // Nested items will be added in answerChangedCallback in the QuestionnaireViewModel - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(), - ) - } - } displayValidationResult(questionnaireViewItem.validationResult) } @@ -81,7 +60,7 @@ internal object GroupViewHolderFactory : } override fun setReadOnly(isReadOnly: Boolean) { - addItemButton.isEnabled = !isReadOnly + // No-op } } } diff --git a/datacapture/src/main/res/layout/add_repeated_item.xml b/datacapture/src/main/res/layout/add_repeated_item.xml new file mode 100644 index 0000000000..85bba8edc5 --- /dev/null +++ b/datacapture/src/main/res/layout/add_repeated_item.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/datacapture/src/main/res/layout/group_header_view.xml b/datacapture/src/main/res/layout/group_header_view.xml index f8e58a23c0..ee87e23163 100644 --- a/datacapture/src/main/res/layout/group_header_view.xml +++ b/datacapture/src/main/res/layout/group_header_view.xml @@ -50,12 +50,4 @@ android:layout_height="wrap_content" /> - - 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 659429a95a..cc9f73cb71 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 @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 Google LLC + * Copyright 2023-2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -4654,6 +4654,7 @@ class QuestionnaireViewModelTest { is QuestionnaireAdapterItem.Question -> it.item.questionnaireItem.linkId is QuestionnaireAdapterItem.RepeatedGroupHeader -> "RepeatedGroupHeader:${it.index}" is QuestionnaireAdapterItem.Navigation -> TODO() + is QuestionnaireAdapterItem.RepeatedGroupAddButton -> "Add repeated group item" } }, ) @@ -4665,6 +4666,7 @@ class QuestionnaireViewModelTest { "RepeatedGroupHeader:1", "nested-item-a", "another-nested-item-a", + "Add repeated group item", "repeated-group-b", "RepeatedGroupHeader:0", "nested-item-b", @@ -4672,6 +4674,7 @@ class QuestionnaireViewModelTest { "RepeatedGroupHeader:1", "nested-item-b", "another-nested-item-b", + "Add repeated group item", ) .inOrder() @@ -4905,16 +4908,21 @@ class QuestionnaireViewModelTest { val viewModel = createQuestionnaireViewModel(questionnaire) viewModel.runViewModelBlocking { - viewModel.getQuestionnaireItemViewItemList().single().asQuestion().apply { - this.answersChangedCallback( - this.questionnaireItem, - this.getQuestionnaireResponseItem(), - listOf( - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(), - ), - null, - ) - } + viewModel + .getQuestionnaireItemViewItemList() + .filterIsInstance() + .single() + .asQuestion() + .apply { + this.answersChangedCallback( + this.questionnaireItem, + this.getQuestionnaireResponseItem(), + listOf( + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent(), + ), + null, + ) + } assertThat( viewModel diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/views/RepeatedGroupAddItemViewHolderTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/RepeatedGroupAddItemViewHolderTest.kt new file mode 100644 index 0000000000..97bba697fb --- /dev/null +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/views/RepeatedGroupAddItemViewHolderTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright 2025 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 + +import android.view.View +import android.widget.Button +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import com.google.android.fhir.datacapture.R +import com.google.android.fhir.datacapture.validation.NotValidated +import com.google.common.truth.Truth.assertThat +import org.hl7.fhir.r4.model.Questionnaire +import org.hl7.fhir.r4.model.QuestionnaireResponse +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class RepeatedGroupAddItemViewHolderTest { + + private val parent = + FrameLayout( + Robolectric.buildActivity(AppCompatActivity::class.java).create().get().apply { + setTheme(com.google.android.material.R.style.Theme_Material3_DayNight) + }, + ) + private val viewHolder: RepeatedGroupAddItemViewHolder = + RepeatedGroupAddItemViewHolder.create(parent) + + @Test + fun testRepeatedGroupIsReadOnlyDisablesAddButton() { + 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