Skip to content
Snippets Groups Projects
Verified Commit d0c2c030 authored by Torsten Grote's avatar Torsten Grote
Browse files

First prototype of Espresso test infrastructure with automatic screenshoting

parent db11e010
Loading
Showing
with 435 additions and 4 deletions
......@@ -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
......
......@@ -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
......
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
-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
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;
}
}
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);
}
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");
}
}
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);
}
}
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.");
}
}
}
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();
}
};
}
}
......@@ -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
......
......@@ -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();
......
<?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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment