1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *   http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.android.apps.common.testing.ui.espresso;
18
19import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
20import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.pressMenuKey;
21import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isDisplayed;
22import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.isRoot;
23import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withClassName;
24import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withContentDescription;
25import static com.google.common.base.Preconditions.checkNotNull;
26import static org.hamcrest.Matchers.allOf;
27import static org.hamcrest.Matchers.anyOf;
28import static org.hamcrest.Matchers.endsWith;
29
30import com.google.android.apps.common.testing.ui.espresso.action.ViewActions;
31import com.google.android.apps.common.testing.ui.espresso.base.BaseLayerModule;
32import com.google.android.apps.common.testing.ui.espresso.base.IdlingResourceRegistry;
33import com.google.android.apps.common.testing.ui.espresso.util.TreeIterables;
34
35import android.content.Context;
36import android.os.Build;
37import android.os.Looper;
38import android.view.View;
39import android.view.ViewConfiguration;
40
41import dagger.ObjectGraph;
42
43import org.hamcrest.Matcher;
44
45/**
46 * Entry point to the Espresso framework. Test authors can initiate testing by using one of the on*
47 * methods (e.g. onView) or perform top-level user actions (e.g. pressBack).
48 */
49public final class Espresso {
50
51  static ObjectGraph espressoGraph() {
52    return GraphHolder.graph();
53  }
54
55  private Espresso() {}
56
57  /**
58   * Creates an {@link PartiallyScopedViewInteraction} for a given view. Note: the view has
59   * to be part of the  view hierarchy. This may not be the case if it is rendered as part of
60   * an AdapterView (e.g. ListView). If this is the case, use Espresso.onData to load the view
61   * first.
62   *
63   * @param viewMatcher used to select the view.
64   * @see #onData
65   */
66  public static ViewInteraction onView(final Matcher<View> viewMatcher) {
67    return espressoGraph().plus(new ViewInteractionModule(viewMatcher)).get(ViewInteraction.class);
68  }
69
70
71
72  /**
73   * Creates an {@link DataInteraction} for a data object displayed by the application. Use this
74   * method to load (into the view hierarchy) items from AdapterView widgets (e.g. ListView).
75   *
76   * @param dataMatcher a matcher used to find the data object.
77   */
78  public static DataInteraction onData(Matcher<Object> dataMatcher) {
79    return new DataInteraction(dataMatcher);
80  }
81
82  /**
83   * Registers a Looper for idle checking with the framework. This is intended for use with
84   * non-UI thread loopers.
85   *
86   * @throws IllegalArgumentException if looper is the main looper.
87   */
88  public static void registerLooperAsIdlingResource(Looper looper) {
89    registerLooperAsIdlingResource(looper, false);
90  }
91
92  /**
93   * Registers a Looper for idle checking with the framework. This is intended for use with
94   * non-UI thread loopers.
95   *
96   * This method allows the caller to consider Thread.State.WAIT to be 'idle'.
97   *
98   * This is useful in the case where a looper is sending a message to the UI thread synchronously
99   * through a wait/notify mechanism.
100   *
101   * @throws IllegalArgumentException if looper is the main looper.
102   */
103  public static void registerLooperAsIdlingResource(Looper looper, boolean considerWaitIdle) {
104    espressoGraph().get(IdlingResourceRegistry.class).registerLooper(looper, considerWaitIdle);
105  }
106
107  /**
108   * Registers one or more {@link IdlingResource}s with the framework. It is expected, although not
109   * strictly required, that this method will be called at test setup time prior to any interaction
110   * with the application under test. When registering more than one resource, ensure that each has
111   * a unique name.
112   */
113  public static void registerIdlingResources(IdlingResource... resources) {
114    checkNotNull(resources);
115    IdlingResourceRegistry registry = espressoGraph().get(IdlingResourceRegistry.class);
116    for (IdlingResource resource : resources) {
117      checkNotNull(resource.getName(), "IdlingResource.getName() should not be null");
118      registry.register(resource);
119    }
120  }
121
122  /**
123   * Changes the default {@link FailureHandler} to the given one.
124   */
125  public static void setFailureHandler(FailureHandler failureHandler) {
126    espressoGraph().get(BaseLayerModule.FailureHandlerHolder.class)
127        .update(checkNotNull(failureHandler));
128  }
129
130  /********************************** Top Level Actions ******************************************/
131
132  // Ideally, this should be only allOf(isDisplayed(), withContentDescription("More options"))
133  // But the ActionBarActivity compat lib is missing a content description for this element, so
134  // we add the class name matcher as another option to find the view.
135  @SuppressWarnings("unchecked")
136  private static final Matcher<View> OVERFLOW_BUTTON_MATCHER = anyOf(
137    allOf(isDisplayed(), withContentDescription("More options")),
138    allOf(isDisplayed(), withClassName(endsWith("OverflowMenuButton"))));
139
140
141  /**
142   * Closes soft keyboard if open.
143   */
144  public static void closeSoftKeyboard() {
145    onView(isRoot()).perform(ViewActions.closeSoftKeyboard());
146  }
147
148  /**
149   * Opens the overflow menu displayed in the contextual options of an ActionMode.
150   *
151   * This works with both native and SherlockActionBar action modes.
152   *
153   * Note the significant difference in UX between ActionMode and ActionBar overflows - ActionMode
154   * will always present an overflow icon and that icon only responds to clicks. The menu button
155   * (if present) has no impact on it.
156   */
157  @SuppressWarnings("unchecked")
158  public static void openContextualActionModeOverflowMenu() {
159    onView(isRoot())
160        .perform(new TransitionBridgingViewAction());
161
162    onView(OVERFLOW_BUTTON_MATCHER)
163        .perform(click());
164  }
165
166  /**
167   * Press on the back button.
168   *
169   * @throws PerformException if currently displayed activity is root activity, since pressing back
170   *         button would result in application closing.
171   */
172  public static void pressBack() {
173    onView(isRoot()).perform(ViewActions.pressBack());
174  }
175
176  /**
177   * Opens the overflow menu displayed within an ActionBar.
178   *
179   * This works with both native and SherlockActionBar ActionBars.
180   *
181   * Note the significant differences of UX between ActionMode and ActionBars with respect to
182   * overflows. If a hardware menu key is present, the overflow icon is never displayed in
183   * ActionBars and can only be interacted with via menu key presses.
184   */
185  @SuppressWarnings("unchecked")
186  public static void openActionBarOverflowOrOptionsMenu(Context context) {
187    if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.HONEYCOMB) {
188      // regardless of the os level of the device, this app will be rendering a menukey
189      // in the virtual navigation bar (if present) or responding to hardware option keys on
190      // any activity.
191      onView(isRoot())
192          .perform(pressMenuKey());
193    } else if (hasVirtualOverflowButton(context)) {
194      // If we're using virtual keys - theres a chance we're in mid animation of switching
195      // between a contextual action bar and the non-contextual action bar. In this case there
196      // are 2 'More Options' buttons present. Lets wait till that is no longer the case.
197      onView(isRoot())
198          .perform(new TransitionBridgingViewAction());
199
200      onView(OVERFLOW_BUTTON_MATCHER)
201          .perform(click());
202    } else {
203      // either a hardware button exists, or we're on a pre-HC os.
204      onView(isRoot())
205          .perform(pressMenuKey());
206    }
207  }
208
209  private static boolean hasVirtualOverflowButton(Context context) {
210    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
211      return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
212    } else {
213      return !ViewConfiguration.get(context).hasPermanentMenuKey();
214    }
215  }
216
217  /**
218   * Handles the cases where the app is transitioning between a contextual action bar and a
219   * non contextual action bar.
220   */
221  private static class TransitionBridgingViewAction implements ViewAction {
222    @Override
223    public void perform(UiController controller, View view) {
224      int loops = 0;
225      while (isTransitioningBetweenActionBars(view) && loops < 100) {
226        loops++;
227        controller.loopMainThreadForAtLeast(50);
228      }
229      // if we're not transitioning properly the next viewaction
230      // will give a decent enough exception.
231    }
232
233    @Override
234    public String getDescription() {
235      return "Handle transition between action bar and action bar context.";
236    }
237
238    @Override
239    public Matcher<View> getConstraints() {
240      return isRoot();
241    }
242
243    private boolean isTransitioningBetweenActionBars(View view) {
244      int actionButtonCount = 0;
245      for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
246        if (OVERFLOW_BUTTON_MATCHER.matches(child)) {
247          actionButtonCount++;
248        }
249      }
250      return actionButtonCount > 1;
251    }
252  }
253
254
255}
256