-
-
Notifications
You must be signed in to change notification settings - Fork 178
Add UI Tests using Espresso Testing Framework #1900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 9 commits
72ed403
ed1b5ba
65f98a6
81e2103
b49ea56
150332f
d329c02
34a588c
99cbace
f3e6bcc
3831392
4bc5a72
8ea7f02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,8 @@ jobs: | |
run: timeout 5m ./gradlew testReleaseUnitTest || { ./gradlew --stop && timeout 5m ./gradlew testReleaseUnitTest; } | ||
- name: SpotBugs | ||
run: ./gradlew spotbugsRelease | ||
- name: Espresso | ||
run: ./gradlew connectedAndroidTest | ||
- name: Archive test results | ||
if: always() | ||
uses: actions/[email protected] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,8 @@ android { | |
multiDexEnabled = true | ||
|
||
resourceConfigurations += listOf("ar", "bg", "bn", "bn-rIN", "bs", "cs", "da", "de", "el-rGR", "en", "eo", "es", "es-rAR", "fi", "fr", "he-rIL", "hi", "hr", "hu", "in-rID", "is", "it", "ja", "ko", "lt", "lv", "nb-rNO", "nl", "oc", "pl", "pt-rPT", "ro-rRO", "ru", "sk", "sl", "sv", "tr", "uk", "vi", "zh-rCN", "zh-rTW") | ||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | ||
testInstrumentationRunnerArguments["clearPackageData"] = "true" | ||
} | ||
|
||
buildTypes { | ||
|
@@ -94,6 +96,7 @@ dependencies { | |
implementation("androidx.preference:preference:1.2.1") | ||
implementation("com.google.android.material:material:1.12.0") | ||
implementation("com.github.yalantis:ucrop:2.2.9") | ||
implementation("androidx.tracing:tracing:1.2.0") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this dependency necessary? Given only new tests are being added, it seems weird to me to add a new runtime dependency. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are probably right this dependency might not be necessary. While trying to integrate fastlane screengrab (unsuccessfully) within these tests I had to add some more dependencies and probably did not remove them. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can see that the tests fail if you remove this, it seems to be a dependency of Espresso. However, given it is an Espresso test dependency and not a runtime dependency, it seems to me this should be an So that's what I tried... and it didn't work. And then I researched more. And... it seems to be an AGP bug. Sigh: android/android-test#1755 (comment) I decided to ask yet again for a way to track this issue as the previous request for that got no answer. It's definitely an ugly workaround, but it seems the only option currently: android/android-test#1755 (comment) |
||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") | ||
|
||
// Splash Screen | ||
|
@@ -113,6 +116,16 @@ dependencies { | |
testImplementation("androidx.test:core:1.5.0") | ||
testImplementation("junit:junit:4.13.2") | ||
testImplementation("org.robolectric:robolectric:4.12.2") | ||
|
||
// Espresso | ||
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.0-beta01") | ||
androidTestImplementation("androidx.test:core:1.5.0") | ||
androidTestImplementation("androidx.test:rules:1.6.0-beta01") | ||
androidTestImplementation("androidx.test.ext:junit:1.2.0-beta01") | ||
androidTestImplementation("androidx.test:runner:1.5.2") | ||
androidTestImplementation("androidx.test.espresso:espresso-contrib:3.6.0-beta01") | ||
|
||
androidTestUtil("androidx.test:orchestrator:1.1.0") | ||
} | ||
|
||
tasks.withType<SpotBugsTask>().configureEach { | ||
|
TheLastProject marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,178 @@ | ||||||||||||||||||||
package protect.card_locker; | ||||||||||||||||||||
|
||||||||||||||||||||
|
||||||||||||||||||||
import static androidx.test.espresso.Espresso.onData; | ||||||||||||||||||||
import static androidx.test.espresso.Espresso.onView; | ||||||||||||||||||||
import static androidx.test.espresso.action.ViewActions.click; | ||||||||||||||||||||
import static androidx.test.espresso.assertion.ViewAssertions.matches; | ||||||||||||||||||||
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition; | ||||||||||||||||||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; | ||||||||||||||||||||
import static androidx.test.espresso.matcher.ViewMatchers.withClassName; | ||||||||||||||||||||
import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription; | ||||||||||||||||||||
import static androidx.test.espresso.matcher.ViewMatchers.withId; | ||||||||||||||||||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent; | ||||||||||||||||||||
import static androidx.test.espresso.matcher.ViewMatchers.withText; | ||||||||||||||||||||
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; | ||||||||||||||||||||
import static org.hamcrest.Matchers.allOf; | ||||||||||||||||||||
import static org.hamcrest.Matchers.anything; | ||||||||||||||||||||
import static org.hamcrest.Matchers.is; | ||||||||||||||||||||
|
||||||||||||||||||||
import android.view.View; | ||||||||||||||||||||
import android.view.ViewGroup; | ||||||||||||||||||||
import android.view.ViewParent; | ||||||||||||||||||||
|
||||||||||||||||||||
import androidx.test.espresso.DataInteraction; | ||||||||||||||||||||
import androidx.test.espresso.ViewInteraction; | ||||||||||||||||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule; | ||||||||||||||||||||
import androidx.test.ext.junit.runners.AndroidJUnit4; | ||||||||||||||||||||
import androidx.test.filters.LargeTest; | ||||||||||||||||||||
|
||||||||||||||||||||
import org.hamcrest.Description; | ||||||||||||||||||||
import org.hamcrest.Matcher; | ||||||||||||||||||||
import org.hamcrest.TypeSafeMatcher; | ||||||||||||||||||||
import org.hamcrest.core.IsInstanceOf; | ||||||||||||||||||||
import org.junit.Rule; | ||||||||||||||||||||
import org.junit.Test; | ||||||||||||||||||||
import org.junit.runner.RunWith; | ||||||||||||||||||||
|
||||||||||||||||||||
@LargeTest | ||||||||||||||||||||
@RunWith(AndroidJUnit4.class) | ||||||||||||||||||||
public class LanguageChangeUITest { | ||||||||||||||||||||
|
||||||||||||||||||||
@Rule | ||||||||||||||||||||
public ActivityScenarioRule<MainActivity> mActivityScenarioRule = | ||||||||||||||||||||
new ActivityScenarioRule<>(MainActivity.class); | ||||||||||||||||||||
|
||||||||||||||||||||
@Test | ||||||||||||||||||||
public void languageChangeUITest() { | ||||||||||||||||||||
ViewInteraction overflowMenuButton = onView( | ||||||||||||||||||||
allOf(withContentDescription(R.string.action_more_options), | ||||||||||||||||||||
isDisplayed())); | ||||||||||||||||||||
overflowMenuButton.perform(click()); | ||||||||||||||||||||
|
||||||||||||||||||||
String expectedText = getInstrumentation().getTargetContext().getString(R.string.settings); | ||||||||||||||||||||
ViewInteraction materialTextView = onView( | ||||||||||||||||||||
allOf(withId(androidx.recyclerview.R.id.title), withText(expectedText), | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
withId(androidx.appcompat.R.id.content), | ||||||||||||||||||||
0), | ||||||||||||||||||||
0), | ||||||||||||||||||||
isDisplayed())); | ||||||||||||||||||||
materialTextView.perform(click()); | ||||||||||||||||||||
|
||||||||||||||||||||
ViewInteraction recyclerView = onView( | ||||||||||||||||||||
allOf(withId(com.jaredrummler.android.colorpicker.R.id.recycler_view), | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
withId(android.R.id.list_container), | ||||||||||||||||||||
0))); | ||||||||||||||||||||
recyclerView.perform(actionOnItemAtPosition(4, click())); | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just realized this text will break whenever we add a new language. That seems unnecessarily fragile. Can we instead make the test pick the tested language by string name? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes indeed adding new languages would make the tests fail. I will make the necessary changes to pick the tested language by string name. |
||||||||||||||||||||
|
||||||||||||||||||||
String selectedLanguage = "de"; | ||||||||||||||||||||
int position = getPositionOfItem(selectedLanguage); | ||||||||||||||||||||
|
||||||||||||||||||||
DataInteraction appCompatCheckedTextView = onData(anything()) | ||||||||||||||||||||
.inAdapterView(allOf(withId(androidx.appcompat.R.id.select_dialog_listview), | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
withId(androidx.appcompat.R.id.contentPanel), | ||||||||||||||||||||
0))) | ||||||||||||||||||||
.atPosition(position); | ||||||||||||||||||||
appCompatCheckedTextView.perform(click()); | ||||||||||||||||||||
|
||||||||||||||||||||
ViewInteraction textView = onView( | ||||||||||||||||||||
allOf(withText("Einstellungen"), | ||||||||||||||||||||
withParent(allOf(withId(R.id.toolbar), | ||||||||||||||||||||
withParent(IsInstanceOf.<View>instanceOf(android.widget.LinearLayout.class)))), | ||||||||||||||||||||
isDisplayed())); | ||||||||||||||||||||
textView.check(matches(withText("Einstellungen"))); | ||||||||||||||||||||
|
||||||||||||||||||||
ViewInteraction recyclerView2 = onView( | ||||||||||||||||||||
allOf(withId(com.jaredrummler.android.colorpicker.R.id.recycler_view), | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
withId(android.R.id.list_container), | ||||||||||||||||||||
0))); | ||||||||||||||||||||
recyclerView2.perform(actionOnItemAtPosition(4, click())); | ||||||||||||||||||||
|
||||||||||||||||||||
selectedLanguage = "el-rGR"; | ||||||||||||||||||||
position = getPositionOfItem(selectedLanguage); | ||||||||||||||||||||
|
||||||||||||||||||||
DataInteraction appCompatCheckedTextView2 = onData(anything()) | ||||||||||||||||||||
.inAdapterView(allOf(withId(androidx.appcompat.R.id.select_dialog_listview), | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
withId(androidx.appcompat.R.id.contentPanel), | ||||||||||||||||||||
0))) | ||||||||||||||||||||
.atPosition(position); | ||||||||||||||||||||
appCompatCheckedTextView2.perform(click()); | ||||||||||||||||||||
|
||||||||||||||||||||
ViewInteraction textView2 = onView( | ||||||||||||||||||||
allOf(withText("Ρυθμίσεις"), | ||||||||||||||||||||
withParent(allOf(withId(R.id.toolbar), | ||||||||||||||||||||
withParent(IsInstanceOf.<View>instanceOf(android.widget.LinearLayout.class)))), | ||||||||||||||||||||
isDisplayed())); | ||||||||||||||||||||
textView2.check(matches(withText("Ρυθμίσεις"))); | ||||||||||||||||||||
|
||||||||||||||||||||
ViewInteraction recyclerView3 = onView( | ||||||||||||||||||||
allOf(withId(com.jaredrummler.android.colorpicker.R.id.recycler_view), | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
withId(android.R.id.list_container), | ||||||||||||||||||||
0))); | ||||||||||||||||||||
recyclerView3.perform(actionOnItemAtPosition(4, click())); | ||||||||||||||||||||
|
||||||||||||||||||||
DataInteraction appCompatCheckedTextView3 = onData(anything()) | ||||||||||||||||||||
.inAdapterView(allOf(withId(androidx.appcompat.R.id.select_dialog_listview), | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
withId(androidx.appcompat.R.id.contentPanel), | ||||||||||||||||||||
0))) | ||||||||||||||||||||
.atPosition(0); | ||||||||||||||||||||
appCompatCheckedTextView3.perform(click()); | ||||||||||||||||||||
|
||||||||||||||||||||
ViewInteraction appCompatImageButton = onView( | ||||||||||||||||||||
allOf(withContentDescription("Navigate up"), | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
allOf(withId(R.id.toolbar), | ||||||||||||||||||||
childAtPosition( | ||||||||||||||||||||
withClassName(is("com.google.android.material.appbar.AppBarLayout")), | ||||||||||||||||||||
0)), | ||||||||||||||||||||
1), | ||||||||||||||||||||
isDisplayed())); | ||||||||||||||||||||
appCompatImageButton.perform(click()); | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The test fails for me here when testing on an Android 14 emulator:
I've attached the full view-hierarchy: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems weird to me because I am also running the tests on an Android 14 emulator, but I will examine this further. |
||||||||||||||||||||
|
||||||||||||||||||||
ViewInteraction textView3 = onView( | ||||||||||||||||||||
allOf(withId(R.id.welcome_text), withText("Welcome to Catima"), | ||||||||||||||||||||
withParent(allOf(withId(R.id.helpSection), | ||||||||||||||||||||
withParent(withId(R.id.include)))), | ||||||||||||||||||||
isDisplayed())); | ||||||||||||||||||||
textView3.check(matches(withText("Welcome to Catima"))); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
private static Matcher<View> childAtPosition( | ||||||||||||||||||||
final Matcher<View> parentMatcher, final int position) { | ||||||||||||||||||||
|
||||||||||||||||||||
return new TypeSafeMatcher<View>() { | ||||||||||||||||||||
@Override | ||||||||||||||||||||
public void describeTo(Description description) { | ||||||||||||||||||||
description.appendText("Child at position " + position + " in parent "); | ||||||||||||||||||||
parentMatcher.describeTo(description); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
@Override | ||||||||||||||||||||
public boolean matchesSafely(View view) { | ||||||||||||||||||||
ViewParent parent = view.getParent(); | ||||||||||||||||||||
return parent instanceof ViewGroup && parentMatcher.matches(parent) | ||||||||||||||||||||
&& view.equals(((ViewGroup) parent).getChildAt(position)); | ||||||||||||||||||||
} | ||||||||||||||||||||
}; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
private static int getPositionOfItem(String selectedLanguage) { | ||||||||||||||||||||
String[] availableLocales = getInstrumentation().getTargetContext().getResources().getStringArray(R.array.locale_values); | ||||||||||||||||||||
int position = 0; | ||||||||||||||||||||
for (String locale : availableLocales) { | ||||||||||||||||||||
if (locale.equals(selectedLanguage)) { | ||||||||||||||||||||
break; | ||||||||||||||||||||
} | ||||||||||||||||||||
position++; | ||||||||||||||||||||
} | ||||||||||||||||||||
return position; | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for your suggestion! I've integrated your changes into the PR. |
||||||||||||||||||||
} | ||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking this might also need the following change as documented on https://developer.android.com/training/testing/instrumented-tests/androidx-test-libraries/runner to ensure that running the "LoyaltyCardCreationTest" twice in a row won't cause failures but I haven't been able to test this yet as the LoyaltyCardCreationTest currently fails for me no matter what 😅