diff --git a/briar-android/src/androidTest/java/org/briarproject/briar/android/ViewActions.java b/briar-android/src/androidTest/java/org/briarproject/briar/android/ViewActions.java index dc71aded2..a1d631e81 100644 --- a/briar-android/src/androidTest/java/org/briarproject/briar/android/ViewActions.java +++ b/briar-android/src/androidTest/java/org/briarproject/briar/android/ViewActions.java @@ -14,7 +14,10 @@ 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 java.lang.System.currentTimeMillis; @@ -25,6 +28,10 @@ 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 viewMatcher) { + onView(isRoot()).perform(waitUntilMatches(hasDescendant(viewMatcher))); + } + public static ViewAction waitUntilMatches(Matcher viewMatcher) { return waitUntilMatches(viewMatcher, TIMEOUT_MS); } diff --git a/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java b/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java index 9670dd8bc..af6d2ce02 100644 --- a/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java +++ b/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/BriarUiTestComponent.java @@ -31,4 +31,6 @@ public interface BriarUiTestComponent extends AndroidComponent { void inject(SettingsActivityScreenshotTest test); + void inject(PromoVideoTest test); + } diff --git a/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/PromoVideoTest.java b/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/PromoVideoTest.java new file mode 100644 index 000000000..f19be7487 --- /dev/null +++ b/briar-android/src/androidTestScreenshot/java/org/briarproject/briar/android/PromoVideoTest.java @@ -0,0 +1,263 @@ +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.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.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 debug to true (to speed up CI) + // and only set it to false when doing recordings + private static final boolean debug = true; + + private static final int DELAY_SMALL = debug ? 0 : 4_000; + private static final int DELAY_MEDIUM = debug ? 0 : 7_500; + private static final int DELAY_LONG = debug ? 0 : 10_000; + + @Rule + public ActivityScenarioRule 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 (!debug) { + // 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())); + + int duration = getApplicationContext().getResources() + .getInteger(R.integer.splashScreenDuration); + sleep(Math.max(DELAY_LONG, duration)); + + // 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); + + waitFor(allOf(withId(R.id.speedDial), isDisplayed())); + + // 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 + doClick(withId(R.id.addButton)); + sleep(DELAY_LONG); + + // wait for pending contact list activity to be shown + waitFor(allOf(withText(R.string.pending_contact_requests), + isDisplayed())); + + // remove pending contact + for (Pair 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, () -> { + }); + + // wait for contact list to be shown + waitFor(allOf(withText(R.string.contact_list_button), 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 viewMatcher, long sleepMs) + throws InterruptedException { + doClick(viewMatcher); + if (!debug) sleep(sleepMs); + } + + private void doClick(final Matcher viewMatcher) + throws InterruptedException { + if (!debug) { + onView(viewMatcher) + .perform(waitUntilMatches(isDisplayed())) + .perform(visualClick(overlayView)); + sleep(500); + } + onView(viewMatcher) + .perform(waitUntilMatches(allOf(isDisplayed(), isEnabled()))) + .perform(click()); + } + + private void doItemClick(final Matcher viewMatcher, int pos) + throws InterruptedException { + if (!debug) { + onView(viewMatcher).perform( + actionOnItemAtPosition(pos, visualClick(overlayView))); + sleep(500); + } + onView(viewMatcher).perform( + actionOnItemAtPosition(pos, click())); + } + + private void closeKeyboard(final Matcher viewMatcher) + throws InterruptedException { + if (!debug) sleep(750); + onView(viewMatcher).perform(closeSoftKeyboard()); + } + +} diff --git a/briar-android/src/main/res/layout/splash.xml b/briar-android/src/main/res/layout/splash.xml index 0cd22a43c..d78083132 100644 --- a/briar-android/src/main/res/layout/splash.xml +++ b/briar-android/src/main/res/layout/splash.xml @@ -5,6 +5,7 @@ android:layout_height="match_parent">