diff --git a/app/BUILD.bazel b/app/BUILD.bazel
index 9cc32896072..69f7a4238c0 100644
--- a/app/BUILD.bazel
+++ b/app/BUILD.bazel
@@ -283,6 +283,7 @@ VIEW_MODELS = [
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/LessonProgressViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/MathExpressionInteractionsViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/NextButtonViewModel.kt",
+ "src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumberWithUnitsInputViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/PreviousButtonViewModel.kt",
"src/main/java/org/oppia/android/app/player/state/itemviewmodel/PreviousResponsesHeaderViewModel.kt",
@@ -383,6 +384,7 @@ VIEWS = [
"src/main/java/org/oppia/android/app/customview/VerticalDashedLineView.kt",
"src/main/java/org/oppia/android/app/customview/interaction/FractionInputInteractionView.kt",
"src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt",
+ "src/main/java/org/oppia/android/app/customview/interaction/NumberWithUnitsInputInteractionView.kt",
"src/main/java/org/oppia/android/app/customview/interaction/NumericInputInteractionView.kt",
"src/main/java/org/oppia/android/app/customview/interaction/RatioInputInteractionView.kt",
"src/main/java/org/oppia/android/app/customview/interaction/TextInputInteractionView.kt",
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7ed5ab01eb4..1f48f60bef5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -220,6 +220,7 @@
+
diff --git a/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt b/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt
index d26c4ca132d..600df9c4288 100644
--- a/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt
+++ b/app/src/main/java/org/oppia/android/app/activity/ActivityComponentImpl.kt
@@ -84,6 +84,7 @@ import org.oppia.android.app.testing.InputInteractionViewTestActivity
import org.oppia.android.app.testing.MarginBindingAdaptersTestActivity
import org.oppia.android.app.testing.MathExpressionInteractionsViewTestActivity
import org.oppia.android.app.testing.NavigationDrawerTestActivity
+import org.oppia.android.app.testing.NumberWithUnitsInputInteractionViewTestActivity
import org.oppia.android.app.testing.PoliciesFragmentTestActivity
import org.oppia.android.app.testing.ProfileChooserFragmentTestActivity
import org.oppia.android.app.testing.ProfileEditFragmentTestActivity
@@ -165,6 +166,11 @@ interface ActivityComponentImpl :
fun inject(imageRegionSelectionTestActivity: ImageRegionSelectionTestActivity)
fun inject(imageViewBindingAdaptersTestActivity: ImageViewBindingAdaptersTestActivity)
fun inject(inputInteractionViewTestActivity: InputInteractionViewTestActivity)
+ fun inject(
+ numberWithUnitsInputInteractionViewTestActivity:
+ NumberWithUnitsInputInteractionViewTestActivity
+ )
+
fun inject(textInputInteractionViewTestActivity: TextInputInteractionViewTestActivity)
fun inject(mathExpressionInteractionsViewTestActivity: MathExpressionInteractionsViewTestActivity)
fun inject(ratioInputInteractionViewTestActivity: RatioInputInteractionViewTestActivity)
diff --git a/app/src/main/java/org/oppia/android/app/customview/interaction/NumberWithUnitsInputInteractionView.kt b/app/src/main/java/org/oppia/android/app/customview/interaction/NumberWithUnitsInputInteractionView.kt
new file mode 100644
index 00000000000..356f057ef81
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/customview/interaction/NumberWithUnitsInputInteractionView.kt
@@ -0,0 +1,72 @@
+package org.oppia.android.app.customview.interaction
+
+import android.content.Context
+import android.graphics.Typeface
+import android.util.AttributeSet
+import android.view.KeyEvent
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import androidx.appcompat.widget.AppCompatEditText
+import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
+import org.oppia.android.app.utility.KeyboardHelper.Companion.hideSoftKeyboard
+import org.oppia.android.app.utility.KeyboardHelper.Companion.showSoftKeyboard
+
+// TODO(#249): These are the attributes which should be defined in XML, that are required for this interaction view to work correctly
+// hint="Write here."
+// inputType="text"
+// background="@drawable/edit_text_background"
+// maxLength="200".
+
+/** The custom AppCompatEditText class for number with units input interaction view. */
+class NumberWithUnitsInputInteractionView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = android.R.attr.editTextStyle
+) : AppCompatEditText(context, attrs, defStyle), View.OnFocusChangeListener {
+ private var hintText: CharSequence = ""
+ private val stateKeyboardButtonListener: StateKeyboardButtonListener
+
+ init {
+ onFocusChangeListener = this
+ // Assume multi-line for the purpose of properly showing long hints.
+ isSingleLine = hint != null
+ stateKeyboardButtonListener = context as StateKeyboardButtonListener
+ }
+
+ // TODO(#4574): Add tests to verify that the placeholder correctly shows/doesn’t show when expected
+ override fun onFocusChange(v: View, hasFocus: Boolean) = if (hasFocus) {
+ hintText = hint
+ hideHint()
+ showSoftKeyboard(v, context)
+ } else {
+ restoreHint()
+ hideSoftKeyboard(v, context)
+ }
+
+ override fun onKeyPreIme(keyCode: Int, event: KeyEvent): Boolean {
+ if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
+ clearFocus()
+ restoreHint()
+ }
+ return super.onKeyPreIme(keyCode, event)
+ }
+
+ override fun onEditorAction(actionCode: Int) {
+ if (actionCode == EditorInfo.IME_ACTION_DONE) {
+ stateKeyboardButtonListener.onEditorAction(EditorInfo.IME_ACTION_DONE)
+ }
+ super.onEditorAction(actionCode)
+ }
+
+ private fun hideHint() {
+ hint = ""
+ typeface = Typeface.DEFAULT
+ isSingleLine = true
+ }
+
+ private fun restoreHint() {
+ hint = hintText
+ if (text?.isEmpty() == true) setTypeface(typeface, Typeface.ITALIC)
+ isSingleLine = false
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt
index 1e7656491e2..29f897fd0d0 100644
--- a/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt
+++ b/app/src/main/java/org/oppia/android/app/player/state/StatePlayerRecyclerViewAssembler.kt
@@ -31,6 +31,7 @@ import org.oppia.android.app.databinding.databinding.LessonProgressIndicatorItem
import org.oppia.android.app.databinding.databinding.MathExpressionInteractionsItemBinding
import org.oppia.android.app.databinding.databinding.MultipleChoiceSubmittedAnswerItemsBinding
import org.oppia.android.app.databinding.databinding.NextButtonItemBinding
+import org.oppia.android.app.databinding.databinding.NumberWithUnitsInputInteractionItemBinding
import org.oppia.android.app.databinding.databinding.NumericInputInteractionItemBinding
import org.oppia.android.app.databinding.databinding.PreviousButtonItemBinding
import org.oppia.android.app.databinding.databinding.PreviousResponsesHeaderItemBinding
@@ -74,6 +75,7 @@ import org.oppia.android.app.player.state.itemviewmodel.ImageRegionSelectionInte
import org.oppia.android.app.player.state.itemviewmodel.LessonProgressViewModel
import org.oppia.android.app.player.state.itemviewmodel.MathExpressionInteractionsViewModel
import org.oppia.android.app.player.state.itemviewmodel.NextButtonViewModel
+import org.oppia.android.app.player.state.itemviewmodel.NumberWithUnitsInputViewModel
import org.oppia.android.app.player.state.itemviewmodel.NumericInputViewModel
import org.oppia.android.app.player.state.itemviewmodel.PreviousButtonViewModel
import org.oppia.android.app.player.state.itemviewmodel.PreviousResponsesHeaderViewModel
@@ -1455,6 +1457,11 @@ class StatePlayerRecyclerViewAssembler private constructor(
inflateDataBinding = TextInputInteractionItemBinding::inflate,
setViewModel = TextInputInteractionItemBinding::setViewModel,
transformViewModel = { it as TextInputViewModel }
+ ).registerViewDataBinder(
+ viewType = StateItemViewModel.ViewType.NUMBER_WITH_UNITS_INPUT_INTERACTION,
+ inflateDataBinding = NumberWithUnitsInputInteractionItemBinding::inflate,
+ setViewModel = NumberWithUnitsInputInteractionItemBinding::setViewModel,
+ transformViewModel = { it as NumberWithUnitsInputViewModel }
).registerViewDataBinder(
viewType = StateItemViewModel.ViewType.RATIO_EXPRESSION_INPUT_INTERACTION,
inflateDataBinding = RatioInputInteractionItemBinding::inflate,
diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelModule.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelModule.kt
index 51ea00e2e6e..aefb73264a0 100644
--- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelModule.kt
+++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/InteractionViewModelModule.kt
@@ -77,6 +77,13 @@ interface InteractionViewModelModule {
factoryImpl: RatioExpressionInputInteractionViewModel.FactoryImpl
): StateItemViewModel.InteractionItemFactory
+ @Binds
+ @IntoMap
+ @StringKey("NumberWithUnits")
+ fun provideNumberWithUnitsInputViewModelFactory(
+ factoryImpl: NumberWithUnitsInputViewModel.FactoryImpl
+ ): StateItemViewModel.InteractionItemFactory
+
// Note that Dagger doesn't support mixing binds & provides methods. See
// https://stackoverflow.com/a/54592300 for the origin of this approach.
@Module
diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumberWithUnitsInputViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumberWithUnitsInputViewModel.kt
new file mode 100644
index 00000000000..8521454dba2
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumberWithUnitsInputViewModel.kt
@@ -0,0 +1,185 @@
+package org.oppia.android.app.player.state.itemviewmodel
+
+import android.text.Editable
+import android.text.TextWatcher
+import androidx.annotation.StringRes
+import androidx.databinding.Observable
+import androidx.databinding.ObservableField
+import org.oppia.android.app.model.AnswerAndResponse
+import org.oppia.android.app.model.AnswerErrorCategory
+import org.oppia.android.app.model.Interaction
+import org.oppia.android.app.model.InteractionObject
+import org.oppia.android.app.model.UserAnswer
+import org.oppia.android.app.model.UserAnswerState
+import org.oppia.android.app.model.WrittenTranslationContext
+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.view.models.R
+import org.oppia.android.domain.translation.TranslationController
+import javax.inject.Inject
+
+/** [StateItemViewModel] for the number with units input interaction. */
+class NumberWithUnitsInputViewModel private constructor(
+ interaction: Interaction,
+ val hasConversationView: Boolean,
+ private val interactionAnswerErrorOrAvailabilityCheckReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver, // ktlint-disable max-line-length
+ val isSplitView: Boolean,
+ private val writtenTranslationContext: WrittenTranslationContext,
+ private val resourceHandler: AppLanguageResourceHandler,
+ private val translationController: TranslationController,
+ userAnswerState: UserAnswerState
+) : StateItemViewModel(ViewType.NUMBER_WITH_UNITS_INPUT_INTERACTION), InteractionAnswerHandler {
+ var answerText: CharSequence = userAnswerState.textInputAnswer
+ private var answerErrorCategory: AnswerErrorCategory = AnswerErrorCategory.NO_ERROR
+ val hintText: CharSequence = deriveHintText(interaction)
+ private var pendingAnswerError: String? = null
+
+ var isAnswerAvailable = ObservableField(false)
+ val errorMessage = ObservableField("")
+
+ init {
+ val callback: Observable.OnPropertyChangedCallback =
+ object : Observable.OnPropertyChangedCallback() {
+ override fun onPropertyChanged(sender: Observable, propertyId: Int) {
+ interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
+ pendingAnswerError = pendingAnswerError,
+ inputAnswerAvailable = true // Allow submit on empty answer.
+ )
+ }
+ }
+ isAnswerAvailable.addOnPropertyChangedCallback(callback)
+ errorMessage.addOnPropertyChangedCallback(callback)
+
+ // Initializing with default values so that submit button is enabled by default.
+ interactionAnswerErrorOrAvailabilityCheckReceiver.onPendingAnswerErrorOrAvailabilityCheck(
+ pendingAnswerError = null,
+ inputAnswerAvailable = true
+ )
+ checkPendingAnswerError(userAnswerState.answerErrorCategory)
+ }
+
+ override fun checkPendingAnswerError(category: AnswerErrorCategory): String? {
+ answerErrorCategory = category
+ return when (category) {
+ AnswerErrorCategory.REAL_TIME -> null
+ AnswerErrorCategory.SUBMIT_TIME -> {
+ TextParsingUiError.createForText(
+ answerText.toString()
+ ).createForText(resourceHandler)
+ }
+ else -> null
+ }.also {
+ pendingAnswerError = it
+ errorMessage.set(it)
+ }
+ }
+
+ fun getAnswerTextWatcher(): TextWatcher {
+ return object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
+ }
+
+ override fun onTextChanged(answer: CharSequence, start: Int, before: Int, count: Int) {
+ answerText = answer.toString().trim()
+ val isAnswerTextAvailable = answerText.isNotEmpty()
+ if (isAnswerTextAvailable != isAnswerAvailable.get()) {
+ isAnswerAvailable.set(isAnswerTextAvailable)
+ }
+ checkPendingAnswerError(AnswerErrorCategory.REAL_TIME)
+ }
+
+ override fun afterTextChanged(s: Editable) {
+ }
+ }
+ }
+
+ override fun getPendingAnswer(): UserAnswer = UserAnswer.newBuilder().apply {
+ if (answerText.isNotEmpty()) {
+ val answerTextString = answerText.toString()
+ answer = InteractionObject.newBuilder().apply {
+ normalizedString = answerTextString
+ }.build()
+ plainAnswer = answerTextString
+ writtenTranslationContext = this@NumberWithUnitsInputViewModel.writtenTranslationContext
+ }
+ }.build()
+
+ override fun getUserAnswerState(): UserAnswerState {
+ return UserAnswerState.newBuilder().apply {
+ this.textInputAnswer = answerText.toString()
+ this.answerErrorCategory = answerErrorCategory
+ }.build()
+ }
+
+ private fun deriveHintText(interaction: Interaction): CharSequence {
+ // The subtitled unicode can apparently exist in the structure in two different formats.
+ val placeholderUnicodeOption1 =
+ interaction.customizationArgsMap["placeholder"]?.subtitledUnicode
+ val placeholderUnicodeOption2 =
+ interaction.customizationArgsMap["placeholder"]?.customSchemaValue?.subtitledUnicode
+ val placeholder1 =
+ placeholderUnicodeOption1?.let { unicode ->
+ translationController.extractString(unicode, writtenTranslationContext)
+ } ?: ""
+ val placeholder2 =
+ placeholderUnicodeOption2?.let { unicode ->
+ translationController.extractString(unicode, writtenTranslationContext)
+ } ?: "" // The default placeholder for text input is empty.
+ return when {
+ placeholder1.isNotEmpty() -> placeholder1
+ placeholder2.isNotEmpty() -> placeholder2
+ else -> resourceHandler.getStringInLocale(R.string.number_with_units_input_hint_text)
+ }
+ }
+
+ /** Implementation of [StateItemViewModel.InteractionItemFactory] for this view model. */
+ class FactoryImpl @Inject constructor(
+ private val resourceHandler: AppLanguageResourceHandler,
+ private val translationController: TranslationController
+ ) : InteractionItemFactory {
+ override fun create(
+ entityId: String,
+ hasConversationView: Boolean,
+ interaction: Interaction,
+ interactionAnswerReceiver: InteractionAnswerReceiver,
+ answerErrorReceiver: InteractionAnswerErrorOrAvailabilityCheckReceiver,
+ hasPreviousButton: Boolean,
+ isSplitView: Boolean,
+ writtenTranslationContext: WrittenTranslationContext,
+ timeToStartNoticeAnimationMs: Long?,
+ userAnswerState: UserAnswerState,
+ wrongAnswerList: List
+ ): StateItemViewModel {
+ return NumberWithUnitsInputViewModel(
+ interaction,
+ hasConversationView,
+ answerErrorReceiver,
+ isSplitView,
+ writtenTranslationContext,
+ resourceHandler,
+ translationController,
+ userAnswerState
+ )
+ }
+ }
+
+ private enum class TextParsingUiError(@StringRes private var error: Int?) {
+ /** Corresponds to non empty input. */
+ VALID(error = null),
+
+ /** Corresponds to empty input. */
+ EMPTY_INPUT(error = R.string.text_error_empty_input);
+
+ /** Returns the string corresponding to this error's string resources, or null if there is none. */
+ fun createForText(resourceHandler: AppLanguageResourceHandler): String? =
+ error?.let(resourceHandler::getStringInLocale)
+
+ companion object {
+ /** Returns the [TextParsingUiError] corresponding to the input. */
+ fun createForText(text: String): TextParsingUiError =
+ if (text.isEmpty()) EMPTY_INPUT else VALID
+ }
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt
index c7f26e82a6e..f3001d3703d 100644
--- a/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt
+++ b/app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/StateItemViewModel.kt
@@ -40,7 +40,8 @@ abstract class StateItemViewModel(val viewType: ViewType) : ObservableViewModel(
FLASHBACK_BUTTON,
RETURN_TO_QUESTION_BUTTON,
FLASHBACK_SOLUTION,
- LESSON_PROGRESS_INDICATOR
+ LESSON_PROGRESS_INDICATOR,
+ NUMBER_WITH_UNITS_INPUT_INTERACTION
}
/** Factory for creating new [StateItemViewModel]s for interactions. */
diff --git a/app/src/main/java/org/oppia/android/app/testing/NumberWithUnitsInputInteractionViewTestActivity.kt b/app/src/main/java/org/oppia/android/app/testing/NumberWithUnitsInputInteractionViewTestActivity.kt
new file mode 100644
index 00000000000..3fe2de0660d
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/testing/NumberWithUnitsInputInteractionViewTestActivity.kt
@@ -0,0 +1,109 @@
+package org.oppia.android.app.testing
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import androidx.databinding.DataBindingUtil
+import org.oppia.android.app.activity.ActivityComponentImpl
+import org.oppia.android.app.activity.InjectableAutoLocalizedAppCompatActivity
+import org.oppia.android.app.customview.interaction.NumberWithUnitsInputInteractionView
+import org.oppia.android.app.databinding.databinding.ActivityNumberWithUnitsInputInteractionViewTestBinding
+import org.oppia.android.app.model.AnswerErrorCategory
+import org.oppia.android.app.model.InputInteractionViewTestActivityParams
+import org.oppia.android.app.model.Interaction
+import org.oppia.android.app.model.UserAnswer
+import org.oppia.android.app.model.WrittenTranslationContext
+import org.oppia.android.app.player.state.answerhandling.InteractionAnswerErrorOrAvailabilityCheckReceiver
+import org.oppia.android.app.player.state.answerhandling.InteractionAnswerReceiver
+import org.oppia.android.app.player.state.itemviewmodel.NumberWithUnitsInputViewModel
+import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel
+import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.InteractionItemFactory
+import org.oppia.android.app.player.state.listener.StateKeyboardButtonListener
+import org.oppia.android.app.ui.R
+import org.oppia.android.util.extensions.getProtoExtra
+import org.oppia.android.util.extensions.putProtoExtra
+import javax.inject.Inject
+
+/**
+ * This is a dummy activity to test [NumberWithUnitsInputInteractionView].
+ */
+class NumberWithUnitsInputInteractionViewTestActivity :
+ InjectableAutoLocalizedAppCompatActivity(),
+ StateKeyboardButtonListener,
+ InteractionAnswerErrorOrAvailabilityCheckReceiver,
+ InteractionAnswerReceiver {
+ private lateinit var binding: ActivityNumberWithUnitsInputInteractionViewTestBinding
+
+ @Inject
+ lateinit var numberWithUnitsInputViewModelFactory: NumberWithUnitsInputViewModel.FactoryImpl
+
+ /** Gives access to the [NumberWithUnitsInputViewModel]. */
+ val numberWithUnitsInputViewModel by lazy {
+ numberWithUnitsInputViewModelFactory.create()
+ }
+
+ /** Gives access to the translation context. */
+ lateinit var writtenTranslationContext: WrittenTranslationContext
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ (activityComponent as ActivityComponentImpl).inject(this)
+ binding = DataBindingUtil
+ .setContentView(
+ this, R.layout.activity_number_with_units_input_interaction_view_test
+ )
+
+ val params =
+ intent.getProtoExtra(
+ TEST_ACTIVITY_PARAMS_ARGUMENT_KEY,
+ InputInteractionViewTestActivityParams.getDefaultInstance()
+ )
+ writtenTranslationContext = params.writtenTranslationContext
+
+ binding.numberWithUnitsInputViewModel = numberWithUnitsInputViewModel
+ binding.getPendingAnswerErrorOnSubmitClick = Runnable {
+ numberWithUnitsInputViewModel.checkPendingAnswerError(AnswerErrorCategory.SUBMIT_TIME)
+ }
+ }
+
+ override fun onPendingAnswerErrorOrAvailabilityCheck(
+ pendingAnswerError: String?,
+ inputAnswerAvailable: Boolean
+ ) {
+ }
+
+ override fun onAnswerReadyForSubmission(answer: UserAnswer) { }
+
+ override fun onEditorAction(actionCode: Int) { }
+
+ private inline fun InteractionItemFactory.create(
+ interaction: Interaction = Interaction.getDefaultInstance()
+ ): T {
+ return create(
+ entityId = "fake_entity_id",
+ hasConversationView = false,
+ interaction = interaction,
+ interactionAnswerReceiver = this@NumberWithUnitsInputInteractionViewTestActivity,
+ answerErrorReceiver = this@NumberWithUnitsInputInteractionViewTestActivity,
+ hasPreviousButton = false,
+ isSplitView = false,
+ writtenTranslationContext,
+ timeToStartNoticeAnimationMs = null
+ ) as T
+ }
+
+ companion object {
+ private const val TEST_ACTIVITY_PARAMS_ARGUMENT_KEY =
+ "NumberWithUnitsInputInteractionViewTestActivity.params"
+
+ /** Creates an intent to open [NumberWithUnitsInputInteractionViewTestActivity]. */
+ fun createIntent(
+ context: Context,
+ extras: InputInteractionViewTestActivityParams
+ ): Intent {
+ return Intent(context, NumberWithUnitsInputInteractionViewTestActivity::class.java).also {
+ it.putProtoExtra(TEST_ACTIVITY_PARAMS_ARGUMENT_KEY, extras)
+ }
+ }
+ }
+}
diff --git a/app/src/main/res/layout/activity_number_with_units_input_interaction_view_test.xml b/app/src/main/res/layout/activity_number_with_units_input_interaction_view_test.xml
new file mode 100644
index 00000000000..4f7bb4ada9f
--- /dev/null
+++ b/app/src/main/res/layout/activity_number_with_units_input_interaction_view_test.xml
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/number_with_units_input_interaction_item.xml b/app/src/main/res/layout/number_with_units_input_interaction_item.xml
new file mode 100644
index 00000000000..42bc3560962
--- /dev/null
+++ b/app/src/main/res/layout/number_with_units_input_interaction_item.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/sharedTest/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentTest.kt
index e0a6e533cdd..8f0e4519f58 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/devoptions/markchapterscompleted/MarkChaptersCompletedFragmentTest.kt
@@ -166,17 +166,17 @@ class MarkChaptersCompletedFragmentTest {
targetViewId = R.id.mark_chapters_completed_story_name_text_view
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 6,
+ itemPosition = 7,
stringToMatch = "Matthew Goes to the Bakery",
targetViewId = R.id.mark_chapters_completed_story_name_text_view
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 9,
+ itemPosition = 10,
stringToMatch = "Ratios: Part 1",
targetViewId = R.id.mark_chapters_completed_story_name_text_view
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 12,
+ itemPosition = 13,
stringToMatch = "Ratios: Part 2",
targetViewId = R.id.mark_chapters_completed_story_name_text_view
)
@@ -199,17 +199,17 @@ class MarkChaptersCompletedFragmentTest {
targetViewId = R.id.mark_chapters_completed_story_name_text_view
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 6,
+ itemPosition = 7,
stringToMatch = "Matthew Goes to the Bakery",
targetViewId = R.id.mark_chapters_completed_story_name_text_view
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 9,
+ itemPosition = 10,
stringToMatch = "Ratios: Part 1",
targetViewId = R.id.mark_chapters_completed_story_name_text_view
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 12,
+ itemPosition = 13,
stringToMatch = "Ratios: Part 2",
targetViewId = R.id.mark_chapters_completed_story_name_text_view
)
@@ -241,32 +241,37 @@ class MarkChaptersCompletedFragmentTest {
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 7,
- stringToMatch = "What is a Fraction?",
+ itemPosition = 6,
+ stringToMatch = "Number With Units Exploration",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
itemPosition = 8,
+ stringToMatch = "What is a Fraction?",
+ targetViewId = R.id.mark_chapters_completed_chapter_check_box
+ )
+ verifyItemTextOnRecyclerViewItemAtPosition(
+ itemPosition = 9,
stringToMatch = "The Meaning of Equal Parts",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 10,
+ itemPosition = 11,
stringToMatch = "What is a Ratio?",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 11,
+ itemPosition = 12,
stringToMatch = "Order is important",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 13,
+ itemPosition = 14,
stringToMatch = "Equivalent Ratios",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 14,
+ itemPosition = 15,
stringToMatch = "Writing Ratios in Simplest Form",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
@@ -299,32 +304,37 @@ class MarkChaptersCompletedFragmentTest {
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 7,
- stringToMatch = "What is a Fraction?",
+ itemPosition = 6,
+ stringToMatch = "Number With Units Exploration",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
itemPosition = 8,
+ stringToMatch = "What is a Fraction?",
+ targetViewId = R.id.mark_chapters_completed_chapter_check_box
+ )
+ verifyItemTextOnRecyclerViewItemAtPosition(
+ itemPosition = 9,
stringToMatch = "The Meaning of Equal Parts",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 10,
+ itemPosition = 11,
stringToMatch = "What is a Ratio?",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 11,
+ itemPosition = 12,
stringToMatch = "Order is important",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 13,
+ itemPosition = 14,
stringToMatch = "Equivalent Ratios",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
verifyItemTextOnRecyclerViewItemAtPosition(
- itemPosition = 14,
+ itemPosition = 15,
stringToMatch = "Writing Ratios in Simplest Form",
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
@@ -359,12 +369,13 @@ class MarkChaptersCompletedFragmentTest {
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 2)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 3)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 5)
- verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 7)
+ verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 6)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 8)
- verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 10)
+ verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 9)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 11)
- verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 13)
+ verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 12)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 14)
+ verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 15)
}
}
@@ -380,12 +391,13 @@ class MarkChaptersCompletedFragmentTest {
verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 2)
verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 3)
verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 5)
- verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 7)
+ verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 6)
verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 8)
- verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 10)
+ verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 9)
verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 11)
- verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 13)
+ verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 12)
verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 14)
+ verifyItemUncheckedOnRecyclerViewItemAtPosition(itemPosition = 15)
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.mark_chapters_completed_all_check_box)).check(matches(not(isChecked())))
}
@@ -401,12 +413,13 @@ class MarkChaptersCompletedFragmentTest {
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 2)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 3)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 5)
- verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 7)
+ verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 6)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 8)
- verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 10)
+ verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 9)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 11)
- verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 13)
+ verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 12)
verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 14)
+ verifyItemCheckedOnRecyclerViewItemAtPosition(itemPosition = 15)
}
}
@@ -523,12 +536,13 @@ class MarkChaptersCompletedFragmentTest {
clickOnRecyclerViewItemAtPosition(itemPosition = 2)
clickOnRecyclerViewItemAtPosition(itemPosition = 3)
clickOnRecyclerViewItemAtPosition(itemPosition = 5)
- clickOnRecyclerViewItemAtPosition(itemPosition = 7)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 6)
clickOnRecyclerViewItemAtPosition(itemPosition = 8)
- clickOnRecyclerViewItemAtPosition(itemPosition = 10)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 9)
clickOnRecyclerViewItemAtPosition(itemPosition = 11)
- clickOnRecyclerViewItemAtPosition(itemPosition = 13)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 12)
clickOnRecyclerViewItemAtPosition(itemPosition = 14)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 15)
onView(withId(R.id.mark_chapters_completed_all_check_box)).check(matches(isChecked()))
}
}
@@ -541,12 +555,13 @@ class MarkChaptersCompletedFragmentTest {
clickOnRecyclerViewItemAtPosition(itemPosition = 2)
clickOnRecyclerViewItemAtPosition(itemPosition = 3)
clickOnRecyclerViewItemAtPosition(itemPosition = 5)
- clickOnRecyclerViewItemAtPosition(itemPosition = 7)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 6)
clickOnRecyclerViewItemAtPosition(itemPosition = 8)
- clickOnRecyclerViewItemAtPosition(itemPosition = 10)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 9)
clickOnRecyclerViewItemAtPosition(itemPosition = 11)
- clickOnRecyclerViewItemAtPosition(itemPosition = 13)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 12)
clickOnRecyclerViewItemAtPosition(itemPosition = 14)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 15)
onView(isRoot()).perform(orientationLandscape())
onView(withId(R.id.mark_chapters_completed_all_check_box)).check(matches(isChecked()))
}
@@ -560,12 +575,13 @@ class MarkChaptersCompletedFragmentTest {
clickOnRecyclerViewItemAtPosition(itemPosition = 2)
clickOnRecyclerViewItemAtPosition(itemPosition = 3)
clickOnRecyclerViewItemAtPosition(itemPosition = 5)
- clickOnRecyclerViewItemAtPosition(itemPosition = 7)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 6)
clickOnRecyclerViewItemAtPosition(itemPosition = 8)
- clickOnRecyclerViewItemAtPosition(itemPosition = 10)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 9)
clickOnRecyclerViewItemAtPosition(itemPosition = 11)
- clickOnRecyclerViewItemAtPosition(itemPosition = 13)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 12)
clickOnRecyclerViewItemAtPosition(itemPosition = 14)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 15)
clickOnRecyclerViewItemAtPosition(itemPosition = 2)
onView(withId(R.id.mark_chapters_completed_all_check_box)).check(matches(not(isChecked())))
}
@@ -579,12 +595,13 @@ class MarkChaptersCompletedFragmentTest {
clickOnRecyclerViewItemAtPosition(itemPosition = 2)
clickOnRecyclerViewItemAtPosition(itemPosition = 3)
clickOnRecyclerViewItemAtPosition(itemPosition = 5)
- clickOnRecyclerViewItemAtPosition(itemPosition = 7)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 6)
clickOnRecyclerViewItemAtPosition(itemPosition = 8)
- clickOnRecyclerViewItemAtPosition(itemPosition = 10)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 9)
clickOnRecyclerViewItemAtPosition(itemPosition = 11)
- clickOnRecyclerViewItemAtPosition(itemPosition = 13)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 12)
clickOnRecyclerViewItemAtPosition(itemPosition = 14)
+ clickOnRecyclerViewItemAtPosition(itemPosition = 15)
clickOnRecyclerViewItemAtPosition(itemPosition = 2)
onView(isRoot()).perform(orientationLandscape())
onView(withId(R.id.mark_chapters_completed_all_check_box)).check(matches(not(isChecked())))
@@ -596,11 +613,11 @@ class MarkChaptersCompletedFragmentTest {
markFractionsFirstChapterCompleted()
launchMarkChaptersCompletedFragmentTestActivity(internalProfileId).use {
testCoroutineDispatchers.runCurrent()
- scrollToPosition(position = 7)
+ scrollToPosition(position = 8)
onView(
atPositionOnView(
recyclerViewId = R.id.mark_chapters_completed_recycler_view,
- position = 7,
+ position = 8,
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
).check(matches(isChecked())).check(matches(not(isEnabled())))
@@ -613,11 +630,11 @@ class MarkChaptersCompletedFragmentTest {
launchMarkChaptersCompletedFragmentTestActivity(internalProfileId).use {
testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
- scrollToPosition(position = 7)
+ scrollToPosition(position = 8)
onView(
atPositionOnView(
recyclerViewId = R.id.mark_chapters_completed_recycler_view,
- position = 7,
+ position = 8,
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
).check(matches(isChecked())).check(matches(not(isEnabled())))
@@ -629,11 +646,11 @@ class MarkChaptersCompletedFragmentTest {
markFractionsFirstChapterCompleted()
launchMarkChaptersCompletedFragmentTestActivity(internalProfileId).use {
testCoroutineDispatchers.runCurrent()
- scrollToPosition(position = 8)
+ scrollToPosition(position = 9)
onView(
atPositionOnView(
recyclerViewId = R.id.mark_chapters_completed_recycler_view,
- position = 8,
+ position = 9,
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
).check(matches(isEnabled()))
@@ -646,11 +663,11 @@ class MarkChaptersCompletedFragmentTest {
launchMarkChaptersCompletedFragmentTestActivity(internalProfileId).use {
testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
- scrollToPosition(position = 8)
+ scrollToPosition(position = 9)
onView(
atPositionOnView(
recyclerViewId = R.id.mark_chapters_completed_recycler_view,
- position = 8,
+ position = 9,
targetViewId = R.id.mark_chapters_completed_chapter_check_box
)
).check(matches(isEnabled()))
diff --git a/app/src/sharedTest/java/org/oppia/android/app/testing/NumberWithUnitsInputInteractionViewTestActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/testing/NumberWithUnitsInputInteractionViewTestActivityTest.kt
new file mode 100644
index 00000000000..aa8ec733a62
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/testing/NumberWithUnitsInputInteractionViewTestActivityTest.kt
@@ -0,0 +1,294 @@
+package org.oppia.android.app.testing
+
+import android.app.Application
+import android.content.res.Configuration
+import androidx.appcompat.app.AppCompatActivity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.action.ViewActions.scrollTo
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.app.activity.ActivityComponent
+import org.oppia.android.app.activity.ActivityComponentFactory
+import org.oppia.android.app.activity.route.ActivityRouterModule
+import org.oppia.android.app.application.ApplicationComponent
+import org.oppia.android.app.application.ApplicationInjector
+import org.oppia.android.app.application.ApplicationInjectorProvider
+import org.oppia.android.app.application.ApplicationModule
+import org.oppia.android.app.application.ApplicationStartupListenerModule
+import org.oppia.android.app.application.testing.TestingBuildFlavorModule
+import org.oppia.android.app.devoptions.DeveloperOptionsModule
+import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
+import org.oppia.android.app.model.InteractionObject
+import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
+import org.oppia.android.app.shim.ViewBindingShimModule
+import org.oppia.android.app.test.R
+import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.data.backends.gae.NetworkConfigProdModule
+import org.oppia.android.data.backends.gae.RetrofitModule
+import org.oppia.android.data.backends.gae.RetrofitServiceModule
+import org.oppia.android.domain.classify.InteractionsModule
+import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule
+import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule
+import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule
+import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.android.domain.exploration.ExplorationProgressModule
+import org.oppia.android.domain.exploration.ExplorationStorageModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule
+import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.android.domain.oppialogger.LogStorageModule
+import org.oppia.android.domain.oppialogger.LoggingIdentifierModule
+import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule
+import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule
+import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule
+import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule
+import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule
+import org.oppia.android.domain.question.QuestionModule
+import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
+import org.oppia.android.testing.DisableAccessibilityChecks
+import org.oppia.android.testing.OppiaTestRule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.espresso.EditTextInputAction
+import org.oppia.android.testing.firebase.TestAuthenticationModule
+import org.oppia.android.testing.junit.InitializeDefaultLocaleRule
+import org.oppia.android.testing.platformparameter.TestPlatformParameterModule
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestCoroutineDispatchers
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
+import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.gcsresource.GcsResourceModule
+import org.oppia.android.util.locale.LocaleProdModule
+import org.oppia.android.util.logging.LoggerModule
+import org.oppia.android.util.logging.SyncStatusModule
+import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule
+import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
+import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
+import org.oppia.android.util.parser.image.GlideImageLoaderModule
+import org.oppia.android.util.parser.image.ImageParsingModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [NumberWithUnitsInputInteractionViewTestActivity]. */
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = NumberWithUnitsInputInteractionViewTestActivityTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class NumberWithUnitsInputInteractionViewTestActivityTest {
+ @get:Rule
+ val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ @get:Rule
+ val oppiaTestRule = OppiaTestRule()
+
+ @Inject
+ lateinit var editTextInputAction: EditTextInputAction
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ }
+
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ @Test
+ @DisableAccessibilityChecks // Disabled, as NumberWithUnitsInputInteractionViewTestActivity is a
+ // test file and will not be used by user
+ fun testNumberWithUnitsInput_withNoInput_hasCorrectPendingAnswerType() {
+ val activityScenario = ActivityScenario.launch(
+ NumberWithUnitsInputInteractionViewTestActivity::class.java
+ )
+ activityScenario.onActivity { activity ->
+ val pendingAnswer = activity.numberWithUnitsInputViewModel.getPendingAnswer()
+ assertThat(pendingAnswer.answer).isInstanceOf(InteractionObject::class.java)
+ assertThat(pendingAnswer.answer.normalizedString).isEmpty()
+ }
+ }
+
+ @Test
+ @DisableAccessibilityChecks // Disabled, as NumberWithUnitsInputInteractionViewTestActivity is a
+ // test file and will not be used by user
+ fun testNumberWithUnitsInput_withValue_hasCorrectPendingAnswer() {
+ val activityScenario = ActivityScenario.launch(
+ NumberWithUnitsInputInteractionViewTestActivity::class.java
+ )
+ onView(withId(R.id.test_number_with_units_input_interaction_view))
+ .perform(editTextInputAction.appendText("42 km"))
+ activityScenario.onActivity { activity ->
+ val pendingAnswer = activity.numberWithUnitsInputViewModel.getPendingAnswer()
+ assertThat(pendingAnswer.answer).isInstanceOf(InteractionObject::class.java)
+ assertThat(pendingAnswer.answer.objectTypeCase).isEqualTo(
+ InteractionObject.ObjectTypeCase.NORMALIZED_STRING
+ )
+ assertThat(pendingAnswer.answer.normalizedString).isEqualTo("42 km")
+ }
+ }
+
+ @Test
+ fun testNumberWithUnitsInput_withValue_configChange_hasCorrectPendingAnswer() {
+ val activityScenario = ActivityScenario.launch(
+ NumberWithUnitsInputInteractionViewTestActivity::class.java
+ )
+ onView(withId(R.id.test_number_with_units_input_interaction_view))
+ .perform(editTextInputAction.appendText("5 m"))
+ activityScenario.onActivity { activity ->
+ activity.requestedOrientation = Configuration.ORIENTATION_LANDSCAPE
+ }
+ onView(withId(R.id.test_number_with_units_input_interaction_view))
+ .check(matches(isDisplayed()))
+ .check(matches(withText("5 m")))
+ }
+
+ @Test
+ @DisableAccessibilityChecks // Disabled, as NumberWithUnitsInputInteractionViewTestActivity is a
+ // test file and will not be used by user
+ fun testNumberWithUnitsInput_withBlankInput_submit_emptyInputErrorIsDisplayed() {
+ ActivityScenario.launch(NumberWithUnitsInputInteractionViewTestActivity::class.java).use {
+ scrollToSubmitButton()
+ onView(withId(R.id.submit_button)).check(matches(isDisplayed())).perform(click())
+ onView(withId(R.id.number_with_units_input_error))
+ .check(matches(withText(R.string.text_error_empty_input)))
+ }
+ }
+
+ private fun scrollToSubmitButton() {
+ onView(withId(R.id.submit_button)).perform(scrollTo())
+ testCoroutineDispatchers.runCurrent()
+ }
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ @Singleton
+ @Component(
+ modules = [
+ AccessibilityTestModule::class,
+ ActivityRecreatorTestModule::class,
+ ActivityRouterModule::class,
+ AlgebraicExpressionInputModule::class,
+ ApplicationLifecycleModule::class,
+ ApplicationModule::class,
+ ApplicationStartupListenerModule::class,
+ AssetModule::class,
+ CachingTestModule::class,
+ ContinueModule::class,
+ CpuPerformanceSnapshotterModule::class,
+ DeveloperOptionsModule::class,
+ DeveloperOptionsStarterModule::class,
+ DragDropSortInputModule::class,
+ ExpirationMetaDataRetrieverModule::class,
+ ExplorationProgressModule::class,
+ ExplorationStorageModule::class,
+ FakeOppiaClockModule::class,
+ FirebaseLogUploaderModule::class,
+ FractionInputModule::class,
+ GcsResourceModule::class,
+ GlideImageLoaderModule::class,
+ HintsAndSolutionConfigModule::class,
+ HintsAndSolutionProdModule::class,
+ HtmlParserEntityTypeModule::class,
+ ImageClickInputModule::class,
+ ImageParsingModule::class,
+ InteractionsModule::class,
+ ItemSelectionInputModule::class,
+ LocaleProdModule::class,
+ LogReportWorkerModule::class,
+ LogStorageModule::class,
+ LoggerModule::class,
+ LoggingIdentifierModule::class,
+ MathEquationInputModule::class,
+ MetricLogSchedulerModule::class,
+ MultipleChoiceInputModule::class,
+ NetworkConfigProdModule::class,
+ NetworkConnectionDebugUtilModule::class,
+ NetworkConnectionUtilDebugModule::class,
+ NumberWithUnitsRuleModule::class,
+ NumericExpressionInputModule::class,
+ NumericInputRuleModule::class,
+ PlatformParameterSingletonModule::class,
+ QuestionModule::class,
+ RatioInputModule::class,
+ RetrofitModule::class,
+ RetrofitServiceModule::class,
+ RobolectricModule::class,
+ SplitScreenInteractionModule::class,
+ SyncStatusModule::class,
+ TestAuthenticationModule::class,
+ TestDispatcherModule::class,
+ TestLogReportingModule::class,
+ TestPlatformParameterModule::class,
+ TestingBuildFlavorModule::class,
+ TextInputRuleModule::class,
+ ViewBindingShimModule::class,
+ WorkManagerConfigurationModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(
+ numberWithUnitsInputInteractionViewTestActivityTest:
+ NumberWithUnitsInputInteractionViewTestActivityTest
+ )
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerNumberWithUnitsInputInteractionViewTestActivityTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(
+ numberWithUnitsInputInteractionViewTestActivityTest:
+ NumberWithUnitsInputInteractionViewTestActivityTest
+ ) {
+ component.inject(numberWithUnitsInputInteractionViewTestActivityTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
+}
diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel
index 974c4ce98cd..4fe8ab687ab 100755
--- a/domain/BUILD.bazel
+++ b/domain/BUILD.bazel
@@ -56,6 +56,7 @@ local_assets_library(
"13",
"test_exp_id_4",
"test_exp_id_5",
+ "test_exp_id_6",
"umPkwp0L1M0-",
"MjZzEVOG47_1",
"2mzzFVDLuAj8",
diff --git a/domain/src/main/assets/dev/test_story_id_2.json b/domain/src/main/assets/dev/test_story_id_2.json
index 4c40c1f1e94..b22df0620bc 100644
--- a/domain/src/main/assets/dev/test_story_id_2.json
+++ b/domain/src/main/assets/dev/test_story_id_2.json
@@ -11,6 +11,19 @@
"outline": "",
"title": "Fifth Exploration",
"acquired_skill_ids": []
+ },
+ {
+ "description": "",
+ "exploration_id": "test_exp_id_6",
+ "destination_node_ids": [],
+ "thumbnail_filename": "",
+ "outline_is_finalized": true,
+ "id": "test_exp_id_6",
+ "prerequisite_skill_ids": [],
+ "thumbnail_bg_color": "#d68f78",
+ "outline": "",
+ "title": "Number With Units Exploration",
+ "acquired_skill_ids": []
}],
"story_title": "Other Interesting Story",
"story_description": ""
diff --git a/domain/src/main/assets/dev/test_story_id_2.textproto b/domain/src/main/assets/dev/test_story_id_2.textproto
index 7c8f773d4f9..bcf0b46dccb 100644
--- a/domain/src/main/assets/dev/test_story_id_2.textproto
+++ b/domain/src/main/assets/dev/test_story_id_2.textproto
@@ -25,6 +25,29 @@ chapters {
content_id: "description"
}
}
+chapters {
+ exploration_id: "test_exp_id_6"
+ chapter_thumbnail {
+ background_color_rgb: 14061432
+ }
+ written_translations {
+ key: "title"
+ value {
+ }
+ }
+ written_translations {
+ key: "description"
+ value {
+ }
+ }
+ translatable_title {
+ html: "Number With Units Exploration"
+ content_id: "title"
+ }
+ translatable_description {
+ content_id: "description"
+ }
+}
written_translations {
key: "title"
value {
diff --git a/domain/src/main/assets/test_exp_id_6.json b/domain/src/main/assets/test_exp_id_6.json
new file mode 100644
index 00000000000..e34aee01aab
--- /dev/null
+++ b/domain/src/main/assets/test_exp_id_6.json
@@ -0,0 +1,96 @@
+{
+ "exploration_id": "test_exp_id_6",
+ "preferred_audio_language_code": "",
+ "correctness_feedback_enabled": false,
+ "version": 0,
+ "record_playthrough_probability": 0.0,
+ "exploration": {
+ "init_state_name": "NumberWithUnits",
+ "param_changes": [],
+ "param_specs": {},
+ "states": {
+ "End": {
+ "content": {
+ "content_id": "content",
+ "html": "This is the end
"
+ },
+ "param_changes": [],
+ "interaction": {
+ "id": "EndExploration",
+ "customization_args": {
+ "recommendedExplorationIds": {
+ "value": []
+ }
+ },
+ "answer_groups": [],
+ "default_outcome": null,
+ "hints": [],
+ "solution": null
+ },
+ "classifier_model_id": "",
+ "recorded_voiceovers": {
+ "voiceovers_mapping": {
+ "content": {}
+ }
+ },
+ "written_translations": {
+ "translations_mapping": {
+ "content": {}
+ }
+ },
+ "solicit_answer_details": false,
+ "next_content_id_index": -1,
+ "linked_skill_id": null
+ },
+ "NumberWithUnits": {
+ "content": {
+ "content_id": "content",
+ "html": "Convert 4.1 m to cm.
"
+ },
+ "interaction": {
+ "customization_args": {},
+ "hints": [],
+ "id": "NumberWithUnits",
+ "answer_groups": [{
+ "rule_specs": [{
+ "rule_type": "IsEqualTo",
+ "inputs": {
+ "f": {
+ "fraction": {
+ "denominator": 1,
+ "isNegative": false,
+ "numerator": 0,
+ "wholeNumber": 0
+ },
+ "real": 410.0,
+ "type": "real",
+ "units": [{
+ "exponent": 1,
+ "unit": "cm"
+ }]
+ }
+ }
+ }],
+ "outcome": {
+ "dest": "End",
+ "feedback": {
+ "content_id": "feedback_1",
+ "html": "Yay.This is correct
"
+ },
+ "labelled_as_correct": true,
+ "param_changes": [],
+ "refresher_exploration_id": "",
+ "missing_prerequisite_skill_id": ""
+ },
+ "tagged_skill_misconception_id": ""
+ }],
+ "solution": []
+ }
+ }
+ },
+ "objective": "learn how to arrange the sentence",
+ "language_code": "en",
+ "correctness_feedback_enabled": false,
+ "title": "Drag and Drop Test Exploration"
+ }
+}
diff --git a/domain/src/main/assets/test_exp_id_6.textproto b/domain/src/main/assets/test_exp_id_6.textproto
new file mode 100644
index 00000000000..67ad2798193
--- /dev/null
+++ b/domain/src/main/assets/test_exp_id_6.textproto
@@ -0,0 +1,98 @@
+id: "test_exp_id_6"
+states {
+ key: "NumberWithUnits"
+ value {
+ name: "NumberWithUnits"
+ recorded_voiceovers {
+ key: "feedback_1"
+ value {
+ }
+ }
+ recorded_voiceovers {
+ key: "content"
+ value {
+ }
+ }
+ content {
+ html: "Convert 4.1 m to cm.
"
+ content_id: "content"
+ }
+ written_translations {
+ key: "feedback_1"
+ value {
+ }
+ }
+ written_translations {
+ key: "content"
+ value {
+ }
+ }
+ interaction {
+ id: "Continue"
+ default_outcome {
+ dest_state_name: "End"
+ feedback {
+ html: "Yay.This is correct
"
+ content_id: "feedback_1"
+ }
+ labelled_as_correct: true
+ }
+ customization_args {
+ key: "buttonText"
+ value {
+ normalized_string: "Continue"
+ }
+ }
+ }
+ }
+}
+states {
+ key: "End"
+ value {
+ name: "End"
+ recorded_voiceovers {
+ key: "content"
+ value {
+ }
+ }
+ content {
+ html: "This is the end
"
+ content_id: "content"
+ }
+ written_translations {
+ key: "content"
+ value {
+ }
+ }
+ interaction {
+ id: "EndExploration"
+ customization_args {
+ key: "recommendedExplorationIds"
+ value {
+ schema_object_list {
+ }
+ }
+ }
+ }
+ }
+}
+init_state_name: "NumberWithUnits"
+language_code: "en"
+written_translations {
+ key: "title"
+ value {
+ }
+}
+written_translations {
+ key: "description"
+ value {
+ }
+}
+translatable_title {
+ html: "Drag and Drop Test Exploration"
+ content_id: "title"
+}
+description {
+ html: "learn how to arrange the sentence"
+ content_id: "description"
+}
diff --git a/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt b/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt
index b424deafecb..f9a9acce1f1 100644
--- a/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt
+++ b/domain/src/main/java/org/oppia/android/domain/topic/StoryProgressController.kt
@@ -27,6 +27,7 @@ const val RATIOS_STORY_ID_1 = "xBSdg4oOClga"
const val TEST_EXPLORATION_ID_2 = "test_exp_id_2"
const val TEST_EXPLORATION_ID_4 = "test_exp_id_4"
const val TEST_EXPLORATION_ID_5 = "test_exp_id_5"
+const val TEST_EXPLORATION_ID_6 = "test_exp_id_6"
const val TEST_EXPLORATION_ID_13 = "13"
const val FRACTIONS_EXPLORATION_ID_0 = "umPkwp0L1M0-"
const val FRACTIONS_EXPLORATION_ID_1 = "MjZzEVOG47_1"
diff --git a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel
index 2bbf388ce58..68a0c80971a 100644
--- a/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel
+++ b/domain/src/main/java/org/oppia/android/domain/util/BUILD.bazel
@@ -22,6 +22,7 @@ kt_android_library(
srcs = [
"InteractionObjectExtensions.kt",
"JsonExtensions.kt",
+ "NumberWithUnitsExtensions.kt",
"WorkDataExtensions.kt",
],
visibility = ["//domain:__subpackages__"],
diff --git a/domain/src/main/java/org/oppia/android/domain/util/NumberWithUnitsExtensions.kt b/domain/src/main/java/org/oppia/android/domain/util/NumberWithUnitsExtensions.kt
new file mode 100644
index 00000000000..c45a69d00c3
--- /dev/null
+++ b/domain/src/main/java/org/oppia/android/domain/util/NumberWithUnitsExtensions.kt
@@ -0,0 +1,25 @@
+package org.oppia.android.domain.util
+
+import org.oppia.android.app.model.NumberUnit
+import org.oppia.android.app.model.NumberWithUnits
+
+/**
+ * Aggregates the units in a [NumberWithUnits] by summing the exponents of any duplicate units.
+ * This is useful for normalizing [NumberWithUnits] instances for comparison, since the order of
+ * units is irrelevant and duplicate units are effectively the same as a single unit with the
+ * exponent equal to the sum of the exponents of the duplicate units.
+ */
+fun NumberWithUnits.aggregate(): NumberWithUnits {
+ val aggregated = unitList.groupBy { it.unit }
+ .map { (unit, units) ->
+ NumberUnit.newBuilder()
+ .setUnit(unit)
+ .setExponent(units.sumOf { it.exponent })
+ .build()
+ }
+
+ return toBuilder()
+ .clearUnit()
+ .addAllUnit(aggregated)
+ .build()
+}
diff --git a/domain/src/main/java/org/oppia/android/domain/util/StateRetriever.kt b/domain/src/main/java/org/oppia/android/domain/util/StateRetriever.kt
index 5fe61ce3d5a..e2811591d06 100644
--- a/domain/src/main/java/org/oppia/android/domain/util/StateRetriever.kt
+++ b/domain/src/main/java/org/oppia/android/domain/util/StateRetriever.kt
@@ -424,6 +424,11 @@ class StateRetriever @Inject constructor() {
mathExpression = inputJson.getStringFromObject("math_expression")
}.build()
}
+ "NumberWithUnits" -> {
+ InteractionObject.newBuilder().apply {
+ numberWithUnits = parseNumberWithUnitsObject(inputJson)
+ }.build()
+ }
else -> error("Encountered unsupported interaction ID for solutions: $interactionId")
}
}
diff --git a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt
index 7465f503070..7d4f99fa701 100644
--- a/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/classify/rules/numberwithunits/NumberWithUnitsIsEqualToRuleClassifierProviderTest.kt
@@ -43,6 +43,10 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest {
InteractionObjectTestBuilder.createNumberUnit(unit = "p", exponent = 5)
private val NUMBER_UNIT_TEST_STRING_TO_POWER_3 =
InteractionObjectTestBuilder.createNumberUnit(unit = "e", exponent = 3)
+ private val NUMBER_UNIT_METER_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "meter", exponent = 1)
+ private val NUMBER_UNIT_METER_TO_POWER_2 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "meter", exponent = 2)
private val ANSWER_TEST_NUMBER_WITH_UNITS =
InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
FRACTION_VALUE_TEST_2_OVER_5,
@@ -83,6 +87,94 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest {
NUMBER_UNIT_TEST_STRING_TO_POWER_3
)
)
+ private val TEST_AGGREGATED_FRACTION_ANSWER_NUMBER_WITH_UNITS =
+ InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2_OVER_5,
+ listOf(NUMBER_UNIT_METER_TO_POWER_2)
+ )
+ private val TEST_DUPLICATED_FRACTION_INPUT_NUMBER_WITH_UNITS =
+ InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2_OVER_5,
+ listOf(NUMBER_UNIT_METER_TO_POWER_1, NUMBER_UNIT_METER_TO_POWER_1)
+ )
+ private val FRACTION_VALUE_TEST_2 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 2, denominator = 1
+ ).fraction
+ private val FRACTION_VALUE_TEST_6_OVER_3 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 6, denominator = 3
+ ).fraction
+ private val FRACTION_VALUE_TEST_2_OVER_3 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 2, denominator = 3
+ ).fraction
+ private val FRACTION_VALUE_TEST_20_OVER_30 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 20, denominator = 30
+ ).fraction
+ private val FRACTION_VALUE_TEST_100 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 100, denominator = 1
+ ).fraction
+ private val FRACTION_VALUE_TEST_2000 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 2000, denominator = 1
+ ).fraction
+ private val FRACTION_VALUE_TEST_200 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 200, denominator = 1
+ ).fraction
+ private val FRACTION_VALUE_TEST_2_OVER_30 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 2, denominator = 30
+ ).fraction
+ private val FRACTION_VALUE_TEST_20 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 20, denominator = 1
+ ).fraction
+ private val FRACTION_VALUE_TEST_10 =
+ InteractionObjectTestBuilder.createFraction(
+ isNegative = false, numerator = 10, denominator = 1
+ ).fraction
+ private val UNIT_KG_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "kg", exponent = 1)
+ private val UNIT_G_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "g", exponent = 1)
+ private val UNIT_M_TO_POWER_NEGATIVE_2 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "m", exponent = -2)
+ private val UNIT_M_TO_POWER_2 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "m", exponent = 2)
+ private val UNIT_METER_TO_POWER_NEGATIVE_2 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "meter", exponent = -2)
+ private val UNIT_CM_TO_POWER_NEGATIVE_2 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "cm", exponent = -2)
+ private val UNIT_RUPEES_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "rupees", exponent = 1)
+ private val UNIT_RUPEE_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "rupee", exponent = 1)
+ private val UNIT_PAISE_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "paise", exponent = 1)
+ private val UNIT_DOLLARS_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "dollars", exponent = 1)
+ private val UNIT_DOLLAR_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "dollar", exponent = 1)
+ private val UNIT_DOLLARS_TITLE_CASE_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "Dollars", exponent = 1)
+ private val UNIT_CENTS_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "cents", exponent = 1)
+ private val UNIT_M_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "m", exponent = 1)
+ private val UNIT_S_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "s", exponent = 1)
+ private val UNIT_S_TO_POWER_2 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "s", exponent = 2)
+ private val UNIT_S_TO_POWER_NEGATIVE_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "s", exponent = -1)
+ private val UNIT_N_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "N", exponent = 1)
+ private val UNIT_J_TO_POWER_1 =
+ InteractionObjectTestBuilder.createNumberUnit(unit = "J", exponent = 1)
@Inject
internal lateinit var numberWithUnitsIsEqualToRuleClassifierProvider:
@@ -181,6 +273,565 @@ class NumberWithUnitsIsEqualToRuleClassifierProviderTest {
assertThat(matches).isTrue()
}
+ @Test
+ fun testFractionInputWithDuplicateUnits_withEquivalentAggregatedFractionAnswer_doesNotMatch() {
+ val inputs = mapOf("f" to TEST_DUPLICATED_FRACTION_INPUT_NUMBER_WITH_UNITS)
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = TEST_AGGREGATED_FRACTION_ANSWER_NUMBER_WITH_UNITS,
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testFractionInputNumberWithUnits_withEquivalentRealAnswer_doesNotMatch() {
+ val inputs = mapOf("f" to INPUT_TEST_NUMBER_WITH_UNITS)
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForReal(
+ number = 0.4,
+ units = listOf(NUMBER_UNIT_TEST_STRING_TO_POWER_3, NUMBER_UNIT_TEST_STRING_TO_POWER_5)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testExactValueMatch_fractionKgMNegative2_matches() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testExactValueMatch_negativeRealM2_matches() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForReal(
+ number = -4.5,
+ units = listOf(UNIT_M_TO_POWER_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForReal(
+ number = -4.5,
+ units = listOf(UNIT_M_TO_POWER_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testExactValueMatch_fractionTwoThirdsKgMNegative2_matches() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2_OVER_3,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2_OVER_3,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testExactValueMatch_hundredRupees_matches() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_RUPEES_TO_POWER_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_RUPEES_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testExactValueMatch_hundredDollars_matches() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_DOLLARS_TO_POWER_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_DOLLARS_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testExactValueMatch_hundredCents_matches() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_CENTS_TO_POWER_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_CENTS_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isTrue()
+ }
+
+ @Test
+ fun testApproximateValueMatch_fractionSixOverThree_vsTwo_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_6_OVER_3,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testApproximateValueMatch_fractionTwentyOverThirty_vsTwoThirds_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_20_OVER_30,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2_OVER_3,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testApproximateValueMatch_realVsFractionEquivalent_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForReal(
+ number = 0.666666666666,
+ units = listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2_OVER_3,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testApproximateValueMatch_realPoint66VsFractionTwoThirds_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForReal(
+ number = 0.66,
+ units = listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2_OVER_3,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testValueMismatch_realTwoPointFiveVsTwo_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForReal(
+ number = 2.5,
+ units = listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForReal(
+ number = 2.0,
+ units = listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testExponentMismatch_kgM2VsKgMNegative2_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitMismatch_kgM2VsKgMNegative2_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitMismatch_hundredRupeesVsHundredPaise_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_RUPEES_TO_POWER_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_PAISE_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitMismatch_hundredDollarsVsHundredCents_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_DOLLARS_TO_POWER_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_CENTS_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitMismatch_hundredRupeesVsHundredDollars_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_RUPEES_TO_POWER_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_100,
+ listOf(UNIT_DOLLARS_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitConversion_twoThousandGmNegative2VsTwoKgMNegative2_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2000,
+ listOf(UNIT_G_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitConversion_realPoint2GcmNegative2VsTwoKgMNegative2_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForReal(
+ number = 0.2,
+ units = listOf(UNIT_G_TO_POWER_1, UNIT_CM_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitConversion_fractionTwoOverThirtyGcmNegative2VsTwoThirdsKgMNegative2_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2_OVER_30,
+ listOf(UNIT_G_TO_POWER_1, UNIT_CM_TO_POWER_NEGATIVE_2)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2_OVER_3,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_NEGATIVE_2)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitConversion_twoRupeesVsTwoHundredPaise_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_RUPEES_TO_POWER_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_200,
+ listOf(UNIT_PAISE_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitConversion_twoDollarsVsTwoHundredCents_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_DOLLARS_TO_POWER_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_200,
+ listOf(UNIT_CENTS_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitReduction_twentyMSquareSecondPerSecondVsTwentyMPerSecond_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_20,
+ listOf(UNIT_M_TO_POWER_1, UNIT_S_TO_POWER_2, UNIT_S_TO_POWER_NEGATIVE_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_20,
+ listOf(UNIT_M_TO_POWER_1, UNIT_S_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitCombination_tenNewtonMeterVsTenJoule_doesNotMatch() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_10,
+ listOf(UNIT_N_TO_POWER_1, UNIT_M_TO_POWER_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_10,
+ listOf(UNIT_J_TO_POWER_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isFalse()
+ }
+
+ @Test
+ fun testUnitReordering_twoMKgPerSecondVsTwoKgMPerSecond_matches() {
+ val inputs = mapOf(
+ "f" to InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_M_TO_POWER_1, UNIT_KG_TO_POWER_1, UNIT_S_TO_POWER_NEGATIVE_1)
+ )
+ )
+
+ val matches =
+ unitsIsEqualsRuleClassifier.matches(
+ answer = InteractionObjectTestBuilder.createNumberWithUnitsForFraction(
+ FRACTION_VALUE_TEST_2,
+ listOf(UNIT_KG_TO_POWER_1, UNIT_M_TO_POWER_1, UNIT_S_TO_POWER_NEGATIVE_1)
+ ),
+ inputs = inputs,
+ classificationContext = ClassificationContext()
+ )
+
+ assertThat(matches).isTrue()
+ }
+
@Test
fun testWholeNumberInputValue_withRealAnswerNumWithUnits_throwsException() {
val inputs = mapOf("f" to WHOLE_NUMBER_VALUE_9)
diff --git a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt
index 0939342f851..6c0a1d060d7 100644
--- a/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt
+++ b/domain/src/test/java/org/oppia/android/domain/topic/TopicListControllerTest.kt
@@ -153,7 +153,7 @@ class TopicListControllerTest {
val topicList = retrieveTopicList()
val secondTopic = topicList.getTopicSummary(1).topicSummary
- assertThat(secondTopic.totalChapterCount).isEqualTo(1)
+ assertThat(secondTopic.totalChapterCount).isEqualTo(2)
}
@Test
@@ -749,7 +749,7 @@ class TopicListControllerTest {
assertThat(promotedStory.nextChapterTitle.html).isEqualTo("Fifth Exploration")
assertThat(promotedStory.completedChapterCount).isEqualTo(0)
assertThat(promotedStory.isTopicLearned).isFalse()
- assertThat(promotedStory.totalChapterCount).isEqualTo(1)
+ assertThat(promotedStory.totalChapterCount).isEqualTo(2)
}
private fun verifyOngoingStoryAsFractionStory0Exploration0(promotedStory: PromotedStory) {
diff --git a/scripts/assets/accessibility_label_exemptions.textproto b/scripts/assets/accessibility_label_exemptions.textproto
index 63788bad5cd..aa8e4e488b9 100644
--- a/scripts/assets/accessibility_label_exemptions.textproto
+++ b/scripts/assets/accessibility_label_exemptions.textproto
@@ -30,6 +30,7 @@ exempted_activity: "app/src/main/java/org/oppia/android/app/testing/LessonThumbn
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/MarginBindingAdaptersTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/MathExpressionInteractionsViewTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/NavigationDrawerTestActivity"
+exempted_activity: "app/src/main/java/org/oppia/android/app/testing/NumberWithUnitsInputInteractionViewTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/PoliciesFragmentTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/ProfileChooserFragmentTestActivity"
exempted_activity: "app/src/main/java/org/oppia/android/app/testing/StateAssemblerMarginBindingAdaptersTestActivity"
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index 79faa5ab53a..4aa052890c2 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -378,6 +378,10 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/customview/interaction/MathExpressionInteractionsView.kt"
source_file_is_incompatible_with_code_coverage: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/customview/interaction/NumberWithUnitsInputInteractionView.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/customview/interaction/NumericInputInteractionView.kt"
test_file_not_required: true
@@ -1832,6 +1836,10 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NextButtonViewModel.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumberWithUnitsInputViewModel.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/player/state/itemviewmodel/NumericInputViewModel.kt"
test_file_not_required: true
@@ -4151,6 +4159,10 @@ test_file_exemption {
exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/JsonExtensions.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/NumberWithUnitsExtensions.kt"
+ test_file_not_required: true
+}
test_file_exemption {
exempted_file_path: "domain/src/main/java/org/oppia/android/domain/util/StateRetriever.kt"
source_file_is_incompatible_with_code_coverage: true
diff --git a/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt b/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt
index 98ef108d16f..fa60c74d821 100644
--- a/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt
+++ b/testing/src/main/java/org/oppia/android/testing/story/StoryProgressTestHelper.kt
@@ -27,6 +27,7 @@ import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_13
import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2
import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_4
import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_5
+import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_6
import org.oppia.android.domain.topic.TEST_STORY_ID_0
import org.oppia.android.domain.topic.TEST_STORY_ID_2
import org.oppia.android.domain.topic.TEST_TOPIC_ID_0
@@ -130,7 +131,7 @@ class StoryProgressTestHelper @Inject constructor(
}
/**
- * Marks the only chapter of test topic 1 story 2 as completed, and any needed prerequisites.
+ * Marks the first chapter of test topic 1 story 2 as completed, and any needed prerequisites.
* See [markCompletedTestTopic0Story0Exp0] for specifics on the parameters passed to this method,
* and any other nuances.
*/
@@ -147,6 +148,26 @@ class StoryProgressTestHelper @Inject constructor(
)
}
+ /**
+ * Marks the second chapter of test topic 1 story 2 as completed, and any needed prerequisites.
+ * See [markCompletedTestTopic0Story0Exp0] for specifics on the parameters passed to this method,
+ * and any other nuances.
+ */
+ fun markCompletedTestTopic1Story0Exp1(
+ profileId: LegacyProfileId,
+ timestampOlderThanOneWeek: Boolean
+ ) {
+ // Must complete prerequisite chapter first.
+ markCompletedTestTopic1Story0Exp0(profileId, timestampOlderThanOneWeek)
+ recordCompletedChapter(
+ profileId,
+ TEST_TOPIC_ID_1,
+ TEST_STORY_ID_2,
+ TEST_EXPLORATION_ID_6,
+ timestampOlderThanOneWeek
+ )
+ }
+
/**
* Marks test topic 0's story 0 as completed. See [markCompletedTestTopic0Story0Exp0] for
* specifics on the parameters passed to this method, and any other nuances.
@@ -167,7 +188,8 @@ class StoryProgressTestHelper @Inject constructor(
profileId: LegacyProfileId,
timestampOlderThanOneWeek: Boolean
) {
- markCompletedTestTopic1Story0Exp0(profileId, timestampOlderThanOneWeek)
+ // Complete last chapter (+ previous automatically).
+ markCompletedTestTopic1Story0Exp1(profileId, timestampOlderThanOneWeek)
}
/**
@@ -485,7 +507,7 @@ class StoryProgressTestHelper @Inject constructor(
}
/**
- * Marks the only chapter of test topic 1 story 2 as in progress saved. For specifics on parameters
+ * Marks the first chapter of test topic 1 story 2 as in progress saved. For specifics on parameters
* and nuances, see: [markInProgressSavedTestTopic0Story0Exp0].
*/
fun markInProgressSavedTestTopic1Story2Exp0(
@@ -502,7 +524,7 @@ class StoryProgressTestHelper @Inject constructor(
}
/**
- * Marks the only chapter of test topic 1 story 2 as in progress not saved. For specifics on parameters
+ * Marks the first chapter of test topic 1 story 2 as in progress not saved. For specifics on parameters
* and nuances, see: [markInProgressNotSavedTestTopic0Story0Exp0].
*/
fun markInProgressNotSavedTestTopic1Story2Exp0(
@@ -518,6 +540,44 @@ class StoryProgressTestHelper @Inject constructor(
)
}
+ /**
+ * Marks the second chapter of test topic 1 story 2 as in progress saved. For specifics on parameters
+ * and nuances, see: [markInProgressSavedTestTopic0Story0Exp0].
+ */
+ fun markInProgressSavedTestTopic1Story2Exp1(
+ profileId: LegacyProfileId,
+ timestampOlderThanOneWeek: Boolean
+ ) {
+ // Must complete the previous chapter first.
+ markCompletedTestTopic1Story0Exp0(profileId, timestampOlderThanOneWeek)
+ recordChapterAsInProgressSaved(
+ profileId,
+ TEST_TOPIC_ID_1,
+ TEST_STORY_ID_2,
+ TEST_EXPLORATION_ID_6,
+ timestampOlderThanOneWeek
+ )
+ }
+
+ /**
+ * Marks the second chapter of test topic 1 story 2 as in progress not saved. For specifics on parameters
+ * and nuances, see: [markInProgressNotSavedTestTopic0Story0Exp0].
+ */
+ fun markInProgressNotSavedTestTopic1Story2Exp1(
+ profileId: LegacyProfileId,
+ timestampOlderThanOneWeek: Boolean
+ ) {
+ // Must complete the previous chapter first.
+ markCompletedTestTopic1Story0Exp0(profileId, timestampOlderThanOneWeek)
+ recordChapterAsInProgressNotSaved(
+ profileId,
+ TEST_TOPIC_ID_1,
+ TEST_STORY_ID_2,
+ TEST_EXPLORATION_ID_6,
+ timestampOlderThanOneWeek
+ )
+ }
+
/**
* Marks test topic 0's story 0 as in progress saved. For specifics on parameters and nuances, see:
* [markInProgressSavedTestTopic0Story0Exp0].