Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix part of #5070: Display empty answer message in selection interaction #5319

Merged
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.oppia.android.app.player.state.itemviewmodel

import androidx.annotation.StringRes
import androidx.databinding.Observable
import androidx.databinding.ObservableBoolean
import androidx.databinding.ObservableField
Expand All @@ -12,6 +13,7 @@ import org.oppia.android.app.model.SubtitledHtml
import org.oppia.android.app.model.TranslatableHtmlContentId
import org.oppia.android.app.model.UserAnswer
import org.oppia.android.app.model.WrittenTranslationContext
import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerHandler
import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
Expand All @@ -26,6 +28,18 @@ enum class SelectionItemInputType {
RADIO_BUTTONS
}

/** Enum to the store the errors of selection input. */
enum class SelectionInputError(@StringRes private var error: Int?) {
VALID(error = null),
EMPTY_INPUT(error = R.string.selection_error_empty_input);

/**
* Returns the string corresponding to this error's string resources, or null if there is none.
*/
fun getErrorMessageFromStringRes(resourceHandler: AppLanguageResourceHandler): String? =
error?.let(resourceHandler::getStringInLocale)
}

/** [StateItemViewModel] for multiple or item-selection input choice list. */
class SelectionInteractionViewModel private constructor(
val entityId: String,
Expand Down Expand Up @@ -64,7 +78,9 @@ class SelectionInteractionViewModel private constructor(
val choiceItems: ObservableList<SelectionInteractionContentViewModel> =
computeChoiceItems(choiceSubtitledHtmls, hasConversationView, this, enabledItemsList)

private var pendingAnswerError: String? = null
private val isAnswerAvailable = ObservableField(false)
var errorMessage = ObservableField<String>("")
adhiamboperes marked this conversation as resolved.
Show resolved Hide resolved
val selectedItemText =
ObservableField(
resourceHandler.getStringInLocale(
Expand All @@ -77,12 +93,19 @@ class SelectionInteractionViewModel private constructor(
object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable, propertyId: Int) {
interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
pendingAnswerError = null,
inputAnswerAvailable = selectedItems.isNotEmpty()
pendingAnswerError,
inputAnswerAvailable = true // Allow blank answer submission.
)
}
}
errorMessage.addOnPropertyChangedCallback(callback)
isAnswerAvailable.addOnPropertyChangedCallback(callback)

// Initializing with default values so that submit button is enabled by default.
interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
pendingAnswerError = null,
inputAnswerAvailable = true
)
}

override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply {
Expand Down Expand Up @@ -113,6 +136,20 @@ class SelectionInteractionViewModel private constructor(
writtenTranslationContext = translationContext
}.build()

/**
* It checks the pending error for the current selection input, and correspondingly
* updates the error string based on the specified error category.
*/
override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
pendingAnswerError = when (category) {
AnswerErrorCategory.REAL_TIME -> null
AnswerErrorCategory.SUBMIT_TIME ->
getSubmitTimeError().getErrorMessageFromStringRes(resourceHandler)
}
errorMessage.set(pendingAnswerError)
return pendingAnswerError
}

/** Returns an HTML list containing all of the HTML string elements as items in the list. */
private fun convertSelectedItemsToHtmlString(itemHtmls: Collection<String>): String {
return when (itemHtmls.size) {
Expand All @@ -135,6 +172,7 @@ class SelectionInteractionViewModel private constructor(

/** Catalogs an item being clicked by the user and returns whether the item should be considered selected. */
fun updateSelection(itemIndex: Int, isCurrentlySelected: Boolean): Boolean {
checkPendingAnswerError(AnswerErrorCategory.REAL_TIME)
return when {
isCurrentlySelected -> {
selectedItems -= itemIndex
Expand Down Expand Up @@ -208,6 +246,13 @@ class SelectionInteractionViewModel private constructor(
}
}

private fun getSubmitTimeError(): SelectionInputError {
return if (selectedItems.isEmpty())
SelectionInputError.EMPTY_INPUT
else
SelectionInputError.VALID
}

/** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */
class FactoryImpl @Inject constructor(
private val translationController: TranslationController,
Expand Down
11 changes: 10 additions & 1 deletion app/src/main/res/layout/selection_interaction_item.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
android:paddingTop="12dp"
android:paddingEnd="@dimen/selection_interaction_item_padding_end"
android:paddingBottom="12dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">

Expand Down Expand Up @@ -60,5 +59,15 @@
app:selectionData="@{viewModel.choiceItems}"
app:writtenTranslationContext="@{viewModel.writtenTranslationContext}" />
</LinearLayout>

<TextView
android:id="@+id/selection_input_error"
style="@style/InputInteractionErrorTextView"
android:text="@{viewModel.errorMessage}"
android:textColor="@color/component_color_shared_input_error_color"
android:visibility="@{viewModel.errorMessage.length()>0? View.VISIBLE : View.GONE}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/interaction_container_linear_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@
<string name="ratio_error_invalid_size" description="The error message for ratio when there is less number of terms than the creator has specified in customization arg.">Number of terms is not equal to the required terms.</string>
<string name="ratio_error_includes_zero" description="The error message for ratio when there is 0 term in the ratio.">Ratios cannot have 0 as an element.</string>
<string name="ratio_error_empty_input" description="The error message for ratio when the answer is empty">Enter a ratio to continue.</string>
<string name="selection_error_empty_input">Choose an answer to continue.</string>
<string name="unknown_size">Unknown size</string>
<string name="size_bytes">%s Bytes</string>
<string name="size_kb">%s KB</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@ class StateFragmentTest {
}

@Test
fun testStateFragment_loadExp_thirdState_hasDisabledSubmitButton() {
fun testStateFragment_loadExp_thirdState_hasEnabledSubmitButton() {
setUpTestWithLanguageSwitchingFeatureOff()
launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use {
startPlayingExploration()
Expand All @@ -569,12 +569,12 @@ class StateFragmentTest {
onView(withId(R.id.submit_answer_button)).check(
matches(withText(R.string.state_submit_button))
)
onView(withId(R.id.submit_answer_button)).check(matches(not(isEnabled())))
onView(withId(R.id.submit_answer_button)).check(matches(isEnabled()))
}
}

@Test
fun testStateFragment_loadExp_changeConfiguration_thirdState_hasDisabledSubmitButton() {
fun testStateFragment_loadExp_changeConfiguration_thirdState_hasEnabledSubmitButton() {
setUpTestWithLanguageSwitchingFeatureOff()
launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use {
startPlayingExploration()
Expand All @@ -587,7 +587,27 @@ class StateFragmentTest {
onView(withId(R.id.submit_answer_button)).check(
matches(withText(R.string.state_submit_button))
)
onView(withId(R.id.submit_answer_button)).check(matches(not(isEnabled())))
onView(withId(R.id.submit_answer_button)).check(matches(isEnabled()))
}
}

@Test
fun testStateFragment_loadExp_thirdState_submitWithoutAnswer_showsErrorMessage() {
setUpTestWithLanguageSwitchingFeatureOff()
launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use {
startPlayingExploration()
playThroughPrototypeState1()
playThroughPrototypeState2()

clickSubmitAnswerButton()
onView(withId(R.id.selection_input_error))
.check(
matches(
withText(
R.string.selection_error_empty_input
)
)
)
}
}

Expand Down Expand Up @@ -660,7 +680,7 @@ class StateFragmentTest {
}

@Test
fun testStateFragment_loadExp_thirdState_submitInvalidAnswer_disablesSubmitButton() {
fun testStateFragment_loadExp_thirdState_submitInvalidAnswer_submitButtonIsEnabled() {
setUpTestWithLanguageSwitchingFeatureOff()
launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use {
startPlayingExploration()
Expand All @@ -671,14 +691,15 @@ class StateFragmentTest {
selectMultipleChoiceOption(optionPosition = 1, expectedOptionText = "Chicken")
clickSubmitAnswerButton()

// The submission button should now be disabled and there should be an error.
// The submission button should now still be enabled as empty input error will be displayed
// if submit button is clicked without choosing an answer.
scrollToViewType(SUBMIT_ANSWER_BUTTON)
onView(withId(R.id.submit_answer_button)).check(matches(not(isEnabled())))
onView(withId(R.id.submit_answer_button)).check(matches(isEnabled()))
}
}

@Test
fun testStateFragment_loadExp_land_thirdState_submitInvalidAnswer_disablesSubmitButton() {
fun testStateFragment_loadExp_land_thirdState_submitInvalidAnswer_submitButtonIsEnabled() {
setUpTestWithLanguageSwitchingFeatureOff()
launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use {
startPlayingExploration()
Expand All @@ -689,9 +710,10 @@ class StateFragmentTest {
selectMultipleChoiceOption(optionPosition = 1, expectedOptionText = "Chicken")
clickSubmitAnswerButton()

// The submission button should now be disabled and there should be an error.
// The submission button should now still be enabled as empty input error will be displayed
// if submit button is clicked without choosing an answer.
scrollToViewType(SUBMIT_ANSWER_BUTTON)
onView(withId(R.id.submit_answer_button)).check(matches(not(isEnabled())))
onView(withId(R.id.submit_answer_button)).check(matches(isEnabled()))
}
}

Expand Down
Loading