Skip to content
This repository was archived by the owner on Oct 25, 2025. It is now read-only.

Commit e194273

Browse files
committed
End of codelab 3
1 parent 7407f2e commit e194273

File tree

16 files changed

+1039
-70
lines changed

16 files changed

+1039
-70
lines changed

app/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ android {
2929
// Always show the result of every unit test when running via command line, even if it passes.
3030
testOptions.unitTests {
3131
includeAndroidResources = true
32+
returnDefaultValues = true
3233
}
3334
}
3435

@@ -43,6 +44,7 @@ dependencies {
4344
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
4445
implementation "com.jakewharton.timber:timber:$timberVersion"
4546
implementation "androidx.legacy:legacy-support-v4:$androidXLegacySupport"
47+
implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion"
4648
implementation "androidx.room:room-runtime:$roomVersion"
4749
kapt "androidx.room:room-compiler:$roomVersion"
4850

@@ -83,6 +85,7 @@ dependencies {
8385
androidTestImplementation "androidx.test.ext:junit:$androidXTestExtKotlinRunnerVersion"
8486
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
8587
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
88+
androidTestImplementation "androidx.arch.core:core-testing:$archTestingVersion"
8689

8790
// Kotlin
8891
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Copyright (C) 2019 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.android.architecture.blueprints.todoapp
17+
18+
import android.app.Activity
19+
import android.view.Gravity
20+
import androidx.appcompat.widget.Toolbar
21+
import androidx.drawerlayout.widget.DrawerLayout
22+
import androidx.test.core.app.ActivityScenario
23+
import androidx.test.core.app.ApplicationProvider.getApplicationContext
24+
import androidx.test.espresso.Espresso.onView
25+
import androidx.test.espresso.Espresso.pressBack
26+
import androidx.test.espresso.IdlingRegistry
27+
import androidx.test.espresso.action.ViewActions.click
28+
import androidx.test.espresso.assertion.ViewAssertions.matches
29+
import androidx.test.espresso.contrib.DrawerMatchers.isClosed
30+
import androidx.test.espresso.contrib.DrawerMatchers.isOpen
31+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
32+
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
33+
import androidx.test.espresso.matcher.ViewMatchers.withId
34+
import androidx.test.espresso.matcher.ViewMatchers.withText
35+
import androidx.test.ext.junit.runners.AndroidJUnit4
36+
import androidx.test.filters.LargeTest
37+
import com.example.android.architecture.blueprints.todoapp.data.Task
38+
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
39+
import com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity
40+
import com.example.android.architecture.blueprints.todoapp.util.DataBindingIdlingResource
41+
import com.example.android.architecture.blueprints.todoapp.util.EspressoIdlingResource
42+
import com.example.android.architecture.blueprints.todoapp.util.monitorActivity
43+
import kotlinx.coroutines.runBlocking
44+
import org.junit.After
45+
import org.junit.Before
46+
import org.junit.Test
47+
import org.junit.runner.RunWith
48+
49+
/**
50+
* Tests for the [DrawerLayout] layout component in [TasksActivity] which manages
51+
* navigation within the app.
52+
*
53+
* UI tests usually use [ActivityTestRule] but there's no API to perform an action before
54+
* each test. The workaround is to use `ActivityScenario.launch()` and `ActivityScenario.close()`.
55+
*/
56+
@RunWith(AndroidJUnit4::class)
57+
@LargeTest
58+
class AppNavigationTest {
59+
60+
private lateinit var tasksRepository: TasksRepository
61+
62+
// An Idling Resource that waits for Data Binding to have no pending bindings
63+
private val dataBindingIdlingResource = DataBindingIdlingResource()
64+
65+
@Before
66+
fun init() {
67+
tasksRepository = ServiceLocator.provideTasksRepository(getApplicationContext())
68+
}
69+
70+
@After
71+
fun reset() {
72+
ServiceLocator.resetRepository()
73+
}
74+
75+
/**
76+
* Idling resources tell Espresso that the app is idle or busy. This is needed when operations
77+
* are not scheduled in the main Looper (for example when executed on a different thread).
78+
*/
79+
@Before
80+
fun registerIdlingResource() {
81+
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
82+
IdlingRegistry.getInstance().register(dataBindingIdlingResource)
83+
}
84+
85+
/**
86+
* Unregister your Idling Resource so it can be garbage collected and does not leak any memory.
87+
*/
88+
@After
89+
fun unregisterIdlingResource() {
90+
IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
91+
IdlingRegistry.getInstance().unregister(dataBindingIdlingResource)
92+
}
93+
94+
@Test
95+
fun tasksScreen_clickOnAndroidHomeIcon_OpensNavigation() {
96+
// Start up Tasks screen
97+
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
98+
dataBindingIdlingResource.monitorActivity(activityScenario)
99+
100+
// Check that left drawer is closed at startup
101+
onView(withId(R.id.drawer_layout))
102+
.check(matches(isClosed(Gravity.START))) // Left Drawer should be closed.
103+
104+
// Open drawer by clicking drawer icon
105+
onView(
106+
withContentDescription(
107+
activityScenario
108+
.getToolbarNavigationContentDescription()
109+
)
110+
).perform(click())
111+
112+
// Check if drawer is open
113+
onView(withId(R.id.drawer_layout))
114+
.check(matches(isOpen(Gravity.START))) // Left drawer is open open.
115+
// When using ActivityScenario.launch, always call close()
116+
activityScenario.close()
117+
}
118+
119+
@Test
120+
fun taskDetailScreen_doubleUpButton() = runBlocking {
121+
val task = Task("Up <- button", "Description")
122+
tasksRepository.saveTask(task)
123+
124+
// Start up Tasks screen
125+
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
126+
dataBindingIdlingResource.monitorActivity(activityScenario)
127+
128+
// Click on the task on the list
129+
onView(withText("Up <- button")).perform(click())
130+
131+
// Click on the edit task button
132+
onView(withId(R.id.edit_task_fab)).perform(click())
133+
134+
// Confirm that if we click up button once, we end up back at the task details page
135+
onView(
136+
withContentDescription(
137+
activityScenario
138+
.getToolbarNavigationContentDescription()
139+
)
140+
).perform(click())
141+
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
142+
143+
// Confirm that if we click up button a second time, we end up back at the home screen
144+
onView(
145+
withContentDescription(
146+
activityScenario
147+
.getToolbarNavigationContentDescription()
148+
)
149+
).perform(click())
150+
onView(withId(R.id.tasks_container_layout)).check(matches(isDisplayed()))
151+
152+
// When using ActivityScenario.launch, always call close()
153+
activityScenario.close()
154+
}
155+
156+
@Test
157+
fun taskDetailScreen_doubleBackButton() = runBlocking {
158+
val task = Task("Back button", "Description")
159+
tasksRepository.saveTask(task)
160+
161+
// Start up Tasks screen
162+
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
163+
dataBindingIdlingResource.monitorActivity(activityScenario)
164+
165+
// Click on the task on the list
166+
onView(withText("Back button")).perform(click())
167+
168+
// Click on the edit task button
169+
onView(withId(R.id.edit_task_fab)).perform(click())
170+
171+
// Confirm that if we click back once, we end up back at the task details page
172+
pressBack()
173+
onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
174+
175+
// Confirm that if we click back a second time, we end up back at the home screen
176+
pressBack()
177+
onView(withId(R.id.tasks_container_layout)).check(matches(isDisplayed()))
178+
179+
// When using ActivityScenario.launch, always call close()
180+
activityScenario.close()
181+
}
182+
}
183+
184+
fun <T : Activity> ActivityScenario<T>.getToolbarNavigationContentDescription()
185+
: String {
186+
var description = ""
187+
onActivity {
188+
description =
189+
it.findViewById<Toolbar>(R.id.toolbar).navigationContentDescription as String
190+
}
191+
return description
192+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright (C) 2019 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.android.architecture.blueprints.todoapp
17+
18+
import androidx.test.core.app.ActivityScenario
19+
import androidx.test.core.app.ApplicationProvider.getApplicationContext
20+
import androidx.test.espresso.Espresso.onView
21+
import androidx.test.espresso.IdlingRegistry
22+
import androidx.test.espresso.action.ViewActions.click
23+
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
24+
import androidx.test.espresso.action.ViewActions.replaceText
25+
import androidx.test.espresso.action.ViewActions.typeText
26+
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
27+
import androidx.test.espresso.assertion.ViewAssertions.matches
28+
import androidx.test.espresso.matcher.ViewMatchers.isChecked
29+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
30+
import androidx.test.espresso.matcher.ViewMatchers.withId
31+
import androidx.test.espresso.matcher.ViewMatchers.withText
32+
import androidx.test.ext.junit.runners.AndroidJUnit4
33+
import androidx.test.filters.LargeTest
34+
import com.example.android.architecture.blueprints.todoapp.data.Task
35+
import com.example.android.architecture.blueprints.todoapp.data.source.TasksRepository
36+
import com.example.android.architecture.blueprints.todoapp.tasks.TasksActivity
37+
import com.example.android.architecture.blueprints.todoapp.util.DataBindingIdlingResource
38+
import com.example.android.architecture.blueprints.todoapp.util.EspressoIdlingResource
39+
import com.example.android.architecture.blueprints.todoapp.util.monitorActivity
40+
import kotlinx.coroutines.runBlocking
41+
import org.hamcrest.core.IsNot.not
42+
import org.junit.After
43+
import org.junit.Before
44+
import org.junit.Test
45+
import org.junit.runner.RunWith
46+
47+
/**
48+
* Large End-to-End test for the tasks module.
49+
*
50+
* UI tests usually use [ActivityTestRule] but there's no API to perform an action before
51+
* each test. The workaround is to use `ActivityScenario.launch()` and `ActivityScenario.close()`.
52+
*/
53+
@RunWith(AndroidJUnit4::class)
54+
@LargeTest
55+
class TasksActivityTest {
56+
57+
private lateinit var repository: TasksRepository
58+
59+
// An Idling Resource that waits for Data Binding to have no pending bindings
60+
private val dataBindingIdlingResource = DataBindingIdlingResource()
61+
62+
@Before
63+
fun init() {
64+
repository =
65+
ServiceLocator.provideTasksRepository(
66+
getApplicationContext()
67+
)
68+
runBlocking {
69+
repository.deleteAllTasks()
70+
}
71+
}
72+
73+
@After
74+
fun reset() {
75+
ServiceLocator.resetRepository()
76+
}
77+
78+
/**
79+
* Idling resources tell Espresso that the app is idle or busy. This is needed when operations
80+
* are not scheduled in the main Looper (for example when executed on a different thread).
81+
*/
82+
@Before
83+
fun registerIdlingResource() {
84+
IdlingRegistry.getInstance().register(EspressoIdlingResource.countingIdlingResource)
85+
IdlingRegistry.getInstance().register(dataBindingIdlingResource)
86+
}
87+
88+
/**
89+
* Unregister your Idling Resource so it can be garbage collected and does not leak any memory.
90+
*/
91+
@After
92+
fun unregisterIdlingResource() {
93+
IdlingRegistry.getInstance().unregister(EspressoIdlingResource.countingIdlingResource)
94+
IdlingRegistry.getInstance().unregister(dataBindingIdlingResource)
95+
}
96+
97+
@Test
98+
fun editTask() = runBlocking {
99+
repository.saveTask(Task("TITLE1", "DESCRIPTION"))
100+
101+
// Start up Tasks screen
102+
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
103+
dataBindingIdlingResource.monitorActivity(activityScenario)
104+
105+
// Click on the task on the list and verify that all the data is correct
106+
onView(withText("TITLE1")).perform(click())
107+
onView(withId(R.id.task_detail_title_text)).check(matches(withText("TITLE1")))
108+
onView(withId(R.id.task_detail_description_text)).check(matches(withText("DESCRIPTION")))
109+
onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
110+
111+
// Click on the edit button, edit, and save
112+
onView(withId(R.id.edit_task_fab)).perform(click())
113+
onView(withId(R.id.add_task_title_edit_text)).perform(replaceText("NEW TITLE"))
114+
onView(withId(R.id.add_task_description_edit_text)).perform(replaceText("NEW DESCRIPTION"))
115+
onView(withId(R.id.save_task_fab)).perform(click())
116+
117+
// Verify task is displayed on screen in the task list.
118+
onView(withText("NEW TITLE")).check(matches(isDisplayed()))
119+
// Verify previous task is not displayed
120+
onView(withText("TITLE1")).check(doesNotExist())
121+
122+
// Make sure the activity is closed before resetting the db:
123+
activityScenario.close()
124+
}
125+
126+
@Test
127+
fun createOneTask_deleteTask() {
128+
129+
// start up Tasks screen
130+
val activityScenario = ActivityScenario.launch(TasksActivity::class.java)
131+
dataBindingIdlingResource.monitorActivity(activityScenario)
132+
133+
// Add active task
134+
onView(withId(R.id.add_task_fab)).perform(click())
135+
onView(withId(R.id.add_task_title_edit_text))
136+
.perform(typeText("TITLE1"), closeSoftKeyboard())
137+
onView(withId(R.id.add_task_description_edit_text)).perform(typeText("DESCRIPTION"))
138+
onView(withId(R.id.save_task_fab)).perform(click())
139+
140+
// Open it in details view
141+
onView(withText("TITLE1")).perform(click())
142+
// Click delete task in menu
143+
onView(withId(R.id.menu_delete)).perform(click())
144+
145+
// Verify it was deleted
146+
onView(withId(R.id.menu_filter)).perform(click())
147+
onView(withText(R.string.nav_all)).perform(click())
148+
onView(withText("TITLE1")).check(doesNotExist())
149+
// Make sure the activity is closed before resetting the db:
150+
activityScenario.close()
151+
}
152+
}

0 commit comments

Comments
 (0)