diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java b/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java index 903931daa21..a0a6bb76ef2 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java +++ b/collect_app/src/androidTest/java/org/odk/collect/android/regression/FillBlankFormTest.java @@ -575,7 +575,7 @@ public void when_scrollQuestionsList_should_questionsNotDisappear() { .copyForm("3403.xml", asList("staff_list.csv", "staff_rights.csv")) .startBlankForm("3403_ODK Version 1.23.3 Tester") .clickOnText("New Farmer Registration") - .scrollToAndClickText("Insemination") + .clickOnText("Insemination") .assertText("New Farmer Registration"); } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt new file mode 100644 index 00000000000..af7609962b9 --- /dev/null +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/Interactions.kt @@ -0,0 +1,47 @@ +package org.odk.collect.android.support + +import android.view.View +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Root +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.scrollTo +import org.hamcrest.Matcher +import org.odk.collect.android.support.WaitFor.tryAgainOnFail + +object Interactions { + + /** + * Click on the view matched by [view]. The root to use can optionally be specified with + * [root] (otherwise Espresso will use heuristics to determine the most likely root). If + * initially clicking on the view fails, this will then attempt to scroll to the view and + * retry the click. + */ + fun clickOn(view: Matcher, root: Matcher? = null) { + val onView = if (root != null) { + onView(view).inRoot(root) + } else { + onView(view) + } + + try { + onView.perform(click()) + } catch (e: Exception) { + onView.perform(scrollTo(), click()) + } + } + + /** + * Like [clickOn], but an [assertion] can be made after the click. If this fails, the click + * action will be reattempted. + * + * This can be useful in cases where [clickOn] itself appears to succeed, but the test fails + * because the click never actually occurs (most likely due to some flakiness in + * [androidx.test.espresso.action.ViewActions.click]). + */ + fun clickOn(view: Matcher, root: Matcher? = null, assertion: () -> Unit) { + tryAgainOnFail { + clickOn(view, root) + assertion() + } + } +} diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt index cf32f73f920..e05553a6285 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/FirstLaunchPage.kt @@ -1,25 +1,35 @@ package org.odk.collect.android.support.pages +import androidx.test.espresso.matcher.ViewMatchers.withSubstring +import org.odk.collect.android.support.Interactions +import org.odk.collect.strings.R.string + class FirstLaunchPage : Page() { override fun assertOnPage(): FirstLaunchPage { - assertText(org.odk.collect.strings.R.string.configure_with_qr_code) + assertText(string.configure_with_qr_code) return this } fun clickTryCollect(): MainMenuPage { - return tryAgainOnFail(MainMenuPage()) { - scrollToAndClickSubtext(org.odk.collect.strings.R.string.try_demo) + Interactions.clickOn(withSubstring(getTranslatedString(string.try_demo))) { + MainMenuPage().assertOnPage() } + + return MainMenuPage() } fun clickManuallyEnterProjectDetails(): ManualProjectCreatorDialogPage { - scrollToAndClickText(org.odk.collect.strings.R.string.configure_manually) - return ManualProjectCreatorDialogPage().assertOnPage() + return clickOnString( + string.configure_manually, + ManualProjectCreatorDialogPage() + ) } fun clickConfigureWithQrCode(): QrCodeProjectCreatorDialogPage { - scrollToAndClickText(org.odk.collect.strings.R.string.configure_with_qr_code) - return QrCodeProjectCreatorDialogPage().assertOnPage() + return clickOnString( + string.configure_with_qr_code, + QrCodeProjectCreatorDialogPage() + ) } } diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt index 7607d6c078c..209a0d47038 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/support/pages/Page.kt @@ -33,7 +33,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withHint import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withSubstring import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice @@ -50,6 +49,7 @@ import org.odk.collect.android.R import org.odk.collect.android.application.Collect import org.odk.collect.android.storage.StoragePathProvider import org.odk.collect.android.support.ActivityHelpers.getLaunchIntent +import org.odk.collect.android.support.Interactions import org.odk.collect.android.support.WaitFor.tryAgainOnFail import org.odk.collect.android.support.WaitFor.wait250ms import org.odk.collect.android.support.WaitFor.waitFor @@ -239,8 +239,7 @@ abstract class Page> { } fun > clickOnString(stringID: Int, destination: D): D { - tryAgainOnFail { - clickOnString(stringID) + Interactions.clickOn(withText(getTranslatedString(stringID))) { destination.assertOnPage() } @@ -253,12 +252,12 @@ abstract class Page> { } fun clickOnText(text: String): T { - onView(withText(text)).perform(click()) + Interactions.clickOn(withText(text)) return this as T } fun clickOnId(id: Int): T { - onView(withId(id)).perform(click()) + Interactions.clickOn(withId(id)) return this as T } @@ -270,26 +269,20 @@ abstract class Page> { fun clickOKOnDialog(): T { closeSoftKeyboard() // Make sure to avoid issues with keyboard being up waitForDialogToSettle() - onView(withId(android.R.id.button1)) - .inRoot(isDialog()) - .perform(click()) + Interactions.clickOn(withId(android.R.id.button1), root = isDialog()) return this as T } fun ?> clickOKOnDialog(destination: D): D { closeSoftKeyboard() // Make sure to avoid issues with keyboard being up waitForDialogToSettle() - onView(withId(android.R.id.button1)) - .inRoot(isDialog()) - .perform(click()) + Interactions.clickOn(withId(android.R.id.button1), root = isDialog()) return destination!!.assertOnPage() } fun clickOnTextInDialog(text: String): T { waitForDialogToSettle() - onView(withText(text)) - .inRoot(isDialog()) - .perform(click()) + Interactions.clickOn(withText(text), root = isDialog()) return this as T } @@ -315,7 +308,7 @@ abstract class Page> { } fun clickOnAreaWithIndex(clazz: String?, index: Int): T { - onView(withIndex(withClassName(endsWith(clazz)), index)).perform(click()) + Interactions.clickOn(withIndex(withClassName(endsWith(clazz)), index)) return this as T } @@ -382,21 +375,6 @@ abstract class Page> { return this as T } - fun scrollToAndClickText(text: Int): T { - onView(withText(getTranslatedString(text))).perform(scrollTo(), click()) - return this as T - } - - fun scrollToAndClickSubtext(text: Int): T { - onView(withSubstring(getTranslatedString(text))).perform(scrollTo(), click()) - return this as T - } - - fun scrollToAndClickText(text: String?): T { - onView(withText(text)).perform(scrollTo(), click()) - return this as T - } - fun scrollToRecyclerViewItemAndClickText(text: String?): T { onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), scrollTo())) onView(withId(androidx.preference.R.id.recycler_view)).perform(RecyclerViewActions.actionOnItem(hasDescendant(withText(text)), click())) @@ -478,7 +456,7 @@ abstract class Page> { } fun closeSnackbar(): T { - onView(withContentDescription(org.odk.collect.strings.R.string.close_snackbar)).perform(click()) + Interactions.clickOn(withContentDescription(org.odk.collect.strings.R.string.close_snackbar)) return this as T } @@ -487,8 +465,7 @@ abstract class Page> { } fun clickOptionsIcon(expectedOptionString: String): T { - tryAgainOnFail { - onView(OVERFLOW_BUTTON_MATCHER).perform(click()) + Interactions.clickOn(OVERFLOW_BUTTON_MATCHER) { assertText(expectedOptionString) } @@ -532,7 +509,7 @@ abstract class Page> { } fun clickOnTextInPopup(text: Int): T { - onView(withText(text)).inRoot(isPlatformPopup()).perform(click()) + Interactions.clickOn(withText(text), root = isPlatformPopup()) return this as T }