diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b40913b276f..675725b3fe7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -194,6 +194,7 @@ android:name=".app.testing.TopicTestActivityForStory" android:theme="@style/OppiaThemeWithoutActionBar" /> + + + + + + + + diff --git a/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt new file mode 100644 index 00000000000..74d3f3754f1 --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/android/app/databinding/ViewBindingAdaptersTest.kt @@ -0,0 +1,246 @@ +package org.oppia.android.app.databinding + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.content.Intent +import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.intent.Intents +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.Component +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.oppia.android.R +import org.oppia.android.app.activity.ActivityComponent +import org.oppia.android.app.application.ActivityComponentFactory +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.databinding.ViewBindingAdapters.setRotationAnimation +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.player.state.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.testing.ViewBindingAdaptersTestActivity +import org.oppia.android.app.topic.PracticeTabModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.lightweightcheckpointing.ExplorationStorageModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.loguploader.LogUploadWorkerModule +import org.oppia.android.domain.oppialogger.loguploader.WorkManagerConfigurationModule +import org.oppia.android.domain.platformparameter.PlatformParameterModule +import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.testing.TestImageLoaderModule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.robolectric.RobolectricModule +import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +import org.oppia.android.util.parser.image.ImageParsingModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +/** Default value for float comparison. */ +private const val TOLERANCE = 1e-5f + +/** Tests for [MarginBindingAdapters]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = ViewBindingAdaptersTest.TestApplication::class, qualifiers = "port-xxhdpi") +class ViewBindingAdaptersTest { + + @Inject + lateinit var context: Context + + @get:Rule + var activityRule: ActivityScenarioRule = + ActivityScenarioRule( + Intent( + ApplicationProvider.getApplicationContext(), + ViewBindingAdaptersTestActivity::class.java + ) + ) + + @Before + fun setUp() { + setUpTestApplicationComponent() + Intents.init() + } + + @After + fun tearDown() { + Intents.release() + } + + @Config(qualifiers = "port") + @Test + fun testViewBindingAdapters_ltrIsEnabled_antiClockwise_rotationAngleForLtrIsCorrect() { + val imageViewDropDown = activityRule.scenario.runWithActivity { + val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon) + setRotationAnimation( + imageViewDropDown, + /* isClockwise= */ false, + /* angle= */ 180f + ) + return@runWithActivity imageViewDropDown + } + assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(180f) + } + + @Config(qualifiers = "port") + @Test + fun testViewBindingAdapters_ltrIsEnabled_clockwise_rotationAngleForLtrIsCorrect() { + val imageViewDropDown = activityRule.scenario.runWithActivity { + val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon) + setRotationAnimation( + imageViewDropDown, + /* isClockwise= */ true, + /* angle= */ 180f + ) + return@runWithActivity imageViewDropDown + } + assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(0f) + } + + @Config(qualifiers = "port") + @Test + fun testViewBindingAdapters_rtlIsEnabled_clockwise_rotationAngleForRtlIsCorrect() { + val imageViewDropDown = activityRule.scenario.runWithActivity { + val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon) + ViewCompat.setLayoutDirection(imageViewDropDown, ViewCompat.LAYOUT_DIRECTION_RTL) + setRotationAnimation( + imageViewDropDown, + /* isClockwise= */ true, + /* angle= */ 180f + ) + return@runWithActivity imageViewDropDown + } + assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(360f) + } + + @Config(qualifiers = "port") + @Test + fun testViewBindingAdapters_rtlIsEnabled_antiClockwise_rotationAngleForRtlIsCorrect() { + val imageViewDropDown = activityRule.scenario.runWithActivity { + val imageViewDropDown: ImageView = it.findViewById(R.id.test_drop_down_icon) + ViewCompat.setLayoutDirection(imageViewDropDown, ViewCompat.LAYOUT_DIRECTION_RTL) + setRotationAnimation( + imageViewDropDown, + /* isClockwise= */ false, + /* angle= */ 180f + ) + return@runWithActivity imageViewDropDown + } + assertThat(imageViewDropDown.rotation).isWithin(TOLERANCE).of(180f) + } + + private inline fun ActivityScenario.runWithActivity( + crossinline action: (A) -> V + ): V { + // Use Mockito to ensure the routine is actually executed before returning the result. + @Suppress("UNCHECKED_CAST") // The unsafe cast is necessary to make the routine generic. + val fakeMock: Consumer = mock(Consumer::class.java) as Consumer + val valueCaptor = ArgumentCaptor.forClass(V::class.java) + onActivity { fakeMock.consume(action(it)) } + verify(fakeMock).consume(valueCaptor.capture()) + return valueCaptor.value + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. + @Singleton + @Component( + modules = [ + RobolectricModule::class, + PlatformParameterModule::class, + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, TestImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, + ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, + WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, PracticeTabModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class + ] + ) + /** Create a TestApplicationComponent. */ + interface TestApplicationComponent : ApplicationComponent { + /** Build the TestApplicationComponent. */ + @Component.Builder + interface Builder : ApplicationComponent.Builder + + /** Inject [ViewBindingAdaptersTest] in TestApplicationComponent . */ + fun inject(ViewBindingAdaptersTest: ViewBindingAdaptersTest) + } + + /** + * Class to override a dependency throughout the test application, instead of overriding the + * dependencies in every test class, we can just do it once by extending the Application class. + */ + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerViewBindingAdaptersTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + /** Inject [ViewBindingAdaptersTest] in TestApplicationComponent . */ + fun inject(viewBindingAdapters: ViewBindingAdaptersTest) { + component.inject(viewBindingAdapters) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } + + private interface Consumer { + /** Represents an operation that accepts a single input argument and returns no result. */ + fun consume(value: T) + } +} diff --git a/scripts/assets/accessibility_label_exemptions.textproto b/scripts/assets/accessibility_label_exemptions.textproto index 751cdc1c57b..b91017e02a2 100644 --- a/scripts/assets/accessibility_label_exemptions.textproto +++ b/scripts/assets/accessibility_label_exemptions.textproto @@ -32,4 +32,5 @@ exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TestFontScal exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivity" exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivity" exempted_activity: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivityForStory" +exempted_activity: "app/src/main/java/org/oppia/android/app/testing/ViewBindingAdaptersTestActivity" exempted_activity: "app/src/main/java/org/oppia/android/app/walkthrough/WalkthroughActivity" diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto index 5db0c9b03a1..48148d020be 100644 --- a/scripts/assets/test_file_exemptions.textproto +++ b/scripts/assets/test_file_exemptions.textproto @@ -385,6 +385,7 @@ exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TestFontSca exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivity.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TopicRevisionTestActivityPresenter.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/TopicTestActivity.kt" +exempted_file_path: "app/src/main/java/org/oppia/android/app/testing/ViewBindingAdaptersTestActivity.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/topic/EnablePracticeTab.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/topic/PracticeTabModule.kt" exempted_file_path: "app/src/main/java/org/oppia/android/app/topic/RouteToConceptCardListener.kt"