diff options
author | cyberta <cyberta@riseup.net> | 2023-01-19 18:47:14 +0100 |
---|---|---|
committer | cyberta <cyberta@riseup.net> | 2023-01-19 18:47:14 +0100 |
commit | 108b886f43feeb861807deddfcb1ab241330e242 (patch) | |
tree | a52ea42ad366067c93faaadc4093ed05cb0dd640 /app/src/androidTest | |
parent | b0f96ee9ca2c612836f1a59222b1148b763fbedd (diff) |
improve screenshot tests, run in different languages, test more Fragments/Activities
Diffstat (limited to 'app/src/androidTest')
-rw-r--r-- | app/src/androidTest/java/se/leap/bitmaskclient/base/ProviderSetupTest.java (renamed from app/src/androidTest/java/base/ProviderSetupTest.java) | 19 | ||||
-rw-r--r-- | app/src/androidTest/java/se/leap/bitmaskclient/base/VpnStartTest.java | 176 | ||||
-rw-r--r-- | app/src/androidTest/java/se/leap/bitmaskclient/suite/ScreenshotTest.java | 19 | ||||
-rw-r--r-- | app/src/androidTest/java/utils/CustomInteractions.java | 81 | ||||
-rw-r--r-- | app/src/androidTest/java/utils/CustomMatchers.java | 31 | ||||
-rw-r--r-- | app/src/androidTest/java/utils/CustomViewActions.java | 89 |
6 files changed, 409 insertions, 6 deletions
diff --git a/app/src/androidTest/java/base/ProviderSetupTest.java b/app/src/androidTest/java/se/leap/bitmaskclient/base/ProviderSetupTest.java index d5a363aa..23db8582 100644 --- a/app/src/androidTest/java/base/ProviderSetupTest.java +++ b/app/src/androidTest/java/se/leap/bitmaskclient/base/ProviderSetupTest.java @@ -1,4 +1,4 @@ -package base; +package se.leap.bitmaskclient.base; import static android.content.Context.MODE_PRIVATE; @@ -7,10 +7,15 @@ import static androidx.test.espresso.Espresso.onData; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static org.hamcrest.Matchers.anything; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasToString; import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static utils.CustomInteractions.tryResolve; import android.content.SharedPreferences; +import androidx.test.espresso.DataInteraction; +import androidx.test.espresso.NoMatchingViewException; import androidx.test.ext.junit.rules.ActivityScenarioRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; @@ -47,15 +52,17 @@ public class ProviderSetupTest { @Test public void testConfigureRiseupVPNScreenshot() { - Screengrab.screenshot("configureRiseupVPN_before_button_click"); - onData(anything()).inAdapterView(withId(R.id.provider_list)).atPosition(2).perform(click()); - Screengrab.screenshot("configureRiseupVPN_after_button_click"); + DataInteraction linearLayout = tryResolve(onData(hasToString(containsString("riseup.net"))) + .inAdapterView(withId(R.id.provider_list)), + 2); + Screengrab.screenshot("ProviderListActivity"); + linearLayout.perform(click()); + Screengrab.screenshot("ProviderListActivity_configureRiseup"); } @Test public void testaddManuallyNewProviderScreenshot() { - Screengrab.screenshot("addManuallyNewProvider_before_button_click"); onData(anything()).inAdapterView(withId(R.id.provider_list)).atPosition(3).perform(click()); - Screengrab.screenshot("addManuallyNewProvider_after_button_click"); + Screengrab.screenshot("ProviderListActivity_addManuallyNewProvider"); } } diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/base/VpnStartTest.java b/app/src/androidTest/java/se/leap/bitmaskclient/base/VpnStartTest.java new file mode 100644 index 00000000..6c99b90e --- /dev/null +++ b/app/src/androidTest/java/se/leap/bitmaskclient/base/VpnStartTest.java @@ -0,0 +1,176 @@ +package se.leap.bitmaskclient.base; + + +import static android.content.Context.MODE_PRIVATE; +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +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.DrawerMatchers.isClosed; +import static androidx.test.espresso.matcher.RootMatchers.isDialog; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withTagValue; +import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.anything; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.is; +import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES; +import static utils.CustomInteractions.tryResolve; + +import android.content.SharedPreferences; +import android.view.Gravity; + +import androidx.test.espresso.DataInteraction; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.espresso.ViewInteraction; +import androidx.test.espresso.contrib.DrawerActions; +import androidx.test.ext.junit.rules.ActivityScenarioRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.FixMethodOrder; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.MethodSorters; + +import se.leap.bitmaskclient.R; +import tools.fastlane.screengrab.Screengrab; +import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy; +import tools.fastlane.screengrab.locale.LocaleTestRule; + +@LargeTest +@RunWith(AndroidJUnit4.class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class VpnStartTest { + + @ClassRule + public static final LocaleTestRule localeTestRule = new LocaleTestRule(); + + @Rule + public ActivityScenarioRule<StartActivity> mActivityScenarioRule = + new ActivityScenarioRule<>(StartActivity.class); + + @Before + public void setup() { + Screengrab.setDefaultScreenshotStrategy(new UiAutomatorScreenshotStrategy()); + SharedPreferences preferences = getApplicationContext().getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE); + preferences.edit().clear().commit(); + } + + @Test + public void test01_vpnStartTest() { + boolean configurationNeeded = configureProviderIfNeeded(); + + ViewInteraction mainButtonStop; + if (!configurationNeeded) { + // click on Main on/off button and start VPN + ViewInteraction mainButton = tryResolve( + onView(withId(R.id.main_button)), + matches(isDisplayed()) + ); + + mainButton.perform(click()); + tryResolve( + onView(allOf( + withId(R.id.button), + withTagValue(is("button_circle_cancel")))), + matches(isDisplayed()), + 2); + Screengrab.screenshot("VPN_connecting"); + + mainButtonStop = tryResolve( + onView(allOf( + withId(R.id.button), + withTagValue(is("button_circle_stop")))), + matches(isDisplayed()), + 20); + Screengrab.screenshot("VPN_connected"); + } else { + // on new configurations the VPN is automatically started + Screengrab.screenshot("VPN_connecting"); + mainButtonStop = tryResolve( + onView(allOf( + withId(R.id.button), + withTagValue(is("button_circle_stop")))), + matches(isDisplayed()), + 20); + Screengrab.screenshot("VPN_connected"); + } + + mainButtonStop.perform(click()); + Screengrab.screenshot("VPN_ask_disconnect"); + + onView(withText(android.R.string.yes)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + .perform(click()); + Screengrab.screenshot("VPN_disconnected"); + } + + @Test + public void test02_SettingsFragmentScreenshots() { + onView(withId(R.id.drawer_layout)) + .check(matches(isClosed(Gravity.LEFT))) // Left Drawer should be closed. + .perform(DrawerActions.open()); // Open Drawer + + Screengrab.screenshot("navigationDrawer"); + + // Start the screen of your activity. + onView(withId(R.id.advancedSettings)) + .perform(click()); + + Screengrab.screenshot("settingsFragment"); + } + + @Test + public void test03_LocationSelectionFragmentScreenshots() { + onView(withId(R.id.drawer_layout)) + .check(matches(isClosed(Gravity.LEFT))) // Left Drawer should be closed. + .perform(DrawerActions.open()); // Open Drawer + + onView(withId(R.id.manualGatewaySelection)) + .perform(click()); + + Screengrab.screenshot("GatewaySelectionFragment"); + } + + @Test + public void test04_AppExclusionFragmentScreenshots() { + onView(withId(R.id.drawer_layout)) + .check(matches(isClosed(Gravity.LEFT))) // Left Drawer should be closed. + .perform(DrawerActions.open()); // Open Drawer + + onView(withId(R.id.advancedSettings)).perform(click()); + + onView(withId(R.id.exclude_apps)).perform(click()); + + tryResolve( + onData(anything()).inAdapterView(withId(android.R.id.list)).atPosition(2), + matches(isDisplayed()), + 5); + + Screengrab.screenshot("App_Exclusion_Fragment"); + } + + public boolean configureProviderIfNeeded() { + try { + DataInteraction linearLayout = tryResolve(onData(hasToString(containsString("riseup.net"))) + .inAdapterView(withId(R.id.provider_list)), + 2); + linearLayout.perform(click()); + return true; + } catch (NoMatchingViewException e) { + // it might be that the provider was already configured, so we print the stack + // trace here and try to continue + e.printStackTrace(); + } + return false; + } +} diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/suite/ScreenshotTest.java b/app/src/androidTest/java/se/leap/bitmaskclient/suite/ScreenshotTest.java new file mode 100644 index 00000000..186a50d1 --- /dev/null +++ b/app/src/androidTest/java/se/leap/bitmaskclient/suite/ScreenshotTest.java @@ -0,0 +1,19 @@ +package se.leap.bitmaskclient.suite; + + +import androidx.test.filters.LargeTest; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +import se.leap.bitmaskclient.base.ProviderSetupTest; +import se.leap.bitmaskclient.base.VpnStartTest; + +@LargeTest +@RunWith(Suite.class) +@Suite.SuiteClasses({ + ProviderSetupTest.class, + VpnStartTest.class, +}) +public class ScreenshotTest { +} diff --git a/app/src/androidTest/java/utils/CustomInteractions.java b/app/src/androidTest/java/utils/CustomInteractions.java new file mode 100644 index 00000000..896e8d9b --- /dev/null +++ b/app/src/androidTest/java/utils/CustomInteractions.java @@ -0,0 +1,81 @@ +package utils; + +import androidx.annotation.Nullable; +import androidx.test.espresso.DataInteraction; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.espresso.ViewAssertion; +import androidx.test.espresso.ViewInteraction; + +public class CustomInteractions { + + public static @Nullable + ViewInteraction tryResolve(ViewInteraction viewInteraction, int maxTries) { + return tryResolve(viewInteraction, null, maxTries); + } + + public static @Nullable + ViewInteraction tryResolve(ViewInteraction viewInteraction, ViewAssertion assertion) { + return tryResolve(viewInteraction, assertion, 10); + } + + public static @Nullable ViewInteraction tryResolve(ViewInteraction viewInteraction, ViewAssertion assertion, int maxTries) { + ViewInteraction resolvedViewInteraction = null; + int attempt = 0; + boolean hasFound = false; + while (!hasFound && attempt < maxTries) { + try { + resolvedViewInteraction = viewInteraction; + if (assertion != null) { + resolvedViewInteraction.check(assertion); + } + hasFound = true; + } catch (NoMatchingViewException exception) { + System.out.println("NoMatchingViewException attempt: " + attempt); + attempt++; + if (attempt == maxTries) { + throw exception; + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + break; + } + } + } + return resolvedViewInteraction; + } + + public static @Nullable + DataInteraction tryResolve(DataInteraction dataInteraction, ViewAssertion assertion, int maxTries) { + DataInteraction resolvedDataInteraction = null; + int attempt = 0; + boolean hasFound = false; + while (!hasFound && attempt < maxTries) { + try { + resolvedDataInteraction = dataInteraction; + if (assertion != null) { + resolvedDataInteraction.check(assertion); + } + hasFound = true; + } catch (Exception exception) { + // TODO: specify expected exception + attempt++; + if (attempt == maxTries) { + throw exception; + } + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + break; + } + } + } + return resolvedDataInteraction; + } + public static @Nullable + DataInteraction tryResolve(DataInteraction dataInteraction, int maxTries) { + return tryResolve(dataInteraction, null, maxTries); + } +} diff --git a/app/src/androidTest/java/utils/CustomMatchers.java b/app/src/androidTest/java/utils/CustomMatchers.java new file mode 100644 index 00000000..5fe11cd2 --- /dev/null +++ b/app/src/androidTest/java/utils/CustomMatchers.java @@ -0,0 +1,31 @@ +package utils; + +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +public class CustomMatchers { + + public 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)); + } + }; + } +} diff --git a/app/src/androidTest/java/utils/CustomViewActions.java b/app/src/androidTest/java/utils/CustomViewActions.java new file mode 100644 index 00000000..fb0dcc27 --- /dev/null +++ b/app/src/androidTest/java/utils/CustomViewActions.java @@ -0,0 +1,89 @@ +package utils; + +import static androidx.test.espresso.matcher.ViewMatchers.isRoot; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import android.view.View; + +import androidx.annotation.StringRes; +import androidx.test.espresso.UiController; +import androidx.test.espresso.ViewAction; +import androidx.test.espresso.util.TreeIterables; + +import org.hamcrest.Matcher; + +public class CustomViewActions { + + public static ViewAction waitForView(int viewId, long timeout) { + return new ViewAction() { + + @Override + public String getDescription() { + return "Wait for view with specific id \n@param viewId: resource ID \n@param timeout: timeout in milli seconds."; + } + + @Override + public Matcher<View> getConstraints() { + return isRoot(); + } + + @Override + public void perform(UiController uiController, View view) { + uiController.loopMainThreadUntilIdle(); + long startTime = System.currentTimeMillis(); + long endTime = startTime + timeout; + Matcher viewMatcher = withId(viewId); + + while (System.currentTimeMillis() < endTime) { + // Iterate through all views on the screen and see if the view we are looking for is there already + for (View child : TreeIterables.breadthFirstViewTraversal(view)) { + // found view with required ID + if (viewMatcher.matches(child)) { + return; + } + } + // Loops the main thread for a specified period of time. + // Control may not return immediately, instead it'll return after the provided delay has passed and the queue is in an idle state again. + uiController.loopMainThreadForAtLeast(100); + } + } + }; + } + + public static ViewAction waitForText(@StringRes int textId, long timeout) { + return new ViewAction() { + + @Override + public String getDescription() { + return "Wait for view with specific id \n@param viewId: resource ID \n@param timeout: timeout in milli seconds."; + } + + @Override + public Matcher<View> getConstraints() { + return isRoot(); + } + + @Override + public void perform(UiController uiController, View view) { + uiController.loopMainThreadUntilIdle(); + long startTime = System.currentTimeMillis(); + long endTime = startTime + timeout; + Matcher viewMatcher = withText(textId); + + while (System.currentTimeMillis() < endTime) { + // Iterate through all views on the screen and see if the view we are looking for is there already + for (View child : TreeIterables.breadthFirstViewTraversal(view)) { + // found view with required ID + if (viewMatcher.matches(child)) { + return; + } + } + // Loops the main thread for a specified period of time. + // Control may not return immediately, instead it'll return after the provided delay has passed and the queue is in an idle state again. + uiController.loopMainThreadForAtLeast(100); + } + } + }; + } +}
\ No newline at end of file |