diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryItemViewModel.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryItemViewModel.kt index ae7fd8f5b56..cb28de75047 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryItemViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryItemViewModel.kt @@ -11,7 +11,7 @@ import org.oppia.android.domain.translation.TranslationController /** Completed story view model for the recycler view in [CompletedStoryListFragment]. */ class CompletedStoryItemViewModel( private val activity: AppCompatActivity, - private val internalProfileId: Int, + private val profileId: ProfileId, val completedStory: CompletedStory, val entityType: String, private val intentFactoryShim: IntentFactoryShim, @@ -33,7 +33,7 @@ class CompletedStoryItemViewModel( /** Called when user clicks on CompletedStoryItem. */ fun onCompletedStoryItemClicked() { routeToTopicPlayStory( - ProfileId.newBuilder().setInternalId(internalProfileId).build(), + profileId, completedStory.classroomId, completedStory.topicId, completedStory.storyId diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivity.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivity.kt index fe2d803f0a7..6d4fe12f3d9 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivity.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivity.kt @@ -21,8 +21,8 @@ class CompletedStoryListActivity : InjectableAutoLocalizedAppCompatActivity() { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) - val internalProfileId: Int = intent?.extractCurrentUserProfileId()?.internalId ?: -1 - completedStoryListActivityPresenter.handleOnCreate(internalProfileId) + val profileId = intent?.extractCurrentUserProfileId() ?: ProfileId.getDefaultInstance() + completedStoryListActivityPresenter.handleOnCreate(profileId) } companion object { diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityPresenter.kt index de44ee70a2c..cb0cebfc2fa 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListActivityPresenter.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.completedstorylist import androidx.appcompat.app.AppCompatActivity import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope +import org.oppia.android.app.model.ProfileId import javax.inject.Inject /** The presenter for [CompletedStoryListActivity]. */ @@ -12,7 +13,7 @@ class CompletedStoryListActivityPresenter @Inject constructor( ) { /** Initializes views for [CompletedStoryListActivity] and binds [CompletedStoryListFragment]. */ - fun handleOnCreate(internalProfileId: Int) { + fun handleOnCreate(profileId: ProfileId) { activity.setContentView(R.layout.completed_story_list_activity) if (getCompletedStoryListFragment() == null) { activity @@ -20,7 +21,7 @@ class CompletedStoryListActivityPresenter @Inject constructor( .beginTransaction() .add( R.id.completed_story_list_fragment_placeholder, - CompletedStoryListFragment.newInstance(internalProfileId), + CompletedStoryListFragment.newInstance(profileId), CompletedStoryListFragment.COMPLETED_STORY_LIST_FRAGMENT_TAG ).commitNow() } diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragment.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragment.kt index 4f11f4df5e1..e1f0bb3b4aa 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragment.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragment.kt @@ -20,8 +20,7 @@ class CompletedStoryListFragment : InjectableFragment() { const val COMPLETED_STORY_LIST_FRAGMENT_TAG = "COMPLETED_STORY_LIST_FRAGMENT_TAG" /** Returns a new [CompletedStoryListFragment] to display corresponding to the specified profile ID. */ - fun newInstance(internalProfileId: Int): CompletedStoryListFragment { - val profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() + fun newInstance(profileId: ProfileId): CompletedStoryListFragment { return CompletedStoryListFragment().apply { arguments = Bundle().apply { decorateWithUserProfileId(profileId) @@ -47,11 +46,10 @@ class CompletedStoryListFragment : InjectableFragment() { "Expected arguments to be passed to CompletedStoryListFragment" } val profileId = arguments.extractCurrentUserProfileId() - val internalProfileId = profileId.internalId return completedStoryListFragmentPresenter.handleCreateView( inflater, container, - internalProfileId + profileId ) } } diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragmentPresenter.kt index ed1a566d4de..02b2ae9cd68 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListFragmentPresenter.kt @@ -7,6 +7,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.recyclerview.widget.GridLayoutManager import org.oppia.android.R +import org.oppia.android.app.model.ProfileId import org.oppia.android.app.recyclerview.BindableAdapter import org.oppia.android.databinding.CompletedStoryItemBinding import org.oppia.android.databinding.CompletedStoryListFragmentBinding @@ -26,9 +27,9 @@ class CompletedStoryListFragmentPresenter @Inject constructor( fun handleCreateView( inflater: LayoutInflater, container: ViewGroup?, - internalProfileId: Int + profileId: ProfileId ): View? { - viewModel.setProfileId(internalProfileId) + viewModel.setProfileId(profileId) binding = CompletedStoryListFragmentBinding .inflate( diff --git a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt index 678d91b02ce..e2a02cc711d 100644 --- a/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt +++ b/app/src/main/java/org/oppia/android/app/completedstorylist/CompletedStoryListViewModel.kt @@ -26,13 +26,11 @@ class CompletedStoryListViewModel @Inject constructor( private val translationController: TranslationController, @StoryHtmlParserEntityType private val entityType: String ) : ObservableViewModel() { - /** [internalProfileId] needs to be set before any of the live data members can be accessed. */ - private var internalProfileId: Int = -1 + /** [profileId] needs to be set before any of the live data members can be accessed. */ + private var profileId: ProfileId = ProfileId.getDefaultInstance() private val completedStoryListResultLiveData: LiveData> by lazy { - topicController.getCompletedStoryList( - ProfileId.newBuilder().setInternalId(internalProfileId).build() - ).toLiveData() + topicController.getCompletedStoryList(profileId).toLiveData() } private val completedStoryLiveData: LiveData by lazy { @@ -45,8 +43,8 @@ class CompletedStoryListViewModel @Inject constructor( } /** Sets internalProfileId to this ViewModel. */ - fun setProfileId(internalProfileId: Int) { - this.internalProfileId = internalProfileId + fun setProfileId(profileId: ProfileId) { + this.profileId = profileId } private fun processCompletedStoryListResult( @@ -74,7 +72,7 @@ class CompletedStoryListViewModel @Inject constructor( completedStoryList.completedStoryList.map { completedStory -> CompletedStoryItemViewModel( activity, - internalProfileId, + profileId, completedStory, entityType, intentFactoryShim, diff --git a/app/src/main/java/org/oppia/android/app/player/audio/LanguageInterface.kt b/app/src/main/java/org/oppia/android/app/player/audio/LanguageInterface.kt index ad3265ebdca..769b990f4d9 100644 --- a/app/src/main/java/org/oppia/android/app/player/audio/LanguageInterface.kt +++ b/app/src/main/java/org/oppia/android/app/player/audio/LanguageInterface.kt @@ -11,7 +11,7 @@ interface LanguageInterface { /** * Returns whether the user is actively seeking a new audio position, that is, dragging the * knob to a new position in the audio track. - * */ + */ fun getUserIsSeeking(): Boolean /** Returns the position of the knob on the audio track. */ diff --git a/app/src/main/java/org/oppia/android/app/shim/IntentFactoryShim.kt b/app/src/main/java/org/oppia/android/app/shim/IntentFactoryShim.kt index 0a8994ce081..208942cb9bb 100644 --- a/app/src/main/java/org/oppia/android/app/shim/IntentFactoryShim.kt +++ b/app/src/main/java/org/oppia/android/app/shim/IntentFactoryShim.kt @@ -16,7 +16,7 @@ interface IntentFactoryShim { /** * Creates a [TopicActivity] intent for [PromotedStoryViewModel] and passes necessary string * data. - * */ + */ fun createTopicPlayStoryActivityIntent( context: Context, internalProfileId: Int, @@ -27,7 +27,7 @@ interface IntentFactoryShim { /** * Creates a [TopicActivity] intent which opens info-tab. - * */ + */ fun createTopicActivityIntent( context: Context, internalProfileId: Int, diff --git a/app/src/main/java/org/oppia/android/app/story/StoryFragmentScroller.kt b/app/src/main/java/org/oppia/android/app/story/StoryFragmentScroller.kt index cb3ee1cf989..50c900d1f68 100644 --- a/app/src/main/java/org/oppia/android/app/story/StoryFragmentScroller.kt +++ b/app/src/main/java/org/oppia/android/app/story/StoryFragmentScroller.kt @@ -4,6 +4,6 @@ interface StoryFragmentScroller { /** * Scrolls smoothly (with animation) to the specified vertical pixel position in * [StoryFragment]. - * */ + */ fun smoothScrollToPosition(position: Int) } diff --git a/app/src/main/java/org/oppia/android/app/utility/FontScaleConfigurationUtil.kt b/app/src/main/java/org/oppia/android/app/utility/FontScaleConfigurationUtil.kt index 6570499f35b..8a1505e21a6 100644 --- a/app/src/main/java/org/oppia/android/app/utility/FontScaleConfigurationUtil.kt +++ b/app/src/main/java/org/oppia/android/app/utility/FontScaleConfigurationUtil.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.utility import android.content.Context import android.content.Context.WINDOW_SERVICE import android.util.DisplayMetrics +import android.util.TypedValue import android.view.WindowManager import org.oppia.android.app.model.ReadingTextSize import javax.inject.Inject @@ -21,16 +22,18 @@ class FontScaleConfigurationUtil @Inject constructor() { // TODO(#3616): Migrate to the proper SDK 30+ APIs. @Suppress("DEPRECATION") // The code is correct for targeted versions of Android. windowManager!!.defaultDisplay.getMetrics(metrics) - // TODO(#5625): Migrate away from scaledDensity. + val scaledDensity = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, 1.0f, metrics + ) * configuration.fontScale @Suppress("DEPRECATION") - metrics.scaledDensity = configuration.fontScale * metrics.density + metrics.scaledDensity = scaledDensity context.createConfigurationContext(configuration) context.resources.displayMetrics.setTo(metrics) } private fun getReadingTextSizeConfigurationUtil(readingTextSize: ReadingTextSize): Float { return when (readingTextSize) { - ReadingTextSize.SMALL_TEXT_SIZE -> .8f + ReadingTextSize.SMALL_TEXT_SIZE -> 0.8f ReadingTextSize.MEDIUM_TEXT_SIZE -> 1.0f ReadingTextSize.LARGE_TEXT_SIZE -> 1.2f ReadingTextSize.EXTRA_LARGE_TEXT_SIZE -> 1.4f diff --git a/domain/src/main/java/org/oppia/android/domain/hintsandsolution/HintHandler.kt b/domain/src/main/java/org/oppia/android/domain/hintsandsolution/HintHandler.kt index e836f381541..35ca867e6a3 100644 --- a/domain/src/main/java/org/oppia/android/domain/hintsandsolution/HintHandler.kt +++ b/domain/src/main/java/org/oppia/android/domain/hintsandsolution/HintHandler.kt @@ -35,7 +35,7 @@ interface HintHandler { * @param trackedWrongAnswerCount the count of wrong answers saved in the checkpoint * @param helpIndex the cached state of hints/solution from the checkpoint * @param state the restored pending state - * */ + */ suspend fun resumeHintsForSavedState( trackedWrongAnswerCount: Int, helpIndex: HelpIndex, diff --git a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsController.kt b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsController.kt index 16e8640a1c9..f0912590b25 100644 --- a/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsController.kt +++ b/domain/src/main/java/org/oppia/android/domain/oppialogger/exceptions/ExceptionsController.kt @@ -113,7 +113,7 @@ class ExceptionsController @Inject constructor( * At first, it checks if the size of the store isn't exceeding [exceptionLogStorageCacheSize]. * If the limit is exceeded then the least recent exception is removed from the [exceptionLogStore]. * After this, the [exceptionLog] is added to the store. - * */ + */ private fun cacheExceptionLog(exceptionLog: ExceptionLog) { exceptionLogStore.storeDataAsync(true) { oppiaExceptionLogs -> val storeSize = oppiaExceptionLogs.exceptionLogList.size diff --git a/scripts/assets/file_content_validation_checks.textproto b/scripts/assets/file_content_validation_checks.textproto index fe0d7d9aba9..788ffc0e86c 100644 --- a/scripts/assets/file_content_validation_checks.textproto +++ b/scripts/assets/file_content_validation_checks.textproto @@ -503,8 +503,8 @@ file_content_checks { } file_content_checks { file_path_regex: ".+?\\.kt$" - prohibited_content_regex: "\\*\\*/" - failure_message: "Badly formatted KDoc or block comment. KDocs and block comments should only end with \"*/\"." + prohibited_content_regex: "\\*(\\s*\\*|\\*)/" + failure_message: "Badly formatted KDoc or block comment. KDocs and block comments should only end with \"*/\". Multiple asterisks or whitespace between asterisks are not allowed." exempted_file_name: "scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt" } file_content_checks { diff --git a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt index 5f47d4b81e1..60b415c68db 100644 --- a/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt +++ b/scripts/src/javatests/org/oppia/android/scripts/regex/RegexPatternValidationCheckTest.kt @@ -206,7 +206,8 @@ class RegexPatternValidationCheckTest { "Badly formatted KDoc. Single-line KDocs should always end with exactly one space before the" + " final \"*/\"." private val badKdocOrBlockCommentShouldEndWithCorrectEnding = - "Badly formatted KDoc or block comment. KDocs and block comments should only end with \"*/\"." + "Badly formatted KDoc or block comment. KDocs and block comments should only" + + " end with \"*/\". Multiple asterisks or whitespace between asterisks are not allowed." private val badKdocParamsAndPropertiesShouldHaveNameFollowing = "Badly formatted KDoc param or property at-clause: the name of the parameter or property" + " should immediately follow the at-clause without any additional linking with brackets." @@ -2607,6 +2608,37 @@ class RegexPatternValidationCheckTest { ) } + @Test + fun testFileContent_kdocWithInvalidEndingSequences_failsValidation() { + val prohibitedContent = + """ + /** + * Incorrect KDoc comment. + * */ + /** + * Incorrect KDoc comment. + **/ + /** + * Correct KDoc comment. + */ + """.trimIndent() + tempFolder.newFolder("testfiles", "app", "src", "main", "java", "org", "oppia", "android") + val stringFilePath = "app/src/main/java/org/oppia/android/TestPresenter.kt" + tempFolder.newFile("testfiles/$stringFilePath").writeText(prohibitedContent) + + val exception = assertThrows() { runScript() } + + assertThat(exception).hasMessageThat().contains(REGEX_CHECK_FAILED_OUTPUT_INDICATOR) + assertThat(outContent.toString().trim()) + .isEqualTo( + """ + $stringFilePath:3: $badKdocOrBlockCommentShouldEndWithCorrectEnding + $stringFilePath:6: $badKdocOrBlockCommentShouldEndWithCorrectEnding + $wikiReferenceNote + """.trimIndent() + ) + } + @Test fun testFileContent_singleLineKdocWithExtraSpacesBeforeEnd_fileContentIsNotCorrect() { val prohibitedContent = diff --git a/testing/src/test/java/org/oppia/android/testing/math/BUILD.bazel b/testing/src/test/java/org/oppia/android/testing/math/BUILD.bazel new file mode 100644 index 00000000000..354acf48077 --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/BUILD.bazel @@ -0,0 +1,69 @@ +""" +Tests for math-related test utilities. +""" + +load("//:oppia_android_test.bzl", "oppia_android_test") + +oppia_android_test( + name = "FractionSubjectTest", + srcs = ["FractionSubjectTest.kt"], + custom_package = "org.oppia.android.testing.math", + test_class = "org.oppia.android.testing.math.FractionSubjectTest", + test_manifest = "//testing:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:fraction_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "MathEquationSubjectTest", + srcs = ["MathEquationSubjectTest.kt"], + custom_package = "org.oppia.android.testing.math", + test_class = "org.oppia.android.testing.math.MathEquationSubjectTest", + test_manifest = "//testing:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_equation_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "MathExpressionSubjectTest", + srcs = ["MathExpressionSubjectTest.kt"], + custom_package = "org.oppia.android.testing.math", + test_class = "org.oppia.android.testing.math.MathExpressionSubjectTest", + test_manifest = "//testing:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:math_expression_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:robolectric_android-all", + ], +) + +oppia_android_test( + name = "RealSubjectTest", + srcs = ["RealSubjectTest.kt"], + custom_package = "org.oppia.android.testing.math", + test_class = "org.oppia.android.testing.math.RealSubjectTest", + test_manifest = "//testing:test_manifest", + deps = [ + "//model/src/main/proto:math_java_proto_lite", + "//testing/src/main/java/org/oppia/android/testing/math:real_subject", + "//testing/src/main/java/org/oppia/android/testing/robolectric:test_module", + "//third_party:com_google_truth_truth", + "//third_party:junit_junit", + "//third_party:robolectric_android-all", + ], +) diff --git a/testing/src/test/java/org/oppia/android/testing/math/FractionSubjectTest.kt b/testing/src/test/java/org/oppia/android/testing/math/FractionSubjectTest.kt new file mode 100644 index 00000000000..79b2873f98e --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/FractionSubjectTest.kt @@ -0,0 +1,206 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.oppia.android.app.model.Fraction + +/** Tests for [FractionSubject]. */ +@RunWith(JUnit4::class) +class FractionSubjectTest { + + @Test + fun testHasNegativeProperty_withNegativeFraction_matchesTrue() { + val fraction = Fraction.newBuilder() + .setIsNegative(true) + .build() + + FractionSubject.assertThat(fraction).hasNegativePropertyThat().isTrue() + } + + @Test + fun testHasNegativeProperty_withPositiveFraction_matchesFalse() { + val fraction = Fraction.newBuilder() + .setIsNegative(false) + .build() + + FractionSubject.assertThat(fraction).hasNegativePropertyThat().isFalse() + } + + @Test + fun testHasNegativeProperty_defaultValue_matchesFalse() { + val fraction = Fraction.newBuilder().build() + + FractionSubject.assertThat(fraction).hasNegativePropertyThat().isFalse() + } + + @Test + fun testHasWholeNumber_withPositiveValue_matchesValue() { + val fraction = Fraction.newBuilder() + .setWholeNumber(5) + .build() + + FractionSubject.assertThat(fraction).hasWholeNumberThat().isEqualTo(5) + } + + @Test + fun testHasWholeNumber_withMaxUint32_matchesValue() { + val fraction = Fraction.newBuilder() + .setWholeNumber(Int.MAX_VALUE) + .build() + + FractionSubject.assertThat(fraction).hasWholeNumberThat().isEqualTo(Int.MAX_VALUE) + } + + @Test + fun testHasWholeNumber_defaultValue_matchesZero() { + val fraction = Fraction.newBuilder().build() + + FractionSubject.assertThat(fraction).hasWholeNumberThat().isEqualTo(0) + } + + @Test + fun testHasNumerator_withPositiveValue_matchesValue() { + val fraction = Fraction.newBuilder() + .setNumerator(3) + .build() + + FractionSubject.assertThat(fraction).hasNumeratorThat().isEqualTo(3) + } + + @Test + fun testHasNumerator_withMaxUint32_matchesValue() { + val fraction = Fraction.newBuilder() + .setNumerator(Int.MAX_VALUE) + .build() + + FractionSubject.assertThat(fraction).hasNumeratorThat().isEqualTo(Int.MAX_VALUE) + } + + @Test + fun testHasNumerator_defaultValue_matchesZero() { + val fraction = Fraction.newBuilder().build() + + FractionSubject.assertThat(fraction).hasNumeratorThat().isEqualTo(0) + } + + @Test + fun testHasDenominator_withPositiveValue_matchesValue() { + val fraction = Fraction.newBuilder() + .setDenominator(4) + .build() + + FractionSubject.assertThat(fraction).hasDenominatorThat().isEqualTo(4) + } + + @Test + fun testHasDenominator_withMaxUint32_matchesValue() { + val fraction = Fraction.newBuilder() + .setDenominator(Int.MAX_VALUE) + .build() + + FractionSubject.assertThat(fraction).hasDenominatorThat().isEqualTo(Int.MAX_VALUE) + } + + @Test + fun testHasDenominator_defaultValue_matchesZero() { + val fraction = Fraction.newBuilder().build() + + FractionSubject.assertThat(fraction).hasDenominatorThat().isEqualTo(0) + } + + @Test + fun testEvaluatesToDouble_withProperFraction_matchesExpectedValue() { + val fraction = Fraction.newBuilder() + .setNumerator(3) + .setDenominator(4) + .build() + + FractionSubject.assertThat(fraction).evaluatesToDoubleThat().isEqualTo(0.75) + } + + @Test + fun testEvaluatesToDouble_withImproperFraction_matchesExpectedValue() { + val fraction = Fraction.newBuilder() + .setNumerator(5) + .setDenominator(2) + .build() + + FractionSubject.assertThat(fraction).evaluatesToDoubleThat().isEqualTo(2.5) + } + + @Test + fun testEvaluatesToDouble_withMixedNumber_matchesExpectedValue() { + val fraction = Fraction.newBuilder() + .setWholeNumber(2) + .setNumerator(3) + .setDenominator(4) + .build() + + FractionSubject.assertThat(fraction).evaluatesToDoubleThat().isEqualTo(2.75) + } + + @Test + fun testEvaluatesToDouble_withNegativeValue_matchesExpectedValue() { + val fraction = Fraction.newBuilder() + .setIsNegative(true) + .setWholeNumber(2) + .setNumerator(1) + .setDenominator(2) + .build() + + FractionSubject.assertThat(fraction).evaluatesToDoubleThat().isEqualTo(-2.5) + } + + @Test + fun testProtoEquality_withIdenticalValues_areEqual() { + val fraction1 = Fraction.newBuilder() + .setIsNegative(true) + .setWholeNumber(3) + .setNumerator(2) + .setDenominator(5) + .build() + + val fraction2 = Fraction.newBuilder() + .setIsNegative(true) + .setWholeNumber(3) + .setNumerator(2) + .setDenominator(5) + .build() + + assertThat(fraction1).isEqualTo(fraction2) + } + + @Test + fun testProtoSerialization_withComplexFraction_maintainsValues() { + val originalFraction = Fraction.newBuilder() + .setIsNegative(true) + .setWholeNumber(3) + .setNumerator(2) + .setDenominator(5) + .build() + + val bytes = originalFraction.toByteArray() + val deserializedFraction = Fraction.parseFrom(bytes) + + FractionSubject.assertThat(deserializedFraction).apply { + hasNegativePropertyThat().isTrue() + hasWholeNumberThat().isEqualTo(3) + hasNumeratorThat().isEqualTo(2) + hasDenominatorThat().isEqualTo(5) + } + } + + @Test + fun testDefaultInstance_hasDefaultValues() { + val defaultFraction = Fraction.getDefaultInstance() + + FractionSubject.assertThat(defaultFraction).apply { + hasNegativePropertyThat().isFalse() + hasWholeNumberThat().isEqualTo(0) + hasNumeratorThat().isEqualTo(0) + hasDenominatorThat().isEqualTo(0) + } + } +} diff --git a/testing/src/test/java/org/oppia/android/testing/math/MathEquationSubjectTest.kt b/testing/src/test/java/org/oppia/android/testing/math/MathEquationSubjectTest.kt new file mode 100644 index 00000000000..7a9a6b8b62f --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/MathEquationSubjectTest.kt @@ -0,0 +1,394 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathEquation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real + +/** Tests for [MathEquationSubject]. */ +@RunWith(JUnit4::class) +class MathEquationSubjectTest { + + @Test + fun testHasLeftHandSide_withValidExpression_matchesExpression() { + val equation = createEquation( + leftSide = createConstantExpression(5), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + + @Test + fun testHasLeftHandSide_withDefaultExpression_hasNoExpressionType() { + val equation = MathEquation.getDefaultInstance() + + MathEquationSubject.assertThat(equation).hasLeftHandSideThat().isEqualTo( + MathExpression.getDefaultInstance() + ) + } + + @Test + fun testHasRightHandSide_withValidExpression_matchesExpression() { + val equation = createEquation( + leftSide = createConstantExpression(0), + rightSide = createConstantExpression(10) + ) + + MathEquationSubject.assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(10) + } + } + } + + @Test + fun testHasRightHandSide_withDefaultExpression_hasNoExpressionType() { + val equation = MathEquation.getDefaultInstance() + + MathEquationSubject.assertThat(equation).hasRightHandSideThat().isEqualTo( + MathExpression.getDefaultInstance() + ) + } + + @Test + fun testConvertsToLatex_simpleEquation_producesCorrectString() { + val equation = createEquation( + leftSide = createConstantExpression(5), + rightSide = createConstantExpression(10) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("5 = 10") + } + + @Test + fun testConvertsToLatex_withDivision_retainsDivisionOperator() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createConstantExpression(10), + createConstantExpression(2) + ), + rightSide = createConstantExpression(5) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("10 \\div 2 = 5") + } + + @Test + fun testConvertsToLatexWithFractions_withDivision_producesFractionNotation() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createConstantExpression(10), + createConstantExpression(2) + ), + rightSide = createConstantExpression(5) + ) + + MathEquationSubject.assertThat(equation) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{10}{2} = 5") + } + + @Test + fun testConvertsToLatex_complexExpression_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(3), + createBinaryOperation( + MathBinaryOperation.Operator.MULTIPLY, + createConstantExpression(4), + createVariableExpression("x") + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("3 + 4 \\times x = 0") + } + + @Test + fun testLeftHandSide_wrongExpression_failsWithAppropriateMessage() { + val equation = createEquation( + leftSide = createConstantExpression(5), + rightSide = createConstantExpression(0) + ) + + val exception = assertThrows(AssertionError::class.java) { + MathEquationSubject.assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + assertThat(exception).hasMessageThat().contains("expected: 6") + } + + @Test + fun testRightHandSide_wrongExpression_failsWithAppropriateMessage() { + val equation = createEquation( + leftSide = createConstantExpression(0), + rightSide = createConstantExpression(10) + ) + + val exception = assertThrows(AssertionError::class.java) { + MathEquationSubject.assertThat(equation).hasRightHandSideThat().hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(11) + } + } + } + assertThat(exception).hasMessageThat().contains("expected: 11") + } + + @Test + fun testConvertsToLatex_withNestedOperations_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(2) + ), + createBinaryOperation( + MathBinaryOperation.Operator.MULTIPLY, + createConstantExpression(3), + createVariableExpression("x") + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("-2 + 3 \\times x = 0") + } + + @Test + fun testConvertsToLatexWithFractions_nestedFractions_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createConstantExpression(1), + createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createConstantExpression(2), + createVariableExpression("x") + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{1}{\\frac{2}{x}} = 0") + } + + @Test + fun testConvertsToLatex_withUnaryOperationInFraction_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.DIVIDE, + createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(1) + ), + createConstantExpression(2) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{-1}{2} = 0") + } + + @Test + fun testConvertsToLatex_withFunctionCallInComplexExpression_producesCorrectString() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(1), + createFunctionCall( + MathFunctionCall.FunctionType.SQUARE_ROOT, + createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(4), + createVariableExpression("x") + ) + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("1 + \\sqrt{4 + x} = 0") + } + + @Test + fun testConvertsToLatex_withInvalidExpression_fails() { + val equation = MathEquation.getDefaultInstance() + + val exception = assertThrows(AssertionError::class.java) { + MathEquationSubject.assertThat(equation) + .convertsToLatexStringThat() + .isEqualTo("5 = 0") + } + assertThat(exception).hasMessageThat().contains( + "expected: 5 = 0\n" + + "but was : =" + ) + } + + @Test + fun testConvertsWithFractionsToLatex_withInvalidExpression_fails() { + val equation = MathEquation.getDefaultInstance() + + val exception = assertThrows(AssertionError::class.java) { + MathEquationSubject.assertThat(equation) + .convertsWithFractionsToLatexStringThat() + .isEqualTo("\\frac{1}{2} = 0") + } + assertThat(exception).hasMessageThat().contains( + "expected: \\frac{1}{2} = 0\n" + + "but was : =" + ) + } + + @Test + fun testHasLeftHandSide_withComplexNestedExpression_matchesExpression() { + val equation = createEquation( + leftSide = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createFunctionCall( + MathFunctionCall.FunctionType.SQUARE_ROOT, + createBinaryOperation( + MathBinaryOperation.Operator.MULTIPLY, + createConstantExpression(4), + createVariableExpression("x") + ) + ), + createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(3) + ) + ), + rightSide = createConstantExpression(0) + ) + + MathEquationSubject.assertThat(equation).hasLeftHandSideThat().hasStructureThatMatches { + addition { + leftOperand { + functionCallTo(MathFunctionCall.FunctionType.SQUARE_ROOT) { + argument { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + variable { + withNameThat().isEqualTo("x") + } + } + } + } + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + } + } + + private fun createFunctionCall( + functionType: MathFunctionCall.FunctionType, + argument: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setFunctionCall( + MathFunctionCall.newBuilder() + .setFunctionType(functionType) + .setArgument(argument) + ) + .build() + } + + private fun createUnaryOperation( + operator: MathUnaryOperation.Operator, + operand: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setUnaryOperation( + MathUnaryOperation.newBuilder() + .setOperator(operator) + .setOperand(operand) + ) + .build() + } + + private fun createEquation( + leftSide: MathExpression, + rightSide: MathExpression + ): MathEquation { + return MathEquation.newBuilder() + .setLeftSide(leftSide) + .setRightSide(rightSide) + .build() + } + + private fun createConstantExpression(value: Int): MathExpression { + return MathExpression.newBuilder() + .setConstant(Real.newBuilder().setInteger(value)) + .build() + } + + private fun createVariableExpression(name: String): MathExpression { + return MathExpression.newBuilder() + .setVariable(name) + .build() + } + + private fun createBinaryOperation( + operator: MathBinaryOperation.Operator, + left: MathExpression, + right: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setOperator(operator) + .setLeftOperand(left) + .setRightOperand(right) + ) + .build() + } +} diff --git a/testing/src/test/java/org/oppia/android/testing/math/MathExpressionSubjectTest.kt b/testing/src/test/java/org/oppia/android/testing/math/MathExpressionSubjectTest.kt new file mode 100644 index 00000000000..d5e29fc9759 --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/MathExpressionSubjectTest.kt @@ -0,0 +1,371 @@ +package org.oppia.android.testing.math + +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.oppia.android.app.model.MathBinaryOperation +import org.oppia.android.app.model.MathExpression +import org.oppia.android.app.model.MathFunctionCall +import org.oppia.android.app.model.MathUnaryOperation +import org.oppia.android.app.model.Real + +/** Tests for [MathExpressionSubject]. */ +@RunWith(JUnit4::class) +class MathExpressionSubjectTest { + + @Test + fun testConstantExpression_withInteger_matchesStructure() { + val expression = createConstantExpression(5) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + + @Test + fun testConstantExpression_withWrongValue_fails() { + val expression = createConstantExpression(5) + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(6) + } + } + } + assertThat(exception).hasMessageThat().contains("expected: 6") + } + + @Test + fun testVariableExpression_matchesStructure() { + val expression = createVariableExpression("x") + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("x") + } + } + } + + @Test + fun testVariableExpression_withWrongName_fails() { + val expression = createVariableExpression("x") + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + variable { + withNameThat().isEqualTo("y") + } + } + } + assertThat(exception).hasMessageThat().contains("expected: y") + } + + @Test + fun testBinaryOperation_addition_matchesStructure() { + val expression = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(3), + createConstantExpression(4) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + + @Test + fun testBinaryOperation_multiplication_withImplicit_matchesStructure() { + val expression = createImplicitMultiplication( + createConstantExpression(2), + createConstantExpression(3) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + multiplication(isImplicit = true) { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(2) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + } + } + } + + @Test + fun testUnaryOperation_negation_matchesStructure() { + val expression = createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(5) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + + @Test + fun testFunctionCall_squareRoot_matchesStructure() { + val expression = createFunctionCall( + MathFunctionCall.FunctionType.SQUARE_ROOT, + createConstantExpression(16) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + functionCallTo(MathFunctionCall.FunctionType.SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(16) + } + } + } + } + } + + @Test + fun testComplexExpression_matchesStructure() { + // Creates expression: 3 + 4 * (-5) + val expression = createBinaryOperation( + MathBinaryOperation.Operator.ADD, + createConstantExpression(3), + createBinaryOperation( + MathBinaryOperation.Operator.MULTIPLY, + createConstantExpression(4), + createUnaryOperation( + MathUnaryOperation.Operator.NEGATE, + createConstantExpression(5) + ) + ) + ) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + multiplication { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + rightOperand { + negation { + operand { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + } + } + } + } + } + } + + @Test + fun testGroupExpression_matchesStructure() { + val expression = createGroupExpression(createConstantExpression(42)) + + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + group { + constant { + withValueThat().isIntegerThat().isEqualTo(42) + } + } + } + } + + @Test + fun testExpression_withUnsetType_fails() { + val expression = MathExpression.getDefaultInstance() + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + constant { + withValueThat().isIntegerThat().isEqualTo(5) + } + } + } + assertThat(exception).hasMessageThat().contains("EXPRESSIONTYPE_NOT_SET") + } + + @Test + fun testBinaryOperation_withUnsetOperator_fails() { + val expression = MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setLeftOperand(createConstantExpression(3)) + .setRightOperand(createConstantExpression(4)) + ) + .build() + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + addition { + leftOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(3) + } + } + rightOperand { + constant { + withValueThat().isIntegerThat().isEqualTo(4) + } + } + } + } + } + assertThat(exception).hasMessageThat().contains("Expected binary operation with operator") + } + + @Test + fun testVariableExpression_withNullName_fails() { + val expression = MathExpression.newBuilder() + .setVariable("") + .build() + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + variable { + withNameThat().isNotEmpty() + } + } + } + assertThat(exception).hasMessageThat().contains("expected not to be empty") + } + + @Test + fun testFunctionCall_withMissingArgument_fails() { + val expression = MathExpression.newBuilder() + .setFunctionCall( + MathFunctionCall.newBuilder() + .setFunctionType(MathFunctionCall.FunctionType.SQUARE_ROOT) + ) + .build() + + val exception = assertThrows(AssertionError::class.java) { + MathExpressionSubject.assertThat(expression).hasStructureThatMatches { + functionCallTo(MathFunctionCall.FunctionType.SQUARE_ROOT) { + argument { + constant { + withValueThat().isIntegerThat().isEqualTo(16) + } + } + } + } + } + assertThat(exception).hasMessageThat().contains("EXPRESSIONTYPE_NOT_SET") + } + + /** Creates a constant [MathExpression] with the specified integer value. */ + private fun createConstantExpression(value: Int): MathExpression { + return MathExpression.newBuilder() + .setConstant(Real.newBuilder().setInteger(value)) + .build() + } + + /** Creates a variable [MathExpression] with the specified variable name. */ + private fun createVariableExpression(name: String): MathExpression { + return MathExpression.newBuilder() + .setVariable(name) + .build() + } + + /** Creates a binary operation [MathExpression] with the specified operator and operands. */ + private fun createBinaryOperation( + operator: MathBinaryOperation.Operator, + left: MathExpression, + right: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setOperator(operator) + .setLeftOperand(left) + .setRightOperand(right) + ) + .build() + } + + /** Creates an implicit multiplication [MathExpression] between two operands. */ + private fun createImplicitMultiplication( + left: MathExpression, + right: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setBinaryOperation( + MathBinaryOperation.newBuilder() + .setOperator(MathBinaryOperation.Operator.MULTIPLY) + .setIsImplicit(true) + .setLeftOperand(left) + .setRightOperand(right) + ) + .build() + } + + /** Creates a unary operation [MathExpression] with the specified operator and operand. */ + private fun createUnaryOperation( + operator: MathUnaryOperation.Operator, + operand: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setUnaryOperation( + MathUnaryOperation.newBuilder() + .setOperator(operator) + .setOperand(operand) + ) + .build() + } + + /** Creates a function call [MathExpression] with the specified function type and argument. */ + private fun createFunctionCall( + functionType: MathFunctionCall.FunctionType, + argument: MathExpression + ): MathExpression { + return MathExpression.newBuilder() + .setFunctionCall( + MathFunctionCall.newBuilder() + .setFunctionType(functionType) + .setArgument(argument) + ) + .build() + } + + /** Creates a group [MathExpression] that wraps the specified inner expression. */ + private fun createGroupExpression(inner: MathExpression): MathExpression { + return MathExpression.newBuilder() + .setGroup(inner) + .build() + } +} diff --git a/testing/src/test/java/org/oppia/android/testing/math/RealSubjectTest.kt b/testing/src/test/java/org/oppia/android/testing/math/RealSubjectTest.kt new file mode 100644 index 00000000000..343321832f6 --- /dev/null +++ b/testing/src/test/java/org/oppia/android/testing/math/RealSubjectTest.kt @@ -0,0 +1,204 @@ +package org.oppia.android.testing.math + +import android.annotation.SuppressLint +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.oppia.android.app.model.Fraction +import org.oppia.android.app.model.Real + +/** Tests for [RealSubject]. */ +@SuppressLint("CheckResult") +@RunWith(JUnit4::class) +class RealSubjectTest { + + @Test + fun testRational_withRationalValue_canAccessRationalSubject() { + val real = Real.newBuilder().setRational( + Fraction.newBuilder().setNumerator(1).setDenominator(2) + ).build() + + val subject = RealSubject.assertThat(real).isRationalThat() + + subject.hasNumeratorThat().isEqualTo(1) + subject.hasDenominatorThat().isEqualTo(2) + } + + @Test + fun testRational_withIrrationalValue_fails() { + val real = Real.newBuilder().setIrrational(3.14).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isRationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be RATIONAL, not: IRRATIONAL" + ) + } + + @Test + fun testRational_withIntegerValue_fails() { + val real = Real.newBuilder().setInteger(42).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isRationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be RATIONAL, not: INTEGER" + ) + } + + @Test + fun testIrrational_withIrrationalValue_canAccessDoubleSubject() { + val real = Real.newBuilder().setIrrational(3.14159).build() + + val subject = RealSubject.assertThat(real).isIrrationalThat() + + subject.isWithin(0.00001).of(3.14159) + } + + @Test + fun testIrrational_withRationalValue_fails() { + val real = Real.newBuilder().setRational( + Fraction.newBuilder().setNumerator(1).setDenominator(2) + ).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIrrationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be IRRATIONAL, not: RATIONAL" + ) + } + + @Test + fun testIrrational_withIntegerValue_fails() { + val real = Real.newBuilder().setInteger(42).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIrrationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be IRRATIONAL, not: INTEGER" + ) + } + + @Test + fun testInteger_withIntegerValue_canAccessIntegerSubject() { + val real = Real.newBuilder().setInteger(42).build() + + val subject = RealSubject.assertThat(real).isIntegerThat() + + subject.isEqualTo(42) + } + + @Test + fun testInteger_withRationalValue_fails() { + val real = Real.newBuilder().setRational( + Fraction.newBuilder().setNumerator(1).setDenominator(2) + ).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIntegerThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be INTEGER, not: RATIONAL" + ) + } + + @Test + fun testInteger_withIrrationalValue_fails() { + val real = Real.newBuilder().setIrrational(3.14).build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIntegerThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be INTEGER, not: IRRATIONAL" + ) + } + + @Test + fun testNull_fails() { + val exception = assertThrows(IllegalStateException::class.java) { + RealSubject.assertThat(null).isRationalThat() + } + + assertThat(exception).hasMessageThat().contains("Expected real to be non-null") + } + + @Test + fun testUnsetType_asRational_fails() { + val real = Real.newBuilder().build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isRationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be RATIONAL, not: REALTYPE_NOT_SET" + ) + } + + @Test + fun testUnsetType_asIrrational_fails() { + val real = Real.newBuilder().build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIrrationalThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be IRRATIONAL, not: REALTYPE_NOT_SET" + ) + } + + @Test + fun testUnsetType_asInteger_fails() { + val real = Real.newBuilder().build() + + val exception = assertThrows(AssertionError::class.java) { + RealSubject.assertThat(real).isIntegerThat() + } + + assertThat(exception).hasMessageThat().contains( + "Expected real type to be INTEGER, not: REALTYPE_NOT_SET" + ) + } + + @Test + fun testInheritedProtoMethods_work() { + val real = Real.newBuilder().setInteger(42).build() + + RealSubject.assertThat(real).isNotNull() + RealSubject.assertThat(real).isNotEqualTo(Real.getDefaultInstance()) + } + + private fun assertThrows( + expectedType: Class, + runnable: () -> Unit + ): T { + try { + runnable() + } catch (t: Throwable) { + if (expectedType.isInstance(t)) { + @Suppress("UNCHECKED_CAST") + return t as T + } + throw AssertionError( + "Expected ${expectedType.simpleName} but got ${t.javaClass.simpleName}", + t + ) + } + throw AssertionError( + "Expected ${expectedType.simpleName} to be thrown but nothing was thrown" + ) + } +} diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/ConceptCardTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/ConceptCardTagHandler.kt index e9dbd1dac86..83625743d87 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/ConceptCardTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/ConceptCardTagHandler.kt @@ -10,12 +10,14 @@ import org.xml.sax.Attributes /** The custom tag corresponding to [ConceptCardTagHandler]. */ const val CUSTOM_CONCEPT_CARD_TAG = "oppia-noninteractive-skillreview" +const val CUSTOM_CONCEPT_CARD_SKILL_ID = "skill_id-with-value" +const val CUSTOM_CONCEPT_CARD_TEXT_VALUE = "text-with-value" // https://mohammedlakkadshaw.com/blog/handling-custom-tags-in-android-using-html-taghandler.html/ class ConceptCardTagHandler( private val listener: ConceptCardLinkClickListener, private val consoleLogger: ConsoleLogger -) : CustomHtmlContentHandler.CustomTagHandler { +) : CustomHtmlContentHandler.CustomTagHandler, CustomHtmlContentHandler.ContentDescriptionProvider { override fun handleTag( attributes: Attributes, openIndex: Int, @@ -24,8 +26,8 @@ class ConceptCardTagHandler( imageRetriever: CustomHtmlContentHandler.ImageRetriever? ) { // Replace the custom tag with a clickable piece of text based on the tag's customizations. - val skillId = attributes.getJsonStringValue("skill_id-with-value") - val text = attributes.getJsonStringValue("text-with-value") + val skillId = attributes.getJsonStringValue(CUSTOM_CONCEPT_CARD_SKILL_ID) + val text = attributes.getJsonStringValue(CUSTOM_CONCEPT_CARD_TEXT_VALUE) if (skillId != null && text != null) { val spannableBuilder = SpannableStringBuilder(text) spannableBuilder.setSpan( @@ -48,4 +50,12 @@ class ConceptCardTagHandler( */ fun onConceptCardLinkClicked(view: View, skillId: String) } + + override fun getContentDescription(attributes: Attributes): String? { + val skillId = attributes.getJsonStringValue(CUSTOM_CONCEPT_CARD_SKILL_ID) + val text = attributes.getJsonStringValue(CUSTOM_CONCEPT_CARD_TEXT_VALUE) + return if (!skillId.isNullOrBlank() && !text.isNullOrBlank()) { + "$text concept card $skillId" + } else "" + } } diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt index f07310b7a9e..bca56be075e 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/CustomHtmlContentHandler.kt @@ -25,9 +25,17 @@ class CustomHtmlContentHandler private constructor( private var originalContentHandler: ContentHandler? = null private var currentTrackedTag: TrackedTag? = null private val currentTrackedCustomTags = ArrayDeque() + private val contentDescriptionBuilder = StringBuilder() + private val tagContentDescriptions = mutableMapOf() + private var isInListItem = false + private var pendingNewline = false + private val blockTags = setOf("p", "ol", "ul", "li", "oppia-ul", "oppia-ol", "oppia-li", "div") override fun endElement(uri: String?, localName: String?, qName: String?) { originalContentHandler?.endElement(uri, localName, qName) + if (localName in blockTags) { + isInListItem = false + } currentTrackedTag = null } @@ -45,6 +53,11 @@ class CustomHtmlContentHandler private constructor( override fun characters(ch: CharArray?, start: Int, length: Int) { originalContentHandler?.characters(ch, start, length) + if (pendingNewline) { + contentDescriptionBuilder.append('\n') + pendingNewline = false + } + ch?.let { contentDescriptionBuilder.append(String(it, start, length)) } } override fun endDocument() { @@ -56,6 +69,10 @@ class CustomHtmlContentHandler private constructor( // Defer custom tag management to the tag handler so that Android's element parsing takes // precedence. currentTrackedTag = TrackedTag(checkNotNull(localName), checkNotNull(atts)) + if (localName in blockTags) { + pendingNewline = true + isInListItem = true + } originalContentHandler?.startElement(uri, localName, qName, atts) } @@ -110,6 +127,14 @@ class CustomHtmlContentHandler private constructor( "Expected tracked tag $currentTrackedTag to match custom tag: $tag" } val (_, attributes, openTagIndex) = currentTrackedCustomTag + + val handler = customTagHandlers.getValue(tag) + if (handler is ContentDescriptionProvider) { + val contentDesc = handler.getContentDescription(attributes) + if (contentDesc != null) { + tagContentDescriptions[openTagIndex] = contentDesc + } + } customTagHandlers.getValue(tag).handleClosingTag(output, indentation = 0, tag) customTagHandlers.getValue(tag) .handleTag(attributes, openTagIndex, output.length, output, imageRetriever) @@ -123,7 +148,26 @@ class CustomHtmlContentHandler private constructor( val attributes: Attributes, val openTagIndex: Int ) - + /** + * Returns the complete content description for the processed HTML, including descriptions + * from all custom tags. + */ + private fun getContentDescription(): String { + val rawDesc = buildString { + var lastIndex = 0 + tagContentDescriptions.entries.sortedBy { it.key }.forEach { (index, description) -> + if (index > lastIndex) { + append(contentDescriptionBuilder.substring(lastIndex, index)) + } + append(description) + lastIndex = index + } + if (lastIndex < contentDescriptionBuilder.length) { + append(contentDescriptionBuilder.substring(lastIndex)) + } + } + return rawDesc.replace(Regex("\n+"), "\n").trim() + } /** Handler interface for a custom tag and its attributes. */ interface CustomTagHandler { /** @@ -166,6 +210,15 @@ class CustomHtmlContentHandler private constructor( fun handleClosingTag(output: Editable, indentation: Int, tag: String) {} } + /** Handler Interface for tag handlers that provide content descriptions. */ + interface ContentDescriptionProvider { + /** + * Returns a content description string for this tag based on its attributes, + * or null if no description is available. + */ + fun getContentDescription(attributes: Attributes): String? + } + /** * Retriever of images for custom tag handlers. The built-in Android analog for this class is * Html's ImageGetter. @@ -196,6 +249,24 @@ class CustomHtmlContentHandler private constructor( } companion object { + /** + * Returns the content description for the HTML content, processing all custom tags that implement + * [ContentDescriptionProvider]. + */ + fun getContentDescription( + html: String, + imageRetriever: T?, + customTagHandlers: Map + ): String where T : Html.ImageGetter, T : ImageRetriever { + val handler = CustomHtmlContentHandler(customTagHandlers, imageRetriever) + HtmlCompat.fromHtml( + "$html", + HtmlCompat.FROM_HTML_MODE_LEGACY, + imageRetriever, + handler + ) + return handler.getContentDescription() + } /** * Returns a new [Spannable] with HTML parsed from [html] using the specified [imageRetriever] * for handling image retrieval, and map of tags to [CustomTagHandler]s for handling custom diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/ImageTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/ImageTagHandler.kt index 499b89fa54d..b86a5995b17 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/ImageTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/ImageTagHandler.kt @@ -22,7 +22,7 @@ private const val CUSTOM_IMG_CAPTION_ATTRIBUTE = "caption-with-value" */ class ImageTagHandler( private val consoleLogger: ConsoleLogger -) : CustomHtmlContentHandler.CustomTagHandler { +) : CustomHtmlContentHandler.CustomTagHandler, CustomHtmlContentHandler.ContentDescriptionProvider { override fun handleTag( attributes: Attributes, openIndex: Int, @@ -101,4 +101,11 @@ class ImageTagHandler( "Failed to parse $CUSTOM_IMG_CAPTION_ATTRIBUTE" ) } + + override fun getContentDescription(attributes: Attributes): String { + val altValue = attributes.getJsonStringValue(CUSTOM_IMG_ALT_TEXT_ATTRIBUTE) + return if (!altValue.isNullOrBlank()) { + "Image illustrating $altValue" + } else "" + } } diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/LiTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/LiTagHandler.kt index d0562b474a1..849f2e226be 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/LiTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/LiTagHandler.kt @@ -5,6 +5,7 @@ import android.text.Editable import android.text.Spannable import android.text.Spanned import org.oppia.android.util.locale.OppiaLocale +import org.xml.sax.Attributes import java.util.Stack /** The custom
  • tag corresponding to [LiTagHandler]. */ @@ -23,7 +24,7 @@ const val CUSTOM_LIST_OL_TAG = "oppia-ol" class LiTagHandler( private val context: Context, private val displayLocale: OppiaLocale.DisplayLocale -) : CustomHtmlContentHandler.CustomTagHandler { +) : CustomHtmlContentHandler.CustomTagHandler, CustomHtmlContentHandler.ContentDescriptionProvider { private val pendingLists = Stack>() private val latestPendingList: ListTag<*, *>? get() = pendingLists.lastOrNull() @@ -291,4 +292,8 @@ class LiTagHandler( private fun > Spannable.addMark(mark: T) = setSpan(mark, length, length, Spanned.SPAN_MARK_MARK) } + + override fun getContentDescription(attributes: Attributes): String { + return "" + } } diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt index 42ce1a0676e..eabfa3836d7 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/MathTagHandler.kt @@ -29,7 +29,7 @@ class MathTagHandler( private val lineHeight: Float, private val cacheLatexRendering: Boolean, private val application: Application -) : CustomHtmlContentHandler.CustomTagHandler { +) : CustomHtmlContentHandler.CustomTagHandler, CustomHtmlContentHandler.ContentDescriptionProvider { override fun handleTag( attributes: Attributes, openIndex: Int, @@ -138,4 +138,9 @@ class MathTagHandler( } } } + + override fun getContentDescription(attributes: Attributes): String { + val mathVal = attributes.getJsonObjectValue(CUSTOM_MATH_MATH_CONTENT_ATTRIBUTE) + return mathVal?.let { "Math content $it" } ?: "" + } } diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/PolicyPageTagHandler.kt b/utility/src/main/java/org/oppia/android/util/parser/html/PolicyPageTagHandler.kt index ec9f494e73e..462a468b7b1 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/PolicyPageTagHandler.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/PolicyPageTagHandler.kt @@ -22,7 +22,7 @@ private const val TERMS_OF_SERVICE = "Terms of Service" class PolicyPageTagHandler( private val listener: PolicyPageLinkClickListener, private val consoleLogger: ConsoleLogger -) : CustomHtmlContentHandler.CustomTagHandler { +) : CustomHtmlContentHandler.CustomTagHandler, CustomHtmlContentHandler.ContentDescriptionProvider { override fun handleTag( attributes: Attributes, openIndex: Int, @@ -83,4 +83,11 @@ class PolicyPageTagHandler( */ fun onPolicyPageLinkClicked(policyType: PolicyType) } + + override fun getContentDescription(attributes: Attributes): String { + return when (attributes.getJsonStringValue("link")) { + TERMS_OF_SERVICE_PAGE, PRIVACY_POLICY_PAGE -> "Link to " + else -> "" + } + } } diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/ConceptCardTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/ConceptCardTagHandlerTest.kt index 4d70a5d1b4f..53dfd421cc6 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/ConceptCardTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/ConceptCardTagHandlerTest.kt @@ -117,6 +117,17 @@ class ConceptCardTagHandlerTest { assertThat(clickableSpans).hasLength(1) } + @Test + fun testGetContentDescription_withConceptCardTag() { + val contentDescription = + CustomHtmlContentHandler.getContentDescription( + html = CONCEPT_CARD_LINK_MARKUP_1, + imageRetriever = mockImageRetriever, + customTagHandlers = tagHandlersWithConceptCardSupport + ) + assertThat(contentDescription).isEqualTo("refresher lesson concept card skill_id_1") + } + @Test fun testParseHtml_withConceptCardMarkup_addsLinkText() { val parsedHtml = diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt index 2042ef72ec8..8b11dcba7f0 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/CustomHtmlContentHandlerTest.kt @@ -298,6 +298,110 @@ class CustomHtmlContentHandlerTest { assertThat(jsonObject?.getString("key")).isEqualTo("value with \\frac{1}{2}") } + @Test + fun testGetContentDescription_withNestedTags_handlesNestingCorrectly() { + val outerHandler = FakeContentDescriptionTagHandler("Outer Tag ") + val innerHandler = FakeContentDescriptionTagHandler("Inner Tag ") + + val contentDescription = CustomHtmlContentHandler.getContentDescription( + html = "before nested after", + imageRetriever = mockImageRetriever, + customTagHandlers = mapOf( + "outer-tag" to outerHandler, + "inner-tag" to innerHandler + ) + ) + + assertThat(contentDescription).isEqualTo("Outer Tag before Inner Tag nested after") + } + + @Test + fun testGetContentDescription_withMultipleTags_preservesOrder() { + val firstHandler = FakeContentDescriptionTagHandler("First ") + val secondHandler = FakeContentDescriptionTagHandler("Second ") + val contentDescription = CustomHtmlContentHandler.getContentDescription( + html = "Start one middle two end", + imageRetriever = mockImageRetriever, + customTagHandlers = mapOf( + "first-tag" to firstHandler, + "second-tag" to secondHandler + ) + ) + + assertThat(contentDescription).isEqualTo("Start First one middle Second two end") + } + + @Test + fun testGetContentDescription_whitespaceHandling_normalizedCorrectly() { + val contentDescription = CustomHtmlContentHandler.getContentDescription( + html = + """ +

    First paragraph

    + +

    Second paragraph

    + +

    Third paragraph

    + """.trimIndent(), + imageRetriever = mockImageRetriever, + customTagHandlers = mapOf() + ) + + assertThat(contentDescription).isEqualTo( + "First paragraph\n" + + "Second paragraph\n" + + "Third paragraph" + ) + } + + @Test + fun testGetContentDescription_blockElements_preserveStructure() { + val contentDescription = CustomHtmlContentHandler.getContentDescription( + html = + """ +
    Header text
    +
    Article content
    +
    Section text
    + +
    Footer text
    + """.trimIndent(), + imageRetriever = mockImageRetriever, + customTagHandlers = mapOf() + ) + + assertThat(contentDescription).isEqualTo( + "Header text\n" + + "Article content\n" + + "Section text\n" + + "Aside content\n" + + "Footer text" + ) + } + + @Test + fun testGetContentDescription_mixedContentTypes_handlesCorrectly() { + val contentDescription = CustomHtmlContentHandler.getContentDescription( + html = + """ +

    Regular paragraph

    + Custom content +
    • List item 1
    • List item 2
    + More custom +

    Final paragraph

    + """.trimIndent(), + imageRetriever = mockImageRetriever, + customTagHandlers = mapOf() + ) + + assertThat(contentDescription).isEqualTo( + "Regular paragraph\n" + + "Custom content\n" + + "List item 1\n" + + "List item 2\n" + + "More custom\n" + + "Final paragraph" + ) + } + private fun Spannable.getSpansFromWholeString(spanClass: KClass): Array = getSpans(/* start= */ 0, /* end= */ length, spanClass.javaObjectType) @@ -316,7 +420,22 @@ class CustomHtmlContentHandlerTest { return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory) } - private class FakeTagHandler : CustomHtmlContentHandler.CustomTagHandler { + private class FakeContentDescriptionTagHandler( + private val contentDesc: String + ) : CustomTagHandler, CustomHtmlContentHandler.ContentDescriptionProvider { + override fun handleTag( + attributes: Attributes, + openIndex: Int, + closeIndex: Int, + output: Editable, + imageRetriever: CustomHtmlContentHandler.ImageRetriever? + ) {} + + override fun getContentDescription(attributes: Attributes): String { + return contentDesc + } + } + private class FakeTagHandler : CustomTagHandler { var handleTagCalled = false var handleTagCallIndex = -1 var handleOpeningTagCalled = false @@ -358,7 +477,7 @@ class CustomHtmlContentHandlerTest { private class ReplacingTagHandler( private val attributeTextToReplaceWith: String - ) : CustomHtmlContentHandler.CustomTagHandler { + ) : CustomTagHandler { override fun handleTag( attributes: Attributes, openIndex: Int, diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/ImageTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/ImageTagHandlerTest.kt index e1bd9e22a2e..894de4bc062 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/ImageTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/ImageTagHandlerTest.kt @@ -325,6 +325,17 @@ class ImageTagHandlerTest { .inOrder() } + @Test + fun testGetContentDescription_withImageTag() { + val contentDescription = + CustomHtmlContentHandler.getContentDescription( + html = IMAGE_TAG_MARKUP_1, + imageRetriever = mockImageRetriever, + customTagHandlers = tagHandlersWithImageTagSupport + ) + assertThat(contentDescription).isEqualTo("Image illustrating alt text 1") + } + private fun Spannable.getSpansFromWholeString(spanClass: KClass): Array = getSpans(/* start= */ 0, /* end= */ length, spanClass.javaObjectType) diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt index 1341bc68776..487cda78b52 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/LiTagHandlerTest.kt @@ -137,6 +137,57 @@ class LiTagHandlerTest { .hasLength(4) } + @Test + fun testGetContentDescription_handlesNestedOrderedList() { + val displayLocale = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) + val htmlString = "

    You should know the following before going on:

    " + + "The counting numbers (1, 2, 3, 4, 5 ….)" + + "How to tell whether one counting number is bigger or " + + "smaller than another Item 1 Item 2" + + "" + val liTaghandler = LiTagHandler(context, displayLocale) + val contentDescription = + CustomHtmlContentHandler.getContentDescription( + html = htmlString, + imageRetriever = mockImageRetriever, + customTagHandlers = mapOf( + CUSTOM_LIST_LI_TAG to liTaghandler, + CUSTOM_LIST_OL_TAG to liTaghandler + ) + ) + assertThat(contentDescription).isEqualTo( + "You should know the following before going on:\n" + + "The counting numbers (1, 2, 3, 4, 5 ….)\n" + + "How to tell whether one counting number is bigger or smaller than another \n" + + "Item 1 \n" + + "Item 2" + ) + } + + @Test + fun testGetContentDescription_handlesSimpleUnorderedList() { + val displayLocale = createDisplayLocaleImpl(US_ENGLISH_CONTEXT) + val htmlString = "

    You should know the following before going on:
    " + + "The counting numbers (1, 2, 3, 4, 5 ….)" + + "How to tell whether one counting number is bigger or " + + "smaller than another

    " + val liTaghandler = LiTagHandler(context, displayLocale) + val contentDescription = + CustomHtmlContentHandler.getContentDescription( + html = htmlString, + imageRetriever = mockImageRetriever, + customTagHandlers = mapOf( + CUSTOM_LIST_LI_TAG to liTaghandler, + CUSTOM_LIST_OL_TAG to liTaghandler + ) + ) + assertThat(contentDescription).isEqualTo( + "You should know the following before going on:\n" + + "The counting numbers (1, 2, 3, 4, 5 ….)\n" + + "How to tell whether one counting number is bigger or smaller than another" + ) + } + private fun createDisplayLocaleImpl(context: OppiaLocaleContext): DisplayLocaleImpl { val formattingLocale = androidLocaleFactory.createOneOffAndroidLocale(context) return DisplayLocaleImpl(context, formattingLocale, machineLocale, formatterFactory) diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt index 535d5c90e58..d2c15cf1c27 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/MathTagHandlerTest.kt @@ -301,6 +301,21 @@ class MathTagHandlerTest { assertThat(imageSpans).hasLength(2) } + @Test + fun testGetContentDescription_withMathTag() { + val contentDescription = + CustomHtmlContentHandler.getContentDescription( + html = MATH_MARKUP_1, + imageRetriever = mockImageRetriever, + customTagHandlers = tagHandlersWithCachedMathSupport + ) + + assertThat(contentDescription).isEqualTo( + "Math content" + + " {\"raw_latex\":\"\\\\frac{2}{5}\",\"svg_filename\":\"math_image1.svg\"}" + ) + } + @Test fun testParseHtml_withMathMarkup_loadsInlineImageForFilename() { CustomHtmlContentHandler.fromHtml( diff --git a/utility/src/test/java/org/oppia/android/util/parser/html/PolicyPageTagHandlerTest.kt b/utility/src/test/java/org/oppia/android/util/parser/html/PolicyPageTagHandlerTest.kt index f258cfe1ef0..2e6976aab3e 100644 --- a/utility/src/test/java/org/oppia/android/util/parser/html/PolicyPageTagHandlerTest.kt +++ b/utility/src/test/java/org/oppia/android/util/parser/html/PolicyPageTagHandlerTest.kt @@ -121,6 +121,21 @@ class PolicyPageTagHandlerTest { assertThat(policyTypeCaptor.value).isEqualTo(PolicyType.PRIVACY_POLICY) } + @Test + fun testGetContentDescription_withPolicyPageTag() { + val contentDescription = + CustomHtmlContentHandler.getContentDescription( + html = POLICY_PAGE_LINK_MARKUP_1, + imageRetriever = null, + customTagHandlers = tagHandlersWithPolicyPageSupport + ) + + assertThat(contentDescription).isEqualTo( + "By using %s, you agree to our " + + " Link to Terms of Service and Link to Privacy Policy." + ) + } + @Test fun testParseHtml_withPolicyPageMarkup_clickSpan_callsClickListenerForTermsOfService() { val parsedHtml =