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