From 676cfb958cbee40f4ace97fddf8dd46a5860782c Mon Sep 17 00:00:00 2001 From: Saptak Manna Date: Thu, 18 Jan 2024 21:15:54 +0530 Subject: [PATCH 1/6] Add empty answer support --- .../SelectionInteractionViewModel.kt | 49 ++++++++++++++++++- .../res/layout/selection_interaction_item.xml | 10 ++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt index 796df10ec86..6d59a321fe4 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt @@ -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 @@ -18,6 +19,7 @@ import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiv import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableArrayList import org.oppia.android.domain.translation.TranslationController +import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory import javax.inject.Inject /** Corresponds to the type of input that should be used for an item selection interaction view. */ @@ -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, @@ -64,7 +78,9 @@ class SelectionInteractionViewModel private constructor( val choiceItems: ObservableList = computeChoiceItems(choiceSubtitledHtmls, hasConversationView, this, enabledItemsList) + private var pendingAnswerError: String? = null private val isAnswerAvailable = ObservableField(false) + var errorMessage = ObservableField("") val selectedItemText = ObservableField( resourceHandler.getStringInLocale( @@ -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 { @@ -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 { return when (itemHtmls.size) { @@ -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 @@ -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, diff --git a/app/src/main/res/layout/selection_interaction_item.xml b/app/src/main/res/layout/selection_interaction_item.xml index d83395e7f1b..e192fe7af02 100644 --- a/app/src/main/res/layout/selection_interaction_item.xml +++ b/app/src/main/res/layout/selection_interaction_item.xml @@ -60,5 +60,15 @@ app:selectionData="@{viewModel.choiceItems}" app:writtenTranslationContext="@{viewModel.writtenTranslationContext}" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc2b39169e5..2c0c0c595a6 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -176,6 +176,7 @@ Number of terms is not equal to the required terms. Ratios cannot have 0 as an element. Enter a ratio to continue. + Choose an answer to continue. Unknown size %s Bytes %s KB From 406b82ddada8126df4e087d247cad87e1e904280 Mon Sep 17 00:00:00 2001 From: Saptak Manna Date: Tue, 23 Jan 2024 08:25:00 +0530 Subject: [PATCH 2/6] Fix ktlint & UI issue --- .../player/state/itemviewmodel/SelectionInteractionViewModel.kt | 2 +- app/src/main/res/layout/selection_interaction_item.xml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt index 6d59a321fe4..0a32854be48 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt @@ -13,13 +13,13 @@ 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 import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.app.viewmodel.ObservableArrayList import org.oppia.android.domain.translation.TranslationController -import org.oppia.android.app.player.state.answerhandling.AnswerErrorCategory import javax.inject.Inject /** Corresponds to the type of input that should be used for an item selection interaction view. */ diff --git a/app/src/main/res/layout/selection_interaction_item.xml b/app/src/main/res/layout/selection_interaction_item.xml index e192fe7af02..c2e963abee5 100644 --- a/app/src/main/res/layout/selection_interaction_item.xml +++ b/app/src/main/res/layout/selection_interaction_item.xml @@ -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"> From b10d08000b204895876f9e9403f73d0e18eefe8f Mon Sep 17 00:00:00 2001 From: Saptak Manna Date: Tue, 23 Jan 2024 08:30:46 +0530 Subject: [PATCH 3/6] Fix kdoc issue --- .../player/state/itemviewmodel/SelectionInteractionViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt index 0a32854be48..b585f90ac4e 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt @@ -28,7 +28,7 @@ enum class SelectionItemInputType { RADIO_BUTTONS } -/** Enum to the store the errors of selection input */ +/** 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); From 255df86d69ad71a85f05f0eefc802b12c2fb0e32 Mon Sep 17 00:00:00 2001 From: Saptak Manna Date: Tue, 23 Jan 2024 09:10:40 +0530 Subject: [PATCH 4/6] Fix tests --- .../app/player/state/StateFragmentTest.kt | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) 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 2888906d8fd..8a546a126e8 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 @@ -557,7 +557,7 @@ class StateFragmentTest { } @Test - fun testStateFragment_loadExp_thirdState_hasDisabledSubmitButton() { + fun testStateFragment_loadExp_thirdState_hasEnabledSubmitButton() { setUpTestWithLanguageSwitchingFeatureOff() launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = false).use { startPlayingExploration() @@ -568,12 +568,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() @@ -586,7 +586,7 @@ 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())) } } @@ -659,7 +659,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() @@ -670,14 +670,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() @@ -688,9 +689,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())) } } From eedd8d7835d01a71254d19a11b1cca53e1d88876 Mon Sep 17 00:00:00 2001 From: Saptak Manna Date: Tue, 23 Jan 2024 09:58:23 +0530 Subject: [PATCH 5/6] Add test for submission without answer --- .../app/player/state/StateFragmentTest.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 8a546a126e8..b0f32bf0fe7 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 @@ -590,6 +590,26 @@ class StateFragmentTest { } } + @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 + ) + ) + ) + } + } + @Test fun testStateFragment_loadExp_thirdState_selectAnswer_submitButtonIsEnabled() { setUpTestWithLanguageSwitchingFeatureOff() From 39a75f9fad24a427cfebd41460f8e2df3b7a3691 Mon Sep 17 00:00:00 2001 From: Saptak Manna Date: Mon, 26 Feb 2024 22:46:01 +0530 Subject: [PATCH 6/6] Change var to val --- .../player/state/itemviewmodel/SelectionInteractionViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt index b585f90ac4e..af2d7e0f6cc 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/SelectionInteractionViewModel.kt @@ -80,7 +80,7 @@ class SelectionInteractionViewModel private constructor( private var pendingAnswerError: String? = null private val isAnswerAvailable = ObservableField(false) - var errorMessage = ObservableField("") + val errorMessage = ObservableField("") val selectedItemText = ObservableField( resourceHandler.getStringInLocale(