summaryrefslogtreecommitdiff
path: root/espresso/espresso-lib/src/main/java/com/google/android/apps/common/testing/ui/espresso/Espresso.java
blob: 5e3d5f4ea112400755acb03df5036191bc3df444 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.apps.common.testing.ui.espresso;

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.pressMenuKey;
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.isRoot;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withClassName;
import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withContentDescription;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.endsWith;

import com.google.android.apps.common.testing.ui.espresso.action.ViewActions;
import com.google.android.apps.common.testing.ui.espresso.base.BaseLayerModule;
import com.google.android.apps.common.testing.ui.espresso.base.IdlingResourceRegistry;
import com.google.android.apps.common.testing.ui.espresso.util.TreeIterables;

import android.content.Context;
import android.os.Build;
import android.os.Looper;
import android.view.View;
import android.view.ViewConfiguration;

import dagger.ObjectGraph;

import org.hamcrest.Matcher;

/**
 * Entry point to the Espresso framework. Test authors can initiate testing by using one of the on*
 * methods (e.g. onView) or perform top-level user actions (e.g. pressBack).
 */
public final class Espresso {

  static ObjectGraph espressoGraph() {
    return GraphHolder.graph();
  }

  private Espresso() {}

  /**
   * Creates an {@link PartiallyScopedViewInteraction} for a given view. Note: the view has
   * to be part of the  view hierarchy. This may not be the case if it is rendered as part of
   * an AdapterView (e.g. ListView). If this is the case, use Espresso.onData to load the view
   * first.
   *
   * @param viewMatcher used to select the view.
   * @see #onData
   */
  public static ViewInteraction onView(final Matcher<View> viewMatcher) {
    return espressoGraph().plus(new ViewInteractionModule(viewMatcher)).get(ViewInteraction.class);
  }



  /**
   * Creates an {@link DataInteraction} for a data object displayed by the application. Use this
   * method to load (into the view hierarchy) items from AdapterView widgets (e.g. ListView).
   *
   * @param dataMatcher a matcher used to find the data object.
   */
  public static DataInteraction onData(Matcher<Object> dataMatcher) {
    return new DataInteraction(dataMatcher);
  }

  /**
   * Registers a Looper for idle checking with the framework. This is intended for use with
   * non-UI thread loopers.
   *
   * @throws IllegalArgumentException if looper is the main looper.
   */
  public static void registerLooperAsIdlingResource(Looper looper) {
    registerLooperAsIdlingResource(looper, false);
  }

  /**
   * Registers a Looper for idle checking with the framework. This is intended for use with
   * non-UI thread loopers.
   *
   * This method allows the caller to consider Thread.State.WAIT to be 'idle'.
   *
   * This is useful in the case where a looper is sending a message to the UI thread synchronously
   * through a wait/notify mechanism.
   *
   * @throws IllegalArgumentException if looper is the main looper.
   */
  public static void registerLooperAsIdlingResource(Looper looper, boolean considerWaitIdle) {
    espressoGraph().get(IdlingResourceRegistry.class).registerLooper(looper, considerWaitIdle);
  }

  /**
   * Registers one or more {@link IdlingResource}s with the framework. It is expected, although not
   * strictly required, that this method will be called at test setup time prior to any interaction
   * with the application under test. When registering more than one resource, ensure that each has
   * a unique name.
   */
  public static void registerIdlingResources(IdlingResource... resources) {
    checkNotNull(resources);
    IdlingResourceRegistry registry = espressoGraph().get(IdlingResourceRegistry.class);
    for (IdlingResource resource : resources) {
      checkNotNull(resource.getName(), "IdlingResource.getName() should not be null");
      registry.register(resource);
    }
  }

  /**
   * Changes the default {@link FailureHandler} to the given one.
   */
  public static void setFailureHandler(FailureHandler failureHandler) {
    espressoGraph().get(BaseLayerModule.FailureHandlerHolder.class)
        .update(checkNotNull(failureHandler));
  }

  /********************************** Top Level Actions ******************************************/

  // Ideally, this should be only allOf(isDisplayed(), withContentDescription("More options"))
  // But the ActionBarActivity compat lib is missing a content description for this element, so
  // we add the class name matcher as another option to find the view.
  @SuppressWarnings("unchecked")
  private static final Matcher<View> OVERFLOW_BUTTON_MATCHER = anyOf(
    allOf(isDisplayed(), withContentDescription("More options")), 
    allOf(isDisplayed(), withClassName(endsWith("OverflowMenuButton"))));


  /**
   * Closes soft keyboard if open.
   */
  public static void closeSoftKeyboard() {
    onView(isRoot()).perform(ViewActions.closeSoftKeyboard());
  }

  /**
   * Opens the overflow menu displayed in the contextual options of an ActionMode.
   *
   * This works with both native and SherlockActionBar action modes.
   *
   * Note the significant difference in UX between ActionMode and ActionBar overflows - ActionMode
   * will always present an overflow icon and that icon only responds to clicks. The menu button
   * (if present) has no impact on it.
   */
  @SuppressWarnings("unchecked")
  public static void openContextualActionModeOverflowMenu() {
    onView(isRoot())
        .perform(new TransitionBridgingViewAction());

    onView(OVERFLOW_BUTTON_MATCHER)
        .perform(click());
  }

  /**
   * Press on the back button.
   *
   * @throws PerformException if currently displayed activity is root activity, since pressing back
   *         button would result in application closing.
   */
  public static void pressBack() {
    onView(isRoot()).perform(ViewActions.pressBack());
  }

  /**
   * Opens the overflow menu displayed within an ActionBar.
   *
   * This works with both native and SherlockActionBar ActionBars.
   *
   * Note the significant differences of UX between ActionMode and ActionBars with respect to
   * overflows. If a hardware menu key is present, the overflow icon is never displayed in
   * ActionBars and can only be interacted with via menu key presses.
   */
  @SuppressWarnings("unchecked")
  public static void openActionBarOverflowOrOptionsMenu(Context context) {
    if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.HONEYCOMB) {
      // regardless of the os level of the device, this app will be rendering a menukey
      // in the virtual navigation bar (if present) or responding to hardware option keys on
      // any activity.
      onView(isRoot())
          .perform(pressMenuKey());
    } else if (hasVirtualOverflowButton(context)) {
      // If we're using virtual keys - theres a chance we're in mid animation of switching
      // between a contextual action bar and the non-contextual action bar. In this case there
      // are 2 'More Options' buttons present. Lets wait till that is no longer the case.
      onView(isRoot())
          .perform(new TransitionBridgingViewAction());

      onView(OVERFLOW_BUTTON_MATCHER)
          .perform(click());
    } else {
      // either a hardware button exists, or we're on a pre-HC os.
      onView(isRoot())
          .perform(pressMenuKey());
    }
  }

  private static boolean hasVirtualOverflowButton(Context context) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
      return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
    } else {
      return !ViewConfiguration.get(context).hasPermanentMenuKey();
    }
  }

  /**
   * Handles the cases where the app is transitioning between a contextual action bar and a
   * non contextual action bar.
   */
  private static class TransitionBridgingViewAction implements ViewAction {
    @Override
    public void perform(UiController controller, View view) {
      int loops = 0;
      while (isTransitioningBetweenActionBars(view) && loops < 100) {
        loops++;
        controller.loopMainThreadForAtLeast(50);
      }
      // if we're not transitioning properly the next viewaction
      // will give a decent enough exception.
    }

    @Override
    public String getDescription() {
      return "Handle transition between action bar and action bar context.";
    }

    @Override
    public Matcher<View> getConstraints() {
      return isRoot();
    }

    private boolean isTransitioningBetweenActionBars(View view) {
      int actionButtonCount = 0;
      for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
        if (OVERFLOW_BUTTON_MATCHER.matches(child)) {
          actionButtonCount++;
        }
      }
      return actionButtonCount > 1;
    }
  }


}