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