1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-23 18:02:15 -05:00

Add end-to-end tests using Espresso

This commit is contained in:
Art O Cathain 2014-11-09 21:14:34 +00:00
parent a725099693
commit 854b1b3ffc
19 changed files with 665 additions and 8 deletions

View File

@ -29,6 +29,25 @@ dependencies {
compile 'com.beetstra.jutf7:jutf7:1.0.0'
compile 'com.android.support:support-v13:19.1.0'
compile 'net.sourceforge.htmlcleaner:htmlcleaner:2.2'
androidTestCompile ('com.jakewharton.espresso:espresso:1.1-r3' ) {
// Note: some of these exclusions may become necessary. See the
// github site https://github.com/JakeWharton/double-espresso
// exclude group: 'com.squareup.dagger'
// exclude group: 'javax.inject'
// exclude group: 'javax.annotation'
// exclude group: 'com.google.guava'
exclude group: 'com.google.code.findbugs'
// exclude group: 'org.hamcrest'
}
androidTestCompile("com.icegreen:greenmail:1.3.1b") {
// Use a better, later version
exclude group: "javax.mail"
}
// this version avoids some "Ignoring InnerClasses attribute for an anonymous inner class" warnings
androidTestCompile "javax.mail:javax.mail-api:1.5.2"
}
project.ext.preDexLibs = !project.hasProperty('disablePreDex')
@ -49,6 +68,7 @@ android {
defaultConfig {
minSdkVersion 15
targetSdkVersion 17
testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner"
}
dexOptions {
@ -89,6 +109,7 @@ android {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
exclude 'META-INF/LICENSE.txt'
exclude 'LICENSE.txt'
exclude 'META-INF/NOTICE'
exclude 'META-INF/NOTICE.txt'
}

View File

@ -5,18 +5,15 @@
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17"/>
<!-- Allow debug trace to be written -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- We add an application tag here just so that we can indicate that
this package needs to link against the android.test library,
which is needed when building test cases. -->
<application>
<uses-library android:name="android.test.runner" />
</application>
<!--
This declares that this application uses the instrumentation test runner targeting
the package of com.fsck.k9. To run the tests use the command:
"adb shell am instrument -w com.fsck.k9.tests/com.zutubi.android.junitreport.JUnitReportTestRunner"
-->
<instrumentation android:name="com.zutubi.android.junitreport.JUnitReportTestRunner"
android:targetPackage="com.fsck.k9"
android:label="Tests for com.fsck.k9"/>
</manifest>

View File

@ -0,0 +1,24 @@
package com.fsck.k9.endtoend;
import com.fsck.k9.activity.setup.WelcomeMessage;
import com.fsck.k9.endtoend.pages.WelcomeMessagePage;
/**
* Creates a new IMAP account via the getting started flow.
*/
public class A000_WelcomeAndSetupAccountIntegrationTest extends AbstractEndToEndTest<WelcomeMessage> {
public A000_WelcomeAndSetupAccountIntegrationTest() {
super(WelcomeMessage.class, false);
}
public void testCreateAccount() throws Exception {
new AccountSetupFlow(this).setupAccountFromWelcomePage(new WelcomeMessagePage());
}
public void testCreateSecondAccount() throws Exception {
new AccountSetupFlow(this).setupAccountFromWelcomePage(new WelcomeMessagePage());
}
}

View File

@ -0,0 +1,40 @@
package com.fsck.k9.endtoend;
import com.fsck.k9.activity.Accounts;
import com.fsck.k9.endtoend.framework.AccountForTest;
import com.fsck.k9.endtoend.framework.ApplicationState;
import com.fsck.k9.endtoend.pages.AccountsPage;
/**
* Creates and removes accounts.
*
* Because of the way K-9 shows the start page, there must already be two accounts
* in existence for this test to work.
*/
public class A010_AccountIntegrationTest extends AbstractEndToEndTest<Accounts>{
public A010_AccountIntegrationTest() {
super(Accounts.class);
}
public void testCreateAccountDirectly() throws Exception {
new AccountSetupFlow(this).setupAccountFromAccountsPage(new AccountsPage());
}
public void testDeleteAccount() {
AccountsPage accountsPage = new AccountsPage();
AccountForTest accountForTest = ApplicationState.getInstance().accounts.get(0);
accountsPage.assertAccountExists(accountForTest.description);
accountsPage.clickLongOnAccount(accountForTest);
accountsPage.clickRemoveInAccountMenu();
accountsPage.clickOK();
accountsPage.assertAccountDoesNotExist(accountForTest.description);
}
}

View File

@ -0,0 +1,60 @@
package com.fsck.k9.endtoend;
import android.app.Activity;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
import com.fsck.k9.R;
import com.fsck.k9.endtoend.framework.ApplicationState;
import com.fsck.k9.endtoend.framework.StubMailServer;
import com.fsck.k9.endtoend.pages.WelcomeMessagePage;
import com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions;
import junit.framework.AssertionFailedError;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
public abstract class AbstractEndToEndTest<T extends Activity> extends ActivityInstrumentationTestCase2<T> {
private ApplicationState state = ApplicationState.getInstance();
private final boolean bypassWelcome;
public AbstractEndToEndTest(Class<T> activityClass) {
this(activityClass, true);
}
public AbstractEndToEndTest(Class<T> activityClass, boolean bypassWelcome) {
super(activityClass);
this.bypassWelcome = bypassWelcome;
}
@Override
protected void setUp() throws Exception {
super.setUp();
getActivity();
if (bypassWelcome) {
bypassWelcomeScreen();
}
}
private void bypassWelcomeScreen() {
try {
onView(withId(R.id.welcome_message)).check(ViewAssertions.doesNotExist());
} catch (AssertionFailedError ex) {
/*
* The view doesn't NOT exist == the view exists, and needs to be bypassed!
*/
Log.d(getClass().getName(), "Bypassing welcome");
new AccountSetupFlow(this).setupAccountFromWelcomePage(new WelcomeMessagePage());
}
}
protected StubMailServer setupMailServer() {
if (null == state.stubMailServer) {
state.stubMailServer = new StubMailServer();
}
return state.stubMailServer;
}
}

View File

@ -0,0 +1,98 @@
package com.fsck.k9.endtoend;
import com.fsck.k9.endtoend.framework.AccountForTest;
import com.fsck.k9.endtoend.framework.ApplicationState;
import com.fsck.k9.endtoend.framework.StubMailServer;
import com.fsck.k9.endtoend.framework.UserForImap;
import com.fsck.k9.endtoend.pages.AccountOptionsPage;
import com.fsck.k9.endtoend.pages.AccountSetupNamesPage;
import com.fsck.k9.endtoend.pages.AccountSetupPage;
import com.fsck.k9.endtoend.pages.AccountTypePage;
import com.fsck.k9.endtoend.pages.AccountsPage;
import com.fsck.k9.endtoend.pages.IncomingServerSettingsPage;
import com.fsck.k9.endtoend.pages.OutgoingServerSettingsPage;
import com.fsck.k9.endtoend.pages.WelcomeMessagePage;
import com.fsck.k9.mail.ConnectionSecurity;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Encapsulated the steps required to set up a new mail account.
*/
public class AccountSetupFlow {
static final String ACCOUNT_NAME = "sendAndReceiveTestName";
private final AbstractEndToEndTest test;
public AccountSetupFlow(AbstractEndToEndTest test) {
this.test = test;
}
public AccountsPage setupAccountFromWelcomePage(WelcomeMessagePage welcomeMessagePage) {
AccountSetupPage accountSetupPage = welcomeMessagePage.clickNext();
return setupAccountFromSetupNewAccountActivity(accountSetupPage);
}
public AccountsPage setupAccountFromAccountsPage(AccountsPage accountPage) {
AccountSetupPage accountSetupPage = accountPage.clickAddNewAccount();
return setupAccountFromSetupNewAccountActivity(accountSetupPage);
}
public AccountsPage setupAccountFromSetupNewAccountActivity(AccountSetupPage accountSetupPage) {
AccountTypePage accountTypePage = fillInCredentialsAndClickManualSetup(accountSetupPage);
IncomingServerSettingsPage incoming = accountTypePage.clickImap();
StubMailServer stubMailServer = test.setupMailServer();
OutgoingServerSettingsPage outgoing = setupIncomingServerAndClickNext(incoming, stubMailServer);
AccountOptionsPage accountOptionsPage = setupOutgoingServerAndClickNext(outgoing, stubMailServer);
AccountSetupNamesPage accountSetupNamesPage = accountOptionsPage.clickNext();
String accountDescription = tempAccountName();
accountSetupNamesPage.inputAccountDescription(accountDescription);
accountSetupNamesPage.inputAccountName(ACCOUNT_NAME);
AccountsPage accountsPage = accountSetupNamesPage.clickDone();
accountsPage.assertAccountExists(accountDescription);
ApplicationState.getInstance().accounts.add(new AccountForTest(ACCOUNT_NAME, accountDescription, stubMailServer));
return accountsPage;
}
private String tempAccountName() {
return "sendAndReceiveTest-" + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(new Date());
}
private AccountTypePage fillInCredentialsAndClickManualSetup(AccountSetupPage page) {
return page
.inputEmailAddress(UserForImap.TEST_USER.emailAddress)
.inputPassword(UserForImap.TEST_USER.password)
.clickManualSetup();
}
private AccountOptionsPage setupOutgoingServerAndClickNext(OutgoingServerSettingsPage page, StubMailServer stubMailServer) {
return page
.inputSmtpServer(stubMailServer.getSmtpBindAddress())
.inputSmtpSecurity(ConnectionSecurity.NONE)
.inputPort(stubMailServer.getSmtpPort())
.inputRequireSignIn(false)
.clickNext();
}
private OutgoingServerSettingsPage setupIncomingServerAndClickNext(IncomingServerSettingsPage page, StubMailServer stubMailServer) {
return page
.inputImapServer(stubMailServer.getImapBindAddress())
.inputImapSecurity(ConnectionSecurity.NONE)
.inputPort(stubMailServer.getImapPort())
.inputUsername(UserForImap.TEST_USER.loginUsername)
.clickNext();
}
}

View File

@ -0,0 +1,17 @@
package com.fsck.k9.endtoend.framework;
/**
* An account that was added by a test.
*/
public class AccountForTest {
public final String name;
public final String description;
public final StubMailServer stubMailServer;
public AccountForTest(String name, String description, StubMailServer stubMailServer) {
this.name = name;
this.description = description;
this.stubMailServer = stubMailServer;
}
}

View File

@ -0,0 +1,22 @@
package com.fsck.k9.endtoend.framework;
import java.util.ArrayList;
import java.util.List;
/**
* Stores the state of the application from the point of view of end-to-end tests.
*/
public class ApplicationState {
private static final ApplicationState state = new ApplicationState();
public final List<AccountForTest> accounts = new ArrayList<AccountForTest>();
public StubMailServer stubMailServer;
public static ApplicationState getInstance() {
return state;
}
}

View File

@ -0,0 +1,41 @@
package com.fsck.k9.endtoend.framework;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.ServerSetup;
/**
* Configuration and management of a pair of stub servers for use by an account.
*/
public class StubMailServer {
private static final ServerSetup IMAP_SERVER_SETUP = new ServerSetup(10143, "127.0.0.2", ServerSetup.PROTOCOL_IMAP);
private static final ServerSetup SMTP_SERVER_SETUP = new ServerSetup(10587, "127.0.0.2", ServerSetup.PROTOCOL_SMTP);
/**
* Stub server that speaks SMTP, IMAP etc., that K-9 can talk to.
*/
private GreenMail greenmail;
public StubMailServer() {
greenmail = new GreenMail(new ServerSetup[]{IMAP_SERVER_SETUP, SMTP_SERVER_SETUP});
greenmail.setUser(UserForImap.TEST_USER.emailAddress, UserForImap.TEST_USER.loginUsername, UserForImap.TEST_USER.password);
greenmail.start();
}
public String getSmtpBindAddress() {
return SMTP_SERVER_SETUP.getBindAddress();
}
public int getSmtpPort() {
return SMTP_SERVER_SETUP.getPort();
}
public String getImapBindAddress() {
return IMAP_SERVER_SETUP.getBindAddress();
}
public int getImapPort() {
return IMAP_SERVER_SETUP.getPort();
}
}

View File

@ -0,0 +1,19 @@
package com.fsck.k9.endtoend.framework;
/**
* Credentials for the stub IMAP/SMTP server
*/
public class UserForImap {
public static final UserForImap TEST_USER = new UserForImap("test-username", "test-password", "test-email@example.com");
public final String loginUsername;
public final String password;
public final String emailAddress;
private UserForImap(String loginUsername, String password, String emailAddress) {
this.loginUsername = loginUsername;
this.password = password;
this.emailAddress = emailAddress;
}
}

View File

@ -0,0 +1,6 @@
package com.fsck.k9.endtoend.pages;
public class AbstractPage {
// used to have some content. Now a placeholder class
}

View File

@ -0,0 +1,17 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
public class AccountOptionsPage extends AbstractPage {
public AccountSetupNamesPage clickNext() {
onView(withId(R.id.next)).perform(click());
return new AccountSetupNamesPage();
}
}

View File

@ -0,0 +1,48 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
import com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
public class AccountSetupNamesPage extends AbstractPage {
public AccountSetupNamesPage inputAccountName(String name) {
onView(withId(R.id.account_name))
.perform(scrollTo())
.perform(clearText())
.perform(typeText(name));
return this;
}
public AccountSetupNamesPage inputAccountDescription(String name) {
onView(withId(R.id.account_description))
.perform(scrollTo())
.perform(clearText())
.perform(typeText(name));
return this;
}
public AccountsPage clickDone() {
onView(withId(R.id.done))
.perform(click());
dismissChangelog();
return new AccountsPage();
}
private void dismissChangelog() {
try {
onView(ViewMatchers.withText("OK")).perform(click());
} catch (NoMatchingViewException ex) {
// Ignored. Not the best way of doing this, but Espresso rightly makes
// conditional flow difficult.
}
}
}

View File

@ -0,0 +1,27 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
public class AccountSetupPage extends AbstractPage {
public AccountSetupPage inputEmailAddress(String emailAddress) {
onView(withId(R.id.account_email)).perform(typeText(emailAddress));
return this;
}
public AccountSetupPage inputPassword(String password) {
onView(withId(R.id.account_password)).perform(typeText(password));
return this;
}
public AccountTypePage clickManualSetup() {
onView(withId(R.id.manual_setup)).perform(click());
return new AccountTypePage();
}
}

View File

@ -0,0 +1,14 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
public class AccountTypePage extends AbstractPage {
public IncomingServerSettingsPage clickImap() {
onView(withId(R.id.imap)).perform(click());
return new IncomingServerSettingsPage();
}
}

View File

@ -0,0 +1,56 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import com.fsck.k9.endtoend.framework.AccountForTest;
import com.google.android.apps.common.testing.ui.espresso.NoMatchingViewException;
import com.google.android.apps.common.testing.ui.espresso.ViewAssertion;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.longClick;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.doesNotExist;
import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
public class AccountsPage extends AbstractPage {
private void assertAccount(String accountDisplayName, boolean exists) {
ViewAssertion assertion = exists ? matches(isDisplayed()) : doesNotExist();
onView(withText(accountDisplayName)).check(assertion);
}
public AccountSetupPage clickAddNewAccount() {
// need to click twice for some reason?
onView(withId(R.id.add_new_account)).perform(click());
try {
onView(withId(R.id.add_new_account)).perform(click());
} catch (NoMatchingViewException ex) {
// Ignore
}
onView(withId(R.id.account_email)).perform(scrollTo());
return new AccountSetupPage();
}
public void assertAccountExists(String accountDisplayName) {
assertAccount(accountDisplayName, true);
}
public void assertAccountDoesNotExist(String accountDisplayName) {
assertAccount(accountDisplayName, false);
}
public void clickLongOnAccount(AccountForTest accountForTest) {
onView(withText(accountForTest.description)).perform(longClick());
}
public void clickRemoveInAccountMenu() {
onView(withText("Remove account")).perform(click());
}
public void clickOK() {
onView(withText("OK")).perform(click());
}
}

View File

@ -0,0 +1,66 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import com.fsck.k9.mail.ConnectionSecurity;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isClickable;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
public class IncomingServerSettingsPage extends AbstractPage {
public IncomingServerSettingsPage inputImapServer(String imapServer) {
onView(withId(R.id.account_server))
.perform(scrollTo())
.perform(clearText())
.perform(typeText(imapServer));
return this;
}
public IncomingServerSettingsPage inputImapSecurity(ConnectionSecurity security) {
onView(withId(R.id.account_security_type))
.perform(scrollTo())
.perform(click());
onData(allOf(is(instanceOf(ConnectionSecurity.class)), is(security))).perform(click());
return this;
}
public IncomingServerSettingsPage inputPort(int port) {
onView(withId(R.id.account_port))
.perform(scrollTo())
.perform(clearText())
.perform(typeText(String.valueOf(port)));
return this;
}
public OutgoingServerSettingsPage clickNext() {
onView(withId(R.id.next))
// .perform(scrollTo())
.check(matches(isClickable()))
.perform(click());
// We know this view is on the next page, this functions as a wait.
onView(withText("SMTP server")).perform(scrollTo());
return new OutgoingServerSettingsPage();
}
public IncomingServerSettingsPage inputUsername(String loginUsername) {
onView(withId(R.id.account_username))
.perform(scrollTo())
.perform(clearText())
.perform(typeText(loginUsername));
return this;
}
}

View File

@ -0,0 +1,69 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import com.fsck.k9.mail.ConnectionSecurity;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.clearText;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.scrollTo;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
public class OutgoingServerSettingsPage extends AbstractPage {
public OutgoingServerSettingsPage inputSmtpServer(String serverAddress) {
onView(withId(R.id.account_server))
.perform(scrollTo())
.perform(clearText())
.perform(typeText(serverAddress));
return this;
}
public OutgoingServerSettingsPage inputSmtpSecurity(ConnectionSecurity security) {
onView(withId(R.id.account_security_type))
.perform(scrollTo())
.perform(click());
onData(allOf(is(instanceOf(ConnectionSecurity.class)), is(security))).perform(click());
return this;
}
public OutgoingServerSettingsPage inputPort(int port) {
onView(withId(R.id.account_port))
.perform(scrollTo())
.perform(clearText())
.perform(typeText(String.valueOf(port)));
return this;
}
public OutgoingServerSettingsPage inputRequireSignIn(boolean requireSignInInput) {
onView(withId(R.id.account_require_login))
.perform(scrollTo());
/*
* Make this smarter; click if necessary.
*/
if (!requireSignInInput) {
onView(withId(R.id.account_require_login))
.perform(click());
}
// Matcher<View> checkedOrNot = requireSignInInput ? isChecked(): isNotChecked();
// try {
// onView(withId(R.id.account_require_login)).check((matches(checkedOrNot)));
// } catch (AssertionFailedWithCauseError ex) {
// onView(withId(R.id.account_require_login)).perform(click());
// }
return this;
}
public AccountOptionsPage clickNext() {
onView(withId(R.id.next)).perform(click());
return new AccountOptionsPage();
}
}

View File

@ -0,0 +1,15 @@
package com.fsck.k9.endtoend.pages;
import com.fsck.k9.R;
import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
public class WelcomeMessagePage extends AbstractPage {
public final AccountSetupPage clickNext() {
onView(withId(R.id.next)).perform(click());
return new AccountSetupPage();
}
}