Skip to content

Commit

Permalink
Fix part of #5070: Display empty answer message in selection interact…
Browse files Browse the repository at this point in the history
…ion (#5319)

<!-- READ ME FIRST: Please fill in the explanation section below and
check off every point from the Essential Checklist! -->
## Explanation
<!--
- Explain what your PR does. If this PR fixes an existing bug, please
include
- "Fixes #bugnum:" in the explanation so that GitHub can auto-close the
issue
  - when this PR is merged.
  -->

Fixes part of #5070

Enables the `submit_answer_button` when the pending answer is empty.
Instead of disabling the button, an error message, stating "_**Choose an
answer to continue.**_", is now displayed when the user attempts to
submit without selecting any input.


https://github.com/oppia/oppia-android/assets/84731134/ba918fb9-5857-4daf-b851-8645791e1acb

## Essential Checklist
<!-- Please tick the relevant boxes by putting an "x" in them. -->
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).

## For UI-specific PRs only
<!-- Delete these section if this PR does not include UI-related
changes. -->
If your PR includes UI-related changes, then:
- Add screenshots for portrait/landscape for both a tablet & phone of
the before & after UI changes
- For the screenshots above, include both English and pseudo-localized
(RTL) screenshots (see [RTL
guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines))
- Add a video showing the full UX flow with a screen reader enabled (see
[accessibility
guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide))
- For PRs introducing new UI elements or color changes, both light and
dark mode screenshots must be included
- Add a screenshot demonstrating that you ran affected Espresso tests
locally & that they're passing
  • Loading branch information
theMr17 authored Feb 26, 2024
1 parent 67c7a92 commit e8dfdd7
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 13 deletions.
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)
val errorMessage = ObservableField<String>("")
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

0 comments on commit e8dfdd7

Please sign in to comment.