From 965861fd3fca3bc6a52c2e15cce73d540bd5eb1d Mon Sep 17 00:00:00 2001 From: Tobiloba Oyelekan Date: Fri, 10 Jan 2025 12:57:45 +0100 Subject: [PATCH 1/6] refactor completedstorylist to use profileId (#5620) ## Explanation ### Fixes part of #4865 This PR aim to refactor `completedstorylist` package to use ProfileId Changes include CompletedStoryListActivity, Fragment and Presenters ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). --- .../CompletedStoryItemViewModel.kt | 4 ++-- .../CompletedStoryListActivity.kt | 4 ++-- .../CompletedStoryListActivityPresenter.kt | 5 +++-- .../CompletedStoryListFragment.kt | 6 ++---- .../CompletedStoryListFragmentPresenter.kt | 5 +++-- .../CompletedStoryListViewModel.kt | 14 ++++++-------- 6 files changed, 18 insertions(+), 20 deletions(-) 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, From 11b1d89259957f32e73f41bc230e757187d2af3b Mon Sep 17 00:00:00 2001 From: YASH SHARMA <158801815+whyash8@users.noreply.github.com> Date: Tue, 14 Jan 2025 02:34:08 +0530 Subject: [PATCH 2/6] Fix #5625: Migrate away from scaledDensity (#5644) This PR updates the `FontScaleConfigurationUtil` class to remove the usage of the deprecated `scaledDensity` property and replace it with `TypedValue.applyDimension` for scaling font sizes. This change ensures that the code adheres to modern Android API standards . Fixes #5625: This issue was related to the usage of the deprecated `scaledDensity` property ### Changes: **Removed deprecated `scaledDensity`:** - Updated the `adjustFontScale` method to calculate the scaled density using `TypedValue.applyDimension`. - Applied the new scaled density without using the deprecated property. ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [ x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x ] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [ x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x ] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [ x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). --------- Co-authored-by: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> --- .../android/app/utility/FontScaleConfigurationUtil.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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 From 4f5aec630c8b4826cd94fe63bc2ab99f0cb35ad7 Mon Sep 17 00:00:00 2001 From: Manas <119405883+manas-yu@users.noreply.github.com> Date: Tue, 14 Jan 2025 14:43:00 +0530 Subject: [PATCH 3/6] Fix #4848: Ensure Complete Content Descriptions for Custom HTML Tags in CustomHtmlContentHandler (#5614) ## Explanation Fix #4848: This PR resolves the issue with incomplete content descriptions being generated for custom HTML tags. The existing implementation stripped out spans and custom tag content, leading to incomplete descriptions. The updated implementation introduces a custom generation pipeline for content descriptions in `CustomHtmlContentHandler`. It ensures that all custom tag content is correctly processed and appended, resulting in complete and accurate descriptions. ### Key Changes: - Added a `StringBuilder` to accumulate content descriptions and a `MutableMap` to handle custom tag descriptions. - Implemented `getContentDescription()` to combine default text with custom tag content descriptions, ensuring seamless integration. - Introduced a `ContentDescriptionProvider` interface for handling custom tags. - Updated relevant tag handlers (`ImageTagHandler`, `ConceptCardTagHandler`, `LiTagHandler`, `PolicyPageTagHandler` and `MathTagHandler`) to implement the new interface. - Verified the implementation with appropriate test cases. ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). --------- Co-authored-by: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> --- .../util/parser/html/ConceptCardTagHandler.kt | 16 ++- .../parser/html/CustomHtmlContentHandler.kt | 73 ++++++++++- .../util/parser/html/ImageTagHandler.kt | 9 +- .../android/util/parser/html/LiTagHandler.kt | 7 +- .../util/parser/html/MathTagHandler.kt | 7 +- .../util/parser/html/PolicyPageTagHandler.kt | 9 +- .../parser/html/ConceptCardTagHandlerTest.kt | 11 ++ .../html/CustomHtmlContentHandlerTest.kt | 123 +++++++++++++++++- .../util/parser/html/ImageTagHandlerTest.kt | 11 ++ .../util/parser/html/LiTagHandlerTest.kt | 51 ++++++++ .../util/parser/html/MathTagHandlerTest.kt | 15 +++ .../parser/html/PolicyPageTagHandlerTest.kt | 15 +++ 12 files changed, 337 insertions(+), 10 deletions(-) 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 = From 164956c95a0a2b02143dad42e3b75b5a7f6c6ba9 Mon Sep 17 00:00:00 2001 From: Manas <119405883+manas-yu@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:34:39 +0530 Subject: [PATCH 4/6] Fix #3394: Validating Kdoc ending sequence (#5622) ## Explanation Fix #3394 Updated the `file_content_checks` to validate against `\\*(\\s*\\*|\\*)/` for KDoc and block comment endings. This change ensures the script correctly detects improperly formatted endings, such as those with multiple asterisks or unnecessary whitespace before the closing /. Additionally, updated the existing incorrect KDoc endings flagged by the RegexPatternValidationCheck.kt script to conform to the correct format. ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). --- .../app/player/audio/LanguageInterface.kt | 2 +- .../android/app/shim/IntentFactoryShim.kt | 4 +-- .../app/story/StoryFragmentScroller.kt | 2 +- .../domain/hintsandsolution/HintHandler.kt | 2 +- .../exceptions/ExceptionsController.kt | 2 +- .../file_content_validation_checks.textproto | 4 +-- .../regex/RegexPatternValidationCheckTest.kt | 34 ++++++++++++++++++- 7 files changed, 41 insertions(+), 9 deletions(-) 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/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 = From fc929329988eee9642fdcf20fc4d8073022f518a Mon Sep 17 00:00:00 2001 From: Manas <119405883+manas-yu@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:58:14 +0530 Subject: [PATCH 5/6] Fix #4097: Adding tests for math utils (#5627) ## Explanation Fix #4097 This PR adds test files for the `FractionSubject`, `RealSubject`, `MathExpressionSubject`, and `MathEquationSubject` classes and includes a `BUILD.bazel` file to enable building and running these tests. ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). --- .../oppia/android/testing/math/BUILD.bazel | 69 +++ .../testing/math/FractionSubjectTest.kt | 206 +++++++++ .../testing/math/MathEquationSubjectTest.kt | 394 ++++++++++++++++++ .../testing/math/MathExpressionSubjectTest.kt | 371 +++++++++++++++++ .../android/testing/math/RealSubjectTest.kt | 204 +++++++++ 5 files changed, 1244 insertions(+) create mode 100644 testing/src/test/java/org/oppia/android/testing/math/BUILD.bazel create mode 100644 testing/src/test/java/org/oppia/android/testing/math/FractionSubjectTest.kt create mode 100644 testing/src/test/java/org/oppia/android/testing/math/MathEquationSubjectTest.kt create mode 100644 testing/src/test/java/org/oppia/android/testing/math/MathExpressionSubjectTest.kt create mode 100644 testing/src/test/java/org/oppia/android/testing/math/RealSubjectTest.kt 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" + ) + } +} From f3c6dffab44079d57a8815c3ddddc3c357f556ab Mon Sep 17 00:00:00 2001 From: Subhajit Mallick <153619690+subhajitxyz@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:41:53 +0530 Subject: [PATCH 6/6] Fix part of #5485: Create means for verifying Fragment Arguments (#5606) ## Explanation Fix part of #5485 Added tests for 11 fragment arguments and 'saveInstanceState'. I have not added tests for the following: 1. onSaveInstanceState of StateFragment. 2. OptionsFragment. Problems: 1. When trying to recreate StateFragment, an error occurs: "State has not been initialized." 2. In the OptionsFragmentTest class, when I try to run the test on Robolectric, it shows "No tests were found. ## Essential Checklist - [x] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [ ] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). ## For UI-specific PRs only If your PR includes UI-related changes, then: - Add screenshots for portrait/landscape for both a tablet & phone of the before & after UI changes - For the screenshots above, include both English and pseudo-localized (RTL) screenshots (see [RTL guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines)) - Add a video showing the full UX flow with a screen reader enabled (see [accessibility guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide)) - For PRs introducing new UI elements or color changes, both light and dark mode screenshots must be included - Add a screenshot demonstrating that you ran affected Espresso tests locally & that they're passing --------- Co-authored-by: Sneha Datta Co-authored-by: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Co-authored-by: Mr. 17 Co-authored-by: Ben Henning Co-authored-by: RD Rama Devi <122200035+Rd4dev@users.noreply.github.com> Co-authored-by: Tobiloba Oyelekan --- .../thirdparty/LicenseTextViewerFragment.kt | 3 +- .../android/app/policies/PoliciesFragment.kt | 3 +- .../AdministratorControlsFragmentTest.kt | 33 ++++++++ .../app/home/RecentlyPlayedFragmentTest.kt | 26 +++++++ .../app/options/OptionsFragmentTest.kt | 37 +++++++++ .../app/player/state/StateFragmentTest.kt | 34 ++++++++ .../app/policies/PoliciesFragmentTest.kt | 42 ++++++++++ .../ProfileProgressFragmentTest.kt | 22 ++++++ .../LicenseTextViewerFragmentTest.kt | 34 ++++++++ .../app/topic/info/TopicInfoFragmentTest.kt | 61 +++++++++++++++ .../topic/lessons/TopicLessonsFragmentTest.kt | 78 +++++++++++++++++++ .../practice/TopicPracticeFragmentTest.kt | 74 ++++++++++++++++++ .../revision/TopicRevisionFragmentTest.kt | 37 +++++++++ .../WalkthroughFinalFragmentTest.kt | 32 ++++++++ .../testing/options/OptionsFragmentTest.kt | 39 ++++++++++ 15 files changed, 552 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseTextViewerFragment.kt b/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseTextViewerFragment.kt index 64d3585b5a5..3e8a5dc9dce 100644 --- a/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseTextViewerFragment.kt +++ b/app/src/main/java/org/oppia/android/app/help/thirdparty/LicenseTextViewerFragment.kt @@ -19,8 +19,7 @@ class LicenseTextViewerFragment : InjectableFragment() { companion object { /** Argument key for LicenseTextViewerFragment. */ - private const val LICENSE_TEXT_VIEWER_FRAGMENT_ARGUMENTS_KEY = - "LicenseTextViewerFragment.arguments" + const val LICENSE_TEXT_VIEWER_FRAGMENT_ARGUMENTS_KEY = "LicenseTextViewerFragment.arguments" /** Returns an instance of [LicenseTextViewerFragment]. */ fun newInstance(dependencyIndex: Int, licenseIndex: Int): LicenseTextViewerFragment { diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt index 06cd8bf8b67..44a2111579f 100644 --- a/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragment.kt @@ -12,7 +12,8 @@ import org.oppia.android.util.extensions.getProto import org.oppia.android.util.extensions.putProto import javax.inject.Inject -private const val POLICIES_FRAGMENT_POLICY_PAGE_ARGUMENT_PROTO = "PoliciesFragment.policy_page" +/** Argument key for PoliciesFragment. */ +const val POLICIES_FRAGMENT_POLICY_PAGE_ARGUMENT_PROTO = "PoliciesFragment.policy_page" /** Fragment that contains policies flow of the app. */ class PoliciesFragment : InjectableFragment() { diff --git a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt index 6d543ac59af..71ad148b0f2 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/administratorcontrols/AdministratorControlsFragmentTest.kt @@ -8,6 +8,7 @@ import android.view.ViewParent import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import androidx.core.widget.NestedScrollView +import androidx.drawerlayout.widget.DrawerLayout import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onView @@ -23,6 +24,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isClickable import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.util.HumanReadables import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.Matcher import org.hamcrest.Matchers @@ -44,6 +46,7 @@ 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.AdministratorControlsFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.scrollToPosition @@ -98,6 +101,7 @@ 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.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -433,6 +437,35 @@ class AdministratorControlsFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launch( + createAdministratorControlsFragmentTestActivityIntent( + profileId = internalProfileId + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val administratorControlsFragment = activity.supportFragmentManager + .findFragmentById(R.id.administrator_controls_fragment_test_activity_fragment_container) + as AdministratorControlsFragment + val isMultipane = activity + .findViewById(R.id.administrator_controls_activity_drawer_layout) != null + + val arguments = checkNotNull(administratorControlsFragment.arguments) { + "Expected arguments to be passed to AdministratorControlsFragment" + }.getProto( + ADMINISTRATOR_CONTROLS_FRAGMENT_ARGUMENTS_KEY, + AdministratorControlsFragmentArguments.getDefaultInstance() + ) + val receivedIsMultipane = arguments.isMultipane + + assertThat(receivedIsMultipane).isEqualTo(isMultipane) + } + } + } + private fun clickAutoUpdateTopicContainer() { onView( atPositionOnView( diff --git a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt index 0916c25d749..b1a9e948d59 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/home/RecentlyPlayedFragmentTest.kt @@ -131,6 +131,7 @@ 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.ImageParsingModule +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -1649,6 +1650,31 @@ class RecentlyPlayedFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + ActivityScenario.launch( + createRecentlyPlayedActivityIntent( + internalProfileId = internalProfileId, + RecentlyPlayedActivityTitle.STORIES_FOR_YOU + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val recentlyPlayedFragment = activity.supportFragmentManager + .findFragmentById(R.id.recently_played_fragment_placeholder) as RecentlyPlayedFragment + + val arguments = checkNotNull(recentlyPlayedFragment.arguments) { + "Expected arguments to be passed to RecentlyPlayedFragment" + } + val profileId = arguments.extractCurrentUserProfileId() + val receivedInternalProfileId = profileId.internalId + + assertThat(receivedInternalProfileId).isEqualTo(internalProfileId) + } + } + } + private fun setUpTestFragment(activity: RecentlyPlayedActivity) { activity.supportFragmentManager .beginTransaction() diff --git a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt index f7abd23fcc6..aefc561573a 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/options/OptionsFragmentTest.kt @@ -3,6 +3,7 @@ package org.oppia.android.app.options import android.app.Application import android.content.Context import android.content.Intent +import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import androidx.drawerlayout.widget.DrawerLayout import androidx.test.core.app.ActivityScenario @@ -21,6 +22,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription 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.hamcrest.Matchers.allOf import org.junit.After @@ -44,6 +46,7 @@ import org.oppia.android.app.model.AppLanguageActivityParams import org.oppia.android.app.model.AudioLanguage import org.oppia.android.app.model.AudioLanguageActivityParams import org.oppia.android.app.model.OppiaLanguage +import org.oppia.android.app.model.OptionsFragmentArguments import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.ReadingTextSize import org.oppia.android.app.model.ReadingTextSizeActivityParams @@ -99,6 +102,7 @@ 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.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -559,6 +563,39 @@ class OptionsFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launch( + createOptionActivityIntent( + internalProfileId = 0, + isFromNavigationDrawer = true + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val optionsFragment = activity.supportFragmentManager + .findFragmentById(R.id.options_fragment_placeholder) as OptionsFragment + + val args = optionsFragment.arguments?.getProto( + OPTIONS_FRAGMENT_ARGUMENTS_KEY, + OptionsFragmentArguments.getDefaultInstance() + ) + + val isMultipane = + activity.findViewById(R.id.multipane_options_container) != null + + val receivedIsMultipane = args?.isMultipane + val receivedIsFirstOpen = args?.isFirstOpen + val receivedSelectedFragment = checkNotNull(args?.selectedFragment) + + assertThat(receivedIsMultipane).isEqualTo(isMultipane) + assertThat(receivedIsFirstOpen).isEqualTo(true) + assertThat(receivedSelectedFragment).isEqualTo(READING_TEXT_SIZE_FRAGMENT) + } + } + } + private fun rotateToLandscape() { onView(isRoot()).perform(orientationLandscape()) testCoroutineDispatchers.runCurrent() diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index 09baf65b707..5f436a48dc7 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -80,6 +80,7 @@ import org.oppia.android.app.devoptions.DeveloperOptionsModule import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule import org.oppia.android.app.model.OppiaLanguage import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.StateFragmentArguments import org.oppia.android.app.model.WrittenTranslationLanguageSelection import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel @@ -178,6 +179,7 @@ import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.LoadImagesFromAssets import org.oppia.android.util.caching.LoadLessonProtosFromAssets +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -5238,6 +5240,38 @@ class StateFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + setUpTestWithLanguageSwitchingFeatureOff() + launchForExploration( + FRACTIONS_EXPLORATION_ID_1, + shouldSavePartialProgress = false + ).use { scenario -> + startPlayingExploration() + + scenario.onActivity { activity -> + val stateFragment = activity.supportFragmentManager + .findFragmentById(R.id.state_fragment_placeholder) as StateFragment + + val args = + stateFragment.arguments?.getProto( + StateFragment.STATE_FRAGMENT_ARGUMENTS_KEY, + StateFragmentArguments.getDefaultInstance() + ) + + val receivedInternalProfileId = args?.internalProfileId ?: -1 + val receivedTopicId = args?.topicId!! + val receivedStoryId = args.storyId!! + val reveivedExplorationId = args.explorationId!! + + assertThat(receivedInternalProfileId).isEqualTo(profileId.internalId) + assertThat(receivedTopicId).isEqualTo(TEST_TOPIC_ID_0) + assertThat(receivedStoryId).isEqualTo(TEST_STORY_ID_0) + assertThat(reveivedExplorationId).isEqualTo(FRACTIONS_EXPLORATION_ID_1) + } + } + } + private fun playThroughRatioExplorationState1() { clickContinueInteractionButton() } diff --git a/app/src/sharedTest/java/org/oppia/android/app/policies/PoliciesFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/policies/PoliciesFragmentTest.kt index 3db8bca24cd..3e94869a44a 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/policies/PoliciesFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/policies/PoliciesFragmentTest.kt @@ -52,6 +52,8 @@ 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.PoliciesActivityParams +import org.oppia.android.app.model.PoliciesFragmentArguments import org.oppia.android.app.model.PolicyPage import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.ViewBindingShimModule @@ -101,6 +103,8 @@ 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.extensions.getProto +import org.oppia.android.util.extensions.getProtoExtra import org.oppia.android.util.gcsresource.DefaultResourceBucketName import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule @@ -324,6 +328,44 @@ class PoliciesFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launch( + createPoliciesFragmentTestIntent( + getApplicationContext(), + PolicyPage.TERMS_OF_SERVICE + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val policiesFragment = activity.supportFragmentManager + .findFragmentById(R.id.policies_fragment_placeholder) as PoliciesFragment + + val policiesActivityParams = activity.intent.getProtoExtra( + PoliciesFragmentTestActivity.POLICIES_FRAGMENT_TEST_POLICY_PAGE_PARAMS_PROTO, + PoliciesActivityParams.getDefaultInstance() + ) + val policiesFragmentArguments = + PoliciesFragmentArguments + .newBuilder() + .setPolicyPage(policiesActivityParams.policyPage) + .build() + + val args = checkNotNull(policiesFragment.arguments) { + "Expected arguments to be passed to PoliciesFragment" + } + val receivedPolicies = + args.getProto( + POLICIES_FRAGMENT_POLICY_PAGE_ARGUMENT_PROTO, + PoliciesFragmentArguments.getDefaultInstance() + ) + + assertThat(receivedPolicies.policyPage).isEqualTo(policiesFragmentArguments.policyPage) + } + } + } + private fun setUpTestApplicationComponent() { getApplicationContext().inject(this) } diff --git a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt index 1bfc769e383..dd3d4d3e0de 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/profileprogress/ProfileProgressFragmentTest.kt @@ -133,6 +133,7 @@ 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.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.oppia.android.util.profile.PROFILE_ID_INTENT_DECORATOR import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode @@ -825,6 +826,27 @@ class ProfileProgressFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launch( + createProfileProgressActivityIntent(internalProfileId) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val profileProgressFragment = activity.supportFragmentManager + .findFragmentById(R.id.profile_progress_fragment_placeholder) as ProfileProgressFragment + + val args = checkNotNull(profileProgressFragment.arguments) { + "Expected arguments to be passed to ProfileProgressFragment" + } + val receivedInternalProfileId = args.extractCurrentUserProfileId().internalId + + assertThat(receivedInternalProfileId).isEqualTo(internalProfileId) + } + } + } + private fun createGalleryPickActivityResultStub(): Instrumentation.ActivityResult { val resources: Resources = context.resources val imageUri = Uri.parse( diff --git a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt index f264a3cc398..21608551c08 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/thirdparty/LicenseTextViewerFragmentTest.kt @@ -12,6 +12,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot 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 @@ -31,6 +32,8 @@ 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.help.thirdparty.LicenseTextViewerActivity +import org.oppia.android.app.help.thirdparty.LicenseTextViewerFragment +import org.oppia.android.app.model.LicenseTextViewerFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule @@ -77,6 +80,7 @@ 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.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -316,6 +320,36 @@ class LicenseTextViewerFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launch( + createLicenseTextViewerActivity( + dependencyIndex = 3, + licenseIndex = 1 + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val licenseTextViewerFragment = activity.supportFragmentManager + .findFragmentById(R.id.license_text_viewer_fragment_placeholder) + as LicenseTextViewerFragment + val arguments = checkNotNull(licenseTextViewerFragment.arguments) { + "Expected arguments to be passed to LicenseTextViewerFragment" + } + val args = arguments.getProto( + LicenseTextViewerFragment.LICENSE_TEXT_VIEWER_FRAGMENT_ARGUMENTS_KEY, + LicenseTextViewerFragmentArguments.getDefaultInstance() + ) + val receivedDependencyIndex = args.dependencyIndex + val receivedLicenseIndex = args.licenseIndex + + assertThat(receivedDependencyIndex).isEqualTo(3) + assertThat(receivedLicenseIndex).isEqualTo(1) + } + } + } + private fun createLicenseTextViewerActivity(dependencyIndex: Int, licenseIndex: Int): Intent { return LicenseTextViewerActivity.createLicenseTextViewerActivityIntent( ApplicationProvider.getApplicationContext(), diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt index 6af4d3d4a4f..e4550bf1fcf 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/info/TopicInfoFragmentTest.kt @@ -23,11 +23,13 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.rule.ActivityTestRule +import androidx.viewpager2.widget.ViewPager2 import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.CoreMatchers import org.hamcrest.Description import org.hamcrest.Matcher +import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString import org.hamcrest.TypeSafeMatcher import org.junit.After @@ -48,10 +50,13 @@ 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.ProfileId +import org.oppia.android.app.model.TopicInfoFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.topic.TopicActivity import org.oppia.android.app.topic.TopicActivity.Companion.createTopicActivityIntent +import org.oppia.android.app.topic.TopicFragment +import org.oppia.android.app.topic.TopicTab import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape @@ -102,6 +107,7 @@ 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.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -111,6 +117,9 @@ 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.ImageParsingModule +import org.oppia.android.util.platformparameter.EnableExtraTopicTabsUi +import org.oppia.android.util.platformparameter.PlatformParameterValue +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -154,6 +163,9 @@ class TopicInfoFragmentTest { @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @field:[Inject EnableExtraTopicTabsUi] + lateinit var enableExtraTopicTabsUi: PlatformParameterValue + @get:Rule var activityTestRule: ActivityTestRule = ActivityTestRule( TopicActivity::class.java, /* initialTouchMode= */ true, /* launchActivity= */ false @@ -439,6 +451,40 @@ class TopicInfoFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launchTopicActivityIntent( + profileId = profileId, + classroomId = TEST_CLASSROOM_ID, + topicId = TEST_TOPIC_ID + ).use { scenario -> + clickInfoTab() + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val topicFragment = activity.supportFragmentManager + .findFragmentById(R.id.topic_fragment_placeholder) as TopicFragment + val viewPager = topicFragment.requireView() + .findViewById(R.id.topic_tabs_viewpager) + val topicInfoFragment = topicFragment.childFragmentManager + .findFragmentByTag("f${viewPager.currentItem}") as TopicInfoFragment + + val args = topicInfoFragment.arguments?.getProto( + TopicInfoFragment.TOPIC_INFO_FRAGMENT_ARGUMENTS_KEY, + TopicInfoFragmentArguments.getDefaultInstance() + ) + val receivedInternalProfileId = topicInfoFragment + .arguments?.extractCurrentUserProfileId()?.internalId ?: -1 + val receivedTopicId = checkNotNull(args?.topicId) { + "Expected topic ID to be included in arguments for TopicInfoFragment." + } + + assertThat(receivedInternalProfileId).isEqualTo(profileId.internalId) + assertThat(receivedTopicId).isEqualTo(TEST_TOPIC_ID) + } + } + } + private fun launchTopicActivityIntent( profileId: ProfileId, classroomId: String, @@ -454,6 +500,21 @@ class TopicInfoFragmentTest { return ActivityScenario.launch(intent) } + private fun clickInfoTab() { + onView( + allOf( + withText( + TopicTab.getTabForPosition( + position = 0, + enableExtraTopicTabsUi = enableExtraTopicTabsUi.value + ).name + ), + ViewMatchers.isDescendantOfA(withId(R.id.topic_tabs_container)) + ) + ).perform(click()) + testCoroutineDispatchers.runCurrent() + } + /** Custom function to set dummy text in the TextView. */ private fun setTextInTextView(value: String): ViewAction { return object : ViewAction { diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt index 9ceba1db737..e54dea276a9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/lessons/TopicLessonsFragmentTest.kt @@ -26,6 +26,8 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.viewpager2.widget.ViewPager2 +import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.containsString @@ -55,6 +57,7 @@ import org.oppia.android.app.model.Spotlight.FeatureCase.FIRST_CHAPTER import org.oppia.android.app.model.Spotlight.FeatureCase.TOPIC_LESSON_TAB import org.oppia.android.app.model.Spotlight.FeatureCase.TOPIC_REVISION_TAB import org.oppia.android.app.model.StoryActivityParams +import org.oppia.android.app.model.TopicLessonsFragmentArguments import org.oppia.android.app.player.exploration.ExplorationActivity import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition @@ -64,6 +67,7 @@ import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.story.StoryActivity import org.oppia.android.app.story.StoryActivity.Companion.STORY_ACTIVITY_PARAMS_KEY import org.oppia.android.app.topic.TopicActivity +import org.oppia.android.app.topic.TopicFragment import org.oppia.android.app.topic.TopicTab import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.app.utility.EspressoTestsMatchers.hasProtoExtra @@ -124,6 +128,7 @@ import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.accessibility.FakeAccessibilityService import org.oppia.android.util.caching.AssetModule import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -136,6 +141,7 @@ import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule import org.oppia.android.util.platformparameter.EnableExtraTopicTabsUi import org.oppia.android.util.platformparameter.PlatformParameterValue +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -1203,6 +1209,78 @@ class TopicLessonsFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launch( + createTopicPlayStoryActivityIntent( + profileId, + TEST_CLASSROOM_ID_1, + RATIOS_TOPIC_ID, + RATIOS_STORY_ID_0 + ) + ).use { scenario -> + clickLessonTab() + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val topicFragment = activity.supportFragmentManager + .findFragmentById(R.id.topic_fragment_placeholder) as TopicFragment + val viewPager = topicFragment.requireView() + .findViewById(R.id.topic_tabs_viewpager) + val topicLessonsFragment = topicFragment.childFragmentManager + .findFragmentByTag("f${viewPager.currentItem}") as TopicLessonsFragment + + val receivedInternalProfileId = topicLessonsFragment + .arguments?.extractCurrentUserProfileId()?.internalId ?: -1 + val args = topicLessonsFragment.arguments?.getProto( + TopicLessonsFragment.TOPIC_LESSONS_FRAGMENT_ARGUMENTS_KEY, + TopicLessonsFragmentArguments.getDefaultInstance() + ) + val receivedClassroomId = checkNotNull(args?.classroomId) { + "Expected classroom ID to be included in arguments for TopicLessonsFragment." + } + val receivedTopicId = checkNotNull(args?.topicId) { + "Expected topic ID to be included in arguments for TopicLessonsFragment." + } + val receivedStoryId = args?.storyId ?: "" + + assertThat(receivedInternalProfileId).isEqualTo(profileId.internalId) + assertThat(receivedClassroomId).isEqualTo(TEST_CLASSROOM_ID_1) + assertThat(receivedTopicId).isEqualTo(RATIOS_TOPIC_ID) + assertThat(receivedStoryId).isEqualTo(RATIOS_STORY_ID_0) + } + } + } + + @Test + fun testTopicLessonsFragment_saveInstanceState_verifyCorrectStateRestored() { + launch( + createTopicPlayStoryActivityIntent( + profileId, + TEST_CLASSROOM_ID_1, + RATIOS_TOPIC_ID, + RATIOS_STORY_ID_0 + ) + ).use { scenario -> + clickLessonTab() + testCoroutineDispatchers.runCurrent() + + scrollToPosition(position = 2) + clickStoryItem(position = 2, targetViewId = R.id.chapter_list_drop_down_icon) + + scenario.recreate() + + scrollToPosition(position = 2) + onView( + atPositionOnView( + recyclerViewId = R.id.story_summary_recycler_view, + position = 2, + targetViewId = R.id.chapter_recycler_view + ) + ).check(matches(isDisplayed())) + } + } + private fun markAllSpotlightsSeen() { spotlightStateController.markSpotlightViewed(profileId, TOPIC_LESSON_TAB) testCoroutineDispatchers.runCurrent() diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt index 94fa885b5f3..8916ec5acbd 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/practice/TopicPracticeFragmentTest.kt @@ -20,6 +20,8 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.viewpager2.widget.ViewPager2 +import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.not @@ -45,10 +47,12 @@ import org.oppia.android.app.model.QuestionPlayerActivityParams import org.oppia.android.app.model.Spotlight.FeatureCase.FIRST_CHAPTER import org.oppia.android.app.model.Spotlight.FeatureCase.TOPIC_LESSON_TAB import org.oppia.android.app.model.Spotlight.FeatureCase.TOPIC_REVISION_TAB +import org.oppia.android.app.model.TopicPracticeFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.topic.TopicActivity +import org.oppia.android.app.topic.TopicFragment import org.oppia.android.app.topic.TopicTab import org.oppia.android.app.topic.questionplayer.QuestionPlayerActivity import org.oppia.android.app.topic.questionplayer.QuestionPlayerActivity.Companion.QUESTION_PLAYER_ACTIVITY_PARAMS_KEY @@ -100,6 +104,7 @@ 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.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -112,6 +117,7 @@ import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule import org.oppia.android.util.platformparameter.EnableExtraTopicTabsUi import org.oppia.android.util.platformparameter.PlatformParameterValue +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -382,6 +388,74 @@ class TopicPracticeFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launchTopicActivityIntent( + profileId = profileId, + classroomId = TEST_CLASSROOM_ID_1, + topicId = FRACTIONS_TOPIC_ID + ).use { scenario -> + clickPracticeTab() + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val topicFragment = activity.supportFragmentManager + .findFragmentById(R.id.topic_fragment_placeholder) as TopicFragment + val viewPager = topicFragment.requireView() + .findViewById(R.id.topic_tabs_viewpager) + val topicPracticeFragment = topicFragment.childFragmentManager + .findFragmentByTag("f${viewPager.currentItem}") as TopicPracticeFragment + + val args = topicPracticeFragment.arguments?.getProto( + TopicPracticeFragment.TOPIC_PRACTICE_FRAGMENT_ARGUMENTS_KEY, + TopicPracticeFragmentArguments.getDefaultInstance() + ) + val receivedInternalProfileId = topicPracticeFragment + .arguments?.extractCurrentUserProfileId()?.internalId ?: -1 + val receivedTopicId = checkNotNull(args?.topicId) { + "Expected topic ID to be included in arguments for TopicPracticeFragment." + } + + assertThat(receivedInternalProfileId).isEqualTo(profileId.internalId) + assertThat(receivedTopicId).isEqualTo(FRACTIONS_TOPIC_ID) + } + } + } + + @Test + fun testTopicPracticeFragment_saveInstanceState_verifyCorrectStateRestored() { + launchTopicActivityIntent( + profileId = profileId, + classroomId = TEST_CLASSROOM_ID_1, + topicId = FRACTIONS_TOPIC_ID + ).use { scenario -> + clickPracticeTab() + testCoroutineDispatchers.runCurrent() + + clickPracticeItem(position = 1, targetViewId = R.id.subtopic_check_box) + clickPracticeItem(position = 2, targetViewId = R.id.subtopic_check_box) + + scenario.recreate() + + scrollToPosition(position = 1) + onView( + atPositionOnView( + recyclerViewId = R.id.topic_practice_skill_list, + position = 1, + targetViewId = R.id.subtopic_check_box + ) + ).check(matches(isChecked())) + scrollToPosition(position = 2) + onView( + atPositionOnView( + recyclerViewId = R.id.topic_practice_skill_list, + position = 2, + targetViewId = R.id.subtopic_check_box + ) + ).check(matches(isChecked())) + } + } + private fun launchTopicActivityIntent( profileId: ProfileId, classroomId: String, diff --git a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt index 2b86bba92c9..d437b8feb3d 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/topic/revision/TopicRevisionFragmentTest.kt @@ -22,6 +22,8 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.viewpager2.widget.ViewPager2 +import com.google.common.truth.Truth.assertThat import dagger.Component import org.hamcrest.Matchers.allOf import org.junit.After @@ -45,11 +47,13 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.app.model.Spotlight.FeatureCase.FIRST_CHAPTER import org.oppia.android.app.model.Spotlight.FeatureCase.TOPIC_LESSON_TAB import org.oppia.android.app.model.Spotlight.FeatureCase.TOPIC_REVISION_TAB +import org.oppia.android.app.model.TopicRevisionFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPosition import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.topic.TopicActivity +import org.oppia.android.app.topic.TopicFragment import org.oppia.android.app.topic.TopicTab import org.oppia.android.app.topic.revisioncard.RevisionCardActivity import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule @@ -102,6 +106,7 @@ 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.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -113,6 +118,7 @@ import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule import org.oppia.android.util.parser.image.ImageParsingModule import org.oppia.android.util.platformparameter.EnableExtraTopicTabsUi import org.oppia.android.util.platformparameter.PlatformParameterValue +import org.oppia.android.util.profile.CurrentUserProfileIdIntentDecorator.extractCurrentUserProfileId import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import javax.inject.Inject @@ -285,6 +291,37 @@ class TopicRevisionFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launchTopicActivityIntent( + profileId = profileId, + classroomId = TEST_CLASSROOM_ID_1, + topicId = FRACTIONS_TOPIC_ID + ).use { scenario -> + clickRevisionTab() + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val topicFragment = activity.supportFragmentManager + .findFragmentById(R.id.topic_fragment_placeholder) as TopicFragment + val viewPager = topicFragment.requireView() + .findViewById(R.id.topic_tabs_viewpager) + val topicRevisionFragment = topicFragment.childFragmentManager + .findFragmentByTag("f${viewPager.currentItem}") as TopicRevisionFragment + + val receivedInternalProfileId = topicRevisionFragment + .arguments?.extractCurrentUserProfileId()?.internalId ?: -1 + val args = topicRevisionFragment.arguments?.getProto( + TopicRevisionFragment.TOPIC_REVISION_FRAGMENT_ARGUMENTS_KEY, + TopicRevisionFragmentArguments.getDefaultInstance() + ) + + assertThat(receivedInternalProfileId).isEqualTo(profileId.internalId) + assertThat(args?.topicId).isEqualTo(FRACTIONS_TOPIC_ID) + } + } + } + private fun markAllSpotlightsSeen() { spotlightStateController.markSpotlightViewed(profileId, TOPIC_LESSON_TAB) testCoroutineDispatchers.runCurrent() diff --git a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt index 608d84b0b19..cff6fdf44d9 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/walkthrough/WalkthroughFinalFragmentTest.kt @@ -18,6 +18,7 @@ import androidx.test.espresso.matcher.ViewMatchers.isRoot 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.hamcrest.CoreMatchers.containsString import org.junit.After @@ -37,12 +38,14 @@ 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.WalkthroughFinalFragmentArguments import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape import org.oppia.android.app.utility.ProgressMatcher.Companion.withProgress +import org.oppia.android.app.walkthrough.end.WalkthroughFinalFragment import org.oppia.android.data.backends.gae.NetworkConfigProdModule import org.oppia.android.data.backends.gae.NetworkModule import org.oppia.android.domain.classify.InteractionsModule @@ -73,6 +76,7 @@ import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule import org.oppia.android.domain.platformparameter.PlatformParameterModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.FRACTIONS_TOPIC_ID import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule import org.oppia.android.testing.OppiaTestRule import org.oppia.android.testing.TestLogReportingModule @@ -85,6 +89,7 @@ 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.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -267,6 +272,33 @@ class WalkthroughFinalFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launch(createWalkthroughActivityIntent(0)).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + activity.pageWithTopicId(WalkthroughPages.FINAL.value, FRACTIONS_TOPIC_ID) + + val walkthroughFinalFragment = activity.supportFragmentManager + .findFragmentById(R.id.walkthrough_fragment_placeholder) as WalkthroughFinalFragment + + val arguments = + checkNotNull(walkthroughFinalFragment.arguments) { + "Expected arguments to be passed to WalkthroughFinalFragment" + } + val args = arguments.getProto( + WalkthroughFinalFragment.WALKTHROUGH_FINAL_FRAGMENT_ARGUMENTS_KEY, + WalkthroughFinalFragmentArguments.getDefaultInstance() + ) + val receivedTopicId = checkNotNull(args.topicId) { + "Expected topicId to be passed to WalkthroughFinalFragment" + } + + assertThat(receivedTopicId).isEqualTo(FRACTIONS_TOPIC_ID) + } + } + } + private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) } diff --git a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt index 54178d81774..08185ec5033 100644 --- a/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt +++ b/app/src/test/java/org/oppia/android/app/testing/options/OptionsFragmentTest.kt @@ -2,6 +2,7 @@ package org.oppia.android.app.testing.options import android.app.Application import android.content.Intent +import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider @@ -27,10 +28,14 @@ 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.OptionsFragmentArguments import org.oppia.android.app.model.ProfileId import org.oppia.android.app.options.AppLanguageFragment import org.oppia.android.app.options.AudioLanguageFragment +import org.oppia.android.app.options.OPTIONS_FRAGMENT_ARGUMENTS_KEY import org.oppia.android.app.options.OptionsActivity +import org.oppia.android.app.options.OptionsFragment +import org.oppia.android.app.options.READING_TEXT_SIZE_FRAGMENT import org.oppia.android.app.options.ReadingTextSizeFragment import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView @@ -78,6 +83,7 @@ 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.extensions.getProto import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -197,6 +203,39 @@ class OptionsFragmentTest { } } + @Test + fun testFragment_argumentsAreCorrect() { + launch( + createOptionActivityIntent( + internalProfileId = 0, + isFromNavigationDrawer = true + ) + ).use { scenario -> + testCoroutineDispatchers.runCurrent() + scenario.onActivity { activity -> + + val optionsFragment = activity.supportFragmentManager + .findFragmentById(R.id.options_fragment_placeholder) as OptionsFragment + + val args = optionsFragment.arguments?.getProto( + OPTIONS_FRAGMENT_ARGUMENTS_KEY, + OptionsFragmentArguments.getDefaultInstance() + ) + + val isMultipane = + activity.findViewById(R.id.multipane_options_container) != null + + val receivedIsMultipane = args?.isMultipane + val receivedIsFirstOpen = args?.isFirstOpen + val receivedSelectedFragment = checkNotNull(args?.selectedFragment) + + assertThat(receivedIsMultipane).isEqualTo(isMultipane) + assertThat(receivedIsFirstOpen).isEqualTo(true) + assertThat(receivedSelectedFragment).isEqualTo(READING_TEXT_SIZE_FRAGMENT) + } + } + } + private fun createOptionActivityIntent( internalProfileId: Int, isFromNavigationDrawer: Boolean