Merge branch 'promo-video' into 'master'

Instrumentation test for tutorial video and sign-in

Closes #1967

See merge request briar/briar!1423
This commit is contained in:
akwizgran
2021-04-12 10:46:03 +00:00
33 changed files with 767 additions and 54 deletions

View File

@@ -34,12 +34,16 @@ android test:
# start emulator first, so it can fail early
- start-emulator.sh
# run normal and screenshot tests together (exclude Large tests)
- ./gradlew -Djava.security.egd=file:/dev/urandom connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest
- ./gradlew -Djava.security.egd=file:/dev/urandom connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.package=org.briarproject.briar.android -Pandroid.testInstrumentationRunnerArguments.notAnnotation=androidx.test.filters.LargeTest
after_script:
- adb pull /sdcard/Pictures/screenshots
artifacts:
name: "${CI_PROJECT_PATH}_${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}_${CI_COMMIT_SHA}"
paths:
- kernel.log
- logcat.txt
- briar-android/build/reports/androidTests/connected/flavors/*
- screenshots
expire_in: 3 days
when: on_failure
when: manual

View File

@@ -80,6 +80,7 @@ android {
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
unitTests {
includeAndroidResources = true
}
@@ -148,6 +149,8 @@ dependencies {
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion"
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestUtil 'androidx.test:orchestrator:1.3.0'
androidTestAnnotationProcessor "com.google.dagger:dagger-compiler:2.24"
androidTestCompileOnly 'javax.annotation:jsr250-api:1.0'
androidTestImplementation 'junit:junit:4.13.1'

View File

@@ -19,4 +19,9 @@ public class BriarTestComponentApplication extends BriarApplicationImpl {
return component;
}
@Override
public boolean isInstrumentationTest() {
return true;
}
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.briar.android;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import java.io.IOException;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicBoolean;
import androidx.test.espresso.Espresso;
import androidx.test.espresso.FailureHandler;
import androidx.test.espresso.base.DefaultFailureHandler;
import androidx.test.runner.screenshot.BasicScreenCaptureProcessor;
import androidx.test.runner.screenshot.ScreenCapture;
import androidx.test.runner.screenshot.ScreenCaptureProcessor;
import androidx.test.runner.screenshot.Screenshot;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
@NotNullByDefault
public class ScreenshotOnFailureRule implements TestRule {
FailureHandler defaultFailureHandler =
new DefaultFailureHandler(getApplicationContext());
@Override
public Statement apply(Statement base, Description description) {
HashSet<ScreenCaptureProcessor> processors = new HashSet<>(1);
processors.add(new BasicScreenCaptureProcessor());
Screenshot.addScreenCaptureProcessors(processors);
return new Statement() {
@Override
public void evaluate() throws Throwable {
AtomicBoolean errorHandled = new AtomicBoolean(false);
Espresso.setFailureHandler((throwable, matcher) -> {
takeScreenshot(description);
errorHandled.set(true);
defaultFailureHandler.handle(throwable, matcher);
});
try {
base.evaluate();
} catch (Throwable t) {
if (!errorHandled.get()) {
takeScreenshot(description);
}
throw t;
}
}
};
}
private void takeScreenshot(Description description) {
String name = description.getTestClass().getSimpleName();
ScreenCapture capture = Screenshot.capture();
capture.setName(name);
try {
capture.process();
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@@ -4,25 +4,27 @@ import android.app.Activity;
import android.content.Intent;
import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.briar.R;
import org.junit.ClassRule;
import javax.inject.Inject;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static org.briarproject.briar.android.controller.BriarControllerImpl.DOZE_ASK_AGAIN;
import static org.briarproject.briar.android.settings.SettingsFragment.SETTINGS_NAMESPACE;
@SuppressWarnings("WeakerAccess")
public abstract class UiTest {
@ClassRule
public static final ScreenshotOnFailureRule screenshotOnFailureRule =
new ScreenshotOnFailureRule();
protected final String USERNAME =
getApplicationContext().getString(R.string.screenshot_alice);
protected static final String PASSWORD = "123456";
@@ -41,6 +43,12 @@ public abstract class UiTest {
protected abstract void inject(BriarUiTestComponent component);
protected void startActivity(Class<? extends Activity> clazz) {
Intent i = new Intent(getApplicationContext(), clazz);
i.addFlags(FLAG_ACTIVITY_NEW_TASK);
getApplicationContext().startActivity(i);
}
@NotNullByDefault
protected class CleanAccountTestRule<A extends Activity>
extends IntentsTestRule<A> {
@@ -59,11 +67,7 @@ public abstract class UiTest {
getApplicationContext().startService(serviceIntent);
try {
lifecycleManager.waitForStartup();
// do not show doze white-listing dialog
Settings settings = new Settings();
settings.putBoolean(DOZE_ASK_AGAIN, false);
settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE);
} catch (InterruptedException | DbException e) {
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android;
import android.app.Activity;
import android.util.Log;
import android.view.View;
import org.hamcrest.Matcher;
@@ -14,9 +15,13 @@ import androidx.test.runner.lifecycle.ActivityLifecycleMonitor;
import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry;
import androidx.test.runner.lifecycle.Stage;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.util.HumanReadables.describe;
import static androidx.test.espresso.util.TreeIterables.breadthFirstViewTraversal;
import static androidx.test.runner.lifecycle.Stage.RESUMED;
import static java.lang.System.currentTimeMillis;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -25,13 +30,26 @@ public class ViewActions {
private final static long TIMEOUT_MS = SECONDS.toMillis(10);
private final static long WAIT_MS = 50;
public static void waitFor(final Matcher<View> viewMatcher) {
onView(isRoot()).perform(waitUntilMatches(hasDescendant(viewMatcher)));
}
public static void waitFor(final Class<? extends Activity> clazz) {
onView(isRoot()).perform(waitForActivity(clazz, RESUMED, TIMEOUT_MS));
}
public static void waitFor(final Class<? extends Activity> clazz,
long timeout) {
onView(isRoot()).perform(waitForActivity(clazz, RESUMED, timeout));
}
public static ViewAction waitUntilMatches(Matcher<View> viewMatcher) {
return waitUntilMatches(viewMatcher, TIMEOUT_MS);
}
private static ViewAction waitUntilMatches(Matcher<View> viewMatcher,
long timeout) {
return new CustomViewAction() {
return new CustomViewAction(timeout) {
@Override
protected boolean exitConditionTrue(View view) {
for (View child : breadthFirstViewTraversal(view)) {
@@ -48,24 +66,62 @@ public class ViewActions {
};
}
public static ViewAction waitForActivity(Activity activity, Stage stage) {
return new CustomViewAction() {
public static ViewAction waitForActivity(Class<? extends Activity> clazz,
Stage stage, long timeout) {
return new CustomViewAction(timeout) {
@Override
protected boolean exitConditionTrue(View view) {
boolean found = false;
ActivityLifecycleMonitor lifecycleMonitor =
ActivityLifecycleMonitorRegistry.getInstance();
return lifecycleMonitor.getLifecycleStageOf(activity) == stage;
log(lifecycleMonitor);
for (Activity a : lifecycleMonitor
.getActivitiesInStage(stage)) {
if (a.getClass().equals(clazz)) found = true;
}
return found;
}
private void log(ActivityLifecycleMonitor lifecycleMonitor) {
log(lifecycleMonitor, Stage.PRE_ON_CREATE);
log(lifecycleMonitor, Stage.CREATED);
log(lifecycleMonitor, Stage.STARTED);
log(lifecycleMonitor, Stage.RESUMED);
log(lifecycleMonitor, Stage.PAUSED);
log(lifecycleMonitor, Stage.STOPPED);
log(lifecycleMonitor, Stage.RESTARTED);
log(lifecycleMonitor, Stage.DESTROYED);
}
private void log(ActivityLifecycleMonitor lifecycleMonitor,
Stage stage) {
for (Activity a : lifecycleMonitor
.getActivitiesInStage(stage)) {
Log.e("TEST", a.getClass().getSimpleName() +
" is in state " + stage);
}
}
@Override
public String getDescription() {
return "Wait for activity " + activity.getClass().getName() +
" to resume within " + TIMEOUT_MS + " milliseconds.";
return "Wait for activity " + clazz.getName() + " in stage " +
stage.name() + " within " + timeout +
" milliseconds.";
}
};
}
private static abstract class CustomViewAction implements ViewAction {
private final long timeout;
public CustomViewAction() {
this(TIMEOUT_MS);
}
public CustomViewAction(long timeout) {
this.timeout = timeout;
}
@Override
public Matcher<View> getConstraints() {
return isDisplayed();
@@ -74,7 +130,7 @@ public class ViewActions {
@Override
public void perform(UiController uiController, View view) {
uiController.loopMainThreadUntilIdle();
long endTime = currentTimeMillis() + TIMEOUT_MS;
long endTime = currentTimeMillis() + timeout;
do {
if (exitConditionTrue(view)) return;
uiController.loopMainThreadForAtLeast(WAIT_MS);

View File

@@ -4,6 +4,8 @@ import org.briarproject.bramble.BrambleAndroidModule;
import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.briar.BriarCoreModule;
import org.briarproject.briar.android.account.SignInTestCreateAccount;
import org.briarproject.briar.android.account.SignInTestSignIn;
import org.briarproject.briar.android.attachment.AttachmentModule;
import org.briarproject.briar.android.attachment.media.MediaModule;
import org.briarproject.briar.android.navdrawer.NavDrawerActivityTest;
@@ -26,4 +28,8 @@ public interface BriarUiTestComponent extends AndroidComponent {
void inject(NavDrawerActivityTest test);
void inject(SignInTestCreateAccount test);
void inject(SignInTestSignIn test);
}

View File

@@ -0,0 +1,65 @@
package org.briarproject.briar.android.account;
import android.view.Gravity;
import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarUiTestComponent;
import org.briarproject.briar.android.UiTest;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.espresso.contrib.DrawerActions;
import androidx.test.ext.junit.runners.AndroidJUnit4;
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.ViewMatchers.hasDescendant;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
import static androidx.test.espresso.matcher.ViewMatchers.withClassName;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static org.briarproject.briar.android.ViewActions.waitFor;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.hamcrest.Matchers.endsWith;
@RunWith(AndroidJUnit4.class)
public class SignInTestCreateAccount extends UiTest {
@Override
protected void inject(BriarUiTestComponent component) {
component.inject(this);
}
@Test
public void createAccount() throws Exception {
accountManager.deleteAccount();
accountManager.createAccount(USERNAME, PASSWORD);
startActivity(SplashScreenActivity.class);
lifecycleManager.waitForStartup();
waitFor(NavDrawerActivity.class);
// open nav drawer
onView(withId(R.id.drawer_layout))
.check(matches(isClosed(Gravity.START)))
.perform(DrawerActions.open());
// click onboarding away (once shown)
onView(isRoot()).perform(waitUntilMatches(hasDescendant(
withClassName(endsWith("PromptView")))));
onView(withClassName(endsWith("PromptView")))
.perform(click());
// sign-out manually
onView(withText(R.string.sign_out_button))
.check(matches(isDisplayed()))
.perform(click());
lifecycleManager.waitForShutdown();
}
}

View File

@@ -0,0 +1,59 @@
package org.briarproject.briar.android.account;
import android.view.Gravity;
import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarUiTestComponent;
import org.briarproject.briar.android.UiTest;
import org.briarproject.briar.android.login.StartupActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.DrawerMatchers.isClosed;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.briarproject.briar.android.ViewActions.waitFor;
import static org.hamcrest.CoreMatchers.allOf;
/**
* This relies on class sorting to run after {@link SignInTestCreateAccount}.
*/
@RunWith(AndroidJUnit4.class)
public class SignInTestSignIn extends UiTest {
@Override
protected void inject(BriarUiTestComponent component) {
component.inject(this);
}
@Test
public void signIn() throws Exception {
startActivity(SplashScreenActivity.class);
waitFor(StartupActivity.class);
// enter password
onView(withId(R.id.edit_password))
.check(matches(isDisplayed()))
.perform(replaceText(PASSWORD));
onView(withId(R.id.btn_sign_in))
.check(matches(allOf(isDisplayed(), isEnabled())))
.perform(click());
lifecycleManager.waitForStartup();
waitFor(NavDrawerActivity.class);
// ensure nav drawer is visible
onView(withId(R.id.drawer_layout))
.check(matches(isClosed(Gravity.START)));
}
}

View File

@@ -31,4 +31,6 @@ public interface BriarUiTestComponent extends AndroidComponent {
void inject(SettingsActivityScreenshotTest test);
void inject(PromoVideoTest test);
}

View File

@@ -0,0 +1,41 @@
package org.briarproject.briar.android;
import android.view.View;
import org.hamcrest.Matcher;
import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import static androidx.test.espresso.action.GeneralLocation.VISIBLE_CENTER;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast;
public class OverlayTapViewAction implements ViewAction {
public static ViewAction visualClick(OverlayView overlayView) {
return new OverlayTapViewAction(overlayView);
}
private final OverlayView overlayView;
public OverlayTapViewAction(OverlayView overlayView) {
this.overlayView = overlayView;
}
@Override
public Matcher<View> getConstraints() {
return isDisplayingAtLeast(90);
}
@Override
public String getDescription() {
return null;
}
@Override
public void perform(UiController uiController, View view) {
float[] coordinates = VISIBLE_CENTER.calculateCoordinates(view);
overlayView.tap(coordinates);
}
}

View File

@@ -0,0 +1,86 @@
package org.briarproject.briar.android;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.view.View;
import android.view.WindowManager;
import java.util.Random;
import javax.annotation.Nullable;
import static android.content.Context.WINDOW_SERVICE;
import static android.provider.Settings.canDrawOverlays;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread;
import static org.junit.Assert.assertTrue;
/**
* A full-screen overlay used to make taps visible in instrumentation tests.
*/
public class OverlayView extends View {
public static OverlayView attach(Context ctx) throws Throwable {
assertTrue(canDrawOverlays(ctx));
OverlayView view = new OverlayView(getApplicationContext());
runOnUiThread(() -> attachInternal(ctx, view));
return view;
}
private static void attachInternal(Context ctx, OverlayView view) {
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT);
WindowManager wm = (WindowManager) ctx.getSystemService(WINDOW_SERVICE);
wm.addView(view, params);
}
private final Random random = new Random();
private final Paint paint;
private final int yOffset;
@Nullable
private float[] coordinates;
public OverlayView(Context ctx) {
super(ctx);
int resourceId = getResources()
.getIdentifier("status_bar_height", "dimen", "android");
yOffset = getResources().getDimensionPixelSize(resourceId);
paint = new Paint();
paint.setAntiAlias(true);
paint.setARGB(175, 255, 0, 0);
setWillNotDraw(false);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
void tap(float[] coordinates) {
this.coordinates = coordinates;
invalidate();
new Handler().postDelayed(this::untap, 750);
}
private void untap() {
this.coordinates = null;
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (coordinates == null) return;
float x = coordinates[0] + random.nextInt(42);
float y = coordinates[1] - yOffset + random.nextInt(13);
canvas.drawCircle(x, y, 42, paint);
}
}

View File

@@ -0,0 +1,285 @@
package org.briarproject.briar.android;
import android.view.View;
import org.briarproject.bramble.api.Pair;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.PendingContact;
import org.briarproject.bramble.api.contact.PendingContactState;
import org.briarproject.briar.R;
import org.briarproject.briar.android.account.SetupActivity;
import org.briarproject.briar.android.contact.add.remote.PendingContactListActivity;
import org.briarproject.briar.android.navdrawer.NavDrawerActivity;
import org.briarproject.briar.android.splash.SplashScreenActivity;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.inject.Inject;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiSelector;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.scrollTo;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static java.lang.Thread.sleep;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
import static org.briarproject.briar.android.OverlayTapViewAction.visualClick;
import static org.briarproject.briar.android.ViewActions.waitFor;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
import static org.hamcrest.CoreMatchers.allOf;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class PromoVideoTest extends ScreenshotTest {
// we can leave isFilming to false (to speed up CI)
// and only set it to true when doing recordings
private static final boolean isFilming = false;
private static final int DELAY_SMALL = isFilming ? 4_000 : 0;
private static final int DELAY_MEDIUM = isFilming ? 7_500 : 0;
private static final int DELAY_LONG = isFilming ? 10_000 : 0;
@Rule
public ActivityScenarioRule<SplashScreenActivity> testRule =
new ActivityScenarioRule<>(SplashScreenActivity.class);
@Inject
protected ContactManager contactManager;
private OverlayView overlayView;
@Override
protected void inject(BriarUiTestComponent component) {
component.inject(this);
accountManager.deleteAccount();
}
@Test
public void createAccountAddContact() throws Throwable {
if (isFilming) {
// Using this breaks emulator CI tests for some reason.
// Only use it for filming for now until we have time to debug this.
overlayView = OverlayView.attach(getApplicationContext());
}
// Splash screen shows logo
onView(withId(R.id.logoView))
.perform(waitUntilMatches(isDisplayed()));
// It takes a long time for SetupActivity to start after the splash,
// (because it is shown longer for videos), so increase timeout.
if (!isFilming) waitFor(SetupActivity.class, 20_000);
// Note: We use waiting code only when not filming,
// to make the test reliable for CI. Otherwise, we used fixed
// delays to deterministically align with subtitles.
sleep(DELAY_LONG);
// Enter username
onView(withText(R.string.setup_title))
.perform(waitUntilMatches(isDisplayed()));
sleep(DELAY_SMALL);
onView(withId(R.id.nickname_entry))
.check(matches(isDisplayed()))
.perform(replaceText(USERNAME));
closeKeyboard(withId(R.id.nickname_entry));
sleep(DELAY_SMALL);
doClick(withId(R.id.next));
sleep(DELAY_MEDIUM);
// Enter password
doClick(withId(R.id.password_entry), 1000);
onView(withId(R.id.password_entry))
.check(matches(isDisplayed()))
.perform(replaceText(PASSWORD));
sleep(DELAY_SMALL);
doClick(withId(R.id.password_confirm), 1000);
onView(withId(R.id.password_confirm))
.check(matches(isDisplayed()))
.perform(replaceText(PASSWORD));
sleep(DELAY_SMALL);
// click next or create account
doClick(withId(R.id.next));
sleep(DELAY_SMALL);
// White-list Doze if needed
if (needsDozeWhitelisting(getApplicationContext())) {
doClick(withText(R.string.setup_doze_button));
UiDevice device = UiDevice.getInstance(getInstrumentation());
UiObject allowButton = device.findObject(
new UiSelector().className("android.widget.Button")
.index(1));
allowButton.click();
doClick(withId(R.id.next));
}
lifecycleManager.waitForStartup();
assertTrue(accountManager.hasDatabaseKey());
sleep(DELAY_SMALL);
// wait for contact list to be shown
if (!isFilming) waitFor(NavDrawerActivity.class);
// clicking the FAB doesn't work, so we click its inner FAB as well
onView(withId(R.id.speedDial))
.check(matches(isDisplayed()))
.perform(click());
doClick(withId(R.id.fab_main)); // this is inside R.id.speedDial
sleep(DELAY_MEDIUM);
// click adding contact at a distance menu item
doClick(withText(R.string.add_contact_remotely_title));
sleep(DELAY_LONG);
// enter briar:// link
String link =
"briar://ab54fpik6sjyetzjhlwto2fv7tspibx2uhpdnei4tdidkvjpbphvy";
doClick(withId(R.id.pasteButton));
onView(withId(R.id.linkInput))
.perform(waitUntilMatches(isDisplayed()))
.perform(replaceText(link));
sleep(DELAY_MEDIUM);
doClick(withId(R.id.addButton));
sleep(DELAY_MEDIUM);
// enter contact alias
String contactName = getApplicationContext()
.getString(R.string.screenshot_bob);
doClick(withId(R.id.contactNameInput), 1000);
onView(withId(R.id.contactNameInput))
.perform(waitUntilMatches(isDisplayed()))
.perform(replaceText(contactName));
sleep(DELAY_SMALL);
closeKeyboard(withId(R.id.contactNameInput));
sleep(DELAY_SMALL);
// add pending contact
onView(withId(R.id.addButton)).perform(scrollTo());
doClick(withId(R.id.addButton));
sleep(DELAY_LONG);
// wait for pending contact list activity to be shown
if (!isFilming) {
waitFor(PendingContactListActivity.class);
waitFor(allOf(withText(R.string.pending_contact_requests),
isDisplayed()));
}
// remove pending contact
for (Pair<PendingContact, PendingContactState> p : contactManager
.getPendingContacts()) {
contactManager.removePendingContact(p.getFirst().getId());
}
// add contact and make them appear online
Contact bob = testDataCreator.addContact(contactName, false, true);
sleep(DELAY_SMALL);
connectionRegistry.registerIncomingConnection(bob.getId(), ID, () -> {
});
sleep(DELAY_LONG);
// wait for contact list to be shown
if (!isFilming) {
waitFor(NavDrawerActivity.class);
waitFor(allOf(withText(R.string.contact_list_button),
isDisplayed()));
waitFor(allOf(withId(R.id.recyclerView), isDisplayed()));
}
// click on new contact
doItemClick(withId(R.id.recyclerView), 0);
sleep(DELAY_MEDIUM);
// bring up keyboard
doClick(withId(R.id.input_text), DELAY_SMALL);
String msg1 = getApplicationContext()
.getString(R.string.screenshot_message_1);
onView(withId(R.id.input_text))
.perform(waitUntilMatches(isEnabled()))
.perform(replaceText(msg1));
sleep(DELAY_SMALL);
doClick(withId(R.id.compositeSendButton));
sleep(DELAY_SMALL);
// send emoji
doClick(withId(R.id.emoji_toggle), DELAY_SMALL);
onView(withId(R.id.input_text))
.perform(replaceText("\uD83D\uDE0E"));
sleep(DELAY_SMALL);
doClick(withId(R.id.compositeSendButton));
// close keyboard
closeKeyboard(withId(R.id.compositeSendButton));
sleep(DELAY_LONG);
}
private void doClick(final Matcher<View> viewMatcher, long sleepMs)
throws InterruptedException {
doClick(viewMatcher);
if (isFilming) sleep(sleepMs);
}
private void doClick(final Matcher<View> viewMatcher)
throws InterruptedException {
if (isFilming) {
onView(viewMatcher)
.perform(waitUntilMatches(isDisplayed()))
.perform(visualClick(overlayView));
sleep(500);
}
onView(viewMatcher)
.perform(waitUntilMatches(allOf(isDisplayed(), isEnabled())))
.perform(click());
}
private void doItemClick(final Matcher<View> viewMatcher, int pos)
throws InterruptedException {
if (isFilming) {
onView(viewMatcher).perform(
actionOnItemAtPosition(pos, visualClick(overlayView)));
sleep(500);
}
onView(viewMatcher).perform(
actionOnItemAtPosition(pos, click()));
}
private void closeKeyboard(final Matcher<View> viewMatcher)
throws InterruptedException {
if (isFilming) sleep(750);
onView(viewMatcher).perform(closeSoftKeyboard());
}
}

View File

@@ -10,6 +10,7 @@ import org.junit.ClassRule;
import javax.inject.Inject;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import tools.fastlane.screengrab.FalconScreenshotStrategy;
import tools.fastlane.screengrab.Screengrab;
import tools.fastlane.screengrab.locale.LocaleTestRule;
@@ -26,6 +27,10 @@ public abstract class ScreenshotTest extends UiTest {
@Inject
protected Clock clock;
protected void screenshot(String name, ActivityScenarioRule<?> rule) {
rule.getScenario().onActivity(activity -> screenshot(name, activity));
}
protected void screenshot(String name, Activity activity) {
try {
Screengrab.screenshot(name, new FalconScreenshotStrategy(activity));

View File

@@ -10,7 +10,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import androidx.test.espresso.intent.rule.IntentsTestRule;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
@@ -22,30 +22,27 @@ import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static org.briarproject.bramble.api.plugin.LanTcpConstants.ID;
import static org.briarproject.briar.android.ViewActions.waitUntilMatches;
import static org.briarproject.briar.android.util.UiUtils.needsDozeWhitelisting;
import static org.hamcrest.Matchers.allOf;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public class SetupDataTest extends ScreenshotTest {
@Rule
public IntentsTestRule<SetupActivity> testRule =
new IntentsTestRule<SetupActivity>(SetupActivity.class) {
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
accountManager.deleteAccount();
}
};
public ActivityScenarioRule<SetupActivity> testRule =
new ActivityScenarioRule<>(SetupActivity.class);
@Override
protected void inject(BriarUiTestComponent component) {
component.inject(this);
accountManager.deleteAccount();
}
@Test
@@ -59,7 +56,7 @@ public class SetupDataTest extends ScreenshotTest {
onView(withId(R.id.nickname_entry))
.perform(waitUntilMatches(withText(USERNAME)));
screenshot("manual_create_account", testRule.getActivity());
screenshot("manual_create_account", testRule);
onView(withId(R.id.next))
.check(matches(isDisplayed()))
@@ -73,7 +70,7 @@ public class SetupDataTest extends ScreenshotTest {
.check(matches(isDisplayed()))
.perform(typeText(PASSWORD));
onView(withId(R.id.next))
.check(matches(isDisplayed()))
.check(matches(allOf(isDisplayed(), isEnabled())))
.perform(click());
// White-list Doze if needed
@@ -94,14 +91,6 @@ public class SetupDataTest extends ScreenshotTest {
lifecycleManager.waitForStartup();
assertTrue(accountManager.hasDatabaseKey());
createTestData();
// close expiry warning
onView(withId(R.id.expiryWarning))
.perform(waitUntilMatches(isDisplayed()));
onView(withId(R.id.expiryWarningClose))
.check(matches(isDisplayed()));
onView(withId(R.id.expiryWarningClose))
.perform(click());
}
private void createTestData() {
@@ -116,7 +105,7 @@ public class SetupDataTest extends ScreenshotTest {
throws DbException {
Context ctx = getApplicationContext();
String bobName = ctx.getString(R.string.screenshot_bob);
Contact bob = testDataCreator.addContact(bobName, true);
Contact bob = testDataCreator.addContact(bobName, false, true);
// TODO add messages

View File

@@ -14,7 +14,6 @@ import org.junit.runner.RunWith;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.espresso.contrib.DrawerActions;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ActivityTestRule;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static androidx.test.espresso.Espresso.onView;
@@ -38,8 +37,8 @@ import static org.junit.Assume.assumeTrue;
public class SettingsActivityScreenshotTest extends ScreenshotTest {
@Rule
public ActivityTestRule<SettingsActivity> testRule =
new ActivityTestRule<>(SettingsActivity.class);
public CleanAccountTestRule<SettingsActivity> testRule =
new CleanAccountTestRule<>(SettingsActivity.class);
@Override
protected void inject(BriarUiTestComponent component) {

View File

@@ -19,4 +19,6 @@ public interface BriarApplication extends BrambleApplication {
SharedPreferences getDefaultSharedPreferences();
boolean isRunningInBackground();
boolean isInstrumentationTest();
}

View File

@@ -151,4 +151,9 @@ public class BriarApplicationImpl extends Application
ActivityManager.getMyMemoryState(info);
return (info.importance != IMPORTANCE_FOREGROUND);
}
@Override
public boolean isInstrumentationTest() {
return false;
}
}

View File

@@ -301,7 +301,9 @@ public class BriarService extends Service {
LOG.info("Interrupted while waiting for shutdown");
}
LOG.info("Exiting");
System.exit(0);
if (!app.isInstrumentationTest()) {
System.exit(0);
}
}, "BackgroundShutdown");
}, "BackgroundShutdown");
}

View File

@@ -193,6 +193,9 @@ public abstract class BaseActivity extends AppCompatActivity
}
private boolean showScreenFilterWarning() {
if (((BriarApplication) getApplication()).isInstrumentationTest()) {
return false;
}
// If the dialog is already visible, filter the tap
ScreenFilterDialogFragment f = findDialogFragment();
if (f != null && f.isVisible()) return false;

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.bramble.api.system.Wakeful;
import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.account.UnlockActivity;
import org.briarproject.briar.android.controller.BriarController;
import org.briarproject.briar.android.controller.DbController;
@@ -236,7 +237,8 @@ public abstract class BriarActivity extends BaseActivity {
if (SDK_INT >= 21) finishAndRemoveTask();
else supportFinishAfterTransition();
LOG.info("Exiting");
System.exit(0);
BriarApplication app = (BriarApplication) getApplication();
if (!app.isInstrumentationTest()) System.exit(0);
}
@Deprecated

View File

@@ -12,6 +12,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.system.AndroidWakeLockManager;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.BriarService;
import org.briarproject.briar.android.BriarService.BriarServiceConnection;
import org.briarproject.briar.android.controller.handler.ResultHandler;
@@ -104,7 +105,8 @@ public class BriarControllerImpl implements BriarController {
@Override
public void hasDozed(ResultHandler<Boolean> handler) {
if (!dozeWatchdog.getAndResetDozeFlag()
BriarApplication app = (BriarApplication) activity.getApplication();
if (app.isInstrumentationTest() || !dozeWatchdog.getAndResetDozeFlag()
|| !needsDozeWhitelisting(activity)) {
handler.onResult(false);
return;

View File

@@ -27,6 +27,7 @@ import org.briarproject.bramble.api.plugin.Plugin.State;
import org.briarproject.bramble.api.plugin.TorConstants;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
import org.briarproject.briar.android.blog.FeedFragment;
@@ -134,7 +135,8 @@ public class NavDrawerActivity extends BriarActivity implements
navDrawerViewModel = provider.get(NavDrawerViewModel.class);
pluginViewModel = provider.get(PluginViewModel.class);
if (IS_DEBUG_BUILD) {
BriarApplication app = (BriarApplication) getApplication();
if (IS_DEBUG_BUILD && !app.isInstrumentationTest()) {
navDrawerViewModel.showExpiryWarning()
.observe(this, this::showExpiryWarning);
}

View File

@@ -10,6 +10,7 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.settings.Settings;
import org.briarproject.bramble.api.settings.SettingsManager;
import org.briarproject.bramble.api.system.AndroidExecutor;
import org.briarproject.briar.android.BriarApplication;
import org.briarproject.briar.android.viewmodel.DbViewModel;
import java.util.concurrent.Executor;
@@ -120,7 +121,9 @@ public class NavDrawerViewModel extends DbViewModel {
@UiThread
void checkDozeWhitelisting() {
// check this first, to hit the DbThread only when really necessary
if (!needsDozeWhitelisting(getApplication())) {
BriarApplication app = (BriarApplication) getApplication();
if (app.isInstrumentationTest() ||
!needsDozeWhitelisting(getApplication())) {
shouldAskForDozeWhitelisting.setValue(false);
return;
}

View File

@@ -61,6 +61,8 @@ public class SplashScreenActivity extends BaseActivity {
startNextActivity(ENTRY_ACTIVITY);
finish();
} else {
int duration =
getResources().getInteger(R.integer.splashScreenDuration);
new Handler().postDelayed(() -> {
if (currentTimeMillis() >= EXPIRY_DATE) {
LOG.info("Expired");
@@ -69,7 +71,7 @@ public class SplashScreenActivity extends BaseActivity {
startNextActivity(ENTRY_ACTIVITY);
}
supportFinishAfterTransition();
}, 500);
}, duration);
}
}

View File

@@ -5,6 +5,7 @@
android:layout_height="match_parent">
<ImageView
android:id="@+id/logoView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"

View File

@@ -3,6 +3,8 @@
<integer name="animationSpeed">@android:integer/config_mediumAnimTime</integer>
<integer name="splashScreenDuration">500</integer>
<declare-styleable name="BriarRecyclerView">
<attr name="scrollToEnd" format="boolean" />
<attr name="emptyImage" format="integer" />

View File

@@ -16,4 +16,7 @@
<uses-permission
android:name="android.permission.CHANGE_CONFIGURATION"
tools:ignore="ProtectedPermissions" />
<!-- For drawing on top of the app -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
</manifest>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="splashScreenDuration">7500</integer>
</resources>

View File

@@ -51,9 +51,11 @@ dependencyVerification {
'androidx.test.espresso:espresso-idling-resource:3.3.0:espresso-idling-resource-3.3.0.aar:29519b112731f289cc6e2f9b2eccc5ea72c754b04272bb93370f45d7e170a7c6',
'androidx.test.espresso:espresso-intents:3.3.0:espresso-intents-3.3.0.aar:5b6cd6aadce78edc705d93c1e81ace3b59be97128aca0e88fd9c5c176aa9bf10',
'androidx.test.ext:junit:1.1.2:junit-1.1.2.aar:6c6ab120c640bf16fcaae69cb83c144d0ed6b6298562be0ac35e37ed969c0409',
'androidx.test.services:test-services:1.3.0:test-services-1.3.0.apk:1b88faab6864baf25c5d0b92a610c283c159a566e7a56c03307117fa1b542993',
'androidx.test.uiautomator:uiautomator:2.2.0:uiautomator-2.2.0.aar:2838e9d961dbffefbbd229a2bd4f6f82ac4fb2462975862a9e75e9ed325a3197',
'androidx.test:core:1.3.0:core-1.3.0.aar:86549cae8c5b848f817e2c716e174c7dab61caf0b4df9848680eeb753089a337',
'androidx.test:monitor:1.3.0:monitor-1.3.0.aar:f73a31306a783e63150c60c49e140dc38da39a1b7947690f4b73387b5ebad77e',
'androidx.test:orchestrator:1.3.0:orchestrator-1.3.0.apk:676f808d08a3d05050eae30c3b7d92ce5cef1e00a54d68355bb7e7d4b72366fe',
'androidx.test:rules:1.3.0:rules-1.3.0.aar:c1753946c498b0d5d7cf341cfed661f66915c4c9deb4ed10462a08ae33b2429a',
'androidx.test:runner:1.3.0:runner-1.3.0.aar:61d13f5a9fcbbd73ba18fa84e1d6a0111c6e1c665a89b418126966e61fffd93b',
'androidx.tracing:tracing:1.0.0:tracing-1.0.0.aar:07b8b6139665b884a162eccf97891ca50f7f56831233bf25168ae04f7b568612',

View File

@@ -24,5 +24,6 @@ public interface TestDataCreator {
int numBlogPosts, int numForums, int numForumPosts);
@IoExecutor
Contact addContact(String name, boolean avatar) throws DbException;
Contact addContact(String name, boolean alias, boolean avatar)
throws DbException;
}

View File

@@ -158,15 +158,15 @@ public class TestDataCreatorImpl implements TestDataCreator {
LocalAuthor localAuthor = identityManager.getLocalAuthor();
for (int i = 0; i < numContacts; i++) {
LocalAuthor remote = getRandomAuthor();
Contact contact =
addContact(localAuthor.getId(), remote, avatarPercent);
Contact contact = addContact(localAuthor.getId(), remote,
random.nextBoolean(), avatarPercent);
contacts.add(contact);
}
return contacts;
}
private Contact addContact(AuthorId localAuthorId, LocalAuthor remote,
int avatarPercent) throws DbException {
boolean alias, int avatarPercent) throws DbException {
// prepare to add contact
SecretKey secretKey = getSecretKey();
long timestamp = clock.currentTimeMillis();
@@ -179,7 +179,7 @@ public class TestDataCreatorImpl implements TestDataCreator {
Contact contact = db.transactionWithResult(false, txn -> {
ContactId contactId = contactManager.addContact(txn, remote,
localAuthorId, secretKey, timestamp, true, verified, true);
if (random.nextBoolean()) {
if (alias) {
contactManager.setContactAlias(txn, contactId,
getRandomAuthorName());
}
@@ -197,11 +197,12 @@ public class TestDataCreatorImpl implements TestDataCreator {
}
@Override
public Contact addContact(String name, boolean avatar) throws DbException {
public Contact addContact(String name, boolean alias, boolean avatar)
throws DbException {
LocalAuthor localAuthor = identityManager.getLocalAuthor();
LocalAuthor remote = authorFactory.createLocalAuthor(name);
int avatarPercent = avatar ? 100 : 0;
return addContact(localAuthor.getId(), remote, avatarPercent);
return addContact(localAuthor.getId(), remote, alias, avatarPercent);
}
private String getRandomAuthorName() {

View File

@@ -25,7 +25,7 @@ class ContactControllerIntegrationTest: IntegrationTest() {
// add one test contact
val testContactName= "testContactName"
testDataCreator.addContact(testContactName, false)
testDataCreator.addContact(testContactName, true, false)
// retrieve list with one test contact
response = get("$url/contacts")