diff --git a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivity.kt b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivity.kt index b8a404f524a..99113fcfe80 100644 --- a/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivity.kt +++ b/app/src/main/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivity.kt @@ -27,5 +27,9 @@ class QuestionPlayerActivity : InjectableAppCompatActivity() { intent.putExtra(QUESTION_PLAYER_ACTIVITY_SKILL_ID_LIST_ARGUMENT_KEY, skillIdList) return intent } + + fun getIntentKey(): String { + return QUESTION_PLAYER_ACTIVITY_SKILL_ID_LIST_ARGUMENT_KEY + } } } diff --git a/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt b/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt index 39423e79895..3b93aea5741 100644 --- a/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/recyclerview/BindableAdapterTest.kt @@ -10,7 +10,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.Espresso.onIdle import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withSubstring import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -26,12 +25,9 @@ import org.oppia.app.testing.BindableAdapterTestActivity import org.oppia.app.testing.BindableAdapterTestFragment import org.oppia.app.testing.BindableAdapterTestFragmentPresenter import org.oppia.app.testing.BindableAdapterTestViewModel -import androidx.test.espresso.matcher.BoundedMatcher -import android.view.View -import org.hamcrest.Description -import org.hamcrest.Matcher import org.oppia.app.databinding.TestTextViewForIntWithDataBindingBinding import org.oppia.app.databinding.TestTextViewForStringWithDataBindingBinding +import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition /** Tests for [BindableAdapter]. */ @RunWith(AndroidJUnit4::class) @@ -49,6 +45,7 @@ class BindableAdapterTest { // Ensure that the bindable fragment's test state is properly reset each time. BindableAdapterTestFragmentPresenter.testBindableAdapter = null } + @After fun tearDown() { // Ensure that the bindable fragment's test state is properly cleaned up. @@ -88,7 +85,7 @@ class BindableAdapterTest { assertThat(recyclerView.childCount).isEqualTo(1) } // Perform onView() verification off the the main thread to avoid deadlocking. - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(0, withText(STR_VALUE_0.strValue)))) + onView(atPosition(R.id.test_recycler_view, 0)).check(matches(withText(STR_VALUE_0.strValue))) } } @@ -109,9 +106,9 @@ class BindableAdapterTest { val recyclerView: RecyclerView = getTestFragment(activity).view!!.findViewById(R.id.test_recycler_view) assertThat(recyclerView.childCount).isEqualTo(3) } - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(0, withText(STR_VALUE_1.strValue)))) - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(1, withText(STR_VALUE_0.strValue)))) - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(2, withText(STR_VALUE_2.strValue)))) + onView(atPosition(R.id.test_recycler_view, 0)).check(matches(withText(STR_VALUE_1.strValue))) + onView(atPosition(R.id.test_recycler_view, 1)).check(matches(withText(STR_VALUE_0.strValue))) + onView(atPosition(R.id.test_recycler_view, 2)).check(matches(withText(STR_VALUE_2.strValue))) } } @@ -133,11 +130,10 @@ class BindableAdapterTest { val recyclerView: RecyclerView = getTestFragment(activity).view!!.findViewById(R.id.test_recycler_view) assertThat(recyclerView.childCount).isEqualTo(3) } - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(0, withText(STR_VALUE_1.strValue)))) - onView(withId(R.id.test_recycler_view)) - .check(matches(atPosition(1, withSubstring(INT_VALUE_0.intValue.toString())))) - onView(withId(R.id.test_recycler_view)) - .check(matches(atPosition(2, withSubstring(INT_VALUE_1.intValue.toString())))) + + onView(atPosition(R.id.test_recycler_view, 0)).check(matches(withText(STR_VALUE_1.strValue))) + onView(atPosition(R.id.test_recycler_view, 1)).check(matches(withSubstring(INT_VALUE_0.intValue.toString()))) + onView(atPosition(R.id.test_recycler_view, 2)).check(matches(withSubstring(INT_VALUE_1.intValue.toString()))) } } @@ -159,7 +155,7 @@ class BindableAdapterTest { assertThat(recyclerView.childCount).isEqualTo(1) } // Perform onView() verification off the the main thread to avoid deadlocking. - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(0, withText(STR_VALUE_0.strValue)))) + onView(atPosition(R.id.test_recycler_view, 0)).check(matches(withText(STR_VALUE_0.strValue))) } } @@ -180,9 +176,10 @@ class BindableAdapterTest { val recyclerView: RecyclerView = getTestFragment(activity).view!!.findViewById(R.id.test_recycler_view) assertThat(recyclerView.childCount).isEqualTo(3) } - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(0, withText(STR_VALUE_1.strValue)))) - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(1, withText(STR_VALUE_0.strValue)))) - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(2, withText(STR_VALUE_2.strValue)))) + + onView(atPosition(R.id.test_recycler_view, 0)).check(matches(withText(STR_VALUE_1.strValue))) + onView(atPosition(R.id.test_recycler_view, 1)).check(matches(withText(STR_VALUE_0.strValue))) + onView(atPosition(R.id.test_recycler_view, 2)).check(matches(withText(STR_VALUE_2.strValue))) } } @@ -204,11 +201,10 @@ class BindableAdapterTest { val recyclerView: RecyclerView = getTestFragment(activity).view!!.findViewById(R.id.test_recycler_view) assertThat(recyclerView.childCount).isEqualTo(3) } - onView(withId(R.id.test_recycler_view)).check(matches(atPosition(0, withText(STR_VALUE_1.strValue)))) - onView(withId(R.id.test_recycler_view)) - .check(matches(atPosition(1, withSubstring(INT_VALUE_0.intValue.toString())))) - onView(withId(R.id.test_recycler_view)) - .check(matches(atPosition(2, withSubstring(INT_VALUE_1.intValue.toString())))) + + onView(atPosition(R.id.test_recycler_view, 0)).check(matches(withText(STR_VALUE_1.strValue))) + onView(atPosition(R.id.test_recycler_view, 1)).check(matches(withSubstring(INT_VALUE_0.intValue.toString()))) + onView(atPosition(R.id.test_recycler_view, 2)).check(matches(withSubstring(INT_VALUE_1.intValue.toString()))) } } @@ -273,13 +269,15 @@ class BindableAdapterTest { private fun inflateTextViewForStringWithoutDataBinding(viewGroup: ViewGroup): TextView { val inflater = LayoutInflater.from(ApplicationProvider.getApplicationContext()) return inflater.inflate( - R.layout.test_text_view_for_string_no_data_binding, viewGroup, /* attachToRoot= */ false) as TextView + R.layout.test_text_view_for_string_no_data_binding, viewGroup, /* attachToRoot= */ false + ) as TextView } private fun inflateTextViewForIntWithoutDataBinding(viewGroup: ViewGroup): TextView { val inflater = LayoutInflater.from(ApplicationProvider.getApplicationContext()) return inflater.inflate( - R.layout.test_text_view_for_int_no_data_binding, viewGroup, /* attachToRoot= */ false) as TextView + R.layout.test_text_view_for_int_no_data_binding, viewGroup, /* attachToRoot= */ false + ) as TextView } private fun bindTextViewForStringWithoutDataBinding(textView: TextView, data: TestModel) { @@ -310,20 +308,4 @@ class BindableAdapterTest { // This must be done off the main thread for Espresso otherwise it deadlocks. onIdle() } - - // TODO(#89): Move this to a consolidated test library. - // https://stackoverflow.com/a/34795431 - private fun atPosition(position: Int, itemMatcher: Matcher): Matcher { - return object : BoundedMatcher(RecyclerView::class.java) { - override fun describeTo(description: Description) { - description.appendText("has item at position $position: ") - itemMatcher.describeTo(description) - } - - override fun matchesSafely(view: RecyclerView): Boolean { - val viewHolder = view.findViewHolderForAdapterPosition(position) ?: return false - return itemMatcher.matches(viewHolder.itemView) - } - } - } } diff --git a/app/src/sharedTest/java/org/oppia/app/recyclerview/RecyclerViewMatcher.kt b/app/src/sharedTest/java/org/oppia/app/recyclerview/RecyclerViewMatcher.kt new file mode 100644 index 00000000000..92296136d1d --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/app/recyclerview/RecyclerViewMatcher.kt @@ -0,0 +1,63 @@ +package org.oppia.app.recyclerview + +import android.content.res.Resources +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher + +// Reference Link: https://github.com/dannyroa/espresso-samples/blob/master/RecyclerView/app/src/androidTest/java/com/dannyroa/espresso_samples/recyclerview/RecyclerViewMatcher.java +class RecyclerViewMatcher { + companion object { + /** + * This function returns a Matcher for an item inside RecyclerView from a specified position. + */ + fun atPosition(recyclerViewId: Int, position: Int): Matcher { + return atPositionOnView(recyclerViewId, position, -1) + } + + /** + * This function returns a Matcher for a specific view within the item inside RecyclerView from a specified position. + */ + fun atPositionOnView(recyclerViewId: Int, position: Int, targetViewId: Int): Matcher { + return object : TypeSafeMatcher() { + var resources: Resources? = null + var childView: View? = null + + override fun describeTo(description: Description) { + var idDescription = Integer.toString(recyclerViewId) + if (this.resources != null) { + idDescription = try { + this.resources!!.getResourceName(recyclerViewId) + } catch (var4: Resources.NotFoundException) { + String.format( + "%s (resource name not found)", + recyclerViewId + ) + } + } + description.appendText("with id: $idDescription") + } + + public override fun matchesSafely(view: View): Boolean { + this.resources = view.resources + if (childView == null) { + val recyclerView = view.rootView.findViewById(recyclerViewId) as RecyclerView + if (recyclerView.id == recyclerViewId) { + childView = recyclerView.findViewHolderForAdapterPosition(position)!!.itemView + } else { + return false + } + } + return if (targetViewId == -1) { + view === childView + } else { + val targetView = childView!!.findViewById(targetViewId) + view === targetView + } + } + } + } + } +} diff --git a/app/src/sharedTest/java/org/oppia/app/topic/TopicActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/topic/TopicActivityTest.kt index 341b0afbe66..be8a8e8bcaa 100644 --- a/app/src/sharedTest/java/org/oppia/app/topic/TopicActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/topic/TopicActivityTest.kt @@ -16,7 +16,6 @@ import kotlinx.coroutines.CoroutineDispatcher import org.junit.Test import org.junit.runner.RunWith import org.oppia.app.R -import org.oppia.app.player.exploration.ExplorationActivity import org.oppia.util.threading.BackgroundDispatcher import org.oppia.util.threading.BlockingDispatcher import javax.inject.Singleton @@ -27,7 +26,7 @@ class TopicActivityTest { @Test fun testTopicActivity_loadTopicFragment_hasDummyString() { - ActivityScenario.launch(ExplorationActivity::class.java).use { + ActivityScenario.launch(TopicActivity::class.java).use { onView(withId(R.id.dummy_text_view)).check(matches(withText("This is dummy TextView for testing"))) } } diff --git a/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt b/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt new file mode 100644 index 00000000000..7bc3c1df511 --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/app/topic/questionplayer/QuestionPlayerActivityTest.kt @@ -0,0 +1,65 @@ +package org.oppia.app.topic.questionplayer + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.runners.AndroidJUnit4 +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import kotlinx.coroutines.CoroutineDispatcher +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.app.R +import org.oppia.util.threading.BackgroundDispatcher +import org.oppia.util.threading.BlockingDispatcher +import javax.inject.Singleton + +/** Tests for [QuestionPlayerActivity]. */ +@RunWith(AndroidJUnit4::class) +class QuestionPlayerActivityTest { + + @Test + fun testQuestionPlayerActivity_loadQuestionPlayerFragment_hasDummyString() { + ActivityScenario.launch(QuestionPlayerActivity::class.java).use { + onView(withId(R.id.dummy_text_view)).check(matches(withText("This is dummy TextView for testing"))) + } + } + + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + + // TODO(#89): Introduce a proper IdlingResource for background dispatchers to ensure they all complete before + // proceeding in an Espresso test. This solution should also be interoperative with Robolectric contexts by using a + // test coroutine dispatcher. + + @Singleton + @Provides + @BackgroundDispatcher + fun provideBackgroundDispatcher(@BlockingDispatcher blockingDispatcher: CoroutineDispatcher): CoroutineDispatcher { + return blockingDispatcher + } + } + + @Singleton + @Component(modules = [TestModule::class]) + interface TestApplicationComponent { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + } +} diff --git a/app/src/sharedTest/java/org/oppia/app/topic/train/TopicTrainFragmentTest.kt b/app/src/sharedTest/java/org/oppia/app/topic/train/TopicTrainFragmentTest.kt index b55c8fa420f..4dea2d9763e 100644 --- a/app/src/sharedTest/java/org/oppia/app/topic/train/TopicTrainFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/app/topic/train/TopicTrainFragmentTest.kt @@ -2,36 +2,149 @@ package org.oppia.app.topic.train import android.app.Application import android.content.Context +import android.content.res.Configuration import androidx.test.core.app.ActivityScenario 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.intended +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant +import androidx.test.espresso.matcher.ViewMatchers.isClickable import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra +import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import dagger.BindsInstance import dagger.Component import dagger.Module import dagger.Provides import kotlinx.coroutines.CoroutineDispatcher +import org.hamcrest.Matchers.not import org.junit.Test import org.junit.runner.RunWith -import org.oppia.app.R import org.oppia.app.topic.TopicActivity import org.oppia.util.threading.BackgroundDispatcher import org.oppia.util.threading.BlockingDispatcher import javax.inject.Singleton +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.matcher.ViewMatchers.isChecked +import org.oppia.app.R +import org.junit.After +import org.junit.Before +import org.junit.Rule +import androidx.test.rule.ActivityTestRule +import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPosition +import org.oppia.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView +import org.oppia.app.topic.questionplayer.QuestionPlayerActivity /** Tests for [TopicTrainFragment]. */ @RunWith(AndroidJUnit4::class) class TopicTrainFragmentTest { + private var skillIdList = ArrayList() + + private lateinit var activityScenario: ActivityScenario + + @get:Rule + var activityTestRule: ActivityTestRule = ActivityTestRule( + TopicActivity::class.java, /* initialTouchMode= */ true, /* launchActivity= */ false + ) + + @Before + fun setUp() { + activityScenario = ActivityScenario.launch(TopicActivity::class.java) + + Intents.init() + skillIdList.add("test_skill_id_0") + } + + @Test + fun testTopicTrainFragment_loadFragment_displaySkills_startButtonIsInactive() { + ActivityScenario.launch(TopicActivity::class.java).use { + onView(withId(R.id.master_skills_text_view)).check(matches(withText(R.string.topic_train_master_these_skills))) + onView(atPosition(R.id.skill_recycler_view, 0)).check(matches(hasDescendant(withId(R.id.skill_check_box)))) + onView(withId(R.id.topic_train_start_button)).check(matches(not(isClickable()))) + } + } + + @Test + fun testTopicTrainFragment_loadFragment_selectSkills_isSuccessful() { + ActivityScenario.launch(TopicActivity::class.java).use { + onView(atPosition(R.id.skill_recycler_view, 0)).perform(click()) + onView(atPosition(R.id.skill_recycler_view, 1)).perform(click()) + } + } + + @Test + fun testTopicTrainFragment_loadFragment_selectSkills_startButtonIsActive() { + ActivityScenario.launch(TopicActivity::class.java).use { + onView(atPosition(R.id.skill_recycler_view, 0)).perform(click()) + onView(withId(R.id.topic_train_start_button)).check(matches(isClickable())) + } + } + @Test - fun testTopicTrainFragment_loadFragment_textIsDisplayed() { + fun testTopicTrainFragment_loadFragment_selectSkills_deselectSkills_isSuccessful() { ActivityScenario.launch(TopicActivity::class.java).use { - onView(withId(R.id.dummy_text_view)).check(matches(withText("This is dummy TextView for testing"))) + onView(atPosition(R.id.skill_recycler_view, 0)).perform(click()) + onView(atPosition(R.id.skill_recycler_view, 0)).perform(click()) } } + @Test + fun testTopicTrainFragment_loadFragment_selectSkills_deselectSkills_startButtonIsInactive() { + ActivityScenario.launch(TopicActivity::class.java).use { + onView(atPosition(R.id.skill_recycler_view, 0)).perform(click()) + onView(atPosition(R.id.skill_recycler_view, 0)).perform(click()) + onView(withId(R.id.topic_train_start_button)).check(matches(not(isClickable()))) + } + } + + @Test + fun testTopicTrainFragment_loadFragment_selectSkills_clickStartButton_skillListTransferSuccessfully() { + activityTestRule.launchActivity(null) + onView(atPosition(R.id.skill_recycler_view, 0)).perform(click()) + onView(withId(R.id.topic_train_start_button)).perform(click()) + intended(hasComponent(QuestionPlayerActivity::class.java.name)) + intended(hasExtra(QuestionPlayerActivity.getIntentKey(), skillIdList)) + } + + @Test + fun testTopicTrainFragment_loadFragment_selectSkills_configurationChange_skillsAreSelected() { + onView(atPosition(R.id.skill_recycler_view, 0)).perform(click()) + activityScenario.onActivity { activity -> + activity.requestedOrientation = Configuration.ORIENTATION_LANDSCAPE + } + activityScenario.recreate() + onView(atPositionOnView(R.id.skill_recycler_view, 0, R.id.skill_check_box)).check(matches(isChecked())) + } + + @Test + fun testTopicTrainFragment_loadFragment_configurationChange_startButtonRemainsInactive() { + onView(withId(R.id.topic_train_start_button)).check(matches(not(isClickable()))) + activityScenario.onActivity { activity -> + activity.requestedOrientation = Configuration.ORIENTATION_LANDSCAPE + } + activityScenario.recreate() + onView(withId(R.id.topic_train_start_button)).check(matches(not(isClickable()))) + } + + @Test + fun testTopicTrainFragment_loadFragment_selectSkills_configurationChange_startButtonRemainsActive() { + onView(atPosition(R.id.skill_recycler_view, 0)).perform(click()) + activityScenario.onActivity { activity -> + activity.requestedOrientation = Configuration.ORIENTATION_LANDSCAPE + } + activityScenario.recreate() + onView(withId(R.id.topic_train_start_button)).check(matches(isClickable())) + } + + @After + fun tearDown() { + Intents.release() + } + @Module class TestModule { @Provides