From df472536554d53f21181f3e564ea57c0074d7d79 Mon Sep 17 00:00:00 2001
From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com>
Date: Mon, 1 Jul 2024 01:30:19 +0300
Subject: [PATCH] Fix Part of #4938: Introduce Create profile screen (#5380)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Explanation
Fix Part of #4938: Add a new Activity and associated Fragments and
Presenters to allow a new learner to create a profile.
This does not include domain changes.
- Learner should be able to click “Continue” if they have entered their
nickname, even without adding a profile picture.
- Learner should not be able to click “Continue” if they have not
entered their nickname and an error message should be displayed.
- The learner can select a profile picture.
Placeholder tests have been added to ensure navigation tests are not
forgotten. These will fail once navigation has been implemented.
## Essential Checklist
- [x] The PR title and explanation each start with "Fix #bugnum: " (If
this PR fixes part of an issue, prefix the title with "Fix part of
#bugnum: ...".)
- [x] Any changes to
[scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets)
files have their rationale included in the PR explanation.
- [x] The PR follows the [style
guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide).
- [x] The PR does not contain any unnecessary code changes from Android
Studio
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)).
- [x] The PR is made from a branch that's **not** called "develop" and
is up-to-date with "develop".
- [x] The PR is **assigned** to the appropriate reviewers
([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)).
## For UI-specific PRs only
||||
| --- | --- | --- |
|| Portrait | Landscape |
|Mobile Light
Mode|![Screenshot_1719774012](https://github.com/oppia/oppia-android/assets/59600948/189f7e62-8761-4d38-b859-e73df23d1221)|![Screenshot_1719774028](https://github.com/oppia/oppia-android/assets/59600948/9e8b01c5-4b60-40aa-94ae-da4093241759)|
|Tablet Dark
Mode|![Screenshot_1719774236](https://github.com/oppia/oppia-android/assets/59600948/e5e161f9-77e7-4b1d-a2bc-b7b0646b71c2)|![Screenshot_1719774244](https://github.com/oppia/oppia-android/assets/59600948/97820f77-a628-48b0-a325-f929118594bc)|
## All Tests Passing on Espresso
![Screenshot 2024-05-24 at 01 42
41](https://github.com/oppia/oppia-android/assets/59600948/9e301e33-d11f-48a5-9f55-fca3e155b00e)
---
app/BUILD.bazel | 1 +
app/src/main/AndroidManifest.xml | 5 +-
.../app/activity/ActivityComponentImpl.kt | 2 +
.../app/fragment/FragmentComponentImpl.kt | 2 +
.../app/onboarding/CreateProfileActivity.kt | 32 +
.../CreateProfileActivityPresenter.kt | 39 ++
.../app/onboarding/CreateProfileFragment.kt | 38 ++
.../CreateProfileFragmentPresenter.kt | 120 ++++
.../app/onboarding/CreateProfileViewModel.kt | 14 +
.../OnboardingProfileTypeFragmentPresenter.kt | 6 +
.../drawable/create_profile_picture_icon.xml | 33 ++
...dit_text_white_background_error_border.xml | 9 +
...edit_text_white_background_with_border.xml | 9 +
.../main/res/drawable/ic_outline_edit_24.xml | 5 +
app/src/main/res/drawable/ic_profile_icon.xml | 9 +
.../layout-land/create_profile_fragment.xml | 155 +++++
.../create_profile_fragment.xml | 153 +++++
.../create_profile_fragment.xml | 159 +++++
.../res/layout/create_profile_activity.xml | 10 +
.../res/layout/create_profile_fragment.xml | 163 ++++++
.../main/res/values-night/color_palette.xml | 9 +-
app/src/main/res/values/color_defs.xml | 2 +-
app/src/main/res/values/color_palette.xml | 7 +-
app/src/main/res/values/component_colors.xml | 7 +-
app/src/main/res/values/strings.xml | 10 +
app/src/main/res/values/styles.xml | 46 ++
.../onboarding/CreateProfileActivityTest.kt | 214 +++++++
.../onboarding/CreateProfileFragmentTest.kt | 553 ++++++++++++++++++
.../OnboardingProfileTypeFragmentTest.kt | 7 +-
model/src/main/proto/screens.proto | 3 +
scripts/assets/test_file_exemptions.textproto | 12 +
.../util/logging/EventBundleCreator.kt | 1 +
32 files changed, 1823 insertions(+), 12 deletions(-)
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivity.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragment.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt
create mode 100644 app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt
create mode 100644 app/src/main/res/drawable/create_profile_picture_icon.xml
create mode 100644 app/src/main/res/drawable/edit_text_white_background_error_border.xml
create mode 100644 app/src/main/res/drawable/edit_text_white_background_with_border.xml
create mode 100644 app/src/main/res/drawable/ic_outline_edit_24.xml
create mode 100644 app/src/main/res/drawable/ic_profile_icon.xml
create mode 100644 app/src/main/res/layout-land/create_profile_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml
create mode 100644 app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml
create mode 100644 app/src/main/res/layout/create_profile_activity.xml
create mode 100644 app/src/main/res/layout/create_profile_fragment.xml
create mode 100644 app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileActivityTest.kt
create mode 100644 app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
diff --git a/app/BUILD.bazel b/app/BUILD.bazel
index 7ddcebe5300..26ee5509faf 100644
--- a/app/BUILD.bazel
+++ b/app/BUILD.bazel
@@ -211,6 +211,7 @@ VIEW_MODELS_WITH_RESOURCE_IMPORTS = [
"src/main/java/org/oppia/android/app/home/recentlyplayed/PromotedStoryViewModel.kt",
"src/main/java/org/oppia/android/app/home/recentlyplayed/RecentlyPlayedViewModel.kt",
"src/main/java/org/oppia/android/app/home/topiclist/TopicSummaryViewModel.kt",
+ "src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt",
"src/main/java/org/oppia/android/app/onboarding/OnboadingSlideViewModel.kt",
"src/main/java/org/oppia/android/app/onboarding/OnboardingViewModel.kt",
"src/main/java/org/oppia/android/app/ongoingtopiclist/OngoingTopicItemViewModel.kt",
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b25b8ccfc86..0c545bf411e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -340,7 +340,10 @@
android:name=".app.onboarding.OnboardingProfileTypeActivity"
android:label="@string/onboarding_profile_type_activity_title"
android:theme="@style/OppiaThemeWithoutActionBar" />
-
+
+ if (result.resultCode == Activity.RESULT_OK) {
+ createProfileFragmentPresenter.handleOnActivityResult(result.data)
+ }
+ }
+ return createProfileFragmentPresenter.handleCreateView(inflater, container)
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt
new file mode 100644
index 00000000000..d3a57c61988
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt
@@ -0,0 +1,120 @@
+package org.oppia.android.app.onboarding
+
+import android.content.Intent
+import android.graphics.PorterDuff
+import android.provider.MediaStore
+import android.text.Editable
+import android.text.TextWatcher
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.activity.result.ActivityResultLauncher
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.res.ResourcesCompat
+import androidx.fragment.app.Fragment
+import org.oppia.android.R
+import org.oppia.android.app.fragment.FragmentScope
+import org.oppia.android.databinding.CreateProfileFragmentBinding
+import org.oppia.android.util.parser.image.ImageLoader
+import org.oppia.android.util.parser.image.ImageViewTarget
+import javax.inject.Inject
+
+/** Presenter for [CreateProfileFragment]. */
+@FragmentScope
+class CreateProfileFragmentPresenter @Inject constructor(
+ private val fragment: Fragment,
+ private val activity: AppCompatActivity,
+ private val createProfileViewModel: CreateProfileViewModel,
+ private val imageLoader: ImageLoader
+) {
+ private lateinit var binding: CreateProfileFragmentBinding
+ private lateinit var uploadImageView: ImageView
+ private lateinit var selectedImage: String
+
+ /** Launcher for picking an image from device gallery. */
+ lateinit var activityResultLauncher: ActivityResultLauncher
+
+ /** Initialize layout bindings. */
+ fun handleCreateView(inflater: LayoutInflater, container: ViewGroup?): View {
+ binding = CreateProfileFragmentBinding.inflate(
+ inflater,
+ container,
+ /* attachToRoot= */ false
+ )
+ binding.let {
+ it.lifecycleOwner = fragment
+ it.viewModel = createProfileViewModel
+ }
+
+ uploadImageView = binding.createProfileUserImageView
+
+ uploadImageView.apply {
+ setColorFilter(
+ ResourcesCompat.getColor(
+ activity.resources,
+ R.color.component_color_avatar_background_25_color,
+ null
+ ),
+ PorterDuff.Mode.DST_OVER
+ )
+
+ imageLoader.loadDrawable(
+ R.drawable.ic_profile_icon,
+ ImageViewTarget(this)
+ )
+ }
+
+ binding.onboardingNavigationContinue.setOnClickListener {
+ val nickname = binding.createProfileNicknameEdittext.text.toString().trim()
+
+ createProfileViewModel.hasErrorMessage.set(nickname.isBlank())
+ }
+
+ binding.createProfileNicknameEdittext.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+ override fun afterTextChanged(s: Editable?) {}
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ createProfileViewModel.hasErrorMessage.set(false)
+ }
+ })
+
+ addViewOnClickListeners(binding)
+
+ return binding.root
+ }
+
+ /** Receive the result of image upload and load it into the image view. */
+ fun handleOnActivityResult(intent: Intent?) {
+ intent?.let {
+ binding.createProfilePicturePrompt.visibility = View.GONE
+ selectedImage =
+ checkNotNull(intent.data.toString()) { "Could not find the selected image." }
+ imageLoader.loadBitmap(
+ selectedImage,
+ ImageViewTarget(uploadImageView)
+ )
+ }
+ }
+
+ private fun addViewOnClickListeners(binding: CreateProfileFragmentBinding) {
+ val galleryIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
+
+ binding.onboardingNavigationBack.setOnClickListener { activity.finish() }
+ binding.createProfileEditPictureIcon.setOnClickListener {
+ activityResultLauncher.launch(
+ galleryIntent
+ )
+ }
+ binding.createProfilePicturePrompt.setOnClickListener {
+ activityResultLauncher.launch(
+ galleryIntent
+ )
+ }
+ binding.createProfileUserImageView.setOnClickListener {
+ activityResultLauncher.launch(
+ galleryIntent
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt
new file mode 100644
index 00000000000..e6ef763f23c
--- /dev/null
+++ b/app/src/main/java/org/oppia/android/app/onboarding/CreateProfileViewModel.kt
@@ -0,0 +1,14 @@
+package org.oppia.android.app.onboarding
+
+import androidx.databinding.ObservableField
+import org.oppia.android.app.fragment.FragmentScope
+import org.oppia.android.app.viewmodel.ObservableViewModel
+import javax.inject.Inject
+
+/** The ViewModel for [CreateProfileFragment]. */
+@FragmentScope
+class CreateProfileViewModel @Inject constructor() : ObservableViewModel() {
+
+ /** ObservableField that tracks whether creating a nickname has triggered an error condition. */
+ val hasErrorMessage = ObservableField(false)
+}
diff --git a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
index 893960b55c7..72ae543dd0c 100644
--- a/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
+++ b/app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt
@@ -27,6 +27,11 @@ class OnboardingProfileTypeFragmentPresenter @Inject constructor(
binding.apply {
lifecycleOwner = fragment
+ profileTypeLearnerNavigationCard.setOnClickListener {
+ val intent = CreateProfileActivity.createProfileActivityIntent(activity)
+ fragment.startActivity(intent)
+ }
+
profileTypeSupervisorNavigationCard.setOnClickListener {
val intent = ProfileChooserActivity.createProfileChooserActivity(activity)
fragment.startActivity(intent)
@@ -36,6 +41,7 @@ class OnboardingProfileTypeFragmentPresenter @Inject constructor(
activity.finish()
}
}
+
return binding.root
}
}
diff --git a/app/src/main/res/drawable/create_profile_picture_icon.xml b/app/src/main/res/drawable/create_profile_picture_icon.xml
new file mode 100644
index 00000000000..0aa1cd192f0
--- /dev/null
+++ b/app/src/main/res/drawable/create_profile_picture_icon.xml
@@ -0,0 +1,33 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/edit_text_white_background_error_border.xml b/app/src/main/res/drawable/edit_text_white_background_error_border.xml
new file mode 100644
index 00000000000..2851e1fc4cb
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text_white_background_error_border.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/edit_text_white_background_with_border.xml b/app/src/main/res/drawable/edit_text_white_background_with_border.xml
new file mode 100644
index 00000000000..90e111c7c1a
--- /dev/null
+++ b/app/src/main/res/drawable/edit_text_white_background_with_border.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_edit_24.xml b/app/src/main/res/drawable/ic_outline_edit_24.xml
new file mode 100644
index 00000000000..407f3e2f737
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_edit_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_icon.xml b/app/src/main/res/drawable/ic_profile_icon.xml
new file mode 100644
index 00000000000..7b7c22f999b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout-land/create_profile_fragment.xml b/app/src/main/res/layout-land/create_profile_fragment.xml
new file mode 100644
index 00000000000..93c01c2f32d
--- /dev/null
+++ b/app/src/main/res/layout-land/create_profile_fragment.xml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml b/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml
new file mode 100644
index 00000000000..5eba49ebb23
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-land/create_profile_fragment.xml
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml b/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml
new file mode 100644
index 00000000000..0edd5932959
--- /dev/null
+++ b/app/src/main/res/layout-sw600dp-port/create_profile_fragment.xml
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/create_profile_activity.xml b/app/src/main/res/layout/create_profile_activity.xml
new file mode 100644
index 00000000000..c61355aa7d5
--- /dev/null
+++ b/app/src/main/res/layout/create_profile_activity.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/create_profile_fragment.xml b/app/src/main/res/layout/create_profile_fragment.xml
new file mode 100644
index 00000000000..ab53fbfc69a
--- /dev/null
+++ b/app/src/main/res/layout/create_profile_fragment.xml
@@ -0,0 +1,163 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-night/color_palette.xml b/app/src/main/res/values-night/color_palette.xml
index 901d95b33df..19e760c1943 100644
--- a/app/src/main/res/values-night/color_palette.xml
+++ b/app/src/main/res/values-night/color_palette.xml
@@ -48,7 +48,7 @@
@color/color_def_oppia_grayish_black
@color/color_def_dark_blue
@color/color_def_oppia_grayish_black
- @color/color_def_oppia_grey_border
+ @color/color_def_oppia_grey
@color/color_def_oppia_turquoise
@color/color_def_white
@color/color_def_white
@@ -231,7 +231,12 @@
@color/color_def_dark_green
@color/color_def_accessible_grey
- @color/color_def_dark_jade
+ @color/color_def_jade
+ @color/color_def_oppia_green
+ @color/color_def_black
+
+ @color/color_def_oppia_reddish_brown
+ @color/color_def_accessible_light_grey_2
@color/color_def_greenish_black
@color/color_def_white
diff --git a/app/src/main/res/values/color_defs.xml b/app/src/main/res/values/color_defs.xml
index af6a0ba28cd..7e09cb35dce 100644
--- a/app/src/main/res/values/color_defs.xml
+++ b/app/src/main/res/values/color_defs.xml
@@ -63,7 +63,7 @@
#FF938F
#32363B
#395FD0
- #DDDDDD
+ #DDDDDD
#EEEEEE
#707070
#FFFFF0
diff --git a/app/src/main/res/values/color_palette.xml b/app/src/main/res/values/color_palette.xml
index abeed016fca..f31cfb8734e 100644
--- a/app/src/main/res/values/color_palette.xml
+++ b/app/src/main/res/values/color_palette.xml
@@ -51,7 +51,7 @@
@color/color_def_oppia_solid_blue
@color/color_def_oppia_stroke_blue
@color/color_def_white
- @color/color_def_oppia_grey_border
+ @color/color_def_oppia_grey
@color/color_def_oppia_dark_blue
@color/color_def_oppia_dark_blue
@color/color_def_oppia_dark_blue
@@ -244,6 +244,7 @@
@color/color_def_avatar_background_22
@color/color_def_avatar_background_23
@color/color_def_avatar_background_24
+ @color/color_def_oppia_grey
@color/color_def_white_f5
@color/color_def_white_f6
@@ -274,6 +275,10 @@
@color/color_def_jade
@color/color_def_oppia_brown
@color/color_def_light_orange
+ @color/color_def_persian_green
+ @color/color_def_black
+ @color/color_def_accessible_light_grey_2
+ @color/color_def_error_text
@color/color_def_greenish_white
@color/color_def_green
diff --git a/app/src/main/res/values/component_colors.xml b/app/src/main/res/values/component_colors.xml
index 94dd01bd602..7ba51ce5092 100644
--- a/app/src/main/res/values/component_colors.xml
+++ b/app/src/main/res/values/component_colors.xml
@@ -148,6 +148,7 @@
@color/color_palette_avatar_background_22_color
@color/color_palette_avatar_background_23_color
@color/color_palette_avatar_background_24_color
+ @color/color_palette_avatar_background_25_color
@color/color_palette_forgot_pin_color
@color/color_palette_show_hide_color
@@ -307,10 +308,14 @@
@color/color_palette_white_text_color
@color/color_palette_onboarding_primary_color
+ @color/color_palette_onboarding_black_color
@color/color_palette_onboarding_primary_text_color
@color/color_palette_onboarding_profile_type_background_color
@color/color_palette_learner_profile_type_background_color
- @color/color_palette_supervisor_profile_type_background_color
+ @color/color_palette_supervisor_profile_type_background_color
+ @color/color_palette_onboarding_edit_icon_color
+ @color/color_palette_edittext_stroke_color
+ @color/color_palette_text_error_color
@color/color_palette_classroom_card_color
@color/color_palette_classroom_shared_text_color
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 307e20dcc92..7dac7187713 100755
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -653,6 +653,16 @@
I\'m the parent, teacher, or guardian of a student.
STEP 2 OF 5
+
+ Create Profile
+ What should we call you?
+ Nickname
+ Tap here to add a picture
+ Click in the box above to type your nickname.
+ Edit profile picture
+ Current profile picture
+ STEP 3 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 c9bf5fae5eb..0123d3d0b7b 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -731,4 +731,50 @@
- @color/component_color_onboarding_shared_green_color
- @dimen/onboarding_shared_text_size_medium
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileActivityTest.kt
new file mode 100644
index 00000000000..3763cf7d57c
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileActivityTest.kt
@@ -0,0 +1,214 @@
+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 [CreateProfileActivity]. */
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = CreateProfileActivityTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class CreateProfileActivityTest {
+ @get:Rule
+ val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+
+ @get:Rule
+ val oppiaTestRule = OppiaTestRule()
+
+ @Inject
+ lateinit var context: Context
+
+ @Inject
+ lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+
+ @Before
+ fun setUp() {
+ Intents.init()
+ setUpTestApplicationComponent()
+ }
+
+ @After
+ fun tearDown() {
+ Intents.release()
+ }
+
+ @Test
+ fun testActivity_createIntent_verifyScreenNameInIntent() {
+ val screenName =
+ CreateProfileActivity.createProfileActivityIntent(context)
+ .extractCurrentAppScreenName()
+
+ assertThat(screenName).isEqualTo(ScreenName.CREATE_PROFILE_ACTIVITY)
+ }
+
+ @Test
+ fun testNewLearnerProfileActivity_hasCorrectActivityLabel() {
+ launchNewLearnerProfileActivity().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.create_profile_activity_title))
+ }
+ }
+
+ private fun launchNewLearnerProfileActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ CreateProfileActivity.createProfileActivityIntent(context)
+ )
+ 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(newLearnerProfileActivityTest: CreateProfileActivityTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerCreateProfileActivityTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(newLearnerProfileActivityTest: CreateProfileActivityTest) {
+ component.inject(newLearnerProfileActivityTest)
+ }
+
+ 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/CreateProfileFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
new file mode 100644
index 00000000000..40f782fbd55
--- /dev/null
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/CreateProfileFragmentTest.kt
@@ -0,0 +1,553 @@
+package org.oppia.android.app.onboarding
+
+import android.app.Activity
+import android.app.Application
+import android.app.Instrumentation
+import android.content.ContentResolver
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import android.net.Uri
+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.action.ViewActions.closeSoftKeyboard
+import androidx.test.espresso.assertion.ViewAssertions.matches
+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.matcher.ViewMatchers.Visibility
+import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
+import androidx.test.espresso.matcher.ViewMatchers.isRoot
+import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.espresso.matcher.ViewMatchers.withText
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import dagger.Component
+import org.hamcrest.Matcher
+import org.hamcrest.Matchers.allOf
+import org.hamcrest.Matchers.not
+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.TestImageLoaderModule
+import org.oppia.android.testing.TestLogReportingModule
+import org.oppia.android.testing.espresso.EditTextInputAction
+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.ImageParsingModule
+import org.oppia.android.util.parser.image.TestGlideImageLoader
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.LooperMode
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/** Tests for [CreateProfileFragment]. */
+// FunctionName: test names are conventionally named with underscores.
+@Suppress("FunctionName")
+@RunWith(AndroidJUnit4::class)
+@LooperMode(LooperMode.Mode.PAUSED)
+@Config(
+ application = CreateProfileFragmentTest.TestApplication::class,
+ qualifiers = "port-xxhdpi"
+)
+class CreateProfileFragmentTest {
+ @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule()
+ @get:Rule val oppiaTestRule = OppiaTestRule()
+ @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers
+ @Inject lateinit var context: Context
+ @Inject lateinit var editTextInputAction: EditTextInputAction
+ @Inject lateinit var testGlideImageLoader: TestGlideImageLoader
+
+ @Before
+ fun setUp() {
+ Intents.init()
+ setUpTestApplicationComponent()
+ testCoroutineDispatchers.registerIdlingResource()
+ }
+
+ @After
+ fun tearDown() {
+ testCoroutineDispatchers.unregisterIdlingResource()
+ Intents.release()
+ }
+
+ @Test
+ fun testFragment_nicknameLabelIsDisplayed() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_label))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ withText(
+ context.getString(
+ R.string.create_profile_activity_nickname_label
+ )
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_stepCountText_isDisplayed() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.onboarding_steps_count))
+ .check(
+ matches(
+ allOf(
+ isDisplayed(),
+ withText(
+ context.getString(
+ R.string.onboarding_step_count_three
+ )
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_continueButtonClicked_filledNickname_launchesLearnerIntroScreen() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .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
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_continueButtonClicked_filledNickname_doesNotShowErrorText() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(withEffectiveVisibility(Visibility.GONE)))
+ }
+ }
+
+ @Test
+ fun testFragment_continueButtonClicked_emptyNickname_showNicknameErrorText() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_continueButtonClicked_filledNickname_afterError_launchesLearnerIntroScreen() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .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
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_onTextChanged_afterError_hidesErrorMessage() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(not(isDisplayed())))
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_filledNickname_continueButtonClicked_launchesLearnerIntroScreen() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .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
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_filledNickname_continueButtonClicked_doesNotShowErrorText() {
+ launchNewLearnerProfileActivity().use {
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(withEffectiveVisibility(Visibility.GONE)))
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_continueButtonClicked_emptyNickname_showNicknameErrorText() {
+ launchNewLearnerProfileActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+ }
+ }
+
+ @Test
+ fun testFragment_landscape_continueButtonClicked_afterErrorShown_launchesLearnerIntroScreen() {
+ launchNewLearnerProfileActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_nickname_error))
+ .check(matches(isDisplayed()))
+
+ onView(withId(R.id.create_profile_nickname_edittext))
+ .perform(
+ editTextInputAction.appendText("John"),
+ closeSoftKeyboard()
+ )
+ testCoroutineDispatchers.runCurrent()
+ onView(withId(R.id.onboarding_navigation_continue))
+ .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
+ )
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testFragment_backButtonPressed_currentScreenIsDestroyed() {
+ launchNewLearnerProfileActivity().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() {
+ launchNewLearnerProfileActivity().use { scenario ->
+ onView(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_tapToAddPictureClicked_hasGalleryIntent() {
+ launchNewLearnerProfileActivity().use {
+ onView(withText(R.string.create_profile_activity_profile_picture_prompt))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasAction(Intent.ACTION_PICK))
+ }
+ }
+
+ @Test
+ fun testFragment_landscapeMode_tapToAddPictureClicked_hasGalleryIntent() {
+ launchNewLearnerProfileActivity().use {
+ onView(isRoot()).perform(orientationLandscape())
+ testCoroutineDispatchers.runCurrent()
+ onView(withText(R.string.create_profile_activity_profile_picture_prompt))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ intended(hasAction(Intent.ACTION_PICK))
+ }
+ }
+
+ @Test
+ fun testFragment_tapToAddPictureClicked_loadsTheImageFromGallery() {
+ val expectedIntent: Matcher = hasAction(Intent.ACTION_PICK)
+
+ val activityResult = createGalleryPickActivityResultStub()
+ intending(expectedIntent).respondWith(activityResult)
+
+ launchNewLearnerProfileActivity().use {
+ onView(withText(R.string.create_profile_activity_profile_picture_prompt))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+
+ val loadedImageUri = activityResult.resultData.data.toString()
+ assertThat(loadedImageUri).contains("launcher_icon")
+ }
+ }
+
+ @Test
+ fun testFragment_uploadProfilePicture_displaysImageInTarget() {
+ val expectedIntent: Matcher = hasAction(Intent.ACTION_PICK)
+
+ val activityResult = createGalleryPickActivityResultStub()
+ intending(expectedIntent).respondWith(activityResult)
+
+ launchNewLearnerProfileActivity().use {
+ onView(withText(R.string.create_profile_activity_profile_picture_prompt))
+ .perform(click())
+ testCoroutineDispatchers.runCurrent()
+ val expectedImage = activityResult.resultData.data.toString()
+ val loadedImages = testGlideImageLoader.getLoadedBitmaps()
+ assertThat(loadedImages.first()).isEqualTo(expectedImage)
+ }
+ }
+
+ private fun createGalleryPickActivityResultStub(): Instrumentation.ActivityResult {
+ val resources: Resources = context.resources
+ val imageUri = Uri.parse(
+ ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
+ resources.getResourcePackageName(R.mipmap.launcher_icon) + '/' +
+ resources.getResourceTypeName(R.mipmap.launcher_icon) + '/' +
+ resources.getResourceEntryName(R.mipmap.launcher_icon)
+ )
+ val resultIntent = Intent()
+ resultIntent.data = imageUri
+ return Instrumentation.ActivityResult(Activity.RESULT_OK, resultIntent)
+ }
+
+ private fun launchNewLearnerProfileActivity():
+ ActivityScenario? {
+ val scenario = ActivityScenario.launch(
+ CreateProfileActivity.createProfileActivityIntent(context)
+ )
+ 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, 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, TestImageLoaderModule::class,
+ ]
+ )
+ interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder
+
+ fun inject(newLearnerProfileFragmentTest: CreateProfileFragmentTest)
+ }
+
+ class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider {
+ private val component: TestApplicationComponent by lazy {
+ DaggerCreateProfileFragmentTest_TestApplicationComponent.builder()
+ .setApplication(this)
+ .build() as TestApplicationComponent
+ }
+
+ fun inject(newLearnerProfileFragmentTest: CreateProfileFragmentTest) {
+ component.inject(newLearnerProfileFragmentTest)
+ }
+
+ 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/OnboardingProfileTypeFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
index ad8f036e214..2649e11c610 100644
--- a/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
+++ b/app/src/sharedTest/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentTest.kt
@@ -273,12 +273,7 @@ class OnboardingProfileTypeFragmentTest {
testCoroutineDispatchers.runCurrent()
onView(withId(R.id.profile_type_learner_navigation_card)).perform(click())
testCoroutineDispatchers.runCurrent()
- // Does nothing for now, but should fail once navigation is implemented in a future PR.
- onView(withId(R.id.profile_type_learner_navigation_card))
- .check(matches(isDisplayed()))
-
- onView(withId(R.id.profile_type_supervisor_navigation_card))
- .check(matches(isDisplayed()))
+ intended(hasComponent(CreateProfileActivity::class.java.name))
}
}
diff --git a/model/src/main/proto/screens.proto b/model/src/main/proto/screens.proto
index 9e323e5aa72..48631a4a3e4 100644
--- a/model/src/main/proto/screens.proto
+++ b/model/src/main/proto/screens.proto
@@ -164,6 +164,9 @@ enum ScreenName {
// Screen name value for the scenario when the profile type activity is visible to the user.
ONBOARDING_PROFILE_TYPE_ACTIVITY = 51;
+
+ // Screen name value for the scenario when the create new learner profile activity is visible to the user.
+ CREATE_PROFILE_ACTIVITY = 52;
}
// 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 4f5600d8576..107658a4fcc 100644
--- a/scripts/assets/test_file_exemptions.textproto
+++ b/scripts/assets/test_file_exemptions.textproto
@@ -1074,6 +1074,18 @@ test_file_exemption {
exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/OnboardingProfileTypeFragmentPresenter.kt"
test_file_not_required: true
}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileActivityPresenter.kt"
+ test_file_not_required: true
+}
+test_file_exemption {
+ exempted_file_path: "app/src/main/java/org/oppia/android/app/onboarding/CreateProfileFragmentPresenter.kt"
+ test_file_not_required: true
+}
+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/ongoingtopiclist/OngoingTopicItemViewModel.kt"
test_file_not_required: true
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 bc52286b4e7..24f5db6dabf 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
@@ -856,6 +856,7 @@ class EventBundleCreator @Inject constructor(
ScreenName.SURVEY_ACTIVITY -> "survey_activity"
ScreenName.CLASSROOM_LIST_ACTIVITY -> "classroom_list_activity"
ScreenName.ONBOARDING_PROFILE_TYPE_ACTIVITY -> "onboarding_profile_type_activity"
+ ScreenName.CREATE_PROFILE_ACTIVITY -> "create_profile_activity"
}
private fun AppLanguageSelection.toAnalyticsText(): String {