diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceAdapter.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceAdapter.kt deleted file mode 100644 index aef895b904..0000000000 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceAdapter.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.ground.ui.datacollection.tasks.multiplechoice - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.google.android.ground.databinding.MultipleChoiceCheckboxItemBinding -import com.google.android.ground.databinding.MultipleChoiceRadiobuttonItemBinding - -/** - * An implementation of [ListAdapter] that associates [MultipleChoiceItem]s with their [ViewHolder]. - */ -class MultipleChoiceAdapter( - private val viewModel: MultipleChoiceTaskViewModel, - private val canSelectMultiple: Boolean, -) : ListAdapter(MultipleChoiceItemCallback) { - - override fun getItemViewType(position: Int): Int = - if (canSelectMultiple) CHECKBOX_VIEW_TYPE else RADIO_VIEW_TYPE - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = - if (viewType == CHECKBOX_VIEW_TYPE) - CheckBoxViewHolder( - MultipleChoiceCheckboxItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false, - ) - ) - else - RadioButtonViewHolder( - MultipleChoiceRadiobuttonItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false, - ) - ) - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) = - (holder as MultipleChoiceViewHolder).bind(getItem(position), viewModel) - - private object MultipleChoiceItemCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: MultipleChoiceItem, - newItem: MultipleChoiceItem, - ): Boolean = oldItem.isTheSameItem(newItem) - - override fun areContentsTheSame( - oldItem: MultipleChoiceItem, - newItem: MultipleChoiceItem, - ): Boolean = oldItem.areContentsTheSame(newItem) - } - - private interface MultipleChoiceViewHolder { - fun bind(item: MultipleChoiceItem, viewModel: MultipleChoiceTaskViewModel) - } - - class CheckBoxViewHolder(internal val binding: MultipleChoiceCheckboxItemBinding) : - RecyclerView.ViewHolder(binding.root), MultipleChoiceViewHolder { - override fun bind(item: MultipleChoiceItem, viewModel: MultipleChoiceTaskViewModel) { - binding.item = item - binding.viewModel = viewModel - } - } - - class RadioButtonViewHolder(internal val binding: MultipleChoiceRadiobuttonItemBinding) : - RecyclerView.ViewHolder(binding.root), MultipleChoiceViewHolder { - override fun bind(item: MultipleChoiceItem, viewModel: MultipleChoiceTaskViewModel) { - binding.item = item - binding.viewModel = viewModel - } - } - - companion object { - private const val CHECKBOX_VIEW_TYPE = 1 - private const val RADIO_VIEW_TYPE = 2 - } -} diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceItem.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceItem.kt index f89e78c625..8444150823 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceItem.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceItem.kt @@ -24,9 +24,4 @@ data class MultipleChoiceItem( val isSelected: Boolean = false, val isOtherOption: Boolean = false, val otherText: String = "", -) { - fun isTheSameItem(otherItem: MultipleChoiceItem): Boolean = this.option.id == otherItem.option.id - - fun areContentsTheSame(otherItem: MultipleChoiceItem): Boolean = - otherItem.isSelected == this.isSelected -} +) diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceItemView.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceItemView.kt new file mode 100644 index 0000000000..ab7a7edd21 --- /dev/null +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceItemView.kt @@ -0,0 +1,172 @@ +/* + * Copyright 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.ground.ui.datacollection.tasks.multiplechoice + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.Checkbox +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.android.ground.ExcludeFromJacocoGeneratedReport +import com.google.android.ground.R +import com.google.android.ground.model.task.MultipleChoice +import com.google.android.ground.model.task.Option +import com.google.android.ground.ui.theme.AppTheme + +const val MULTIPLE_CHOICE_ITEM_TEST_TAG = "multiple choice item test tag" +const val OTHER_INPUT_TEXT_TEST_TAG = "other input test tag" +const val SELECT_MULTIPLE_RADIO_TEST_TAG = "select multiple radio test tag" + +/** + * A composable function that displays a single item in a multiple-choice list. + * + * This composable provides a visually consistent and interactive way to present an option within a + * list of choices where the user can select one or more items. It typically includes a text label + * and a selectable indicator (e.g., a checkbox). + */ +@Composable +fun MultipleChoiceItemView( + item: MultipleChoiceItem, + modifier: Modifier = Modifier, + isLastIndex: Boolean = false, + toggleItem: (item: MultipleChoiceItem) -> Unit = {}, + otherValueChanged: (text: String) -> Unit = {}, +) { + Column(modifier = Modifier.testTag(MULTIPLE_CHOICE_ITEM_TEST_TAG)) { + Row(modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + when (item.cardinality) { + MultipleChoice.Cardinality.SELECT_ONE -> { + RadioButton( + modifier = Modifier.testTag(SELECT_MULTIPLE_RADIO_TEST_TAG), + selected = item.isSelected, + onClick = { toggleItem(item) }, + ) + } + + MultipleChoice.Cardinality.SELECT_MULTIPLE -> { + Checkbox(checked = item.isSelected, onCheckedChange = { toggleItem(item) }) + } + } + + ClickableText( + text = item.toTextLabel(), + modifier = modifier, + style = MaterialTheme.typography.bodyLarge, + onClick = { toggleItem(item) }, + ) + } + + if (item.isOtherOption) { + Row(modifier = modifier.padding(horizontal = 48.dp)) { + TextField( + value = item.otherText, + textStyle = MaterialTheme.typography.bodyLarge, + onValueChange = { otherValueChanged(it) }, + modifier = Modifier.testTag(OTHER_INPUT_TEXT_TEST_TAG), + ) + } + } + + if (!isLastIndex) { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + } + } +} + +@Composable +private fun MultipleChoiceItem.toTextLabel() = + AnnotatedString(if (isOtherOption) stringResource(id = R.string.other) else option.label) + +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) +@Composable +@ExcludeFromJacocoGeneratedReport +fun SelectOneListItemPreview() { + AppTheme { + MultipleChoiceItemView( + item = + MultipleChoiceItem( + Option(id = "id", code = "code", label = "Option 1"), + cardinality = MultipleChoice.Cardinality.SELECT_ONE, + isSelected = false, + ) + ) + } +} + +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) +@Composable +@ExcludeFromJacocoGeneratedReport +fun SelectMultipleListItemPreview() { + AppTheme { + MultipleChoiceItemView( + item = + MultipleChoiceItem( + Option(id = "id", code = "code", label = "Option 2"), + cardinality = MultipleChoice.Cardinality.SELECT_MULTIPLE, + isSelected = false, + ) + ) + } +} + +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) +@Composable +@ExcludeFromJacocoGeneratedReport +fun SelectOneOtherListItemPreview() { + AppTheme { + MultipleChoiceItemView( + item = + MultipleChoiceItem( + Option(id = "id", code = "code", label = "Option 3"), + cardinality = MultipleChoice.Cardinality.SELECT_ONE, + isSelected = true, + isOtherOption = true, + otherText = "Other text", + ) + ) + } +} + +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) +@Composable +@ExcludeFromJacocoGeneratedReport +fun SelectMultipleOtherListItemPreview() { + AppTheme { + MultipleChoiceItemView( + item = + MultipleChoiceItem( + Option(id = "id", code = "code", label = "Option 4"), + cardinality = MultipleChoice.Cardinality.SELECT_MULTIPLE, + isSelected = true, + isOtherOption = true, + otherText = "Other text", + ) + ) + } +} diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragment.kt index 7d94947d09..49528747c0 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskFragment.kt @@ -17,16 +17,23 @@ package com.google.android.ground.ui.datacollection.tasks.multiplechoice import android.view.LayoutInflater import android.view.View -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.google.android.ground.databinding.MultipleChoiceTaskFragBinding -import com.google.android.ground.model.task.MultipleChoice +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.testTag +import androidx.lifecycle.asLiveData import com.google.android.ground.ui.datacollection.components.TaskView import com.google.android.ground.ui.datacollection.components.TaskViewFactory import com.google.android.ground.ui.datacollection.tasks.AbstractTaskFragment +import com.google.android.ground.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch + +const val MULTIPLE_CHOICE_LIST_TEST_TAG = "multiple choice items test tag" /** * Fragment allowing the user to answer single selection multiple choice questions to complete a @@ -34,31 +41,27 @@ import kotlinx.coroutines.launch */ @AndroidEntryPoint class MultipleChoiceTaskFragment : AbstractTaskFragment() { - private lateinit var binding: MultipleChoiceTaskFragBinding - private lateinit var multipleChoiceAdapter: - ListAdapter override fun onCreateTaskView(inflater: LayoutInflater): TaskView = TaskViewFactory.createWithHeader(inflater) - override fun onCreateTaskBody(inflater: LayoutInflater): View { - binding = MultipleChoiceTaskFragBinding.inflate(inflater) - setupMultipleChoice(binding.selectOptionList) - return binding.root - } + override fun onCreateTaskBody(inflater: LayoutInflater): View = + ComposeView(requireContext()).apply { setContent { AppTheme { ShowMultipleChoiceItems() } } } - // TODO: Test comment for adding links to repo. - private fun setupMultipleChoice(recyclerView: RecyclerView) { - val multipleChoice = checkNotNull(getTask().multipleChoice) - val canSelectMultiple = multipleChoice.cardinality == MultipleChoice.Cardinality.SELECT_MULTIPLE - multipleChoiceAdapter = MultipleChoiceAdapter(viewModel, canSelectMultiple) - recyclerView.apply { - adapter = multipleChoiceAdapter - itemAnimator = null - setHasFixedSize(true) - } - lifecycleScope.launch { - viewModel.itemsFlow.collect { items -> multipleChoiceAdapter.submitList(items) } + @Composable + private fun ShowMultipleChoiceItems() { + val list by viewModel.itemsFlow.asLiveData().observeAsState() + list?.let { items -> + LazyColumn(Modifier.fillMaxSize().testTag(MULTIPLE_CHOICE_LIST_TEST_TAG)) { + items(items) { item -> + MultipleChoiceItemView( + item = item, + isLastIndex = items.indexOf(item) == items.lastIndex, + toggleItem = { viewModel.onItemToggled(it) }, + otherValueChanged = { viewModel.onOtherTextChanged(it) }, + ) + } + } } } } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskViewModel.kt index 2d173c4558..501479073d 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/multiplechoice/MultipleChoiceTaskViewModel.kt @@ -15,8 +15,6 @@ */ package com.google.android.ground.ui.datacollection.tasks.multiplechoice -import android.text.Editable -import android.text.TextWatcher import com.google.android.ground.model.job.Job import com.google.android.ground.model.submission.MultipleChoiceTaskData import com.google.android.ground.model.submission.MultipleChoiceTaskData.Companion.fromList @@ -39,33 +37,13 @@ class MultipleChoiceTaskViewModel @Inject constructor() : AbstractTaskViewModel( private val selectedIds: MutableSet = mutableSetOf() private var otherText: String = "" - val otherTextWatcher = - object : TextWatcher { - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - otherText = s.toString() - // Set the other option. - _items.value - .firstOrNull { it.isOtherOption } - ?.let { setItem(item = it, selection = isOtherTextValid()) } - updateResponse() - } - - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { - // Not implemented. - } - - override fun afterTextChanged(s: Editable) { - // Not implemented. - } - } - override fun initialize(job: Job, task: Task, taskData: TaskData?) { super.initialize(job, task, taskData) loadPendingSelections() updateMultipleChoiceItems() } - fun setItem(item: MultipleChoiceItem, selection: Boolean) { + private fun setItem(item: MultipleChoiceItem, selection: Boolean) { if (item.cardinality != SELECT_MULTIPLE && selection) { selectedIds.clear() } @@ -78,12 +56,32 @@ class MultipleChoiceTaskViewModel @Inject constructor() : AbstractTaskViewModel( updateMultipleChoiceItems() } - fun toggleItem(item: MultipleChoiceItem) { + /** + * Invoked when "other" text input field is modified. It updates the internal state with the new + * text, if valid. + * + * @param text new text entered in the "other" input field. + */ + fun onOtherTextChanged(text: String) { + otherText = text + // Set the other option. + _items.value + .firstOrNull { it.isOtherOption } + ?.let { setItem(item = it, selection = isOtherTextValid()) } + updateResponse() + } + + /** + * Invoked when a multiple choice item is selected/unselected. + * + * @param item multiple choice item which was modified. + */ + fun onItemToggled(item: MultipleChoiceItem) { val wasSelected = selectedIds.contains(item.option.id) setItem(item, !wasSelected) } - fun updateResponse() { + private fun updateResponse() { // Check if "other" text is missing or not. if (selectedIds.contains(OTHER_ID) && !isOtherTextValid()) { clearResponse() diff --git a/ground/src/main/res/layout/multiple_choice_checkbox_item.xml b/ground/src/main/res/layout/multiple_choice_checkbox_item.xml deleted file mode 100644 index d20736c954..0000000000 --- a/ground/src/main/res/layout/multiple_choice_checkbox_item.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ground/src/main/res/layout/multiple_choice_radiobutton_item.xml b/ground/src/main/res/layout/multiple_choice_radiobutton_item.xml deleted file mode 100644 index 43370390be..0000000000 --- a/ground/src/main/res/layout/multiple_choice_radiobutton_item.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/ground/src/main/res/layout/multiple_choice_task_frag.xml b/ground/src/main/res/layout/multiple_choice_task_frag.xml deleted file mode 100644 index 2f1f95cba9..0000000000 --- a/ground/src/main/res/layout/multiple_choice_task_frag.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/ground/src/main/res/layout/task_frag_with_header.xml b/ground/src/main/res/layout/task_frag_with_header.xml index 76cf0c8203..1b13e22894 100644 --- a/ground/src/main/res/layout/task_frag_with_header.xml +++ b/ground/src/main/res/layout/task_frag_with_header.xml @@ -32,14 +32,17 @@ @@ -50,9 +53,10 @@ android:layout_height="0dp" android:layout_marginStart="20dp" android:layout_marginEnd="20dp" + app:layout_constraintBottom_toTopOf="@+id/action_buttons" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/data_collection_header" - app:layout_constraintBottom_toTopOf="@+id/action_buttons" /> + app:layout_constraintTop_toBottomOf="@+id/data_collection_header" /> (job, task.copy(multipleChoice = multipleChoice)) - - onView(withText("Option 1")).perform(click()) - onView(withText("Option 2")).perform(click()) - - hasValue(MultipleChoiceTaskData(multipleChoice, listOf("option id 2"))) - runner().assertButtonIsEnabled("Next") + runner().assertOptionsDisplayed("Option 1", "Option 2") } @Test @@ -129,9 +100,17 @@ class MultipleChoiceTaskFragmentTest : ), ) - onView(withId(R.id.select_option_list)).check(matches(allOf(isDisplayed(), hasChildCount(2)))) - onView(withText("Option 1")) - .check(matches(allOf(isDisplayed(), instanceOf(MaterialCheckBox::class.java)))) + runner().assertOptionsDisplayed("Option 1", "Option 2") + } + + @Test + fun `allows only one selection for SELECT_ONE cardinality`() = runWithTestDispatcher { + val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE) + setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) + + runner().selectOption("Option 1").selectOption("Option 2").assertButtonIsEnabled("Next") + + hasValue(MultipleChoiceTaskData(multipleChoice, listOf("option id 2"))) } @Test @@ -139,11 +118,9 @@ class MultipleChoiceTaskFragmentTest : val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_MULTIPLE) setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) - onView(withText("Option 1")).perform(click()) - onView(withText("Option 2")).perform(click()) + runner().selectOption("Option 1").selectOption("Option 2").assertButtonIsEnabled("Next") hasValue(MultipleChoiceTaskData(multipleChoice, listOf("option id 1", "option id 2"))) - runner().assertButtonIsEnabled("Next") } @Test @@ -155,13 +132,9 @@ class MultipleChoiceTaskFragmentTest : ) val userInput = "User inputted text" - onView(withText("Other")).perform(click()) - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.forceTypeText(userInput)) + runner().selectOption("Other").inputOtherText(userInput).assertButtonIsEnabled("Next") - onView(withText("Other")).check(matches(isChecked())) hasValue(MultipleChoiceTaskData(multipleChoice, listOf("[ $userInput ]"))) - runner().assertButtonIsEnabled("Next") } @Test @@ -169,13 +142,16 @@ class MultipleChoiceTaskFragmentTest : runWithTestDispatcher { val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE, true) setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) - onView(withText("Option 1")).perform(click()) - onView(withText("Other")).check(matches(isNotChecked())) + val userInput = "A" - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.forceTypeText(userInput)) - onView(withText("Option 1")).check(matches(isNotChecked())) - onView(withText("Other")).check(matches(isChecked())) + + runner() + .selectOption("Option 1") + .assertOptionNotSelected("Other") + .inputOtherText(userInput) + .assertOptionNotSelected("Option 1") + .assertOptionSelected("Other") + hasValue(MultipleChoiceTaskData(multipleChoice, listOf("[ $userInput ]"))) } @@ -187,16 +163,12 @@ class MultipleChoiceTaskFragmentTest : task.copy(multipleChoice = multipleChoice, isRequired = true), ) - onView(withText("Other")).check(matches(isNotChecked())) - val userInput = "A" - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.forceTypeText(userInput)) - onView(withText("Other")).check(matches(isChecked())) - - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.clearText()) - - onView(withText("Other")).check(matches(isNotChecked())) + runner() + .assertOptionNotSelected("Other") + .inputOtherText("A") + .assertOptionSelected("Other") + .clearOtherText() + .assertOptionNotSelected("Other") } @Test @@ -204,16 +176,12 @@ class MultipleChoiceTaskFragmentTest : val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE, true) setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) - onView(withText("Other")).check(matches(isNotChecked())) - val userInput = "A" - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.forceTypeText(userInput)) - onView(withText("Other")).check(matches(isChecked())) - - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.clearText()) - - onView(withText("Other")).check(matches(isChecked())) + runner() + .assertOptionNotSelected("Other") + .inputOtherText("A") + .assertOptionSelected("Other") + .clearOtherText() + .assertOptionSelected("Other") } @Test @@ -224,18 +192,14 @@ class MultipleChoiceTaskFragmentTest : task.copy(multipleChoice = multipleChoice, isRequired = true), ) - val userInput = "A" - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.forceTypeText(userInput)) - onView(withText("Option 1")).perform(click()) - onView(withText("Other")).check(matches(isNotChecked())) - onView(withText("Option 1")).check(matches(isChecked())) - - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.clearText()) - - onView(withText("Option 1")).check(matches(isChecked())) - onView(withText("Other")).check(matches(isNotChecked())) + runner() + .inputOtherText("A") + .selectOption("Option 1") + .assertOptionNotSelected("Other") + .assertOptionSelected("Option 1") + .clearOtherText() + .assertOptionSelected("Option 1") + .assertOptionNotSelected("Other") } @Test @@ -271,9 +235,7 @@ class MultipleChoiceTaskFragmentTest : val multipleChoice = MultipleChoice(options, MultipleChoice.Cardinality.SELECT_ONE) setupTaskFragment(job, task.copy(multipleChoice = multipleChoice)) - onView(withText("Option 1")).perform(click()) - - runner().assertButtonIsHidden("Skip") + runner().selectOption("Option 1").assertButtonIsHidden("Skip") } @Test @@ -317,12 +279,9 @@ class MultipleChoiceTaskFragmentTest : task.copy(multipleChoice = multipleChoice, isRequired = true), ) - onView(withText("Other")).perform(click()) - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.forceTypeText("")) + runner().selectOption("Other").inputOtherText("").assertButtonIsDisabled("Next") hasValue(null) - runner().assertButtonIsDisabled("Next") } @Test @@ -334,13 +293,12 @@ class MultipleChoiceTaskFragmentTest : task.copy(multipleChoice = multipleChoice, isRequired = true), ) - onView(withText("Option 1")).perform(click()) - onView(withText("Option 2")).perform(click()) - onView(withText("Other")).perform(click()) - onView(allOf(isDisplayed(), withId(R.id.user_response_text))) - .perform(CustomViewActions.forceTypeText("")) + runner() + .selectOption("Option 1") + .selectOption("Option 2") + .selectOption("Other") + .assertButtonIsDisabled("Next") hasValue(null) - runner().assertButtonIsDisabled("Next") } }