First prototype of Espresso test infrastructure with automatic screenshoting

This commit is contained in:
Torsten Grote
2018-07-24 14:04:23 -03:00
parent db11e0101a
commit d0c2c03057
13 changed files with 435 additions and 4 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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.");
}
}
}

View File

@@ -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();
}
};
}
}