From 14a43d15caa340a13641b57e264248d907cbd98e Mon Sep 17 00:00:00 2001 From: "Mr. 17" Date: Mon, 18 Mar 2024 14:47:10 +0530 Subject: [PATCH] Fix part of #5070: Display empty answer message in drag & drop sort interaction (#5323) ## Explanation Fixes part of #5070 Enables the `submit_answer_button` when the pending answer is empty. Instead of disabling the button, an error message, stating "_**Arrange the boxes to continue.**_", is now displayed when the user attempts to submit without arranging the boxes. https://github.com/oppia/oppia-android/assets/84731134/c0083266-8641-414a-b14d-de662123d4a2 ## Essential Checklist - [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 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 --------- Co-authored-by: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> --- app/BUILD.bazel | 2 +- .../DragAndDropSortInteractionViewModel.kt | 54 ++++++++++- .../res/layout/drag_drop_interaction_item.xml | 7 ++ app/src/main/res/values/strings.xml | 1 + .../app/player/state/StateFragmentTest.kt | 90 +++++++++++++++++++ 5 files changed, 149 insertions(+), 5 deletions(-) diff --git a/app/BUILD.bazel b/app/BUILD.bazel index 4019ad3cd6c..48ab86b20b1 100644 --- a/app/BUILD.bazel +++ b/app/BUILD.bazel @@ -221,6 +221,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [ "src/main/java/org/oppia/android/app/parser/StringToNumberParser.kt", "src/main/java/org/oppia/android/app/parser/StringToRatioParser.kt", "src/main/java/org/oppia/android/app/player/audio/AudioViewModel.kt", + "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragDropInteractionContentViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/FractionInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ImageRegionSelectionInteractionViewModel.kt", @@ -320,7 +321,6 @@ VIEW_MODELS = [ "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContentViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/ContinueNavigationButtonViewModel.kt", - "src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/FeedbackViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/NextButtonViewModel.kt", "src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt", diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt index 6cd22508141..d6f88ea9cec 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/DragAndDropSortInteractionViewModel.kt @@ -1,8 +1,10 @@ package org.oppia.android.app.player.state.itemviewmodel +import androidx.annotation.StringRes import androidx.databinding.Observable import androidx.databinding.ObservableField import androidx.recyclerview.widget.RecyclerView +import org.oppia.android.R import org.oppia.android.app.model.Interaction import org.oppia.android.app.model.InteractionObject import org.oppia.android.app.model.ListOfSetsOfHtmlStrings @@ -13,6 +15,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 @@ -23,6 +26,18 @@ import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.domain.translation.TranslationController import javax.inject.Inject +/** Represents the type of errors that can be thrown by drag and drop sort interaction. */ +enum class DragAndDropSortInteractionError(@StringRes private var error: Int?) { + VALID(error = null), + EMPTY_INPUT(error = R.string.drag_and_drop_interaction_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 drag drop & sort choice list. */ class DragAndDropSortInteractionViewModel private constructor( val entityId: String, @@ -55,25 +70,34 @@ class DragAndDropSortInteractionViewModel private constructor( subtitledHtml.contentId to translatedHtml } - private val _choiceItems: MutableList = + private val _originalChoiceItems: MutableList = computeChoiceItems(contentIdHtmlMap, choiceSubtitledHtmls, this, resourceHandler) + private val _choiceItems = _originalChoiceItems.toMutableList() val choiceItems: List = _choiceItems + private var pendingAnswerError: String? = null private val isAnswerAvailable = ObservableField(false) + var errorMessage = ObservableField("") init { val callback: Observable.OnPropertyChangedCallback = object : Observable.OnPropertyChangedCallback() { override fun onPropertyChanged(sender: Observable, propertyId: Int) { interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck( - pendingAnswerError = null, - inputAnswerAvailable = true + pendingAnswerError, + inputAnswerAvailable = true // Allow submission without arranging or merging items. ) } } isAnswerAvailable.addOnPropertyChangedCallback(callback) - isAnswerAvailable.set(true) // For drag drop submit button will be enabled by default. + errorMessage.addOnPropertyChangedCallback(callback) + + // Initializing with default values so that submit button is enabled by default. + interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck( + pendingAnswerError = null, + inputAnswerAvailable = true + ) } override fun onItemDragged( @@ -98,6 +122,7 @@ class DragAndDropSortInteractionViewModel private constructor( if (allowMultipleItemsInSamePosition) { (adapter as BindableAdapter<*>).setDataUnchecked(_choiceItems) } + checkPendingAnswerError(AnswerErrorCategory.REAL_TIME) } fun onItemMoved( @@ -129,6 +154,20 @@ class DragAndDropSortInteractionViewModel private constructor( this@DragAndDropSortInteractionViewModel.writtenTranslationContext }.build() + /** + * It checks the pending error for the current drag and drop sort interaction, 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 convertItemsToAnswer(htmlItems: List): ListOfSetsOfHtmlStrings { return ListOfSetsOfHtmlStrings.newBuilder() @@ -190,6 +229,13 @@ class DragAndDropSortInteractionViewModel private constructor( (adapter as BindableAdapter<*>).setDataUnchecked(_choiceItems) } + private fun getSubmitTimeError(): DragAndDropSortInteractionError { + return if (_originalChoiceItems == _choiceItems) + DragAndDropSortInteractionError.EMPTY_INPUT + else + DragAndDropSortInteractionError.VALID + } + /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */ class FactoryImpl @Inject constructor( private val resourceHandler: AppLanguageResourceHandler, diff --git a/app/src/main/res/layout/drag_drop_interaction_item.xml b/app/src/main/res/layout/drag_drop_interaction_item.xml index 53a7074bcac..bcc6c1053ef 100644 --- a/app/src/main/res/layout/drag_drop_interaction_item.xml +++ b/app/src/main/res/layout/drag_drop_interaction_item.xml @@ -67,5 +67,12 @@ app:draggableData="@{viewModel.choiceItems}" app:onDragEnded="@{(adapter) -> viewModel.onDragEnded(adapter)}" app:onItemDrag="@{(indexFrom, indexTo, adapter) -> viewModel.onItemDragged(indexFrom, indexTo, adapter)}" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c0598ab6bf4..9a46247bf65 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -453,6 +453,7 @@ Return to lesson Explanation: If two items are equal, merge them. + Arrange the boxes to continue. Link to item %s Unlink items at %s Move item down to %s diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index d6a15aa0e00..a05e4756684 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -747,6 +747,96 @@ class StateFragmentTest { } } + @Test + fun testStateFragment_loadDragDropExp_submitWithoutArranging_showsErrorMessage() { + setUpTestWithLanguageSwitchingFeatureOff() + launchForExploration(TEST_EXPLORATION_ID_4, shouldSavePartialProgress = false).use { + startPlayingExploration() + clickSubmitAnswerButton() + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + withText( + R.string.drag_and_drop_interaction_empty_input + ) + ) + ) + } + } + + @Test + fun testStateFragment_loadDragDropExp_withGrouping_submitWithoutArranging_showsErrorMessage_dragItem_errorMessageIsReset() { // ktlint-disable max-line-length + setUpTestWithLanguageSwitchingFeatureOff() + launchForExploration(TEST_EXPLORATION_ID_4, shouldSavePartialProgress = false).use { + startPlayingExploration() + + // Drag and drop interaction with grouping. + // Submit answer without any changes. + clickSubmitAnswerButton() + // Empty input error is displayed. + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + isDisplayed() + ) + ) + // Submit button is disabled due to the error. + verifySubmitAnswerButtonIsDisabled() + // Drag and rearrange an item. + dragAndDropItem(fromPosition = 0, toPosition = 1) + // Empty input error is reset. + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + not(isDisplayed()) + ) + ) + // Submit button is enabled back. + verifySubmitAnswerButtonIsEnabled() + } + } + + @Test + fun testStateFragment_loadDragDropExp_withoutGrouping_submitWithoutArranging_showsErrorMessage_dragItem_errorMessageIsReset() { // ktlint-disable max-line-length + setUpTestWithLanguageSwitchingFeatureOff() + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { + startPlayingExploration() + playThroughPrototypeState1() + playThroughPrototypeState2() + playThroughPrototypeState3() + playThroughPrototypeState4() + playThroughPrototypeState5() + playThroughPrototypeState6() + playThroughPrototypeState7() + playThroughPrototypeState8() + + // Drag and drop interaction without grouping. + // Ninth state: Drag Drop Sort. Correct answer: Move 1st item to 4th position. + // Submit answer without any changes. + clickSubmitAnswerButton() + // Empty input error is displayed. + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + isDisplayed() + ) + ) + // Submit button is disabled due to the error. + verifySubmitAnswerButtonIsDisabled() + // Drag and rearrange an item. + dragAndDropItem(fromPosition = 0, toPosition = 1) + // Empty input error is reset. + onView(withId(R.id.drag_drop_interaction_error)) + .check( + matches( + not(isDisplayed()) + ) + ) + // Submit button is enabled back. + verifySubmitAnswerButtonIsEnabled() + } + } + @Test fun testStateFragment_loadDragDropExp_mergeFirstTwoItems_worksCorrectly() { setUpTestWithLanguageSwitchingFeatureOff()