diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 0c545bf411e..f036a892fbf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -344,6 +344,10 @@
android:name=".app.onboarding.CreateProfileActivity"
android:label="@string/create_profile_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />
+
+
+
diff --git a/app/src/main/res/layout-land/learner_intro_fragment.xml b/app/src/main/res/layout-land/learner_intro_fragment.xml
new file mode 100644
index 00000000000..1e1beb75386
--- /dev/null
+++ b/app/src/main/res/layout-land/learner_intro_fragment.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-land/learner_intro_fragment.xml b/app/src/main/res/layout-sw600dp-land/learner_intro_fragment.xml
new file mode 100644
index 00000000000..45ada6f06a3
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-land/learner_intro_fragment.xml
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-port/learner_intro_fragment.xml b/app/src/main/res/layout-sw600dp-port/learner_intro_fragment.xml
new file mode 100644
index 00000000000..59f9d0c3097
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-port/learner_intro_fragment.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/intro_activity.xml b/app/src/main/res/layout/intro_activity.xml
new file mode 100644
index 00000000000..85514b1b723
--- /dev/null
+++ b/app/src/main/res/layout/intro_activity.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/learner_intro_fragment.xml b/app/src/main/res/layout/learner_intro_fragment.xml
new file mode 100644
index 00000000000..7f0b4be7583
--- /dev/null
+++ b/app/src/main/res/layout/learner_intro_fragment.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/color_palette.xml b/app/src/main/res/values-night/color_palette.xml
index 19e760c1943..bed7ab257be 100644
--- a/app/src/main/res/values-night/color_palette.xml
+++ b/app/src/main/res/values-night/color_palette.xml
@@ -234,6 +234,10 @@
@color/color_def_jade
@color/color_def_oppia_green
@color/color_def_black
+ @color/color_def_highlight_blue_darker
+ @color/color_def_oppia_turquoise
+ @color/color_def_sky_blue
+ @color/color_def_oppia_turquoise
@color/color_def_oppia_reddish_brown
@color/color_def_accessible_light_grey_2
diff --git a/app/src/main/res/values/color_defs.xml b/app/src/main/res/values/color_defs.xml
index 7e09cb35dce..4eb98853686 100644
--- a/app/src/main/res/values/color_defs.xml
+++ b/app/src/main/res/values/color_defs.xml
@@ -149,4 +149,5 @@
#8EBBB6
#64817E
#F8BF74
+ #B3D8F1
diff --git a/app/src/main/res/values/color_palette.xml b/app/src/main/res/values/color_palette.xml
index f31cfb8734e..cb6a86c2ff9 100644
--- a/app/src/main/res/values/color_palette.xml
+++ b/app/src/main/res/values/color_palette.xml
@@ -277,6 +277,11 @@
@color/color_def_light_orange
@color/color_def_persian_green
@color/color_def_black
+ @color/color_def_sky_blue
+ @color/color_def_oppia_green
+ @color/color_def_black
+ @color/color_def_oppia_green
+
@color/color_def_accessible_light_grey_2
@color/color_def_error_text
diff --git a/app/src/main/res/values/component_colors.xml b/app/src/main/res/values/component_colors.xml
index 7ba51ce5092..6ed6c6a5df8 100644
--- a/app/src/main/res/values/component_colors.xml
+++ b/app/src/main/res/values/component_colors.xml
@@ -314,6 +314,10 @@
@color/color_palette_learner_profile_type_background_color
@color/color_palette_supervisor_profile_type_background_color
@color/color_palette_onboarding_edit_icon_color
+ @color/color_palette_onboarding_learner_intro_background_color
+ @color/color_palette_onboarding_learner_intro_header_color
+ @color/color_palette_onboarding_learner_intro_list_color
+ @color/color_palette_onboarding_learner_intro_check_color
@color/color_palette_edittext_stroke_color
@color/color_palette_text_error_color
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7dac7187713..e935ea76583 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -663,6 +663,14 @@
Current profile picture
STEP 3 OF 5
+
+ Welcome
+ Welcome, %s!
+ Learn Math through fun, story-based lessons.
+ Try practice questions to test your knowledge.
+ Get feedback to improve using %s\'s corrections.
+ STEP 4 OF 5
+
Cute otter wearing glasses.
Cute otter with books.
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 0123d3d0b7b..2231353378b 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -777,4 +777,15 @@
- @dimen/onboarding_shared_text_size_medium
- @color/component_color_shared_error_color
+
+
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
index 40f782fbd55..5e9c8ada80e 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
@@ -19,6 +19,7 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.Intents.intending
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
+import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
@@ -27,10 +28,13 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
+import com.google.protobuf.MessageLite
import dagger.Component
+import org.hamcrest.Description
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
+import org.hamcrest.TypeSafeMatcher
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -48,6 +52,7 @@ import org.oppia.android.app.application.ApplicationStartupListenerModule
import org.oppia.android.app.application.testing.TestingBuildFlavorModule
import org.oppia.android.app.devoptions.DeveloperOptionsModule
import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
+import org.oppia.android.app.model.IntroActivityParams
import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
import org.oppia.android.app.shim.ViewBindingShimModule
import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
@@ -96,6 +101,7 @@ import org.oppia.android.testing.time.FakeOppiaClockModule
import org.oppia.android.util.accessibility.AccessibilityTestModule
import org.oppia.android.util.caching.AssetModule
import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.extensions.getProtoExtra
import org.oppia.android.util.gcsresource.GcsResourceModule
import org.oppia.android.util.locale.LocaleProdModule
import org.oppia.android.util.logging.EventLoggingConfigurationModule
@@ -193,21 +199,13 @@ class CreateProfileFragmentTest {
.perform(click())
testCoroutineDispatchers.runCurrent()
- onView(withText(R.string.create_profile_activity_nickname_error))
- .check(matches(withEffectiveVisibility(Visibility.GONE)))
-
- // No screen change as the navigation to the next screen is not implemented yet.
- // This should fail in the future once the screen has been implemented.
- onView(withId(R.id.create_profile_nickname_label))
- .check(
- matches(
- withText(
- context.getString(
- R.string.create_profile_activity_nickname_label
- )
- )
- )
+ val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build()
+ intended(
+ allOf(
+ hasComponent(IntroActivity::class.java.name),
+ hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
)
+ )
}
}
@@ -259,18 +257,13 @@ class CreateProfileFragmentTest {
.perform(click())
testCoroutineDispatchers.runCurrent()
- // No screen change as the navigation to the next screen is not implemented yet.
- // This should fail in the future once the screen has been implemented.
- onView(withId(R.id.create_profile_nickname_label))
- .check(
- matches(
- withText(
- context.getString(
- R.string.create_profile_activity_nickname_label
- )
- )
- )
+ val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build()
+ intended(
+ allOf(
+ hasComponent(IntroActivity::class.java.name),
+ hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
)
+ )
}
}
@@ -307,18 +300,13 @@ class CreateProfileFragmentTest {
.perform(click())
testCoroutineDispatchers.runCurrent()
- // No screen change as the navigation to the next screen is not implemented yet.
- // This should fail in the future once the screen has been implemented.
- onView(withId(R.id.create_profile_nickname_label))
- .check(
- matches(
- withText(
- context.getString(
- R.string.create_profile_activity_nickname_label
- )
- )
- )
+ val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build()
+ intended(
+ allOf(
+ hasComponent(IntroActivity::class.java.name),
+ hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
)
+ )
}
}
@@ -373,18 +361,13 @@ class CreateProfileFragmentTest {
.perform(click())
testCoroutineDispatchers.runCurrent()
- // No screen change as the navigation to the next screen is not implemented yet.
- // This should fail in the future once the screen has been implemented.
- onView(withId(R.id.create_profile_nickname_label))
- .check(
- matches(
- withText(
- context.getString(
- R.string.create_profile_activity_nickname_label
- )
- )
- )
+ val expectedParams = IntroActivityParams.newBuilder().setProfileNickname("John").build()
+ intended(
+ allOf(
+ hasComponent(IntroActivity::class.java.name),
+ hasProtoExtra("OnboardingIntroActivity.params", expectedParams)
)
+ )
}
}
@@ -490,6 +473,20 @@ class CreateProfileFragmentTest {
return scenario
}
+ private fun hasProtoExtra(keyName: String, expectedProto: T): Matcher {
+ val defaultProto = expectedProto.newBuilderForType().build()
+ return object : TypeSafeMatcher() {
+ override fun describeTo(description: Description) {
+ description.appendText("Intent with extra: $keyName and proto value: $expectedProto")
+ }
+
+ override fun matchesSafely(intent: Intent): Boolean {
+ return intent.hasExtra(keyName) &&
+ intent.getProtoExtra(keyName, defaultProto) == expectedProto
+ }
+ }
+ }
+
private fun setUpTestApplicationComponent() {
ApplicationProvider.getApplicationContext().inject(this)
}
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt
new file mode 100644
index 00000000000..11ded15d116
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroActivityTest.kt
@@ -0,0 +1,224 @@
+package org.oppia.android.app.onboarding
+
+import android.app.Application
+import android.content.Context
+import androidx.appcompat.app.AppCompatActivity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.intent.Intents
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.R
+import org.oppia.android.app.activity.ActivityComponent
+import org.oppia.android.app.activity.ActivityComponentFactory
+import org.oppia.android.app.activity.route.ActivityRouterModule
+import org.oppia.android.app.application.ApplicationComponent
+import org.oppia.android.app.application.ApplicationInjector
+import org.oppia.android.app.application.ApplicationInjectorProvider
+import org.oppia.android.app.application.ApplicationModule
+import org.oppia.android.app.application.ApplicationStartupListenerModule
+import org.oppia.android.app.application.testing.TestingBuildFlavorModule
+import org.oppia.android.app.devoptions.DeveloperOptionsModule
+import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
+import org.oppia.android.app.model.ScreenName
+import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
+import org.oppia.android.app.shim.ViewBindingShimModule
+import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.data.backends.gae.NetworkConfigProdModule
+import org.oppia.android.data.backends.gae.NetworkModule
+import org.oppia.android.domain.classify.InteractionsModule
+import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule
+import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule
+import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule
+import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.android.domain.exploration.ExplorationProgressModule
+import org.oppia.android.domain.exploration.ExplorationStorageModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule
+import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.android.domain.oppialogger.LogStorageModule
+import org.oppia.android.domain.oppialogger.LoggingIdentifierModule
+import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule
+import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule
+import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule
+import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule
+import org.oppia.android.domain.platformparameter.PlatformParameterModule
+import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule
+import org.oppia.android.domain.question.QuestionModule
+import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
+import org.oppia.android.testing.OppiaTestRule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.firebase.TestAuthenticationModule
+import org.oppia.android.testing.junit.InitializeDefaultLocaleRule
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestCoroutineDispatchers
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
+import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.gcsresource.GcsResourceModule
+import org.oppia.android.util.locale.LocaleProdModule
+import org.oppia.android.util.logging.CurrentAppScreenNameIntentDecorator.extractCurrentAppScreenName
+import org.oppia.android.util.logging.EventLoggingConfigurationModule
+import org.oppia.android.util.logging.LoggerModule
+import org.oppia.android.util.logging.SyncStatusModule
+import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule
+import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
+import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
+import org.oppia.android.util.parser.image.GlideImageLoaderModule
+import org.oppia.android.util.parser.image.ImageParsingModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [IntroActivity]. */
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = IntroActivityTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+
+class IntroActivityTest {
+ @get:Rule
+ val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+
+ @get:Rule
+ val oppiaTestRule = OppiaTestRule()
+
+ @Inject
+ lateinit var context: Context
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ private val testProfileNickname = "John"
+
+ @Before
+ fun setUp() {
+ Intents.init()
+ setUpTestApplicationComponent()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ }
+
+ @Test
+ fun testActivity_createIntent_verifyScreenNameInIntent() {
+ val screenName =
+ IntroActivity.createIntroActivity(
+ context,
+ testProfileNickname
+ )
+ .extractCurrentAppScreenName()
+
+ assertThat(screenName).isEqualTo(ScreenName.INTRO_ACTIVITY)
+ }
+
+ @Test
+ fun testLearnerIntroActivity_hasCorrectActivityLabel() {
+ launchOnboardingLearnerIntroActivity().use { scenario ->
+ lateinit var title: CharSequence
+ scenario?.onActivity { activity -> title = activity.title }
+
+ // Verify that the activity label is correct as a proxy to verify TalkBack will announce the
+ // correct string when it's read out.
+ assertThat(title)
+ .isEqualTo(context.getString(R.string.onboarding_learner_intro_activity_title))
+ }
+ }
+
+ private fun launchOnboardingLearnerIntroActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ IntroActivity.createIntroActivity(
+ context,
+ testProfileNickname
+ )
+ )
+ testCoroutineDispatchers.runCurrent()
+ return scenario
+ }
+
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ @Singleton
+ @Component(
+ modules = [
+ RobolectricModule::class,
+ PlatformParameterModule::class, PlatformParameterSingletonModule::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,
+ AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class,
+ ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class,
+ ApplicationStartupListenerModule::class, LogReportWorkerModule::class,
+ HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class,
+ FirebaseLogUploaderModule::class, FakeOppiaClockModule::class,
+ DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
+ ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class,
+ NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class,
+ MathEquationInputModule::class, SplitScreenInteractionModule::class,
+ LoggingIdentifierModule::class, ApplicationLifecycleModule::class,
+ SyncStatusModule::class, MetricLogSchedulerModule::class, TestingBuildFlavorModule::class,
+ EventLoggingConfigurationModule::class, ActivityRouterModule::class,
+ CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class,
+ TestAuthenticationModule::class
+ ]
+ )
+
+ interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(introActivityTest: IntroActivityTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerIntroActivityTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(introActivityTest: IntroActivityTest) {
+ component.inject(introActivityTest)
+ }
+
+ 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/android/app/onboarding/IntroFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
new file mode 100644
index 00000000000..72fea853fbc
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/IntroFragmentTest.kt
@@ -0,0 +1,304 @@
+package org.oppia.android.app.onboarding
+
+import android.app.Application
+import android.content.Context
+import androidx.appcompat.app.AppCompatActivity
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions.matches
+import androidx.test.espresso.intent.Intents
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.oppia.android.R
+import org.oppia.android.app.activity.ActivityComponent
+import org.oppia.android.app.activity.ActivityComponentFactory
+import org.oppia.android.app.activity.route.ActivityRouterModule
+import org.oppia.android.app.application.ApplicationComponent
+import org.oppia.android.app.application.ApplicationInjector
+import org.oppia.android.app.application.ApplicationInjectorProvider
+import org.oppia.android.app.application.ApplicationModule
+import org.oppia.android.app.application.ApplicationStartupListenerModule
+import org.oppia.android.app.application.testing.TestingBuildFlavorModule
+import org.oppia.android.app.devoptions.DeveloperOptionsModule
+import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule
+import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule
+import org.oppia.android.app.shim.ViewBindingShimModule
+import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule
+import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape
+import org.oppia.android.data.backends.gae.NetworkConfigProdModule
+import org.oppia.android.data.backends.gae.NetworkModule
+import org.oppia.android.domain.classify.InteractionsModule
+import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule
+import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule
+import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule
+import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule
+import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule
+import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule
+import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule
+import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule
+import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule
+import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule
+import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule
+import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule
+import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule
+import org.oppia.android.domain.exploration.ExplorationProgressModule
+import org.oppia.android.domain.exploration.ExplorationStorageModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule
+import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule
+import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule
+import org.oppia.android.domain.oppialogger.LogStorageModule
+import org.oppia.android.domain.oppialogger.LoggingIdentifierModule
+import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule
+import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule
+import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule
+import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule
+import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule
+import org.oppia.android.domain.question.QuestionModule
+import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule
+import org.oppia.android.testing.OppiaTestRule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.firebase.TestAuthenticationModule
+import org.oppia.android.testing.junit.InitializeDefaultLocaleRule
+import org.oppia.android.testing.platformparameter.TestPlatformParameterModule
+import org.oppia.android.testing.robolectric.RobolectricModule
+import org.oppia.android.testing.threading.TestCoroutineDispatchers
+import org.oppia.android.testing.threading.TestDispatcherModule
+import org.oppia.android.testing.time.FakeOppiaClockModule
+import org.oppia.android.util.accessibility.AccessibilityTestModule
+import org.oppia.android.util.caching.AssetModule
+import org.oppia.android.util.caching.testing.CachingTestModule
+import org.oppia.android.util.gcsresource.GcsResourceModule
+import org.oppia.android.util.locale.LocaleProdModule
+import org.oppia.android.util.logging.EventLoggingConfigurationModule
+import org.oppia.android.util.logging.LoggerModule
+import org.oppia.android.util.logging.SyncStatusModule
+import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule
+import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule
+import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule
+import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule
+import org.oppia.android.util.parser.image.GlideImageLoaderModule
+import org.oppia.android.util.parser.image.ImageParsingModule
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [IntroFragmentTest]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = IntroFragmentTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class IntroFragmentTest {
+ @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+ @get:Rule val oppiaTestRule = OppiaTestRule()
+ @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+ @Inject lateinit var context: Context
+
+ private val testProfileNickname = "John"
+
+ @Before
+ fun setUp() {
+ Intents.init()
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ Intents.release()
+ }
+
+ @Test
+ fun testFragment_explanationText_isDisplayed() {
+ launchOnboardingLearnerIntroActivity().use {
+ onView(withId(R.id.onboarding_learner_intro_title))
+ .check(matches(withText("Welcome, John!")))
+ onView(withText(R.string.onboarding_learner_intro_classroom_text))
+ .check(matches(isDisplayed()))
+ onView(withText(R.string.onboarding_learner_intro_practice_text))
+ .check(matches(isDisplayed()))
+ onView(
+ withText(
+ context.getString(
+ R.string.onboarding_learner_intro_feedback_text,
+ context.getString(R.string.app_name)
+ )
+ )
+ ).check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_portraitMode_stepCountText_isDisplayed() {
+ launchOnboardingLearnerIntroActivity().use {
+ onView(withId(R.id.onboarding_steps_count))
+ .check(matches(isDisplayed()))
+ onView(withId(R.id.onboarding_steps_count))
+ .check(matches(withText(R.string.onboarding_step_count_four)))
+ }
+ }
+
+ @Test
+ fun testFragment_portraitMode_backButtonPressed_currentScreenIsDestroyed() {
+ launchOnboardingLearnerIntroActivity().use { scenario ->
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ scenario?.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_backButtonPressed_currentScreenIsDestroyed() {
+ launchOnboardingLearnerIntroActivity().use { scenario ->
+ onView(ViewMatchers.isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_back)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+ scenario?.onActivity { activity ->
+ assertThat(activity.isFinishing).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFragment_portraitMode_continueButtonClicked_launchesAudioLanguageScreen() {
+ launchOnboardingLearnerIntroActivity().use {
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // Do nothing for now, but will fail once navigation is implemented
+ onView(withId(R.id.onboarding_learner_intro_title))
+ .check(matches(withText("Welcome, John!")))
+ onView(withText(R.string.onboarding_learner_intro_classroom_text))
+ .check(matches(isDisplayed()))
+ onView(withText(R.string.onboarding_learner_intro_practice_text))
+ .check(matches(isDisplayed()))
+ onView(
+ withText(
+ context.getString(
+ R.string.onboarding_learner_intro_feedback_text,
+ context.getString(R.string.app_name)
+ )
+ )
+ ).check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_continueButtonClicked_launchesAudioLanguageScreen() {
+ launchOnboardingLearnerIntroActivity().use {
+ onView(ViewMatchers.isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue)).perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ // Do nothing for now, but will fail once navigation is implemented
+ onView(withId(R.id.onboarding_learner_intro_title))
+ .check(matches(withText("Welcome, John!")))
+ onView(withText(R.string.onboarding_learner_intro_classroom_text))
+ .check(matches(isDisplayed()))
+ onView(withText(R.string.onboarding_learner_intro_practice_text))
+ .check(matches(isDisplayed()))
+ onView(
+ withText(
+ context.getString(
+ R.string.onboarding_learner_intro_feedback_text,
+ context.getString(R.string.app_name)
+ )
+ )
+ ).check(matches(isDisplayed()))
+ }
+ }
+
+ private fun launchOnboardingLearnerIntroActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ IntroActivity.createIntroActivity(
+ context,
+ testProfileNickname
+ )
+ )
+ testCoroutineDispatchers.runCurrent()
+ return scenario
+ }
+
+ private fun setUpTestApplicationComponent() {
+ ApplicationProvider.getApplicationContext().inject(this)
+ }
+
+ // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them.
+ @Singleton
+ @Component(
+ modules = [
+ TestPlatformParameterModule::class, RobolectricModule::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,
+ AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class,
+ ExpirationMetaDataRetrieverModule::class,
+ ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class,
+ ApplicationStartupListenerModule::class, LogReportWorkerModule::class,
+ HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class,
+ FirebaseLogUploaderModule::class, FakeOppiaClockModule::class,
+ DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class,
+ ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class,
+ NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class,
+ AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class,
+ PlatformParameterSingletonModule::class,
+ NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class,
+ MathEquationInputModule::class, SplitScreenInteractionModule::class,
+ LoggingIdentifierModule::class, ApplicationLifecycleModule::class,
+ SyncStatusModule::class, MetricLogSchedulerModule::class, TestingBuildFlavorModule::class,
+ EventLoggingConfigurationModule::class, ActivityRouterModule::class,
+ CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class,
+ TestAuthenticationModule::class
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(introFragmentTest: IntroFragmentTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerIntroFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(introFragmentTest: IntroFragmentTest) {
+ component.inject(introFragmentTest)
+ }
+
+ override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent {
+ return component.getActivityComponentBuilderProvider().get().setActivity(activity).build()
+ }
+
+ override fun getApplicationInjector(): ApplicationInjector = component
+ }
+}
diff --git a/model/src/main/proto/arguments.proto b/model/src/main/proto/arguments.proto
index 2aca58d5e01..3784edb8b83 100644
--- a/model/src/main/proto/arguments.proto
+++ b/model/src/main/proto/arguments.proto
@@ -835,3 +835,9 @@ message WalkthroughFinalFragmentArguments {
// The ID of the topic to which the opening exploration belongs.
string topic_id = 1;
}
+
+// Params required when creating a new IntroActivity.
+message IntroActivityParams {
+ // The nickname associated with a newly created profile.
+ string profile_nickname = 1;
+}
diff --git a/model/src/main/proto/screens.proto b/model/src/main/proto/screens.proto
index 48631a4a3e4..2824e72bd6c 100644
--- a/model/src/main/proto/screens.proto
+++ b/model/src/main/proto/screens.proto
@@ -167,6 +167,9 @@ enum ScreenName {
// Screen name value for the scenario when the create new learner profile activity is visible to the user.
CREATE_PROFILE_ACTIVITY = 52;
+
+ // Screen name value for the scenario when the learner welcome activity is visible to the user.
+ INTRO_ACTIVITY = 53;
}
// Defines the current visible UI screen of the application.
diff --git a/scripts/assets/test_file_exemptions.textproto b/scripts/assets/test_file_exemptions.textproto
index 107658a4fcc..02da7bda12d 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -1085,6 +1085,13 @@ test_file_exemption {
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt"
test_file_not_required: true
+}test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/IntroActivityPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/IntroFragmentPresenter.kt"
+ test_file_not_required: true
}
test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt"
diff --git a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt
index 24f5db6dabf..96c5899da27 100644
--- a/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt
+++ b/utility/src/main/java/org/oppia/android/util/logging/EventBundleCreator.kt
@@ -857,6 +857,7 @@ class EventBundleCreator @Inject constructor(
ScreenName.CLASSROOM_LIST_ACTIVITY -> "classroom_list_activity"
ScreenName.ONBOARDING_PROFILE_TYPE_ACTIVITY -> "onboarding_profile_type_activity"
ScreenName.CREATE_PROFILE_ACTIVITY -> "create_profile_activity"
+ ScreenName.INTRO_ACTIVITY -> "intro_activity"
}
private fun AppLanguageSelection.toAnalyticsText(): String {