diff --git a/bramble-android/build.gradle b/bramble-android/build.gradle index f7c9415ed49dc42d6e0c25af540a773d02e9f47e..0e6e49e1769034c7bfeafd56b53ef72362088f5e 100644 --- a/bramble-android/build.gradle +++ b/bramble-android/build.gradle @@ -15,6 +15,12 @@ android { testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } + buildTypes { + debug + screenshot + release + } + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/briar-android/build.gradle b/briar-android/build.gradle index 77b2fcf6e1ed019a631bf8592010fc41594a7408..7d0714bb7cbdf39d7b57873cbab3faa3b40fc011 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -49,6 +49,14 @@ dependencies { testImplementation "org.jmock:jmock-legacy:2.8.2" testImplementation "org.hamcrest:hamcrest-library:1.3" testImplementation "org.hamcrest:hamcrest-core:1.3" + + def espressoVersion = '3.0.2' + androidTestImplementation "com.android.support.test.espresso:espresso-core:$espressoVersion" + androidTestImplementation "com.android.support.test.espresso:espresso-contrib:$espressoVersion" + androidTestImplementation "tools.fastlane:screengrab:1.1.0" + androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:2.0.2" + androidTestCompileOnly 'javax.annotation:jsr250-api:1.0' + androidTestImplementation 'junit:junit:4.12' } dependencyVerification { @@ -248,6 +256,7 @@ android { def now = (long) (System.currentTimeMillis() / 1000) buildConfigField "Long", "BuildTimestamp", "${getStdout(['git', 'log', '-n', '1', '--format=%ct'], now)}000L" + testInstrumentationRunner 'org.briarproject.briar.android.test.BriarTestRunner' } buildTypes { @@ -260,6 +269,13 @@ android { crunchPngs false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' } + screenshot { + initWith debug + applicationIdSuffix ".screenshot" + resValue "string", "app_package", "org.briarproject.briar.android.screenshot" + resValue "string", "app_name", "Briar Screenshot" + testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-test.txt' + } release { shrinkResources false minifyEnabled true @@ -268,6 +284,8 @@ android { } } + testBuildType "screenshot" + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/briar-android/fastlane/Screengrabfile b/briar-android/fastlane/Screengrabfile new file mode 100644 index 0000000000000000000000000000000000000000..690bc4cc89f8739eb440e6ca62a28c4f1dc358fd --- /dev/null +++ b/briar-android/fastlane/Screengrabfile @@ -0,0 +1,5 @@ +app_package_name "org.briarproject.briar.android.screenshot" +locales ['en-US'] +app_apk_path "build/outputs/apk/screenshot/briar-android-screenshot.apk" +tests_apk_path "build/outputs/apk/androidTest/screenshot/briar-android-screenshot-androidTest.apk" +test_instrumentation_runner "org.briarproject.briar.android.test.BriarTestRunner" \ No newline at end of file diff --git a/briar-android/proguard-test.txt b/briar-android/proguard-test.txt new file mode 100644 index 0000000000000000000000000000000000000000..7007db0c24f9135ae365351b24d526e928fdcee8 --- /dev/null +++ b/briar-android/proguard-test.txt @@ -0,0 +1,15 @@ +-dontwarn android.test.** +-dontwarn android.support.test.** +-dontnote android.support.test.** +-dontwarn com.googlecode.eyesfree.compat.CompatUtils + +-keep class org.xmlpull.v1.** { *; } +-dontwarn org.xmlpull.v1.** + +-keep class org.junit.** { *; } +-dontwarn org.junit.** + +-keep class junit.** { *; } +-dontwarn junit.** + +-dontwarn org.briarproject.briar.android.BriarTestApplication \ No newline at end of file diff --git a/briar-android/src/androidTest/java/org/briarproject/briar/android/BriarTestApplication.java b/briar-android/src/androidTest/java/org/briarproject/briar/android/BriarTestApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..8bdd5b63a6c2300394eafd282684227c23a5428a --- /dev/null +++ b/briar-android/src/androidTest/java/org/briarproject/briar/android/BriarTestApplication.java @@ -0,0 +1,20 @@ +package org.briarproject.briar.android; + +import org.briarproject.bramble.BrambleCoreModule; +import org.briarproject.briar.BriarCoreModule; + +public class BriarTestApplication extends BriarApplicationImpl { + + @Override + protected AndroidComponent createApplicationComponent() { + AndroidComponent component = DaggerBriarTestComponent.builder() + .appModule(new AppModule(this)).build(); + // We need to load the eager singletons directly after making the + // dependency graphs + BrambleCoreModule.initEagerSingletons(component); + BriarCoreModule.initEagerSingletons(component); + AndroidEagerSingletons.initEagerSingletons(component); + return component; + } + +} diff --git a/briar-android/src/androidTest/java/org/briarproject/briar/android/BriarTestComponent.java b/briar-android/src/androidTest/java/org/briarproject/briar/android/BriarTestComponent.java new file mode 100644 index 0000000000000000000000000000000000000000..3b1ba59fd13e94d663b280b28cfc1c46d480286e --- /dev/null +++ b/briar-android/src/androidTest/java/org/briarproject/briar/android/BriarTestComponent.java @@ -0,0 +1,23 @@ +package org.briarproject.briar.android; + +import org.briarproject.bramble.BrambleAndroidModule; +import org.briarproject.bramble.BrambleCoreModule; +import org.briarproject.briar.BriarCoreModule; +import org.briarproject.briar.android.settings.DarkThemeTest; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = { + AppModule.class, + BriarCoreModule.class, + BrambleAndroidModule.class, + BrambleCoreModule.class +}) +public interface BriarTestComponent extends AndroidComponent { + + void inject(DarkThemeTest test); + +} diff --git a/briar-android/src/androidTest/java/org/briarproject/briar/android/settings/DarkThemeTest.java b/briar-android/src/androidTest/java/org/briarproject/briar/android/settings/DarkThemeTest.java new file mode 100644 index 0000000000000000000000000000000000000000..274c6e5c0b9b9858b8eec01d83798947a7e6f4d9 --- /dev/null +++ b/briar-android/src/androidTest/java/org/briarproject/briar/android/settings/DarkThemeTest.java @@ -0,0 +1,89 @@ +package org.briarproject.briar.android.settings; + +import android.content.Intent; +import android.support.test.espresso.contrib.DrawerActions; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; +import android.view.Gravity; + +import junit.framework.AssertionFailedError; + +import org.briarproject.briar.R; +import org.briarproject.briar.android.BriarTestComponent; +import org.briarproject.briar.android.navdrawer.NavDrawerActivity; +import org.briarproject.briar.android.test.ScreenshotTest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.contrib.DrawerMatchers.isClosed; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.isRoot; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static org.briarproject.briar.android.test.ViewActions.waitForActivityToResume; + +@RunWith(AndroidJUnit4.class) +public class DarkThemeTest extends ScreenshotTest { + + @Rule + public ActivityTestRule<SettingsActivity> activityRule = + new ActivityTestRule<>(SettingsActivity.class); + + @Override + protected void inject(BriarTestComponent component) { + component.inject(this); + } + + @Before + public void waitForSignIn() { + onView(isRoot()) + .perform(waitForActivityToResume(activityRule.getActivity())); + } + + @Test + public void changeTheme() { + onView(withText(R.string.settings_button)) + .check(matches(isDisplayed())); + onView(withText(R.string.pref_theme_title)) + .check(matches(isDisplayed())) + .perform(click()); + onView(withText(R.string.pref_theme_light)) + .check(matches(isDisplayed())) + .perform(click()); + + screenshot("dark_theme_settings"); + + onView(withText(R.string.pref_theme_title)) + .check(matches(isDisplayed())) + .perform(click()); + onView(withText(R.string.pref_theme_dark)) + .check(matches(isDisplayed())) + .perform(click()); + + Intent i = + new Intent(activityRule.getActivity(), NavDrawerActivity.class); + activityRule.getActivity().startActivity(i); + + try { + onView(withId(R.id.expiryWarningClose)) + .check(matches(isDisplayed())); + onView(withId(R.id.expiryWarningClose)) + .perform(click()); + } catch (AssertionFailedError e){ + // TODO remove try block when starting with fresh account + // ignore since we already removed the expiry warning + } + + onView(withId(R.id.drawer_layout)) + .check(matches(isClosed(Gravity.LEFT))) + .perform(DrawerActions.open()); + + screenshot("dark_theme_nav_drawer"); + } + +} diff --git a/briar-android/src/androidTest/java/org/briarproject/briar/android/test/BriarTestRunner.java b/briar-android/src/androidTest/java/org/briarproject/briar/android/test/BriarTestRunner.java new file mode 100644 index 0000000000000000000000000000000000000000..cefb68766f5ef4e0835c99d5736fb5488f5b763b --- /dev/null +++ b/briar-android/src/androidTest/java/org/briarproject/briar/android/test/BriarTestRunner.java @@ -0,0 +1,20 @@ +package org.briarproject.briar.android.test; + +import android.app.Application; +import android.content.Context; +import android.support.test.runner.AndroidJUnitRunner; + +import org.briarproject.briar.android.BriarTestApplication; + +public class BriarTestRunner extends AndroidJUnitRunner { + + @Override + public Application newApplication(ClassLoader cl, String className, + Context context) + throws InstantiationException, IllegalAccessException, + ClassNotFoundException { + return super.newApplication(cl, BriarTestApplication.class.getName(), + context); + } + +} diff --git a/briar-android/src/androidTest/java/org/briarproject/briar/android/test/ScreenshotTest.java b/briar-android/src/androidTest/java/org/briarproject/briar/android/test/ScreenshotTest.java new file mode 100644 index 0000000000000000000000000000000000000000..b5a71e845dca5926971a809c0a070e94886bdd98 --- /dev/null +++ b/briar-android/src/androidTest/java/org/briarproject/briar/android/test/ScreenshotTest.java @@ -0,0 +1,108 @@ +package org.briarproject.briar.android.test; + +import android.support.test.InstrumentationRegistry; +import android.support.test.espresso.NoMatchingViewException; +import android.util.Log; + +import org.briarproject.briar.R; +import org.briarproject.briar.android.BriarTestApplication; +import org.briarproject.briar.android.BriarTestComponent; +import org.junit.Before; +import org.junit.ClassRule; + +import tools.fastlane.screengrab.Screengrab; +import tools.fastlane.screengrab.UiAutomatorScreenshotStrategy; +import tools.fastlane.screengrab.locale.LocaleTestRule; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.typeText; +import static android.support.test.espresso.assertion.ViewAssertions.matches; +import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static android.support.test.espresso.matcher.ViewMatchers.withText; +import static tools.fastlane.screengrab.Screengrab.setDefaultScreenshotStrategy; + +public abstract class ScreenshotTest { + + @ClassRule + public static final LocaleTestRule localeTestRule = new LocaleTestRule(); + + @Before + public void setupScreenshots() { + setDefaultScreenshotStrategy(new UiAutomatorScreenshotStrategy()); + } + + private static final String USERNAME = "test"; + private static final String PASSWORD = "123456"; + + private final BriarTestApplication app = + (BriarTestApplication) InstrumentationRegistry.getTargetContext() + .getApplicationContext(); + + protected abstract void inject(BriarTestComponent component); + + /** + * Signs the user in. + * + * Note that you need to wait for your UI to show up after this. + * See {@link ViewActions#waitForActivityToResume} for one way to do it. + */ + @Before + public void signIn() throws Exception { + inject((BriarTestComponent) app.getApplicationComponent()); + + try { + onView(withId(R.id.edit_password)) + .check(matches(isDisplayed())) + .perform(typeText(PASSWORD)); + onView(withId(R.id.btn_sign_in)) + .check(matches(isDisplayed())) + .perform(click()); + } catch (NoMatchingViewException e) { + // we start from a blank state and have no account, yet + createAccount(); + } + } + + private void createAccount() { + // TODO use AccountManager to start with fresh account + // TODO move this below into a dedicated test for SetupActivity + + // Enter username + onView(withText(R.string.setup_title)) + .check(matches(isDisplayed())); + onView(withId(R.id.nickname_entry)) + .check(matches(isDisplayed())) + .perform(typeText(USERNAME)); + onView(withId(R.id.next)) + .check(matches(isDisplayed())) + .perform(click()); + + // Enter password + onView(withId(R.id.password_entry)) + .check(matches(isDisplayed())) + .perform(typeText(PASSWORD)); + onView(withId(R.id.password_confirm)) + .check(matches(isDisplayed())) + .perform(typeText(PASSWORD)); + onView(withId(R.id.next)) + .check(matches(isDisplayed())) + .perform(click()); + onView(withId(R.id.progress)) + .check(matches(isDisplayed())); + } + + protected void screenshot(String name) { + try { + Screengrab.screenshot(name); + } catch (RuntimeException e) { + if (!e.getMessage().equals("Unable to capture screenshot.")) + throw e; + // The tests should still pass when run from AndroidStudio + // without manually granting permissions like fastlane does. + Log.w("Screengrab", "Permission to write screenshot is missing."); + } + } + +} diff --git a/briar-android/src/androidTest/java/org/briarproject/briar/android/test/ViewActions.java b/briar-android/src/androidTest/java/org/briarproject/briar/android/test/ViewActions.java new file mode 100644 index 0000000000000000000000000000000000000000..806124d1b6d06c4447ee7a137337dcca16368d05 --- /dev/null +++ b/briar-android/src/androidTest/java/org/briarproject/briar/android/test/ViewActions.java @@ -0,0 +1,104 @@ +package org.briarproject.briar.android.test; + +import android.app.Activity; +import android.support.test.espresso.PerformException; +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.runner.lifecycle.ActivityLifecycleMonitor; +import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; +import android.view.View; + +import org.hamcrest.Matcher; + +import java.util.concurrent.TimeoutException; + +import static android.support.test.espresso.matcher.ViewMatchers.isRoot; +import static android.support.test.espresso.util.HumanReadables.describe; +import static android.support.test.espresso.util.TreeIterables.breadthFirstViewTraversal; +import static android.support.test.runner.lifecycle.Stage.RESUMED; +import static java.lang.System.currentTimeMillis; +import static java.util.concurrent.TimeUnit.SECONDS; + +public class ViewActions { + + private final static long TIMEOUT_MS = SECONDS.toMillis(5); + private final static long WAIT_MS = 50; + + public static ViewAction waitUntilMatches(Matcher<View> viewMatcher) { + return waitUntilMatches(viewMatcher, TIMEOUT_MS); + } + + private static ViewAction waitUntilMatches(Matcher<View> viewMatcher, + long timeout) { + return new ViewAction() { + @Override + public Matcher<View> getConstraints() { + return isRoot(); + } + + @Override + public String getDescription() { + return "Wait for view matcher " + viewMatcher + + " to match within " + timeout + " milliseconds."; + } + + @Override + public void perform(final UiController uiController, + final View view) { + uiController.loopMainThreadUntilIdle(); + long endTime = currentTimeMillis() + timeout; + + do { + for (View child : breadthFirstViewTraversal(view)) { + if (viewMatcher.matches(child)) return; + } + uiController.loopMainThreadForAtLeast(WAIT_MS); + } + while (currentTimeMillis() < endTime); + + throw new PerformException.Builder() + .withActionDescription(getDescription()) + .withViewDescription(describe(view)) + .withCause(new TimeoutException()) + .build(); + } + }; + } + + public static ViewAction waitForActivityToResume(Activity activity) { + return new ViewAction() { + @Override + public Matcher<View> getConstraints() { + return isRoot(); + } + + @Override + public String getDescription() { + return "Wait for activity " + activity.getClass().getName() + + " to resume within " + TIMEOUT_MS + " milliseconds."; + } + + @Override + public void perform(final UiController uiController, + final View view) { + uiController.loopMainThreadUntilIdle(); + long endTime = currentTimeMillis() + TIMEOUT_MS; + ActivityLifecycleMonitor lifecycleMonitor = + ActivityLifecycleMonitorRegistry.getInstance(); + do { + if (lifecycleMonitor.getLifecycleStageOf(activity) == + RESUMED) return; + uiController.loopMainThreadForAtLeast(WAIT_MS); + } + while (currentTimeMillis() < endTime); + + throw new PerformException.Builder() + .withActionDescription(getDescription()) + .withViewDescription(describe(view)) + .withCause(new TimeoutException()) + .build(); + } + }; + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java index 08fc14b5daabbb6e415562b945b3e2ae5ee11b46..e56adf30ec0fec6ac91e9ffc7b4de4c4f3b193b3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/BriarApplicationImpl.java @@ -109,15 +109,20 @@ public class BriarApplicationImpl extends Application LOG.info("Created"); - applicationComponent = DaggerAndroidComponent.builder() + applicationComponent = createApplicationComponent(); + } + + protected AndroidComponent createApplicationComponent() { + AndroidComponent androidComponent = DaggerAndroidComponent.builder() .appModule(new AppModule(this)) .build(); // We need to load the eager singletons directly after making the // dependency graphs - BrambleCoreModule.initEagerSingletons(applicationComponent); - BriarCoreModule.initEagerSingletons(applicationComponent); - AndroidEagerSingletons.initEagerSingletons(applicationComponent); + BrambleCoreModule.initEagerSingletons(androidComponent); + BriarCoreModule.initEagerSingletons(androidComponent); + AndroidEagerSingletons.initEagerSingletons(androidComponent); + return androidComponent; } @Override diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index ee30d8d37f5af99894c65fd7e13a3ad30e12a728..2485f81c95866ca1f8a37d32c7d640795256d4c5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -55,6 +55,7 @@ import static android.text.format.DateUtils.FORMAT_SHOW_DATE; import static android.text.format.DateUtils.MINUTE_IN_MILLIS; import static android.text.format.DateUtils.WEEK_IN_MILLIS; import static org.briarproject.briar.BuildConfig.APPLICATION_ID; +import static org.briarproject.briar.BuildConfig.BUILD_TYPE; import static org.briarproject.briar.android.TestingConstants.EXPIRY_DATE; @MethodsNotNullByDefault @@ -174,6 +175,7 @@ public class UiUtils { public static boolean needsDozeWhitelisting(Context ctx) { if (SDK_INT < 23) return false; + if (BUILD_TYPE.equals("screenshot")) return false; PowerManager pm = (PowerManager) ctx.getSystemService(POWER_SERVICE); String packageName = ctx.getPackageName(); if (pm == null) throw new AssertionError(); diff --git a/briar-android/src/screenshot/AndroidManifest.xml b/briar-android/src/screenshot/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..266a92823b948f59f8822c303de14020bb7e6481 --- /dev/null +++ b/briar-android/src/screenshot/AndroidManifest.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest + package="org.briarproject.briar" + xmlns:android="http://schemas.android.com/apk/res/android"> + + <!-- Allows unlocking your device and activating its screen so UI tests can succeed --> + <uses-permission android:name="android.permission.DISABLE_KEYGUARD"/> + <uses-permission android:name="android.permission.WAKE_LOCK"/> + + <!-- Allows for storing and retrieving screenshots --> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + + <!-- Allows changing locales --> + <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" /> +</manifest>