blob: a8a8d1de29d1b6076a0a5077d1fe354c91929846 [file] [log] [blame]
package com.google.android.apps.common.testing.ui.espresso;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isAssignableFrom;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDescendantOfA;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.android.apps.common.testing.ui.espresso.action.ScrollToAction;
import com.google.android.apps.common.testing.ui.espresso.base.MainThread;
import com.google.android.apps.common.testing.ui.espresso.util.HumanReadables;
import com.google.common.base.Optional;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;
import javax.inject.Inject;
/**
* Provides the primary interface for test authors to perform actions or asserts on views.
* <p>
* Each interaction is associated with a view identified by a view matcher. All view actions and
* asserts are performed on the UI thread (thus ensuring sequential execution). The same goes for
* retrieval of views (this is done to ensure that view state is "fresh" prior to execution of each
* operation).
* <p>
*/
public final class ViewInteraction {
private static final String TAG = ViewInteraction.class.getSimpleName();
private final UiController uiController;
private final ViewFinder viewFinder;
private final Executor mainThreadExecutor;
private final FailureHandler failureHandler;
private final Matcher<View> viewMatcher;
private final AtomicReference<Matcher<Root>> rootMatcherRef;
@Inject
ViewInteraction(
UiController uiController,
ViewFinder viewFinder,
@MainThread Executor mainThreadExecutor,
FailureHandler failureHandler,
Matcher<View> viewMatcher,
AtomicReference<Matcher<Root>> rootMatcherRef) {
this.viewFinder = checkNotNull(viewFinder);
this.uiController = checkNotNull(uiController);
this.failureHandler = checkNotNull(failureHandler);
this.mainThreadExecutor = checkNotNull(mainThreadExecutor);
this.viewMatcher = checkNotNull(viewMatcher);
this.rootMatcherRef = checkNotNull(rootMatcherRef);
}
/**
* Performs the given action(s) on the view selected by the current view matcher. If more than one
* action is provided, actions are executed in the order provided with precondition checks running
* prior to each action.
*
* @param viewActions one or more actions to execute.
* @return this interaction for further perform/verification calls.
*/
public ViewInteraction perform(final ViewAction... viewActions) {
checkNotNull(viewActions);
for (ViewAction action : viewActions) {
doPerform(action);
}
return this;
}
/**
* Makes this ViewInteraction scoped to the root selected by the given root matcher.
*/
public ViewInteraction inRoot(Matcher<Root> rootMatcher) {
this.rootMatcherRef.set(checkNotNull(rootMatcher));
return this;
}
private void doPerform(final ViewAction viewAction) {
checkNotNull(viewAction);
final Matcher<? extends View> constraints = checkNotNull(viewAction.getConstraints());
runSynchronouslyOnUiThread(new Runnable() {
@Override
public void run() {
uiController.loopMainThreadUntilIdle();
View targetView = viewFinder.getView();
Log.i(TAG, String.format(
"Performing '%s' action on view %s", viewAction.getDescription(), viewMatcher));
if (!constraints.matches(targetView)) {
// TODO(user): update this to describeMismatch once hamcrest is updated to new
StringDescription stringDescription = new StringDescription(new StringBuilder(
"Action will not be performed because the target view "
+ "does not match one or more of the following constraints:\n"));
constraints.describeTo(stringDescription);
stringDescription.appendText("\nTarget view: ")
.appendValue(HumanReadables.describe(targetView));
if (viewAction instanceof ScrollToAction
&& isDescendantOfA(isAssignableFrom((AdapterView.class))).matches(targetView)) {
stringDescription.appendText(
"\nFurther Info: ScrollToAction on a view inside an AdapterView will not work. "
+ "Use Espresso.onData to load the view.");
}
throw new PerformException.Builder()
.withActionDescription(viewAction.getDescription())
.withViewDescription(viewMatcher.toString())
.withCause(new RuntimeException(stringDescription.toString()))
.build();
} else {
viewAction.perform(uiController, targetView);
}
}
});
}
/**
* Checks the given {@link ViewAssertion} on the the view selected by the current view matcher.
*
* @param viewAssert the assertion to perform.
* @return this interaction for further perform/verification calls.
*/
public ViewInteraction check(final ViewAssertion viewAssert) {
checkNotNull(viewAssert);
runSynchronouslyOnUiThread(new Runnable() {
@Override
public void run() {
uiController.loopMainThreadUntilIdle();
Optional<View> targetView = Optional.absent();
Optional<NoMatchingViewException> missingViewException = Optional.absent();
try {
targetView = Optional.of(viewFinder.getView());
} catch (NoMatchingViewException nsve) {
missingViewException = Optional.of(nsve);
}
viewAssert.check(targetView, missingViewException);
}
});
return this;
}
private void runSynchronouslyOnUiThread(Runnable action) {
FutureTask<Void> uiTask = new FutureTask<Void>(action, null);
mainThreadExecutor.execute(uiTask);
try {
uiTask.get();
} catch (InterruptedException ie) {
throw new RuntimeException("Interrupted running UI task", ie);
} catch (ExecutionException ee) {
failureHandler.handle(ee.getCause(), viewMatcher);
}
}
}