blob: 9db5d2fed92be3ec6a6af37181185f0012820412 [file] [log] [blame]
/*
* 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 android.support.test.espresso;
import static android.support.test.espresso.action.ViewActions.click;
import static android.support.test.espresso.action.ViewActions.pressMenuKey;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
import static android.support.test.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 android.support.test.espresso.action.ViewActions;
import android.support.test.espresso.base.IdlingResourceRegistry;
import android.support.test.espresso.util.TreeIterables;
import com.google.common.collect.ImmutableList;
import android.content.Context;
import android.os.Build;
import android.os.Looper;
import android.view.View;
import android.view.ViewConfiguration;
import org.hamcrest.Matcher;
import java.util.List;
/**
* 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 {
private static final BaseLayerComponent BASE = GraphHolder.baseLayer();
private static final IdlingResourceRegistry REGISTRY = BASE.idlingResourceRegistry();
private Espresso() {}
/**
* Creates a {@link ViewInteraction} 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(org.hamcrest.Matcher)
*/
// TODO change parameter to type to Matcher<? extends View> which currently causes Dagger issues
public static ViewInteraction onView(final Matcher<View> viewMatcher) {
return BASE.plus(new ViewInteractionModule(viewMatcher)).viewInteraction();
}
/**
* 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<? extends 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.
*
* <p>This method allows the caller to consider Thread.State.WAIT to be 'idle'.
*
* <p>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) {
REGISTRY.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. If any of the given resources is already registered, a warning is logged.
*
* @return {@code true} if all resources were successfully registered
*/
public static boolean registerIdlingResources(IdlingResource... resources) {
return REGISTRY.registerResources(ImmutableList.copyOf(checkNotNull(resources)));
}
/**
* Unregisters one or more {@link IdlingResource}s. If any of the given resources are not already
* registered, a warning is logged.
*
* @return {@code true} if all resources were successfully unregistered
*/
public static boolean unregisterIdlingResources(IdlingResource... resources) {
return REGISTRY.unregisterResources(ImmutableList.copyOf(checkNotNull(resources)));
}
/**
* Returns a list of all currently registered {@link IdlingResource}s.
*/
public static List<IdlingResource> getIdlingResources() {
return REGISTRY.getResources();
}
/**
* Changes the default {@link FailureHandler} to the given one.
*/
public static void setFailureHandler(FailureHandler failureHandler) {
BASE.failureHolder().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.
*
* <p>This works with both native and SherlockActionBar action modes.
*
* <p>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.
*
* <p>This works with both native and SherlockActionBar ActionBars.
*
* <p>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;
}
}
}