diff --git a/WORKSPACE b/WORKSPACE
index 06037a9572a..0347cbfacd6 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -137,6 +137,7 @@ maven_install(
"androidx.test.ext:junit:1.1.1",
"androidx.test:runner:1.2.0",
"androidx.viewpager:viewpager:1.0.0",
+ "androidx.work:work-runtime-ktx:2.4.0",
"com.android.support:support-annotations:28.0.0",
"com.caverock:androidsvg-aar:1.4",
"com.chaos.view:pinview:1.4.3",
diff --git a/app/BUILD.bazel b/app/BUILD.bazel
index 567bdf9f3ab..b1f108627eb 100644
--- a/app/BUILD.bazel
+++ b/app/BUILD.bazel
@@ -578,6 +578,7 @@ kt_android_library(
artifact("androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"),
artifact("androidx.multidex:multidex:2.0.1"),
artifact("androidx.viewpager:viewpager:1.0.0"),
+ artifact("androidx.work:work-runtime-ktx:2.4.0"),
artifact("com.caverock:androidsvg-aar"),
artifact("javax.annotation:javax.annotation-api:jar"),
],
diff --git a/app/build.gradle b/app/build.gradle
index 280f313420f..e768b3fedd9 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -91,6 +91,7 @@ dependencies {
'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-alpha03',
'androidx.multidex:multidex:2.0.1',
'androidx.recyclerview:recyclerview:1.0.0',
+ 'androidx.work:work-runtime-ktx:2.4.0',
'com.chaos.view:pinview:1.4.3',
'com.github.bumptech.glide:glide:4.11.0',
'com.google.android.material:material:1.2.0-alpha02',
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3701dcf2062..b67befe381f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,5 +1,6 @@
@@ -137,9 +138,6 @@
-
@@ -159,5 +157,10 @@
+
diff --git a/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt b/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt
index 3f619cd1496..3df35adb356 100644
--- a/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt
+++ b/app/src/main/java/org/oppia/app/activity/ActivityComponent.kt
@@ -46,7 +46,6 @@ import org.oppia.app.testing.HtmlParserTestActivity
import org.oppia.app.testing.ImageRegionSelectionTestActivity
import org.oppia.app.testing.NavigationDrawerTestActivity
import org.oppia.app.testing.ProfileChooserFragmentTestActivity
-import org.oppia.app.testing.StoryFragmentTestActivity
import org.oppia.app.testing.TestFontScaleConfigurationUtilActivity
import org.oppia.app.testing.TopicRevisionTestActivity
import org.oppia.app.testing.TopicTestActivity
@@ -120,6 +119,5 @@ interface ActivityComponent {
fun inject(topicRevisionTestActivity: TopicRevisionTestActivity)
fun inject(topicTestActivity: TopicTestActivity)
fun inject(topicTestActivityForStory: TopicTestActivityForStory)
- fun inject(storyFragmentTestActivity: StoryFragmentTestActivity)
fun inject(walkthroughActivity: WalkthroughActivity)
}
diff --git a/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt b/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt
index 6173c2863bf..f2fb1553ee8 100644
--- a/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt
+++ b/app/src/main/java/org/oppia/app/application/ApplicationComponent.kt
@@ -2,6 +2,7 @@ package org.oppia.app.application
// TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
import android.app.Application
+import androidx.work.Configuration
import dagger.BindsInstance
import dagger.Component
import org.oppia.app.activity.ActivityComponent
@@ -23,12 +24,15 @@ import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.ApplicationStartupListener
import org.oppia.domain.oppialogger.LogStorageModule
import org.oppia.domain.oppialogger.exceptions.UncaughtExceptionLoggerModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.util.accessibility.AccessibilityModule
import org.oppia.util.caching.CachingModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.logging.firebase.LogReportingModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
@@ -59,7 +63,8 @@ import javax.inject.Singleton
ViewBindingShimModule::class, PrimeTopicAssetsControllerModule::class,
ExpirationMetaDataRetrieverModule::class, RatioInputModule::class,
UncaughtExceptionLoggerModule::class, ApplicationStartupListenerModule::class,
- HintsAndSolutionConfigModule::class
+ LogUploadWorkerModule::class, WorkManagerConfigurationModule::class,
+ HintsAndSolutionConfigModule::class, FirebaseLogUploaderModule::class
]
)
@@ -74,4 +79,6 @@ interface ApplicationComponent : ApplicationInjector {
fun getActivityComponentBuilderProvider(): Provider
fun getApplicationStartupListeners(): Set
+
+ fun getWorkManagerConfiguration(): Configuration
}
diff --git a/app/src/main/java/org/oppia/app/application/OppiaApplication.kt b/app/src/main/java/org/oppia/app/application/OppiaApplication.kt
index 76859166e76..edf741846d7 100644
--- a/app/src/main/java/org/oppia/app/application/OppiaApplication.kt
+++ b/app/src/main/java/org/oppia/app/application/OppiaApplication.kt
@@ -3,6 +3,8 @@ package org.oppia.app.application
import android.app.Application
import androidx.appcompat.app.AppCompatActivity
import androidx.multidex.MultiDexApplication
+import androidx.work.Configuration
+import androidx.work.WorkManager
import com.google.firebase.FirebaseApp
import org.oppia.app.activity.ActivityComponent
import org.oppia.domain.oppialogger.ApplicationStartupListener
@@ -11,7 +13,8 @@ import org.oppia.domain.oppialogger.ApplicationStartupListener
class OppiaApplication :
MultiDexApplication(),
ActivityComponentFactory,
- ApplicationInjectorProvider {
+ ApplicationInjectorProvider,
+ Configuration.Provider {
/** The root [ApplicationComponent]. */
private val component: ApplicationComponent by lazy {
DaggerApplicationComponent.builder()
@@ -28,6 +31,11 @@ class OppiaApplication :
override fun onCreate() {
super.onCreate()
FirebaseApp.initializeApp(applicationContext)
+ WorkManager.initialize(applicationContext, workManagerConfiguration)
component.getApplicationStartupListeners().forEach(ApplicationStartupListener::onCreate)
}
+
+ override fun getWorkManagerConfiguration(): Configuration {
+ return component.getWorkManagerConfiguration()
+ }
}
diff --git a/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivity.kt b/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivity.kt
deleted file mode 100644
index e9af5c68e03..00000000000
--- a/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivity.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package org.oppia.app.testing
-
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import org.oppia.app.activity.InjectableAppCompatActivity
-import org.oppia.app.home.RouteToExplorationListener
-import javax.inject.Inject
-
-const val INTERNAL_PROFILE_ID_TEST_INTENT_EXTRA = "StoryFragmentTestActivity.internalProfileId"
-const val TOPIC_ID_TEST_INTENT_EXTRA = "StoryFragmentTestActivity.topic_id"
-const val STORY_ID_TEST_INTENT_EXTRA = "StoryFragmentTestActivity.story_id"
-
-/** Test activity used for story fragment. */
-class StoryFragmentTestActivity : InjectableAppCompatActivity(), RouteToExplorationListener {
- @Inject
- lateinit var storyFragmentTestActivityPresenter: StoryFragmentTestActivityPresenter
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- activityComponent.inject(this)
- storyFragmentTestActivityPresenter.handleOnCreate()
- }
-
- override fun routeToExploration(
- internalProfileId: Int,
- topicId: String,
- storyId: String,
- explorationId: String,
- backflowScreen: Int?
- ) {
- // Do nothing since routing should be tested at the StoryActivity level.
- }
-
- companion object {
- /** Returns an [Intent] to create new [StoryFragmentTestActivity]s. */
- fun createTestActivityIntent(
- context: Context,
- internalProfileId: Int,
- topicId: String,
- storyId: String
- ): Intent {
- val intent = Intent(context, StoryFragmentTestActivity::class.java)
- intent.putExtra(INTERNAL_PROFILE_ID_TEST_INTENT_EXTRA, internalProfileId)
- intent.putExtra(TOPIC_ID_TEST_INTENT_EXTRA, topicId)
- intent.putExtra(STORY_ID_TEST_INTENT_EXTRA, storyId)
- return intent
- }
- }
-}
diff --git a/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivityPresenter.kt
deleted file mode 100644
index 4fd2083b0eb..00000000000
--- a/app/src/main/java/org/oppia/app/testing/StoryFragmentTestActivityPresenter.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.oppia.app.testing
-
-import androidx.appcompat.app.AppCompatActivity
-import org.oppia.app.R
-import org.oppia.app.activity.ActivityScope
-import org.oppia.app.story.StoryFragment
-import javax.inject.Inject
-
-/** The presenter for [StoryFragmentTestActivity]. */
-@ActivityScope
-class StoryFragmentTestActivityPresenter @Inject constructor(
- private val activity: AppCompatActivity
-) {
- fun handleOnCreate() {
- activity.setContentView(R.layout.story_fragment_test_activity)
- if (getStoryFragment() == null) {
- val internalProfileId = activity.intent.getIntExtra(INTERNAL_PROFILE_ID_TEST_INTENT_EXTRA, -1)
- val topicId = checkNotNull(
- activity.intent.getStringExtra(TOPIC_ID_TEST_INTENT_EXTRA)
- ) {
- "Expected non-null topic ID to be passed in using extra key: $TOPIC_ID_TEST_INTENT_EXTRA"
- }
- val storyId = checkNotNull(
- activity.intent.getStringExtra(STORY_ID_TEST_INTENT_EXTRA)
- ) {
- "Expected non-null story ID to be passed in using extra key: $STORY_ID_TEST_INTENT_EXTRA"
- }
- activity.supportFragmentManager.beginTransaction().add(
- R.id.story_fragment_placeholder,
- StoryFragment.newInstance(internalProfileId, topicId, storyId)
- ).commitNow()
- }
- }
-
- private fun getStoryFragment(): StoryFragment? {
- return activity
- .supportFragmentManager
- .findFragmentById(
- R.id.story_fragment_placeholder
- ) as StoryFragment?
- }
-}
diff --git a/app/src/main/res/layout/story_fragment_test_activity.xml b/app/src/main/res/layout/story_fragment_test_activity.xml
deleted file mode 100644
index ed7d46bb202..00000000000
--- a/app/src/main/res/layout/story_fragment_test_activity.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
diff --git a/app/src/sharedTest/java/org/oppia/app/faq/FAQListFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/faq/FAQListFragmentTest.kt
index 0eaa43f2291..d423d029978 100644
--- a/app/src/sharedTest/java/org/oppia/app/faq/FAQListFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/faq/FAQListFragmentTest.kt
@@ -1,7 +1,9 @@
package org.oppia.app.faq
+import android.app.Application
import android.content.Context
import android.content.res.Resources
+import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
@@ -17,31 +19,78 @@ 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 dagger.Component
import org.hamcrest.Matchers.allOf
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
import org.oppia.app.help.faq.FAQListActivity
import org.oppia.app.help.faq.faqsingle.FAQSingleActivity
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
+import org.oppia.app.shim.ViewBindingShimModule
import org.oppia.app.utility.OrientationChangeAction.Companion.orientationLandscape
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
+import org.oppia.testing.TestAccessibilityModule
+import org.oppia.testing.TestCoroutineDispatchers
+import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.GlideImageLoaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
/** Tests for [FAQListFragment]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = FAQListFragmentTest.TestApplication::class, qualifiers = "port-xxhdpi")
class FAQListFragmentTest {
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
@Before
fun setUp() {
Intents.init()
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
}
@After
fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
Intents.release()
}
@@ -100,7 +149,56 @@ class FAQListFragmentTest {
}
}
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
private fun getResources(): Resources {
return ApplicationProvider.getApplicationContext().resources
}
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
+ @Singleton
+ @Component(
+ modules = [
+ 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, GlideImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(faqListFragmentTest: FAQListFragmentTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerFAQListFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(faqListFragmentTest: FAQListFragmentTest) {
+ component.inject(faqListFragmentTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
}
diff --git a/app/src/sharedTest/java/org/oppia/app/faq/FAQSingleActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/faq/FAQSingleActivityTest.kt
index 9b73ecc5953..28fcb625883 100644
--- a/app/src/sharedTest/java/org/oppia/app/faq/FAQSingleActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/faq/FAQSingleActivityTest.kt
@@ -1,8 +1,10 @@
package org.oppia.app.faq
+import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.res.Resources
+import androidx.appcompat.app.AppCompatActivity
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
@@ -10,17 +12,75 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
+import dagger.Component
+import org.junit.After
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
import org.oppia.app.help.faq.faqsingle.FAQSingleActivity
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.app.shim.ViewBindingShimModule
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
+import org.oppia.testing.TestAccessibilityModule
+import org.oppia.testing.TestCoroutineDispatchers
+import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.GlideImageLoaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
/** Tests for [FAQSingleActivity]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = FAQSingleActivityTest.TestApplication::class, qualifiers = "port-xxhdpi")
class FAQSingleActivityTest {
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ }
+
@Test
fun openFAQSingleActivity_checkQuestion_isDisplayed() {
launch(createFAQSingleActivity()).use {
@@ -35,6 +95,10 @@ class FAQSingleActivityTest {
}
}
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
private fun createFAQSingleActivity(): Intent {
return FAQSingleActivity.createFAQSingleActivityIntent(
ApplicationProvider.getApplicationContext(),
@@ -46,4 +110,49 @@ class FAQSingleActivityTest {
private fun getResources(): Resources {
return ApplicationProvider.getApplicationContext().resources
}
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
+ @Singleton
+ @Component(
+ modules = [
+ 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, GlideImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(faqSingleActivityTest: FAQSingleActivityTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerFAQSingleActivityTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(faqSingleActivityTest: FAQSingleActivityTest) {
+ component.inject(faqSingleActivityTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
}
diff --git a/app/src/sharedTest/java/org/oppia/app/help/HelpFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/help/HelpFragmentTest.kt
index 0414817b590..3967c18f1cc 100644
--- a/app/src/sharedTest/java/org/oppia/app/help/HelpFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/help/HelpFragmentTest.kt
@@ -1,6 +1,8 @@
package org.oppia.app.help
+import android.app.Application
import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
@@ -22,26 +24,79 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.firebase.FirebaseApp
+import dagger.Component
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
import org.oppia.app.help.faq.FAQListActivity
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
+import org.oppia.app.shim.ViewBindingShimModule
import org.oppia.app.utility.OrientationChangeAction.Companion.orientationLandscape
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
+import org.oppia.testing.TestAccessibilityModule
+import org.oppia.testing.TestCoroutineDispatchers
+import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.GlideImageLoaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = HelpFragmentTest.TestApplication::class, qualifiers = "port-xxhdpi")
class HelpFragmentTest {
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
@Before
fun setUp() {
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
Intents.init()
FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext())
}
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ Intents.release()
+ }
+
private fun createHelpActivityIntent(
internalProfileId: Int,
isFromNavigationDrawer: Boolean
@@ -127,8 +182,52 @@ class HelpFragmentTest {
}
}
- @After
- fun tearDown() {
- Intents.release()
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
+ @Singleton
+ @Component(
+ modules = [
+ 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, GlideImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(helpFragmentTest: HelpFragmentTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerHelpFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(helpFragmentTest: HelpFragmentTest) {
+ component.inject(helpFragmentTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
}
}
diff --git a/app/src/sharedTest/java/org/oppia/app/mydownloads/MyDownloadsFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/mydownloads/MyDownloadsFragmentTest.kt
index 29cfedc0a70..f1eb0a405f3 100644
--- a/app/src/sharedTest/java/org/oppia/app/mydownloads/MyDownloadsFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/mydownloads/MyDownloadsFragmentTest.kt
@@ -1,9 +1,10 @@
package org.oppia.app.mydownloads
import android.app.Application
-import android.content.Context
import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
import androidx.test.core.app.ActivityScenario.launch
+import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeLeft
@@ -14,26 +15,75 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
-import dagger.BindsInstance
import dagger.Component
-import dagger.Module
-import dagger.Provides
-import kotlinx.coroutines.CoroutineDispatcher
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.instanceOf
+import org.junit.After
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.app.shim.ViewBindingShimModule
import org.oppia.app.utility.EspressoTestsMatchers.matchCurrentTabTitle
-import org.oppia.util.threading.BackgroundDispatcher
-import org.oppia.util.threading.BlockingDispatcher
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
+import org.oppia.testing.TestAccessibilityModule
+import org.oppia.testing.TestCoroutineDispatchers
+import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.GlideImageLoaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
import javax.inject.Singleton
/** Tests for [MyDownloadsFragment]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = MyDownloadsFragmentTest.TestApplication::class, qualifiers = "port-xxhdpi")
class MyDownloadsFragmentTest {
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ @Before
+ fun setUp() {
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ }
@Test
fun testMyDownloadsFragment_toolbarTitle_isDisplayedSuccessfully() {
@@ -132,37 +182,52 @@ class MyDownloadsFragmentTest {
}
}
- @Module
- class TestModule {
- @Provides
- @Singleton
- fun provideContext(application: Application): Context {
- return application
- }
-
- // TODO(#89): Introduce a proper IdlingResource for background dispatchers to ensure they all complete before
- // proceeding in an Espresso test. This solution should also be interoperative with Robolectric contexts by using a
- // test coroutine dispatcher.
-
- @Singleton
- @Provides
- @BackgroundDispatcher
- fun provideBackgroundDispatcher(
- @BlockingDispatcher blockingDispatcher: CoroutineDispatcher
- ): CoroutineDispatcher {
- return blockingDispatcher
- }
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
}
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
@Singleton
- @Component(modules = [TestModule::class])
- interface TestApplicationComponent {
+ @Component(
+ modules = [
+ 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, GlideImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
@Component.Builder
- interface Builder {
- @BindsInstance
- fun setApplication(application: Application): Builder
+ interface Builder : ApplicationComponent.Builder
- fun build(): TestApplicationComponent
+ fun inject(myDownloadsFragmentTest: MyDownloadsFragmentTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerMyDownloadsFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(myDownloadsFragmentTest: MyDownloadsFragmentTest) {
+ component.inject(myDownloadsFragmentTest)
}
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
}
}
diff --git a/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt b/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt
index f5cffce698f..8426fea61f7 100644
--- a/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt
@@ -2,11 +2,11 @@ package org.oppia.app.parser
import android.app.Activity
import android.app.Application
-import android.content.Context
import android.content.Intent
import android.text.Spannable
import android.text.style.ImageSpan
import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
@@ -18,10 +18,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import com.google.common.truth.Truth.assertThat
import dagger.Binds
-import dagger.BindsInstance
import dagger.Component
import dagger.Module
-import dagger.Provides
import org.hamcrest.Matchers.not
import org.junit.After
import org.junit.Before
@@ -29,20 +27,49 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.app.shim.ViewBindingShimModule
import org.oppia.app.testing.HtmlParserTestActivity
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
+import org.oppia.testing.TestAccessibilityModule
+import org.oppia.testing.TestCoroutineDispatchers
import org.oppia.testing.TestDispatcherModule
-import org.oppia.util.caching.CacheAssetsLocally
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.DefaultResourceBucketName
-import org.oppia.util.logging.EnableConsoleLog
-import org.oppia.util.logging.EnableFileLog
-import org.oppia.util.logging.GlobalLogLevel
-import org.oppia.util.logging.LogLevel
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.CustomBulletSpan
-import org.oppia.util.parser.DefaultGcsPrefix
import org.oppia.util.parser.GlideImageLoader
import org.oppia.util.parser.HtmlParser
-import org.oppia.util.parser.ImageDownloadUrlTemplate
+import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageLoader
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
import javax.inject.Singleton
@@ -51,10 +78,14 @@ import javax.inject.Singleton
/** Tests for [HtmlParser]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = HtmlParserTest.TestApplication::class, qualifiers = "port-xxhdpi")
class HtmlParserTest {
private lateinit var launchedActivity: Activity
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
@Inject
lateinit var htmlParserFactory: HtmlParser.Factory
@@ -70,27 +101,18 @@ class HtmlParserTest {
@Before
fun setUp() {
setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
Intents.init()
val intent = Intent(Intent.ACTION_PICK)
launchedActivity = activityTestRule.launchActivity(intent)
- DaggerHtmlParserTest_TestApplicationComponent.builder()
- .setApplication(ApplicationProvider.getApplicationContext())
- .build()
- .inject(this)
}
@After
fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
Intents.release()
}
- private fun setUpTestApplicationComponent() {
- DaggerHtmlParserTest_TestApplicationComponent.builder()
- .setApplication(ApplicationProvider.getApplicationContext())
- .build()
- .inject(this)
- }
-
@Test
fun testHtmlContent_handleCustomOppiaTags_parsedHtmlDisplaysStyledText() {
val textView = activityTestRule.activity.findViewById(
@@ -232,53 +254,8 @@ class HtmlParserTest {
assertThat(htmlResult.toString()).doesNotContain(" ")
}
- // TODO(#89): Move this to a common test application component.
- @Module
- class TestModule {
- @Provides
- @Singleton
- fun provideContext(application: Application): Context {
- return application
- }
-
- @Provides
- @CacheAssetsLocally
- fun provideCacheAssetsLocally(): Boolean = false
-
- @Provides
- @DefaultGcsPrefix
- @Singleton
- fun provideDefaultGcsPrefix(): String {
- return "https://storage.googleapis.com"
- }
-
- @Provides
- @DefaultResourceBucketName
- @Singleton
- fun provideDefaultGcsResource(): String {
- return "oppiaserver-resources"
- }
-
- @Provides
- @ImageDownloadUrlTemplate
- @Singleton
- fun provideImageDownloadUrlTemplate(): String {
- return "%s/%s/assets/image/%s"
- }
-
- // TODO(#59): Either isolate these to their own shared test module, or use the real logging
- // module in tests to avoid needing to specify these settings for tests.
- @EnableConsoleLog
- @Provides
- fun provideEnableConsoleLog(): Boolean = true
-
- @EnableFileLog
- @Provides
- fun provideEnableFileLog(): Boolean = false
-
- @GlobalLogLevel
- @Provides
- fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
}
@Module
@@ -287,17 +264,48 @@ class HtmlParserTest {
abstract fun provideGlideImageLoader(impl: GlideImageLoader): ImageLoader
}
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
@Singleton
- @Component(modules = [TestModule::class, ImageTestModule::class, TestDispatcherModule::class])
- interface TestApplicationComponent {
+ @Component(
+ modules = [
+ 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, ImageTestModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
@Component.Builder
- interface Builder {
- @BindsInstance
- fun setApplication(application: Application): Builder
+ interface Builder : ApplicationComponent.Builder
- fun build(): TestApplicationComponent
+ fun inject(htmlParserTest: HtmlParserTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerHtmlParserTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
}
- fun inject(htmlParserTest: HtmlParserTest)
+ fun inject(htmlParserTest: HtmlParserTest) {
+ component.inject(htmlParserTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
}
}
diff --git a/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt
index f43e299469d..6b92edfb66c 100644
--- a/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/player/state/StateFragmentTest.kt
@@ -103,6 +103,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.FRACTIONS_EXPLORATION_ID_1
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
@@ -126,6 +128,7 @@ import org.oppia.testing.profile.ProfileTestHelper
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -1431,7 +1434,9 @@ class StateFragmentTest {
TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, RatioInputModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigFastShowTestModule::class
+ ApplicationStartupListenerModule::class, HintsAndSolutionConfigFastShowTestModule::class,
+ WorkManagerConfigurationModule::class, LogUploadWorkerModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/sharedTest/java/org/oppia/app/profileprogress/ProfileProgressFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/profileprogress/ProfileProgressFragmentTest.kt
index 46375a9952f..6050512cce8 100644
--- a/app/src/sharedTest/java/org/oppia/app/profileprogress/ProfileProgressFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/profileprogress/ProfileProgressFragmentTest.kt
@@ -8,15 +8,13 @@ import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.net.Uri
-import android.os.Handler
-import android.os.Looper
import android.provider.MediaStore
import android.view.View
+import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
@@ -24,7 +22,6 @@ import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
-import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending
@@ -42,7 +39,6 @@ import androidx.test.espresso.util.HumanReadables
import androidx.test.espresso.util.TreeIterables
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.firebase.FirebaseApp
-import dagger.BindsInstance
import dagger.Component
import dagger.Module
import dagger.Provides
@@ -55,37 +51,70 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
import org.oppia.app.completedstorylist.CompletedStoryListActivity
import org.oppia.app.home.recentlyplayed.RecentlyPlayedActivity
import org.oppia.app.model.ProfileId
import org.oppia.app.ongoingtopiclist.OngoingTopicListActivity
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
+import org.oppia.app.shim.ViewBindingShimModule
import org.oppia.app.topic.TopicActivity
import org.oppia.app.utility.OrientationChangeAction.Companion.orientationLandscape
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.domain.topic.StoryProgressTestHelper
import org.oppia.domain.topic.TEST_STORY_ID_0
import org.oppia.domain.topic.TEST_TOPIC_ID_0
+import org.oppia.testing.TestAccessibilityModule
import org.oppia.testing.TestCoroutineDispatchers
import org.oppia.testing.TestDispatcherModule
import org.oppia.testing.TestLogReportingModule
import org.oppia.testing.profile.ProfileTestHelper
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.EnableConsoleLog
import org.oppia.util.logging.EnableFileLog
import org.oppia.util.logging.GlobalLogLevel
import org.oppia.util.logging.LogLevel
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.GlideImageLoaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
-import java.util.concurrent.AbstractExecutorService
-import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import javax.inject.Inject
import javax.inject.Singleton
/** Tests for [ProfileProgressFragment]. */
-@Config(qualifiers = "port-xxhdpi")
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = ProfileProgressFragmentTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
class ProfileProgressFragmentTest {
@Inject
@@ -108,7 +137,7 @@ class ProfileProgressFragmentTest {
fun setUp() {
Intents.init()
setUpTestApplicationComponent()
- IdlingRegistry.getInstance().register(MainThreadExecutor.countingResource)
+ testCoroutineDispatchers.registerIdlingResource()
profileTestHelper.initializeProfiles()
profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build()
FirebaseApp.initializeApp(context)
@@ -116,17 +145,10 @@ class ProfileProgressFragmentTest {
@After
fun tearDown() {
- IdlingRegistry.getInstance().unregister(MainThreadExecutor.countingResource)
+ testCoroutineDispatchers.unregisterIdlingResource()
Intents.release()
}
- private fun setUpTestApplicationComponent() {
- DaggerProfileProgressFragmentTest_TestApplicationComponent.builder()
- .setApplication(ApplicationProvider.getApplicationContext())
- .build()
- .inject(this)
- }
-
private fun createProfileProgressActivityIntent(profileId: Int): Intent {
return ProfileProgressActivity.createProfileProgressActivityIntent(
ApplicationProvider.getApplicationContext(),
@@ -137,6 +159,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressFragment_checkProfileName_profileNameIsCorrect() {
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Admin"))
onView(
atPositionOnView(R.id.profile_progress_list, 0, R.id.profile_name_text_view)
@@ -149,6 +172,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressFragment_configurationChange_checkProfileName_profileNameIsCorrect() {
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
waitForTheView(withText("Admin"))
onView(
@@ -162,6 +186,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressFragment_openProfilePictureEditDialog() {
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Admin"))
onView(
atPositionOnView(
@@ -170,6 +195,7 @@ class ProfileProgressFragmentTest {
R.id.profile_edit_image
)
).perform(click())
+ testCoroutineDispatchers.runCurrent()
onView(withText(R.string.profile_progress_edit_dialog_title)).inRoot(isDialog())
.check(matches(isDisplayed()))
}
@@ -178,6 +204,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressFragment_openProfilePictureEditDialog_configurationChange_dialogIsStillOpen() { // ktlint-disable max-line-length
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Admin"))
onView(
atPositionOnView(
@@ -186,6 +213,7 @@ class ProfileProgressFragmentTest {
R.id.profile_edit_image
)
).perform(click())
+ testCoroutineDispatchers.runCurrent()
onView(withText(R.string.profile_progress_edit_dialog_title)).inRoot(isDialog())
.check(matches(isDisplayed()))
onView(isRoot()).perform(orientationLandscape())
@@ -202,6 +230,7 @@ class ProfileProgressFragmentTest {
val activityResult = createGalleryPickActivityResultStub()
intending(expectedIntent).respondWith(activityResult)
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Admin"))
onView(
atPositionOnView(
@@ -210,6 +239,7 @@ class ProfileProgressFragmentTest {
R.id.profile_edit_image
)
).perform(click())
+ testCoroutineDispatchers.runCurrent()
onView(withText(R.string.profile_progress_edit_dialog_title)).inRoot(isDialog())
.check(matches(isDisplayed()))
onView(withText(R.string.profile_picture_edit_alert_dialog_choose_from_library))
@@ -221,6 +251,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressFragmentNoProgress_recyclerViewItem0_checkOngoingTopicsCount_countIsZero() { // ktlint-disable max-line-length
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("0"))
onView(
atPositionOnView(R.id.profile_progress_list, 0, R.id.ongoing_topics_count)
@@ -242,6 +273,7 @@ class ProfileProgressFragmentTest {
)
testCoroutineDispatchers.runCurrent()
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("2"))
onView(
atPositionOnView(R.id.profile_progress_list, 0, R.id.ongoing_topics_count)
@@ -263,7 +295,9 @@ class ProfileProgressFragmentTest {
)
testCoroutineDispatchers.runCurrent()
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("2"))
onView(
atPositionOnView(R.id.profile_progress_list, 0, R.id.ongoing_topics_count)
@@ -276,6 +310,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressFragmentNoProgress_recyclerViewItem0_checkOngoingTopicsString_descriptionIsCorrect() { // ktlint-disable max-line-length
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.topics_in_progress))
onView(
atPositionOnView(
@@ -300,6 +335,7 @@ class ProfileProgressFragmentTest {
)
testCoroutineDispatchers.runCurrent()
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.topics_in_progress))
onView(
atPositionOnView(
@@ -324,7 +360,9 @@ class ProfileProgressFragmentTest {
)
testCoroutineDispatchers.runCurrent()
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.topics_in_progress))
onView(
atPositionOnView(
@@ -340,6 +378,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressFragmentNoProgress_recyclerViewItem0_checkCompletedStoriesCount_countIsZero() { // ktlint-disable max-line-length
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("0"))
onView(
atPositionOnView(R.id.profile_progress_list, 0, R.id.completed_stories_count)
@@ -361,6 +400,7 @@ class ProfileProgressFragmentTest {
)
testCoroutineDispatchers.runCurrent()
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("2"))
onView(
atPositionOnView(R.id.profile_progress_list, 0, R.id.completed_stories_count)
@@ -373,6 +413,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressFragmentNoProgress_recyclerViewItem0_checkCompletedStoriesString_descriptionIsCorrect() { // ktlint-disable max-line-length
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.stories_completed))
onView(
atPositionOnView(
@@ -398,6 +439,7 @@ class ProfileProgressFragmentTest {
)
testCoroutineDispatchers.runCurrent()
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.stories_completed))
onView(
atPositionOnView(
@@ -414,7 +456,9 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressActivity_changeConfiguration_recyclerViewItem1_storyNameIsCorrect() {
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
onView(withId(R.id.profile_progress_list))
.perform(scrollToPosition(1))
waitForTheView(withText("First Story"))
@@ -429,6 +473,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressActivity_recyclerViewItem1_storyNameIsCorrect() {
launch(createProfileProgressActivityIntent(0)).use {
+ testCoroutineDispatchers.runCurrent()
onView(withId(R.id.profile_progress_list)).perform(
scrollToPosition(
1
@@ -449,6 +494,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressActivity_recyclerViewItem1_topicNameIsCorrect() {
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
onView(withId(R.id.profile_progress_list)).perform(
scrollToPosition(
1
@@ -469,6 +515,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressActivity_clickRecyclerViewItem1_intentIsCorrect() {
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
onView(withId(R.id.profile_progress_list)).perform(
scrollToPosition(
1
@@ -481,6 +528,7 @@ class ProfileProgressFragmentTest {
1, R.id.topic_name_text_view
)
).perform(click())
+ testCoroutineDispatchers.runCurrent()
intended(hasComponent(TopicActivity::class.java.name))
intended(hasExtra(TopicActivity.getProfileIdKey(), internalProfileId))
intended(hasExtra(TopicActivity.getTopicIdKey(), TEST_TOPIC_ID_0))
@@ -491,12 +539,14 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressActivity_recyclerViewIndex0_clickViewAll_opensRecentlyPlayedActivity() {
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Admin"))
onView(atPositionOnView(R.id.profile_progress_list, 0, R.id.view_all_text_view))
.check(
matches(withText("View All"))
)
.perform(click())
+ testCoroutineDispatchers.runCurrent()
intended(hasComponent(RecentlyPlayedActivity::class.java.name))
intended(
hasExtra(
@@ -510,6 +560,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressActivityNoProgress_recyclerViewIndex0_clickTopicCount_isNotClickable() {
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.topics_in_progress))
onView(
atPositionOnView(
@@ -525,6 +576,7 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressActivityNoProgress_recyclerViewIndex0_clickStoryCount_isNotClickable() {
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.stories_completed))
onView(
atPositionOnView(
@@ -540,7 +592,9 @@ class ProfileProgressFragmentTest {
@Test
fun testProfileProgressActivityNoProgress_recyclerViewIndex0_changeConfiguration_clickStoryCount_isNotClickable() { // ktlint-disable max-line-length
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.stories_completed))
onView(
atPositionOnView(
@@ -565,6 +619,7 @@ class ProfileProgressFragmentTest {
)
testCoroutineDispatchers.runCurrent()
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.topics_in_progress))
onView(
atPositionOnView(
@@ -573,6 +628,7 @@ class ProfileProgressFragmentTest {
R.id.ongoing_topics_container
)
).perform(click())
+ testCoroutineDispatchers.runCurrent()
intended(hasComponent(OngoingTopicListActivity::class.java.name))
intended(
hasExtra(
@@ -595,6 +651,7 @@ class ProfileProgressFragmentTest {
)
testCoroutineDispatchers.runCurrent()
launch(createProfileProgressActivityIntent(internalProfileId)).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText(R.string.stories_completed))
onView(
atPositionOnView(
@@ -603,6 +660,7 @@ class ProfileProgressFragmentTest {
R.id.completed_stories_container
)
).perform(click())
+ testCoroutineDispatchers.runCurrent()
intended(hasComponent(CompletedStoryListActivity::class.java.name))
intended(
hasExtra(
@@ -675,12 +733,6 @@ class ProfileProgressFragmentTest {
@Module
class TestModule {
- @Provides
- @Singleton
- fun provideContext(application: Application): Context {
- return application
- }
-
// TODO(#59): Either isolate these to their own shared test module, or use the real logging
// module in tests to avoid needing to specify these settings for tests.
@EnableConsoleLog
@@ -696,66 +748,51 @@ class ProfileProgressFragmentTest {
fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE
}
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
@Singleton
@Component(
modules = [
- TestModule::class, TestLogReportingModule::class, LogStorageModule::class,
- TestDispatcherModule::class
+ TestModule::class, TestDispatcherModule::class, ApplicationModule::class,
+ ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class,
+ MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class,
+ NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class,
+ ImageClickInputModule::class, InteractionsModule::class, GcsResourceModule::class,
+ GlideImageLoaderModule::class, ImageParsingModule::class, HtmlParserEntityTypeModule::class,
+ QuestionModule::class, TestLogReportingModule::class, TestAccessibilityModule::class,
+ LogStorageModule::class, CachingTestModule::class, PrimeTopicAssetsControllerModule::class,
+ ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class,
+ RatioInputModule::class, ApplicationStartupListenerModule::class,
+ LogUploadWorkerModule::class, WorkManagerConfigurationModule::class,
+ HintsAndSolutionConfigModule::class, FirebaseLogUploaderModule::class
]
)
- interface TestApplicationComponent {
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
@Component.Builder
- interface Builder {
- @BindsInstance
- fun setApplication(application: Application): Builder
-
- fun build(): TestApplicationComponent
- }
+ interface Builder : ApplicationComponent.Builder
fun inject(optionsFragmentTest: ProfileProgressFragmentTest)
}
- /* ktlint-disable max-line-length */
- // TODO(#59): Move this to a general-purpose testing library that replaces all CoroutineExecutors with an
- // Espresso-enabled executor service. This service should also allow for background threads to run in both Espresso
- // and Robolectric to help catch potential race conditions, rather than forcing parallel execution to be sequential
- // and immediate.
- // NB: This also blocks on #59 to be able to actually create a test-only library.
- /**
- * An executor service that schedules all [Runnable]s to run asynchronously on the main thread. This is based on:
- * https://android.googlesource.com/platform/packages/apps/TV/+/android-live-tv/src/com/android/tv/util/MainThreadExecutor.java.
- */
- /* ktlint-enable max-line-length */
- private object MainThreadExecutor : AbstractExecutorService() {
- override fun isTerminated(): Boolean = false
-
- private val handler = Handler(Looper.getMainLooper())
- val countingResource =
- CountingIdlingResource("main_thread_executor_counting_idling_resource")
-
- override fun execute(command: Runnable?) {
- countingResource.increment()
- handler.post {
- try {
- command?.run()
- } finally {
- countingResource.decrement()
- }
- }
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerProfileProgressFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
}
- override fun shutdown() {
- throw UnsupportedOperationException()
+ fun inject(optionsFragmentTest: ProfileProgressFragmentTest) {
+ component.inject(optionsFragmentTest)
}
- override fun shutdownNow(): MutableList {
- throw UnsupportedOperationException()
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
}
- override fun isShutdown(): Boolean = false
-
- override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean {
- throw UnsupportedOperationException()
- }
+ override fun getApplicationInjector(): ApplicationInjector = component
}
}
diff --git a/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt b/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt
index af49be4fb98..137d0bd23b6 100644
--- a/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt
@@ -4,6 +4,7 @@ import android.app.Application
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
+import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
@@ -14,25 +15,60 @@ import androidx.test.espresso.matcher.ViewMatchers.withSubstring
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
-import dagger.BindsInstance
import dagger.Component
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
import org.oppia.app.databinding.TestTextViewForIntWithDataBindingBinding
import org.oppia.app.databinding.TestTextViewForStringWithDataBindingBinding
import org.oppia.app.model.TestModel
import org.oppia.app.model.TestModel.ModelTypeCase
+import org.oppia.app.parser.HtmlParserTest
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition
+import org.oppia.app.shim.ViewBindingShimModule
import org.oppia.app.testing.BINDABLE_TEST_FRAGMENT_TAG
import org.oppia.app.testing.BindableAdapterTestActivity
import org.oppia.app.testing.BindableAdapterTestFragment
import org.oppia.app.testing.BindableAdapterTestFragmentPresenter
import org.oppia.app.testing.BindableAdapterTestViewModel
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
+import org.oppia.testing.TestAccessibilityModule
import org.oppia.testing.TestCoroutineDispatchers
import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
import javax.inject.Inject
import javax.inject.Singleton
@@ -40,6 +76,7 @@ import javax.inject.Singleton
/** Tests for [BindableAdapter]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = BindableAdapterTest.TestApplication::class, qualifiers = "port-xxhdpi")
class BindableAdapterTest {
companion object {
private val STR_VALUE_0 = TestModel.newBuilder().setStrValue("Item 0").build()
@@ -54,7 +91,8 @@ class BindableAdapterTest {
@Before
fun setUp() {
- setUpTestApplication()
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
// Ensure that the bindable fragment's test state is properly reset each time.
BindableAdapterTestFragmentPresenter.testBindableAdapter = null
@@ -62,6 +100,8 @@ class BindableAdapterTest {
@After
fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+
// Ensure that the bindable fragment's test state is properly cleaned up.
BindableAdapterTestFragmentPresenter.testBindableAdapter = null
}
@@ -264,11 +304,8 @@ class BindableAdapterTest {
}
}
- private fun setUpTestApplication() {
- DaggerBindableAdapterTest_TestApplicationComponent.builder()
- .setApplication(ApplicationProvider.getApplicationContext())
- .build()
- .inject(this)
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
}
private fun createSingleViewTypeNoDataBindingBindableAdapter(): BindableAdapter {
@@ -361,22 +398,48 @@ class BindableAdapterTest {
return activity.supportFragmentManager.findFragmentByTag(BINDABLE_TEST_FRAGMENT_TAG) as BindableAdapterTestFragment // ktlint-disable max-line-length
}
- // TODO(#89): Move this to a common test application component.
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
@Singleton
@Component(
modules = [
- TestDispatcherModule::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, HtmlParserTest.ImageTestModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
]
)
- interface TestApplicationComponent {
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
@Component.Builder
- interface Builder {
- @BindsInstance
- fun setApplication(application: Application): Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(bindableAdapterTest: BindableAdapterTest)
+ }
- fun build(): TestApplicationComponent
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerBindableAdapterTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
}
- fun inject(bindableAdapterTest: BindableAdapterTest)
+ fun inject(bindableAdapterTest: BindableAdapterTest) {
+ component.inject(bindableAdapterTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
}
}
diff --git a/app/src/sharedTest/java/org/oppia/app/splash/SplashActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/splash/SplashActivityTest.kt
index dd35e25fc0c..66bfee987dc 100644
--- a/app/src/sharedTest/java/org/oppia/app/splash/SplashActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/splash/SplashActivityTest.kt
@@ -53,6 +53,8 @@ import org.oppia.domain.onboarding.AppStartupStateController
import org.oppia.domain.onboarding.testing.ExpirationMetaDataRetrieverTestModule
import org.oppia.domain.onboarding.testing.FakeExpirationMetaDataRetriever
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.testing.TestAccessibilityModule
@@ -62,6 +64,7 @@ import org.oppia.testing.TestLogReportingModule
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -281,7 +284,9 @@ class SplashActivityTest {
TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverTestModule::class,
ViewBindingShimModule::class, RatioInputModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class,
+ LogUploadWorkerModule::class, WorkManagerConfigurationModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
diff --git a/app/src/sharedTest/java/org/oppia/app/story/StoryActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/story/StoryActivityTest.kt
index c7125def4d6..5dcb0c38ec8 100644
--- a/app/src/sharedTest/java/org/oppia/app/story/StoryActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/story/StoryActivityTest.kt
@@ -1,6 +1,8 @@
package org.oppia.app.story
+import android.app.Application
import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
@@ -15,33 +17,81 @@ import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.firebase.FirebaseApp
+import dagger.Component
import org.hamcrest.CoreMatchers.allOf
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
import org.oppia.app.player.exploration.ExplorationActivity
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.app.shim.ViewBindingShimModule
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.domain.topic.TEST_EXPLORATION_ID_1
import org.oppia.domain.topic.TEST_STORY_ID_1
import org.oppia.domain.topic.TEST_TOPIC_ID_0
+import org.oppia.testing.TestAccessibilityModule
+import org.oppia.testing.TestCoroutineDispatchers
+import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.GlideImageLoaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
/** Tests for [StoryActivity]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = StoryActivityTest.TestApplication::class, qualifiers = "port-xxhdpi")
class StoryActivityTest {
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
private val internalProfileId = 0
@Before
fun setUp() {
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
Intents.init()
FirebaseApp.initializeApp(ApplicationProvider.getApplicationContext())
}
@After
fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
Intents.release()
}
@@ -54,17 +104,20 @@ class StoryActivityTest {
TEST_STORY_ID_1
)
).use {
+ testCoroutineDispatchers.runCurrent()
onView(withId(R.id.story_chapter_list)).perform(
scrollToPosition(
1
)
)
+ testCoroutineDispatchers.runCurrent()
onView(withId(R.id.story_chapter_list)).perform(
RecyclerViewActions.actionOnItemAtPosition(
1,
click()
)
)
+ testCoroutineDispatchers.runCurrent()
intended(
allOf(
@@ -78,6 +131,10 @@ class StoryActivityTest {
}
}
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
private fun createStoryActivityIntent(
internalProfileId: Int,
topicId: String,
@@ -90,4 +147,49 @@ class StoryActivityTest {
storyId
)
}
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
+ @Singleton
+ @Component(
+ modules = [
+ 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, GlideImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(storyActivityTest: StoryActivityTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerStoryActivityTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(storyActivityTest: StoryActivityTest) {
+ component.inject(storyActivityTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
}
diff --git a/app/src/sharedTest/java/org/oppia/app/story/StoryFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/story/StoryFragmentTest.kt
index fabf3b52447..09bcc64119b 100644
--- a/app/src/sharedTest/java/org/oppia/app/story/StoryFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/story/StoryFragmentTest.kt
@@ -4,14 +4,12 @@ import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.res.Resources
-import android.os.Handler
-import android.os.Looper
import android.view.View
+import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.PerformException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
@@ -19,7 +17,6 @@ import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition
-import androidx.test.espresso.idling.CountingIdlingResource
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
@@ -29,39 +26,65 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.util.HumanReadables
import androidx.test.espresso.util.TreeIterables
import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
import com.google.firebase.FirebaseApp
-import dagger.BindsInstance
import dagger.Component
-import dagger.Module
-import dagger.Provides
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher
import org.junit.After
import org.junit.Before
+import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.oppia.app.R
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
import org.oppia.app.model.ProfileId
import org.oppia.app.player.exploration.ExplorationActivity
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView
import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.hasItemCount
-import org.oppia.app.testing.StoryFragmentTestActivity
+import org.oppia.app.shim.ViewBindingShimModule
import org.oppia.app.utility.OrientationChangeAction.Companion.orientationLandscape
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.FRACTIONS_STORY_ID_0
import org.oppia.domain.topic.FRACTIONS_TOPIC_ID
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.domain.topic.StoryProgressTestHelper
+import org.oppia.testing.TestAccessibilityModule
import org.oppia.testing.TestCoroutineDispatchers
import org.oppia.testing.TestDispatcherModule
import org.oppia.testing.TestLogReportingModule
import org.oppia.testing.profile.ProfileTestHelper
-import org.oppia.util.logging.EnableConsoleLog
-import org.oppia.util.logging.EnableFileLog
-import org.oppia.util.logging.GlobalLogLevel
-import org.oppia.util.logging.LogLevel
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.GlideImageLoaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
-import java.util.concurrent.AbstractExecutorService
-import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import javax.inject.Inject
import javax.inject.Singleton
@@ -69,6 +92,7 @@ import javax.inject.Singleton
/** Tests for [StoryFragment]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = StoryFragmentTest.TestApplication::class, qualifiers = "port-xxhdpi")
class StoryFragmentTest {
@Inject
@@ -91,7 +115,7 @@ class StoryFragmentTest {
fun setUp() {
Intents.init()
setUpTestApplicationComponent()
- IdlingRegistry.getInstance().register(MainThreadExecutor.countingResource)
+ testCoroutineDispatchers.registerIdlingResource()
profileTestHelper.initializeProfiles()
FirebaseApp.initializeApp(context)
profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build()
@@ -104,17 +128,10 @@ class StoryFragmentTest {
@After
fun tearDown() {
- IdlingRegistry.getInstance().unregister(MainThreadExecutor.countingResource)
+ testCoroutineDispatchers.unregisterIdlingResource()
Intents.release()
}
- private fun setUpTestApplicationComponent() {
- DaggerStoryFragmentTest_TestApplicationComponent.builder()
- .setApplication(ApplicationProvider.getApplicationContext())
- .build()
- .inject(this)
- }
-
private fun createStoryActivityIntent(): Intent {
return StoryActivity.createStoryActivityIntent(
ApplicationProvider.getApplicationContext(),
@@ -125,15 +142,22 @@ class StoryFragmentTest {
}
@Test
+ @Ignore("Test wasn't correct originally") // TODO(#1804): Fix this test & re-enable.
fun testStoryFragment_clickOnToolbarNavigationButton_closeActivity() {
- launch(createStoryActivityIntent()).use {
+ launch(createStoryActivityIntent()).use {
+ testCoroutineDispatchers.runCurrent()
+
onView(withId(R.id.story_toolbar)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ it.onActivity { activity -> assertThat(activity.isFinishing).isTrue() }
}
}
@Test
fun testStoryFragment_toolbarTitle_isDisplayedSuccessfully() {
- launch(createStoryActivityIntent()).use {
+ launch(createStoryActivityIntent()).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Chapter 1: What is a Fraction?"))
onView(withId(R.id.story_toolbar_title))
.check(matches(withText("Matthew Goes to the Bakery")))
@@ -142,7 +166,8 @@ class StoryFragmentTest {
@Test
fun testStoryFragment_correctStoryCountLoadedInHeader() {
- launch(createStoryActivityIntent()).use {
+ launch(createStoryActivityIntent()).use {
+ testCoroutineDispatchers.runCurrent()
val headerString: String =
getResources().getQuantityString(R.plurals.story_total_chapters, 2, 1, 2)
waitForTheView(withText("Chapter 1: What is a Fraction?"))
@@ -167,7 +192,8 @@ class StoryFragmentTest {
@Test
fun testStoryFragment_correctNumberOfStoriesLoadedInRecyclerView() {
- launch(createStoryActivityIntent()).use {
+ launch(createStoryActivityIntent()).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Chapter 1: What is a Fraction?"))
onView(withId(R.id.story_chapter_list)).check(hasItemCount(3))
}
@@ -175,8 +201,10 @@ class StoryFragmentTest {
@Test
fun testStoryFragment_changeConfiguration_textViewIsShownCorrectly() {
- launch(createStoryActivityIntent()).use {
+ launch(createStoryActivityIntent()).use {
+ testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Chapter 1: What is a Fraction?"))
onView(allOf(withId(R.id.story_chapter_list))).perform(
scrollToPosition(
@@ -193,8 +221,10 @@ class StoryFragmentTest {
@Test
fun testStoryFragment_chapterSummaryIsShownCorrectly() {
- launch(createStoryActivityIntent()).use {
+ launch(createStoryActivityIntent()).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Chapter 1: What is a Fraction?"))
+ testCoroutineDispatchers.runCurrent()
onView(allOf(withId(R.id.story_chapter_list))).perform(
scrollToPosition(
1
@@ -210,8 +240,10 @@ class StoryFragmentTest {
@Test
fun testStoryFragment_changeConfiguration_chapterSummaryIsShownCorrectly() {
- launch(createStoryActivityIntent()).use {
+ launch(createStoryActivityIntent()).use {
+ testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Chapter 1: What is a Fraction?"))
onView(allOf(withId(R.id.story_chapter_list))).perform(
scrollToPosition(
@@ -228,9 +260,11 @@ class StoryFragmentTest {
@Test
fun testStoryFragment_changeConfiguration_explorationStartCorrectly() {
- launch(createStoryActivityIntent()).use {
+ launch(createStoryActivityIntent()).use {
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Chapter 1: What is a Fraction?"))
onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
waitForTheView(withText("Chapter 1: What is a Fraction?"))
onView(allOf(withId(R.id.story_chapter_list))).perform(
scrollToPosition(
@@ -240,14 +274,17 @@ class StoryFragmentTest {
onView(
atPositionOnView(R.id.story_chapter_list, 1, R.id.story_chapter_card)
).perform(click())
+ testCoroutineDispatchers.runCurrent()
intended(hasComponent(ExplorationActivity::class.java.name))
}
}
@Test
fun testStoryFragment_changeConfiguration_correctStoryCountInHeader() {
- launch(createStoryActivityIntent()).use {
+ launch(createStoryActivityIntent()).use {
+ testCoroutineDispatchers.runCurrent()
onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
val headerString: String =
getResources().getQuantityString(R.plurals.story_total_chapters, 2, 1, 2)
onView(withId(R.id.story_chapter_list)).perform(
@@ -275,6 +312,10 @@ class StoryFragmentTest {
}
}
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
private fun getResources(): Resources {
return ApplicationProvider.getApplicationContext().resources
}
@@ -327,88 +368,48 @@ class StoryFragmentTest {
}
}
- @Module
- class TestModule {
- @Provides
- @Singleton
- fun provideContext(application: Application): Context {
- return application
- }
-
- // TODO(#59): Either isolate these to their own shared test module, or use the real logging
- // module in tests to avoid needing to specify these settings for tests.
- @EnableConsoleLog
- @Provides
- fun provideEnableConsoleLog(): Boolean = true
-
- @EnableFileLog
- @Provides
- fun provideEnableFileLog(): Boolean = false
-
- @GlobalLogLevel
- @Provides
- fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE
- }
-
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
@Singleton
@Component(
modules = [
- TestModule::class, TestLogReportingModule::class, LogStorageModule::class,
- TestDispatcherModule::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, GlideImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
]
)
- interface TestApplicationComponent {
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
@Component.Builder
- interface Builder {
- @BindsInstance
- fun setApplication(application: Application): Builder
-
- fun build(): TestApplicationComponent
- }
+ interface Builder : ApplicationComponent.Builder
fun inject(storyFragmentTest: StoryFragmentTest)
}
- // TODO(#59): Move this to a general-purpose testing library that replaces all CoroutineExecutors with an
- // Espresso-enabled executor service. This service should also allow for background threads to run in both Espresso
- // and Robolectric to help catch potential race conditions, rather than forcing parallel execution to be sequential
- // and immediate.
- // NB: This also blocks on #59 to be able to actually create a test-only library.
-
- /**
- * An executor service that schedules all [Runnable]s to run asynchronously on the main thread. This is based on:
- * https://android.googlesource.com/platform/packages/apps/TV/+/android-live-tv/src/com/android/tv/util/MainThreadExecutor.java.
- */
- private object MainThreadExecutor : AbstractExecutorService() {
- override fun isTerminated(): Boolean = false
-
- private val handler = Handler(Looper.getMainLooper())
- val countingResource =
- CountingIdlingResource("main_thread_executor_counting_idling_resource")
-
- override fun execute(command: Runnable?) {
- countingResource.increment()
- handler.post {
- try {
- command?.run()
- } finally {
- countingResource.decrement()
- }
- }
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerStoryFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
}
- override fun shutdown() {
- throw UnsupportedOperationException()
+ fun inject(storyFragmentTest: StoryFragmentTest) {
+ component.inject(storyFragmentTest)
}
- override fun shutdownNow(): MutableList {
- throw UnsupportedOperationException()
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
}
- override fun isShutdown(): Boolean = false
-
- override fun awaitTermination(timeout: Long, unit: TimeUnit?): Boolean {
- throw UnsupportedOperationException()
- }
+ override fun getApplicationInjector(): ApplicationInjector = component
}
}
diff --git a/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt
index 7141ee85e63..40ce8643692 100644
--- a/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt
@@ -67,6 +67,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionCountPerTrainingSession
import org.oppia.domain.question.QuestionTrainingSeed
import org.oppia.domain.topic.FRACTIONS_SKILL_ID_0
@@ -80,6 +82,7 @@ import org.oppia.testing.profile.ProfileTestHelper
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -346,7 +349,9 @@ class QuestionPlayerActivityTest {
TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, ApplicationStartupListenerModule::class,
- RatioInputModule::class, HintsAndSolutionConfigFastShowTestModule::class
+ RatioInputModule::class, HintsAndSolutionConfigFastShowTestModule::class,
+ WorkManagerConfigurationModule::class, FirebaseLogUploaderModule::class,
+ LogUploadWorkerModule::class
]
)
interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
diff --git a/app/src/sharedTest/java/org/oppia/app/utility/RatioExtensionsTest.kt b/app/src/sharedTest/java/org/oppia/app/utility/RatioExtensionsTest.kt
index be3a9b15ccd..508700d46d1 100644
--- a/app/src/sharedTest/java/org/oppia/app/utility/RatioExtensionsTest.kt
+++ b/app/src/sharedTest/java/org/oppia/app/utility/RatioExtensionsTest.kt
@@ -1,25 +1,69 @@
package org.oppia.app.utility
+import android.app.Application
import android.content.Context
+import androidx.appcompat.app.AppCompatActivity
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
+import dagger.Component
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
import org.oppia.app.model.RatioExpression
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.app.shim.ViewBindingShimModule
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
+import org.oppia.testing.TestAccessibilityModule
+import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.GlideImageLoaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
/** Tests for [RatioExtensions]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = RatioExtensionsTest.TestApplication::class, qualifiers = "port-xxhdpi")
class RatioExtensionsTest {
- private lateinit var context: Context
+ @Inject
+ lateinit var context: Context
@Before
fun setUp() {
- context = ApplicationProvider.getApplicationContext()
+ setUpTestApplicationComponent()
}
@Test
@@ -42,7 +86,56 @@ class RatioExtensionsTest {
).isEqualTo("1 to 2")
}
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
private fun createRatio(element: List): RatioExpression {
return RatioExpression.newBuilder().addAllRatioComponent(element).build()
}
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
+ @Singleton
+ @Component(
+ modules = [
+ 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, GlideImageLoaderModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(ratioExtensionsTest: RatioExtensionsTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerRatioExtensionsTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(ratioExtensionsTest: RatioExtensionsTest) {
+ component.inject(ratioExtensionsTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
}
diff --git a/app/src/test/java/org/oppia/app/home/HomeActivityLocalTest.kt b/app/src/test/java/org/oppia/app/home/HomeActivityLocalTest.kt
index db734e56dec..97ff0f031ac 100644
--- a/app/src/test/java/org/oppia/app/home/HomeActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/app/home/HomeActivityLocalTest.kt
@@ -36,6 +36,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.testing.FakeEventLogger
@@ -45,6 +47,7 @@ import org.oppia.testing.TestLogReportingModule
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -111,7 +114,9 @@ class HomeActivityLocalTest {
ImageClickInputModule::class, LogStorageModule::class, IntentFactoryShimModule::class,
ViewBindingShimModule::class, CachingTestModule::class, RatioInputModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/app/parser/StringToRatioParserTest.kt b/app/src/test/java/org/oppia/app/parser/StringToRatioParserTest.kt
index d7526d1fafb..834a8cd5d54 100644
--- a/app/src/test/java/org/oppia/app/parser/StringToRatioParserTest.kt
+++ b/app/src/test/java/org/oppia/app/parser/StringToRatioParserTest.kt
@@ -1,14 +1,55 @@
package org.oppia.app.parser
+import android.app.Application
import android.content.Context
+import androidx.appcompat.app.AppCompatActivity
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
+import dagger.Component
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.oppia.app.activity.ActivityComponent
+import org.oppia.app.application.ActivityComponentFactory
+import org.oppia.app.application.ApplicationComponent
+import org.oppia.app.application.ApplicationInjector
+import org.oppia.app.application.ApplicationInjectorProvider
+import org.oppia.app.application.ApplicationModule
+import org.oppia.app.application.ApplicationStartupListenerModule
import org.oppia.app.model.RatioExpression
+import org.oppia.app.player.state.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.app.shim.ViewBindingShimModule
+import org.oppia.domain.classify.InteractionsModule
+import org.oppia.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
+import org.oppia.domain.question.QuestionModule
+import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
+import org.oppia.testing.TestAccessibilityModule
+import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.caching.testing.CachingTestModule
+import org.oppia.util.gcsresource.GcsResourceModule
+import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.util.parser.HtmlParserEntityTypeModule
+import org.oppia.util.parser.ImageParsingModule
+import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
import kotlin.reflect.KClass
import kotlin.reflect.full.cast
import kotlin.test.fail
@@ -16,14 +57,16 @@ import kotlin.test.fail
/** Tests for [StringToRatioParser]. */
@RunWith(AndroidJUnit4::class)
@LooperMode(LooperMode.Mode.PAUSED)
+@Config(application = StringToRatioParserTest.TestApplication::class, qualifiers = "port-xxhdpi")
class StringToRatioParserTest {
+ @Inject lateinit var context: Context
+
private lateinit var stringToRatioParser: StringToRatioParser
- private lateinit var context: Context
@Before
fun setUp() {
- context = ApplicationProvider.getApplicationContext()
+ setUpTestApplicationComponent()
stringToRatioParser = StringToRatioParser()
}
@@ -131,6 +174,10 @@ class StringToRatioParserTest {
.contains("Incorrectly formatted ratio: a:b:c")
}
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
// TODO(#89): Move to a common test library.
private fun assertThrows(type: KClass, operation: () -> Unit): T {
try {
@@ -148,4 +195,49 @@ class StringToRatioParserTest {
private fun createRatio(element: List): RatioExpression {
return RatioExpression.newBuilder().addAllRatioComponent(element).build()
}
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ // TODO(#1675): Add NetworkModule once data module is migrated off of Moshi.
+ @Singleton
+ @Component(
+ modules = [
+ 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, HtmlParserTest.ImageTestModule::class, ImageParsingModule::class,
+ HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class,
+ TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
+ PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class,
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(stringToRatioParserTest: StringToRatioParserTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerStringToRatioParserTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(stringToRatioParserTest: StringToRatioParserTest) {
+ component.inject(stringToRatioParserTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
}
diff --git a/app/src/test/java/org/oppia/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/app/player/exploration/ExplorationActivityLocalTest.kt
index 3d4332fc900..cb2a95ae0be 100644
--- a/app/src/test/java/org/oppia/app/player/exploration/ExplorationActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/app/player/exploration/ExplorationActivityLocalTest.kt
@@ -36,6 +36,8 @@ import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.exploration.ExplorationDataController
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.domain.topic.TEST_EXPLORATION_ID_2
@@ -48,6 +50,7 @@ import org.oppia.testing.TestLogReportingModule
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.networking.NetworkConnectionUtil
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
@@ -148,7 +151,9 @@ class ExplorationActivityLocalTest {
ImageClickInputModule::class, LogStorageModule::class, IntentFactoryShimModule::class,
ViewBindingShimModule::class, CachingTestModule::class, RatioInputModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/app/player/state/StateFragmentLocalTest.kt
index 7434783724d..5b7885579b3 100644
--- a/app/src/test/java/org/oppia/app/player/state/StateFragmentLocalTest.kt
+++ b/app/src/test/java/org/oppia/app/player/state/StateFragmentLocalTest.kt
@@ -77,6 +77,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.FRACTIONS_EXPLORATION_ID_1
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
@@ -91,6 +93,7 @@ import org.oppia.testing.profile.ProfileTestHelper
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -1138,7 +1141,9 @@ class StateFragmentLocalTest {
TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, RatioInputModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
diff --git a/app/src/test/java/org/oppia/app/profile/ProfileChooserFragmentLocalTest.kt b/app/src/test/java/org/oppia/app/profile/ProfileChooserFragmentLocalTest.kt
index 8e73c5f09d7..26b97d70d23 100644
--- a/app/src/test/java/org/oppia/app/profile/ProfileChooserFragmentLocalTest.kt
+++ b/app/src/test/java/org/oppia/app/profile/ProfileChooserFragmentLocalTest.kt
@@ -38,6 +38,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.testing.FakeEventLogger
@@ -47,6 +49,7 @@ import org.oppia.testing.TestLogReportingModule
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -112,7 +115,9 @@ class ProfileChooserFragmentLocalTest {
ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, RatioInputModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class,
+ LogUploadWorkerModule::class, WorkManagerConfigurationModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
diff --git a/app/src/test/java/org/oppia/app/story/StoryActivityLocalTest.kt b/app/src/test/java/org/oppia/app/story/StoryActivityLocalTest.kt
index 7986a7f765d..acd80d853a7 100644
--- a/app/src/test/java/org/oppia/app/story/StoryActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/app/story/StoryActivityLocalTest.kt
@@ -37,6 +37,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.testing.FakeEventLogger
@@ -46,6 +48,7 @@ import org.oppia.testing.TestLogReportingModule
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -128,7 +131,9 @@ class StoryActivityLocalTest {
ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, RatioInputModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
diff --git a/app/src/test/java/org/oppia/app/testing/options/AppLanguageFragmentTest.kt b/app/src/test/java/org/oppia/app/testing/options/AppLanguageFragmentTest.kt
index a574039584a..924e4488eda 100644
--- a/app/src/test/java/org/oppia/app/testing/options/AppLanguageFragmentTest.kt
+++ b/app/src/test/java/org/oppia/app/testing/options/AppLanguageFragmentTest.kt
@@ -48,6 +48,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.testing.TestAccessibilityModule
@@ -59,6 +61,7 @@ import org.oppia.util.caching.CacheAssetsLocally
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -218,7 +221,9 @@ class AppLanguageFragmentTest {
ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, ApplicationStartupListenerModule::class,
- RatioInputModule::class, HintsAndSolutionConfigModule::class
+ RatioInputModule::class, HintsAndSolutionConfigModule::class,
+ WorkManagerConfigurationModule::class, FirebaseLogUploaderModule::class,
+ LogUploadWorkerModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/app/testing/options/DefaultAudioFragmentTest.kt b/app/src/test/java/org/oppia/app/testing/options/DefaultAudioFragmentTest.kt
index 4c95046fcec..7f7c35c0f1b 100644
--- a/app/src/test/java/org/oppia/app/testing/options/DefaultAudioFragmentTest.kt
+++ b/app/src/test/java/org/oppia/app/testing/options/DefaultAudioFragmentTest.kt
@@ -48,6 +48,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.testing.TestAccessibilityModule
@@ -59,6 +61,7 @@ import org.oppia.util.caching.CacheAssetsLocally
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -219,7 +222,9 @@ class DefaultAudioFragmentTest {
ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, ApplicationStartupListenerModule::class,
- RatioInputModule::class, HintsAndSolutionConfigModule::class
+ RatioInputModule::class, HintsAndSolutionConfigModule::class,
+ WorkManagerConfigurationModule::class, LogUploadWorkerModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/app/testing/options/ReadingTextSizeFragmentTest.kt b/app/src/test/java/org/oppia/app/testing/options/ReadingTextSizeFragmentTest.kt
index 715d1b4832c..100c5537a4d 100644
--- a/app/src/test/java/org/oppia/app/testing/options/ReadingTextSizeFragmentTest.kt
+++ b/app/src/test/java/org/oppia/app/testing/options/ReadingTextSizeFragmentTest.kt
@@ -56,6 +56,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.testing.TestAccessibilityModule
@@ -67,6 +69,7 @@ import org.oppia.util.caching.CacheAssetsLocally
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -232,7 +235,9 @@ class ReadingTextSizeFragmentTest {
ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, ApplicationStartupListenerModule::class,
- RatioInputModule::class, HintsAndSolutionConfigModule::class
+ RatioInputModule::class, HintsAndSolutionConfigModule::class,
+ WorkManagerConfigurationModule::class, FirebaseLogUploaderModule::class,
+ LogUploadWorkerModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/app/testing/player/state/StateFragmentAccessibilityTest.kt b/app/src/test/java/org/oppia/app/testing/player/state/StateFragmentAccessibilityTest.kt
index 4393e87ec40..7b63cf96382 100644
--- a/app/src/test/java/org/oppia/app/testing/player/state/StateFragmentAccessibilityTest.kt
+++ b/app/src/test/java/org/oppia/app/testing/player/state/StateFragmentAccessibilityTest.kt
@@ -42,6 +42,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.domain.topic.TEST_EXPLORATION_ID_4
@@ -56,6 +58,7 @@ import org.oppia.util.accessibility.FakeAccessibilityManager
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -170,7 +173,9 @@ class StateFragmentAccessibilityTest {
ImageClickInputModule::class, LogStorageModule::class, IntentFactoryShimModule::class,
ViewBindingShimModule::class, CachingTestModule::class, RatioInputModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent {
diff --git a/app/src/test/java/org/oppia/app/topic/info/TopicInfoFragmentLocalTest.kt b/app/src/test/java/org/oppia/app/topic/info/TopicInfoFragmentLocalTest.kt
index bb4f2998136..9657ac55aeb 100644
--- a/app/src/test/java/org/oppia/app/topic/info/TopicInfoFragmentLocalTest.kt
+++ b/app/src/test/java/org/oppia/app/topic/info/TopicInfoFragmentLocalTest.kt
@@ -35,6 +35,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.testing.FakeEventLogger
@@ -44,6 +46,7 @@ import org.oppia.testing.TestLogReportingModule
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -116,7 +119,9 @@ class TopicInfoFragmentLocalTest {
ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, RatioInputModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
diff --git a/app/src/test/java/org/oppia/app/topic/lessons/TopicLessonsFragmentLocalTest.kt b/app/src/test/java/org/oppia/app/topic/lessons/TopicLessonsFragmentLocalTest.kt
index 5199c679aa3..14c7d10b147 100644
--- a/app/src/test/java/org/oppia/app/topic/lessons/TopicLessonsFragmentLocalTest.kt
+++ b/app/src/test/java/org/oppia/app/topic/lessons/TopicLessonsFragmentLocalTest.kt
@@ -34,6 +34,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.testing.FakeEventLogger
@@ -43,6 +45,7 @@ import org.oppia.testing.TestLogReportingModule
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -118,7 +121,9 @@ class TopicLessonsFragmentLocalTest {
ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, RatioInputModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, LogUploadWorkerModule::class,
+ WorkManagerConfigurationModule::class, HintsAndSolutionConfigModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
diff --git a/app/src/test/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt b/app/src/test/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt
index 4d827c6ba96..5c16d901250 100644
--- a/app/src/test/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityLocalTest.kt
@@ -52,6 +52,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
import org.oppia.domain.topic.TEST_SKILL_ID_1
@@ -63,6 +65,7 @@ import org.oppia.testing.profile.ProfileTestHelper
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -271,7 +274,9 @@ class QuestionPlayerActivityLocalTest {
TestAccessibilityModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, ApplicationStartupListenerModule::class,
- RatioInputModule::class, HintsAndSolutionConfigModule::class
+ RatioInputModule::class, HintsAndSolutionConfigModule::class,
+ LogUploadWorkerModule::class, WorkManagerConfigurationModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
diff --git a/app/src/test/java/org/oppia/app/topic/revisioncard/RevisionCardActivityLocalTest.kt b/app/src/test/java/org/oppia/app/topic/revisioncard/RevisionCardActivityLocalTest.kt
index ed169c20dac..afc021b45a4 100644
--- a/app/src/test/java/org/oppia/app/topic/revisioncard/RevisionCardActivityLocalTest.kt
+++ b/app/src/test/java/org/oppia/app/topic/revisioncard/RevisionCardActivityLocalTest.kt
@@ -34,6 +34,8 @@ import org.oppia.domain.classify.rules.ratioinput.RatioInputModule
import org.oppia.domain.classify.rules.textinput.TextInputRuleModule
import org.oppia.domain.onboarding.ExpirationMetaDataRetrieverModule
import org.oppia.domain.oppialogger.LogStorageModule
+import org.oppia.domain.oppialogger.loguploader.LogUploadWorkerModule
+import org.oppia.domain.oppialogger.loguploader.WorkManagerConfigurationModule
import org.oppia.domain.question.QuestionModule
import org.oppia.domain.topic.FRACTIONS_TOPIC_ID
import org.oppia.domain.topic.PrimeTopicAssetsControllerModule
@@ -45,6 +47,7 @@ import org.oppia.testing.TestLogReportingModule
import org.oppia.util.caching.testing.CachingTestModule
import org.oppia.util.gcsresource.GcsResourceModule
import org.oppia.util.logging.LoggerModule
+import org.oppia.util.logging.firebase.FirebaseLogUploaderModule
import org.oppia.util.parser.GlideImageLoaderModule
import org.oppia.util.parser.HtmlParserEntityTypeModule
import org.oppia.util.parser.ImageParsingModule
@@ -108,7 +111,9 @@ class RevisionCardActivityLocalTest {
ImageClickInputModule::class, LogStorageModule::class, CachingTestModule::class,
PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class,
ViewBindingShimModule::class, RatioInputModule::class,
- ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class
+ ApplicationStartupListenerModule::class, HintsAndSolutionConfigModule::class,
+ LogUploadWorkerModule::class, WorkManagerConfigurationModule::class,
+ FirebaseLogUploaderModule::class
]
)
interface TestApplicationComponent : ApplicationComponent, ApplicationInjector {
diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel
index 43b3ea816d3..8993b7f16c1 100644
--- a/domain/BUILD.bazel
+++ b/domain/BUILD.bazel
@@ -18,6 +18,7 @@ kt_android_library(
deps = [
":dagger",
"//data:persistent_cache_store",
+ artifact("androidx.work:work-runtime-ktx:2.4.0"),
],
visibility = ["//visibility:public"],
)
@@ -31,6 +32,7 @@ TEST_DEPS = [
"@robolectric//bazel:android-all",
artifact("androidx.arch.core:core-testing"),
artifact("androidx.test.ext:junit"),
+ artifact("androidx.work:work-testing:2.4.0"),
artifact("com.google.truth:truth"),
artifact("org.jetbrains.kotlin:kotlin-test-junit"),
artifact("org.jetbrains.kotlin:kotlin-reflect"),
diff --git a/domain/build.gradle b/domain/build.gradle
index 6a75af33510..a2294c48bd3 100644
--- a/domain/build.gradle
+++ b/domain/build.gradle
@@ -48,6 +48,7 @@ dependencies {
'androidx.appcompat:appcompat:1.0.2',
'androidx.exifinterface:exifinterface:1.0.0-rc01',
'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03',
+ 'androidx.work:work-runtime-ktx:2.4.0',
'com.google.dagger:dagger:2.24',
'com.google.firebase:firebase-analytics-ktx:17.5.0',
'com.google.firebase:firebase-crashlytics:17.0.0',
@@ -57,6 +58,7 @@ dependencies {
'android.arch.core:core-testing:1.1.1',
'androidx.test.espresso:espresso-core:3.2.0',
'androidx.test.ext:junit:1.1.1',
+ 'androidx.work:work-testing:2.4.0',
'com.google.dagger:dagger:2.24',
'com.google.truth:truth:0.43',
'junit:junit:4.12',
diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/analytics/AnalyticsController.kt b/domain/src/main/java/org/oppia/domain/oppialogger/analytics/AnalyticsController.kt
index 1c5f5b10b61..4ec6b713b80 100644
--- a/domain/src/main/java/org/oppia/domain/oppialogger/analytics/AnalyticsController.kt
+++ b/domain/src/main/java/org/oppia/domain/oppialogger/analytics/AnalyticsController.kt
@@ -1,14 +1,12 @@
package org.oppia.domain.oppialogger.analytics
-import androidx.lifecycle.LiveData
import org.oppia.app.model.EventLog
import org.oppia.app.model.EventLog.EventAction
import org.oppia.app.model.EventLog.Priority
import org.oppia.app.model.OppiaEventLogs
import org.oppia.data.persistence.PersistentCacheStore
import org.oppia.domain.oppialogger.EventLogStorageCacheSize
-import org.oppia.util.data.AsyncResult
-import org.oppia.util.data.DataProviders
+import org.oppia.util.data.DataProvider
import org.oppia.util.logging.ConsoleLogger
import org.oppia.util.logging.EventLogger
import org.oppia.util.logging.ExceptionLogger
@@ -23,7 +21,6 @@ import javax.inject.Inject
class AnalyticsController @Inject constructor(
private val eventLogger: EventLogger,
cacheStoreFactory: PersistentCacheStore.Factory,
- private val dataProviders: DataProviders,
private val consoleLogger: ConsoleLogger,
private val networkConnectionUtil: NetworkConnectionUtil,
private val exceptionLogger: ExceptionLogger,
@@ -154,11 +151,32 @@ class AnalyticsController @Inject constructor(
oppiaEventLogs.eventLogList.withIndex()
.minBy { it.value.timestamp }?.index
+ /** Returns a data provider for log reports that have been recorded for upload. */
+ fun getEventLogStore(): DataProvider {
+ return eventLogStore
+ }
+
/**
- * Returns a [LiveData] result which can be used to get [OppiaEventLogs]
- * for the purpose of uploading in the presence of network connectivity.
+ * Returns a list of event log reports that have been recorded for upload.
+ *
+ * As we are using the await call on the deferred output of readDataAsync, the failure case would be caught and it'll throw an error.
*/
- fun getEventLogs(): LiveData> {
- return dataProviders.convertToLiveData(eventLogStore)
+ suspend fun getEventLogStoreList(): MutableList {
+ return eventLogStore.readDataAsync().await().eventLogList
+ }
+
+ /** Removes the first event log report that had been recorded for upload. */
+ fun removeFirstEventLogFromStore() {
+ eventLogStore.storeDataAsync(updateInMemoryCache = true) { oppiaEventLogs ->
+ return@storeDataAsync oppiaEventLogs.toBuilder().removeEventLog(0).build()
+ }.invokeOnCompletion {
+ it?.let {
+ consoleLogger.e(
+ "Analytics Controller",
+ "Failed to remove event log",
+ it
+ )
+ }
+ }
}
}
diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/exceptions/ExceptionsController.kt b/domain/src/main/java/org/oppia/domain/oppialogger/exceptions/ExceptionsController.kt
index 6d6a0068399..b4b3e4face1 100644
--- a/domain/src/main/java/org/oppia/domain/oppialogger/exceptions/ExceptionsController.kt
+++ b/domain/src/main/java/org/oppia/domain/oppialogger/exceptions/ExceptionsController.kt
@@ -1,6 +1,5 @@
package org.oppia.domain.oppialogger.exceptions
-import androidx.lifecycle.LiveData
import org.oppia.app.model.ExceptionLog
import org.oppia.app.model.ExceptionLog.ExceptionType
import org.oppia.app.model.OppiaExceptionLogs
@@ -159,13 +158,34 @@ class ExceptionsController @Inject constructor(
oppiaExceptionLogs.exceptionLogList.withIndex()
.minBy { it.value.timestampInMillis }?.index
- /**
- * Returns a [LiveData] result which can be used to get [OppiaExceptionLogs]
- * for the purpose of uploading in the presence of network connectivity.
- */
+ /** Returns a data provider for exception log reports that have been recorded for upload. */
fun getExceptionLogStore(): DataProvider {
return exceptionLogStore
}
+
+ /**
+ * Returns a list of exception log reports which have been recorded for upload.
+ *
+ * As we are using the await call on the deferred output of readDataAsync, the failure case would be caught and it'll throw an error.
+ */
+ suspend fun getExceptionLogStoreList(): MutableList {
+ return exceptionLogStore.readDataAsync().await().exceptionLogList
+ }
+
+ /** Removes the first exception log report that had been recorded for upload. */
+ fun removeFirstExceptionLogFromStore() {
+ exceptionLogStore.storeDataAsync(updateInMemoryCache = true) { oppiaExceptionLogs ->
+ return@storeDataAsync oppiaExceptionLogs.toBuilder().removeExceptionLog(0).build()
+ }.invokeOnCompletion {
+ it?.let {
+ consoleLogger.e(
+ "Analytics Controller",
+ "Failed to remove event log",
+ it
+ )
+ }
+ }
+ }
}
/** Returns an [Exception] for an [ExceptionLog] object. */
diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializer.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializer.kt
new file mode 100644
index 00000000000..604c96e713c
--- /dev/null
+++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializer.kt
@@ -0,0 +1,74 @@
+package org.oppia.domain.oppialogger.loguploader
+
+import android.content.Context
+import androidx.work.Constraints
+import androidx.work.Data
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import org.oppia.domain.oppialogger.ApplicationStartupListener
+import org.oppia.util.logging.LogUploader
+import java.util.UUID
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Enqueues unique periodic work requests for uploading events and exceptions to the remote service on application creation. */
+@Singleton
+class LogUploadWorkManagerInitializer @Inject constructor(
+ private val context: Context,
+ private val logUploader: LogUploader
+) : ApplicationStartupListener {
+
+ private val logUploadWorkerConstraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .setRequiresBatteryNotLow(true)
+ .build()
+
+ private val workerCaseForUploadingEvents: Data = Data.Builder()
+ .putString(
+ LogUploadWorker.WORKER_CASE_KEY,
+ LogUploadWorker.EVENT_WORKER
+ )
+ .build()
+
+ private val workerCaseForUploadingExceptions: Data = Data.Builder()
+ .putString(
+ LogUploadWorker.WORKER_CASE_KEY,
+ LogUploadWorker.EXCEPTION_WORKER
+ )
+ .build()
+
+ private val workRequestForUploadingEvents: PeriodicWorkRequest = PeriodicWorkRequest
+ .Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS)
+ .setInputData(workerCaseForUploadingEvents)
+ .setConstraints(logUploadWorkerConstraints)
+ .build()
+
+ private val workRequestForUploadingExceptions: PeriodicWorkRequest = PeriodicWorkRequest
+ .Builder(LogUploadWorker::class.java, 6, TimeUnit.HOURS)
+ .setInputData(workerCaseForUploadingExceptions)
+ .setConstraints(logUploadWorkerConstraints)
+ .build()
+
+ override fun onCreate() {
+ val workManager = WorkManager.getInstance(context)
+ logUploader.enqueueWorkRequestForEvents(workManager, workRequestForUploadingEvents)
+ logUploader.enqueueWorkRequestForExceptions(workManager, workRequestForUploadingExceptions)
+ }
+
+ /** Returns the worker constraints set for the log uploading work requests. */
+ fun getLogUploadWorkerConstraints(): Constraints = logUploadWorkerConstraints
+
+ /** Returns the [UUID] of the work request that is enqueued for uploading event logs. */
+ fun getWorkRequestForEventsId(): UUID = workRequestForUploadingEvents.id
+
+ /** Returns the [UUID] of the work request that is enqueued for uploading exception logs. */
+ fun getWorkRequestForExceptionsId(): UUID = workRequestForUploadingExceptions.id
+
+ /** Returns the [Data] that goes into the work request that is enqueued for uploading event logs. */
+ fun getWorkRequestDataForEvents(): Data = workerCaseForUploadingEvents
+
+ /** Returns the [Data] that goes into the work request that is enqueued for uploading exception logs. */
+ fun getWorkRequestDataForExceptions(): Data = workerCaseForUploadingExceptions
+}
diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorker.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorker.kt
new file mode 100644
index 00000000000..d0c1754bd6f
--- /dev/null
+++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorker.kt
@@ -0,0 +1,105 @@
+package org.oppia.domain.oppialogger.loguploader
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import org.oppia.domain.oppialogger.analytics.AnalyticsController
+import org.oppia.domain.oppialogger.exceptions.ExceptionsController
+import org.oppia.domain.oppialogger.exceptions.toException
+import org.oppia.util.logging.ConsoleLogger
+import org.oppia.util.logging.EventLogger
+import org.oppia.util.logging.ExceptionLogger
+import org.oppia.util.threading.BackgroundDispatcher
+import javax.inject.Inject
+
+/** Worker class that extracts log reports from the cache store and logs them to the remote service. */
+class LogUploadWorker private constructor(
+ context: Context,
+ params: WorkerParameters,
+ private val analyticsController: AnalyticsController,
+ private val exceptionsController: ExceptionsController,
+ private val exceptionLogger: ExceptionLogger,
+ private val eventLogger: EventLogger,
+ private val consoleLogger: ConsoleLogger,
+ @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher
+) : CoroutineWorker(context, params) {
+
+ companion object {
+ const val WORKER_CASE_KEY = "worker_case_key"
+ const val TAG = "LogUploadWorker.tag"
+ const val EVENT_WORKER = "event_worker"
+ const val EXCEPTION_WORKER = "exception_worker"
+ }
+
+ override suspend fun doWork(): Result {
+ return when (inputData.getString(WORKER_CASE_KEY)) {
+ EVENT_WORKER -> {
+ withContext(backgroundDispatcher) { uploadEvents() }
+ }
+ EXCEPTION_WORKER -> {
+ withContext(backgroundDispatcher) { uploadExceptions() }
+ }
+ else -> Result.failure()
+ }
+ }
+
+ /** Extracts exception logs from the cache store and logs them to the remote service. */
+ private suspend fun uploadExceptions(): Result {
+ return try {
+ val exceptionLogs = exceptionsController.getExceptionLogStoreList()
+ exceptionLogs.let {
+ for (exceptionLog in it) {
+ exceptionLogger.logException(exceptionLog.toException())
+ exceptionsController.removeFirstExceptionLogFromStore()
+ }
+ }
+ Result.success()
+ } catch (e: Exception) {
+ consoleLogger.e(TAG, e.toString(), e)
+ Result.failure()
+ }
+ }
+
+ /** Extracts event logs from the cache store and logs them to the remote service. */
+ private suspend fun uploadEvents(): Result {
+ return try {
+ val eventLogs = analyticsController.getEventLogStoreList()
+ eventLogs.let {
+ for (eventLog in it) {
+ eventLogger.logEvent(eventLog)
+ analyticsController.removeFirstEventLogFromStore()
+ }
+ }
+ Result.success()
+ } catch (e: Exception) {
+ consoleLogger.e(TAG, "Failed to upload events", e)
+ Result.failure()
+ }
+ }
+
+ /** Creates an instance of [LogUploadWorker] by properly injecting dependencies. */
+ class Factory @Inject constructor(
+ private val analyticsController: AnalyticsController,
+ private val exceptionsController: ExceptionsController,
+ private val exceptionLogger: ExceptionLogger,
+ private val eventLogger: EventLogger,
+ private val consoleLogger: ConsoleLogger,
+ @BackgroundDispatcher private val backgroundDispatcher: CoroutineDispatcher
+ ) {
+
+ fun create(context: Context, params: WorkerParameters): CoroutineWorker {
+ return LogUploadWorker(
+ context,
+ params,
+ analyticsController,
+ exceptionsController,
+ exceptionLogger,
+ eventLogger,
+ consoleLogger,
+ backgroundDispatcher
+ )
+ }
+ }
+}
diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt
new file mode 100644
index 00000000000..caf751cb63d
--- /dev/null
+++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerFactory.kt
@@ -0,0 +1,22 @@
+package org.oppia.domain.oppialogger.loguploader
+
+import android.content.Context
+import androidx.work.ListenableWorker
+import androidx.work.WorkerFactory
+import androidx.work.WorkerParameters
+import javax.inject.Inject
+
+/** Custom [WorkerFactory] for the [LogUploadWorker]. */
+class LogUploadWorkerFactory @Inject constructor(
+ private val workerFactory: LogUploadWorker.Factory
+) : WorkerFactory() {
+
+ /** Returns a new [LogUploadWorker] for the given context and parameters. */
+ override fun createWorker(
+ appContext: Context,
+ workerClassName: String,
+ workerParameters: WorkerParameters
+ ): ListenableWorker? {
+ return workerFactory.create(appContext, workerParameters)
+ }
+}
diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerModule.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerModule.kt
new file mode 100644
index 00000000000..1f3fb412864
--- /dev/null
+++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerModule.kt
@@ -0,0 +1,17 @@
+package org.oppia.domain.oppialogger.loguploader
+
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+import org.oppia.domain.oppialogger.ApplicationStartupListener
+
+/** Provides [LogUploadWorker] related dependencies. */
+@Module
+interface LogUploadWorkerModule {
+
+ @Binds
+ @IntoSet
+ fun bindLogUploadWorkRequest(
+ logUploadWorkManagerInitializer: LogUploadWorkManagerInitializer
+ ): ApplicationStartupListener
+}
diff --git a/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/WorkManagerConfigurationModule.kt b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/WorkManagerConfigurationModule.kt
new file mode 100644
index 00000000000..7617554770b
--- /dev/null
+++ b/domain/src/main/java/org/oppia/domain/oppialogger/loguploader/WorkManagerConfigurationModule.kt
@@ -0,0 +1,19 @@
+package org.oppia.domain.oppialogger.loguploader
+
+import androidx.work.Configuration
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+/** Provides [Configuration] for the work manager. */
+@Module
+class WorkManagerConfigurationModule {
+
+ @Singleton
+ @Provides
+ fun provideWorkManagerConfiguration(
+ logUploadWorkerFactory: LogUploadWorkerFactory
+ ): Configuration {
+ return Configuration.Builder().setWorkerFactory(logUploadWorkerFactory).build()
+ }
+}
diff --git a/domain/src/test/java/org/oppia/domain/oppialogger/analytics/AnalyticsControllerTest.kt b/domain/src/test/java/org/oppia/domain/oppialogger/analytics/AnalyticsControllerTest.kt
index 1a98b9875a5..90efa104d5f 100644
--- a/domain/src/test/java/org/oppia/domain/oppialogger/analytics/AnalyticsControllerTest.kt
+++ b/domain/src/test/java/org/oppia/domain/oppialogger/analytics/AnalyticsControllerTest.kt
@@ -38,6 +38,7 @@ import org.oppia.testing.TestCoroutineDispatchers
import org.oppia.testing.TestDispatcherModule
import org.oppia.testing.TestLogReportingModule
import org.oppia.util.data.AsyncResult
+import org.oppia.util.data.DataProviders
import org.oppia.util.logging.EnableConsoleLog
import org.oppia.util.logging.EnableFileLog
import org.oppia.util.logging.GlobalLogLevel
@@ -82,6 +83,9 @@ class AnalyticsControllerTest {
@Inject
lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+ @Inject
+ lateinit var dataProviders: DataProviders
+
@Mock
lateinit var mockOppiaEventLogsObserver: Observer>
@@ -366,7 +370,7 @@ class AnalyticsControllerTest {
)
)
- val eventLogs = analyticsController.getEventLogs()
+ val eventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore())
eventLogs.observeForever(this.mockOppiaEventLogsObserver)
testCoroutineDispatchers.advanceUntilIdle()
verify(
@@ -396,7 +400,7 @@ class AnalyticsControllerTest {
)
)
- val eventLogs = analyticsController.getEventLogs()
+ val eventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore())
eventLogs.observeForever(this.mockOppiaEventLogsObserver)
testCoroutineDispatchers.advanceUntilIdle()
verify(
@@ -417,7 +421,7 @@ class AnalyticsControllerTest {
networkConnectionUtil.setCurrentConnectionStatus(NONE)
logMultipleEvents()
- val eventLogs = analyticsController.getEventLogs()
+ val eventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore())
eventLogs.observeForever(this.mockOppiaEventLogsObserver)
testCoroutineDispatchers.advanceUntilIdle()
verify(
@@ -453,7 +457,7 @@ class AnalyticsControllerTest {
)
)
- val eventLogs = analyticsController.getEventLogs()
+ val eventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore())
eventLogs.observeForever(this.mockOppiaEventLogsObserver)
testCoroutineDispatchers.advanceUntilIdle()
verify(
@@ -494,7 +498,7 @@ class AnalyticsControllerTest {
)
)
- val cachedEventLogs = analyticsController.getEventLogs()
+ val cachedEventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore())
cachedEventLogs.observeForever(this.mockOppiaEventLogsObserver)
testCoroutineDispatchers.advanceUntilIdle()
verify(
@@ -523,7 +527,7 @@ class AnalyticsControllerTest {
networkConnectionUtil.setCurrentConnectionStatus(NONE)
logMultipleEvents()
- val cachedEventLogs = analyticsController.getEventLogs()
+ val cachedEventLogs = dataProviders.convertToLiveData(analyticsController.getEventLogStore())
cachedEventLogs.observeForever(this.mockOppiaEventLogsObserver)
testCoroutineDispatchers.advanceUntilIdle()
verify(
diff --git a/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt b/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt
new file mode 100644
index 00000000000..2e8df398651
--- /dev/null
+++ b/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkManagerInitializerTest.kt
@@ -0,0 +1,255 @@
+package org.oppia.domain.oppialogger.loguploader
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.work.Configuration
+import androidx.work.Constraints
+import androidx.work.Data
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import androidx.work.testing.SynchronousExecutor
+import androidx.work.testing.WorkManagerTestInitHelper
+import com.google.common.truth.Truth.assertThat
+import dagger.Binds
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.domain.oppialogger.EventLogStorageCacheSize
+import org.oppia.domain.oppialogger.ExceptionLogStorageCacheSize
+import org.oppia.domain.oppialogger.OppiaLogger
+import org.oppia.domain.oppialogger.analytics.AnalyticsController
+import org.oppia.domain.oppialogger.exceptions.ExceptionsController
+import org.oppia.testing.FakeEventLogger
+import org.oppia.testing.FakeExceptionLogger
+import org.oppia.testing.TestCoroutineDispatchers
+import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.data.DataProviders
+import org.oppia.util.logging.EnableConsoleLog
+import org.oppia.util.logging.EnableFileLog
+import org.oppia.util.logging.GlobalLogLevel
+import org.oppia.util.logging.LogLevel
+import org.oppia.util.logging.LogUploader
+import org.oppia.util.networking.NetworkConnectionUtil
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import java.util.UUID
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.collections.last
+
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class LogUploadWorkManagerInitializerTest {
+
+ @Inject
+ lateinit var logUploadWorkerFactory: LogUploadWorkerFactory
+
+ @Inject
+ lateinit var logUploadWorkManagerInitializer: LogUploadWorkManagerInitializer
+
+ @Inject
+ lateinit var analyticsController: AnalyticsController
+
+ @Inject
+ lateinit var exceptionsController: ExceptionsController
+
+ @Inject
+ lateinit var networkConnectionUtil: NetworkConnectionUtil
+
+ @Inject
+ lateinit var fakeEventLogger: FakeEventLogger
+
+ @Inject
+ lateinit var fakeExceptionLogger: FakeExceptionLogger
+
+ @Inject
+ lateinit var dataProviders: DataProviders
+
+ @Inject
+ lateinit var oppiaLogger: OppiaLogger
+
+ @Inject
+ lateinit var fakeLogUploader: FakeLogUploader
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ networkConnectionUtil = NetworkConnectionUtil(ApplicationProvider.getApplicationContext())
+ setUpTestApplicationComponent()
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ val config = Configuration.Builder()
+ .setExecutor(SynchronousExecutor())
+ .setWorkerFactory(logUploadWorkerFactory)
+ .build()
+ WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
+ }
+
+ @Test
+ fun testWorkRequest_onCreate_enqueuesRequest_verifyRequestId() {
+ logUploadWorkManagerInitializer.onCreate()
+ testCoroutineDispatchers.runCurrent()
+
+ val enqueuedEventWorkRequestId = logUploadWorkManagerInitializer.getWorkRequestForEventsId()
+ val enqueuedExceptionWorkRequestId =
+ logUploadWorkManagerInitializer.getWorkRequestForExceptionsId()
+
+ assertThat(fakeLogUploader.getMostRecentEventRequestId()).isEqualTo(enqueuedEventWorkRequestId)
+ assertThat(fakeLogUploader.getMostRecentExceptionRequestId()).isEqualTo(
+ enqueuedExceptionWorkRequestId
+ )
+ }
+
+ @Test
+ fun testWorkRequest_verifyWorkerConstraints() {
+ val workerConstraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.CONNECTED)
+ .setRequiresBatteryNotLow(true)
+ .build()
+
+ val logUploadingWorkRequestConstraints =
+ logUploadWorkManagerInitializer.getLogUploadWorkerConstraints()
+
+ assertThat(logUploadingWorkRequestConstraints).isEqualTo(workerConstraints)
+ }
+
+ @Test
+ fun testWorkRequest_verifyWorkRequestDataForEvents() {
+ val workerCaseForUploadingEvents: Data = Data.Builder()
+ .putString(
+ LogUploadWorker.WORKER_CASE_KEY,
+ LogUploadWorker.EVENT_WORKER
+ )
+ .build()
+
+ assertThat(logUploadWorkManagerInitializer.getWorkRequestDataForEvents()).isEqualTo(
+ workerCaseForUploadingEvents
+ )
+ }
+
+ @Test
+ fun testWorkRequest_verifyWorkRequestDataForExceptions() {
+ val workerCaseForUploadingExceptions: Data = Data.Builder()
+ .putString(
+ LogUploadWorker.WORKER_CASE_KEY,
+ LogUploadWorker.EXCEPTION_WORKER
+ )
+ .build()
+
+ assertThat(logUploadWorkManagerInitializer.getWorkRequestDataForExceptions()).isEqualTo(
+ workerCaseForUploadingExceptions
+ )
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerLogUploadWorkManagerInitializerTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+
+ // TODO(#59): Either isolate these to their own shared test module, or use the real logging
+ // module in tests to avoid needing to specify these settings for tests.
+ @EnableConsoleLog
+ @Provides
+ fun provideEnableConsoleLog(): Boolean = true
+
+ @EnableFileLog
+ @Provides
+ fun provideEnableFileLog(): Boolean = false
+
+ @GlobalLogLevel
+ @Provides
+ fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE
+ }
+
+ @Module
+ class TestLogStorageModule {
+
+ @Provides
+ @EventLogStorageCacheSize
+ fun provideEventLogStorageCacheSize(): Int = 2
+
+ @Provides
+ @ExceptionLogStorageCacheSize
+ fun provideExceptionLogStorageSize(): Int = 2
+ }
+
+ @Module
+ interface TestFirebaseLogUploaderModule {
+
+ @Binds
+ fun bindsFakeLogUploader(fakeLogUploader: FakeLogUploader): LogUploader
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, TestLogReportingModule::class,
+ TestLogStorageModule::class, TestDispatcherModule::class,
+ LogUploadWorkerModule::class, TestFirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(logUploadWorkRequestTest: LogUploadWorkManagerInitializerTest)
+ }
+}
+
+/** A test specific fake for the log uploader. */
+@Singleton
+class FakeLogUploader @Inject constructor() :
+ LogUploader {
+
+ private val eventRequestIdList = mutableListOf()
+ private val exceptionRequestIdList = mutableListOf()
+
+ override fun enqueueWorkRequestForEvents(
+ workManager: WorkManager,
+ workRequest: PeriodicWorkRequest
+ ) {
+ eventRequestIdList.add(workRequest.id)
+ }
+
+ override fun enqueueWorkRequestForExceptions(
+ workManager: WorkManager,
+ workRequest: PeriodicWorkRequest
+ ) {
+ exceptionRequestIdList.add(workRequest.id)
+ }
+
+ /** Returns the most recent work request id that's stored in the [eventRequestIdList]. */
+ fun getMostRecentEventRequestId() = eventRequestIdList.last()
+
+ /** Returns the most recent work request id that's stored in the [exceptionRequestIdList]. */
+ fun getMostRecentExceptionRequestId() = exceptionRequestIdList.last()
+}
diff --git a/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerTest.kt b/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerTest.kt
new file mode 100644
index 00000000000..86c1ad627e5
--- /dev/null
+++ b/domain/src/test/java/org/oppia/domain/oppialogger/loguploader/LogUploadWorkerTest.kt
@@ -0,0 +1,236 @@
+package org.oppia.domain.oppialogger.loguploader
+
+import android.app.Application
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.work.Configuration
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import androidx.work.testing.SynchronousExecutor
+import androidx.work.testing.WorkManagerTestInitHelper
+import com.google.common.truth.Truth.assertThat
+import dagger.Binds
+import dagger.BindsInstance
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.app.model.EventLog
+import org.oppia.domain.oppialogger.EventLogStorageCacheSize
+import org.oppia.domain.oppialogger.ExceptionLogStorageCacheSize
+import org.oppia.domain.oppialogger.OppiaLogger
+import org.oppia.domain.oppialogger.analytics.AnalyticsController
+import org.oppia.domain.oppialogger.analytics.TEST_TIMESTAMP
+import org.oppia.domain.oppialogger.analytics.TEST_TOPIC_ID
+import org.oppia.domain.oppialogger.exceptions.ExceptionsController
+import org.oppia.testing.FakeEventLogger
+import org.oppia.testing.FakeExceptionLogger
+import org.oppia.testing.TestCoroutineDispatchers
+import org.oppia.testing.TestDispatcherModule
+import org.oppia.testing.TestLogReportingModule
+import org.oppia.util.data.DataProviders
+import org.oppia.util.logging.EnableConsoleLog
+import org.oppia.util.logging.EnableFileLog
+import org.oppia.util.logging.GlobalLogLevel
+import org.oppia.util.logging.LogLevel
+import org.oppia.util.logging.LogUploader
+import org.oppia.util.networking.NetworkConnectionUtil
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(manifest = Config.NONE)
+class LogUploadWorkerTest {
+
+ @Inject
+ lateinit var networkConnectionUtil: NetworkConnectionUtil
+
+ @Inject
+ lateinit var fakeEventLogger: FakeEventLogger
+
+ @Inject
+ lateinit var fakeExceptionLogger: FakeExceptionLogger
+
+ @Inject
+ lateinit var oppiaLogger: OppiaLogger
+
+ @Inject
+ lateinit var analyticsController: AnalyticsController
+
+ @Inject
+ lateinit var exceptionsController: ExceptionsController
+
+ @Inject
+ lateinit var logUploadWorkerFactory: LogUploadWorkerFactory
+
+ @Inject
+ lateinit var dataProviders: DataProviders
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ private lateinit var context: Context
+
+ private val eventLogTopicContext = EventLog.newBuilder()
+ .setActionName(EventLog.EventAction.EVENT_ACTION_UNSPECIFIED)
+ .setContext(
+ EventLog.Context.newBuilder()
+ .setTopicContext(
+ EventLog.TopicContext.newBuilder()
+ .setTopicId(TEST_TOPIC_ID)
+ .build()
+ )
+ .build()
+ )
+ .setPriority(EventLog.Priority.ESSENTIAL)
+ .setTimestamp(TEST_TIMESTAMP)
+ .build()
+
+ private val exception = Exception("TEST")
+
+ @Before
+ fun setUp() {
+ networkConnectionUtil = NetworkConnectionUtil(ApplicationProvider.getApplicationContext())
+ setUpTestApplicationComponent()
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ val config = Configuration.Builder()
+ .setExecutor(SynchronousExecutor())
+ .setWorkerFactory(logUploadWorkerFactory)
+ .build()
+ WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
+ }
+
+ @Test
+ fun testWorker_logEvent_withoutNetwork_enqueueRequest_verifySuccess() {
+ networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.NONE)
+ analyticsController.logTransitionEvent(
+ eventLogTopicContext.timestamp,
+ eventLogTopicContext.actionName,
+ oppiaLogger.createTopicContext(TEST_TOPIC_ID)
+ )
+
+ val workManager = WorkManager.getInstance(ApplicationProvider.getApplicationContext())
+
+ val inputData = Data.Builder().putString(
+ LogUploadWorker.WORKER_CASE_KEY,
+ LogUploadWorker.EVENT_WORKER
+ ).build()
+
+ val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder()
+ .setInputData(inputData)
+ .build()
+
+ workManager.enqueue(request)
+ testCoroutineDispatchers.runCurrent()
+ val workInfo = workManager.getWorkInfoById(request.id)
+
+ assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED)
+ assertThat(fakeEventLogger.getMostRecentEvent()).isEqualTo(eventLogTopicContext)
+ }
+
+ @Test
+ fun testWorker_logException_withoutNetwork_enqueueRequest_verifySuccess() {
+ networkConnectionUtil.setCurrentConnectionStatus(NetworkConnectionUtil.ConnectionStatus.NONE)
+ exceptionsController.logNonFatalException(exception, TEST_TIMESTAMP)
+
+ val workManager = WorkManager.getInstance(ApplicationProvider.getApplicationContext())
+
+ val inputData = Data.Builder().putString(
+ LogUploadWorker.WORKER_CASE_KEY,
+ LogUploadWorker.EXCEPTION_WORKER
+ ).build()
+
+ val request: OneTimeWorkRequest = OneTimeWorkRequestBuilder()
+ .setInputData(inputData)
+ .build()
+ workManager.enqueue(request)
+ testCoroutineDispatchers.runCurrent()
+ val workInfo = workManager.getWorkInfoById(request.id)
+ val exceptionGot = fakeExceptionLogger.getMostRecentException()
+
+ assertThat(workInfo.get().state).isEqualTo(WorkInfo.State.SUCCEEDED)
+ assertThat(exceptionGot.message).isEqualTo("TEST")
+ assertThat(exceptionGot.stackTrace).isEqualTo(exception.stackTrace)
+ assertThat(exceptionGot.cause).isEqualTo(null)
+ }
+
+ private fun setUpTestApplicationComponent() {
+ DaggerLogUploadWorkerTest_TestApplicationComponent.builder()
+ .setApplication(ApplicationProvider.getApplicationContext())
+ .build()
+ .inject(this)
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Module
+ class TestModule {
+ @Provides
+ @Singleton
+ fun provideContext(application: Application): Context {
+ return application
+ }
+
+ // TODO(#59): Either isolate these to their own shared test module, or use the real logging
+ // module in tests to avoid needing to specify these settings for tests.
+ @EnableConsoleLog
+ @Provides
+ fun provideEnableConsoleLog(): Boolean = true
+
+ @EnableFileLog
+ @Provides
+ fun provideEnableFileLog(): Boolean = false
+
+ @GlobalLogLevel
+ @Provides
+ fun provideGlobalLogLevel(): LogLevel = LogLevel.VERBOSE
+ }
+
+ @Module
+ class TestLogStorageModule {
+
+ @Provides
+ @EventLogStorageCacheSize
+ fun provideEventLogStorageCacheSize(): Int = 2
+
+ @Provides
+ @ExceptionLogStorageCacheSize
+ fun provideExceptionLogStorageSize(): Int = 2
+ }
+
+ @Module
+ interface TestFirebaseLogUploaderModule {
+
+ @Binds
+ fun bindsFakeLogUploader(fakeLogUploader: FakeLogUploader): LogUploader
+ }
+
+ // TODO(#89): Move this to a common test application component.
+ @Singleton
+ @Component(
+ modules = [
+ TestModule::class, TestLogReportingModule::class,
+ TestLogStorageModule::class, TestDispatcherModule::class,
+ LogUploadWorkerModule::class, TestFirebaseLogUploaderModule::class
+ ]
+ )
+ interface TestApplicationComponent {
+ @Component.Builder
+ interface Builder {
+ @BindsInstance
+ fun setApplication(application: Application): Builder
+ fun build(): TestApplicationComponent
+ }
+
+ fun inject(logUploadWorkerTest: LogUploadWorkerTest)
+ }
+}
diff --git a/utility/BUILD.bazel b/utility/BUILD.bazel
index e60259fe61a..57c2bc72ae8 100644
--- a/utility/BUILD.bazel
+++ b/utility/BUILD.bazel
@@ -23,6 +23,7 @@ kt_android_library(
"//app:crashlytics_deps",
artifact("org.jetbrains.kotlinx:kotlinx-coroutines-core"),
artifact("androidx.appcompat:appcompat"),
+ artifact("androidx.work:work-runtime-ktx"),
artifact("com.github.bumptech.glide:glide"),
artifact("com.caverock:androidsvg-aar"),
],
diff --git a/utility/build.gradle b/utility/build.gradle
index 3a594578e3a..e629c0ddc86 100644
--- a/utility/build.gradle
+++ b/utility/build.gradle
@@ -47,6 +47,7 @@ dependencies {
implementation(
'androidx.appcompat:appcompat:1.0.2',
'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha03',
+ 'androidx.work:work-runtime-ktx:2.4.0',
'com.caverock:androidsvg-aar:1.4',
'com.github.bumptech.glide:glide:4.11.0',
'com.google.dagger:dagger:2.24',
diff --git a/utility/src/main/java/org/oppia/util/logging/LogUploader.kt b/utility/src/main/java/org/oppia/util/logging/LogUploader.kt
new file mode 100644
index 00000000000..f32dad802b9
--- /dev/null
+++ b/utility/src/main/java/org/oppia/util/logging/LogUploader.kt
@@ -0,0 +1,14 @@
+package org.oppia.util.logging
+
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+
+/** Uploader for uploading events and exceptions to the remote service. */
+interface LogUploader {
+
+ /** Enqueues a [workRequest] using the [workManager] for uploading event logs that are stored in the cache store. */
+ fun enqueueWorkRequestForEvents(workManager: WorkManager, workRequest: PeriodicWorkRequest)
+
+ /** Enqueues a [workRequest] using the [workManager] for uploading exception logs that are stored in the cache store. */
+ fun enqueueWorkRequestForExceptions(workManager: WorkManager, workRequest: PeriodicWorkRequest)
+}
diff --git a/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploader.kt b/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploader.kt
new file mode 100644
index 00000000000..8c5e2d56b96
--- /dev/null
+++ b/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploader.kt
@@ -0,0 +1,37 @@
+package org.oppia.util.logging.firebase
+
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import org.oppia.util.logging.LogUploader
+import javax.inject.Inject
+
+private const val OPPIA_EVENT_WORK = "OPPIA_EVENT_WORK_REQUEST"
+private const val OPPIA_EXCEPTION_WORK = "OPPIA_EXCEPTION_WORK_REQUEST"
+
+/** Enqueues work requests for uploading stored event/exception logs to the remote service. */
+class FirebaseLogUploader @Inject constructor() :
+ LogUploader {
+
+ override fun enqueueWorkRequestForEvents(
+ workManager: WorkManager,
+ workRequest: PeriodicWorkRequest
+ ) {
+ workManager.enqueueUniquePeriodicWork(
+ OPPIA_EVENT_WORK,
+ ExistingPeriodicWorkPolicy.KEEP,
+ workRequest
+ )
+ }
+
+ override fun enqueueWorkRequestForExceptions(
+ workManager: WorkManager,
+ workRequest: PeriodicWorkRequest
+ ) {
+ workManager.enqueueUniquePeriodicWork(
+ OPPIA_EXCEPTION_WORK,
+ ExistingPeriodicWorkPolicy.KEEP,
+ workRequest
+ )
+ }
+}
diff --git a/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploaderModule.kt b/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploaderModule.kt
new file mode 100644
index 00000000000..9d586a128b0
--- /dev/null
+++ b/utility/src/main/java/org/oppia/util/logging/firebase/FirebaseLogUploaderModule.kt
@@ -0,0 +1,12 @@
+package org.oppia.util.logging.firebase
+
+import dagger.Binds
+import dagger.Module
+import org.oppia.util.logging.LogUploader
+
+/** Provides Log Uploader related dependencies. */
+@Module
+interface FirebaseLogUploaderModule {
+ @Binds
+ fun bindFirebaseLogUploader(firebaseLogUploader: FirebaseLogUploader): LogUploader
+}