| /* |
| * 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.action; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| import static org.hamcrest.Matchers.any; |
| import static org.hamcrest.Matchers.is; |
| |
| import android.support.test.espresso.UiController; |
| import android.support.test.espresso.ViewAction; |
| import android.support.test.espresso.ViewAssertion; |
| |
| import android.net.Uri; |
| import android.util.Log; |
| import android.util.Pair; |
| import android.view.KeyEvent; |
| import android.view.View; |
| |
| import org.hamcrest.Matcher; |
| |
| import java.util.Set; |
| import java.util.concurrent.CopyOnWriteArraySet; |
| import javax.annotation.Nonnull; |
| |
| /** |
| * A collection of common {@link ViewActions}. |
| */ |
| public final class ViewActions { |
| |
| private ViewActions() {} |
| |
| /** |
| * The distance of a swipe's start position from the view's edge, in terms of the view's length. |
| * We do not start the swipe exactly on the view's edge, but somewhat more inward, since swiping |
| * from the exact edge may behave in an unexpected way (e.g. may open a navigation drawer). |
| */ |
| private static final float EDGE_FUZZ_FACTOR = 0.083f; |
| |
| /** |
| * A set of {@code ViewAssertion}s to be executed before the ViewActions in this class. |
| */ |
| private static Set<Pair<String, ViewAssertion>> globalAssertions = |
| new CopyOnWriteArraySet<Pair<String, ViewAssertion>>(); |
| |
| /** |
| * Adds a {@code ViewAssertion} to be run every time a {@code ViewAction} in this class is |
| * performed. The assertion will be run prior to performing the action. |
| * |
| * @param name a name of the assertion to be added |
| * @param viewAssertion a {@code ViewAssertion} to be added |
| * @throws IllegalArgumentException if the name/viewAssertion pair is already contained in the |
| * global assertions. |
| */ |
| public static void addGlobalAssertion(String name, ViewAssertion viewAssertion) { |
| checkNotNull(name); |
| checkNotNull(viewAssertion); |
| Pair<String, ViewAssertion> vaPair = new Pair<String, ViewAssertion>(name, viewAssertion); |
| checkArgument(!globalAssertions.contains(vaPair), |
| "ViewAssertion with name %s is already in the global assertions!", name); |
| globalAssertions.add(vaPair); |
| } |
| |
| /** |
| * Removes the given assertion from the set of assertions to be run before actions are performed. |
| * |
| * @param viewAssertion the assertion to remove |
| * @throws IllegalArgumentException if the name/viewAssertion pair is not already contained in the |
| * global assertions. |
| */ |
| public static void removeGlobalAssertion(ViewAssertion viewAssertion) { |
| boolean removed = false; |
| for (Pair<String, ViewAssertion> vaPair : globalAssertions) { |
| if (viewAssertion != null && viewAssertion.equals(vaPair.second)) { |
| removed = removed || globalAssertions.remove(vaPair); |
| } |
| } |
| checkArgument(removed, "ViewAssertion was not in global assertions!"); |
| } |
| |
| public static void clearGlobalAssertions() { |
| globalAssertions.clear(); |
| } |
| |
| /** |
| * Performs all assertions before the {@code ViewAction}s in this class and then performs the |
| * given {@code ViewAction} |
| * |
| * @param viewAction the {@code ViewAction} to perform after the assertions |
| */ |
| public static ViewAction actionWithAssertions(final ViewAction viewAction) { |
| if (globalAssertions.isEmpty()) { |
| return viewAction; |
| } |
| return new ViewAction() { |
| @Override |
| public String getDescription() { |
| StringBuilder msg = new StringBuilder("Running view assertions["); |
| for (Pair<String, ViewAssertion> vaPair : globalAssertions) { |
| msg.append(vaPair.first); |
| msg.append(", "); |
| } |
| msg.append("] and then running: "); |
| msg.append(viewAction.getDescription()); |
| return msg.toString(); |
| } |
| |
| @Override |
| public Matcher<View> getConstraints() { |
| return viewAction.getConstraints(); |
| } |
| |
| @Override |
| public void perform(UiController uic, View view) { |
| for (Pair<String, ViewAssertion> vaPair : globalAssertions) { |
| Log.i("ViewAssertion", "Asserting " + vaPair.first); |
| vaPair.second.check(view, null); |
| } |
| viewAction.perform(uic, view); |
| } |
| }; |
| } |
| |
| /** |
| * Returns an action that clears text on the view.<br> |
| * <br> |
| * View constraints: |
| * <ul> |
| * <li>must be displayed on screen |
| * <ul> |
| */ |
| public static ViewAction clearText() { |
| return actionWithAssertions(new ReplaceTextAction("")); |
| } |
| |
| /** |
| * Returns an action that clicks the view.<br> |
| * <br> |
| * View constraints: |
| * <ul> |
| * <li>must be displayed on screen |
| * <ul> |
| */ |
| public static ViewAction click() { |
| return actionWithAssertions( |
| new GeneralClickAction(Tap.SINGLE, GeneralLocation.VISIBLE_CENTER, Press.FINGER)); |
| } |
| |
| /** |
| * Returns an action that performs a single click on the view. |
| * |
| * If the click takes longer than the 'long press' duration (which is possible) the provided |
| * rollback action is invoked on the view and a click is attempted again. |
| * |
| * This is only necessary if the view being clicked on has some different behaviour for long press |
| * versus a normal tap. |
| * |
| * For example - if a long press on a particular view element opens a popup menu - |
| * ViewActions.pressBack() may be an acceptable rollback action. |
| * |
| * <br> |
| * View constraints: |
| * <ul> |
| * <li>must be displayed on screen</li> |
| * <li>any constraints of the rollbackAction</li> |
| * <ul> |
| */ |
| public static ViewAction click(ViewAction rollbackAction) { |
| checkNotNull(rollbackAction); |
| return actionWithAssertions( |
| new GeneralClickAction(Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER, rollbackAction)); |
| } |
| |
| /** |
| * Returns an action that performs a swipe right-to-left across the vertical center of the |
| * view. The swipe doesn't start at the very edge of the view, but is a bit offset.<br> |
| * <br> |
| * View constraints: |
| * <ul> |
| * <li>must be displayed on screen |
| * <ul> |
| */ |
| public static ViewAction swipeLeft() { |
| return actionWithAssertions(new GeneralSwipeAction(Swipe.FAST, |
| GeneralLocation.translate(GeneralLocation.CENTER_RIGHT, -EDGE_FUZZ_FACTOR, 0), |
| GeneralLocation.CENTER_LEFT, Press.FINGER)); |
| } |
| |
| /** |
| * Returns an action that performs a swipe left-to-right across the vertical center of the |
| * view. The swipe doesn't start at the very edge of the view, but is a bit offset.<br> |
| * <br> |
| * View constraints: |
| * <ul> |
| * <li>must be displayed on screen |
| * <ul> |
| */ |
| public static ViewAction swipeRight() { |
| return actionWithAssertions(new GeneralSwipeAction(Swipe.FAST, |
| GeneralLocation.translate(GeneralLocation.CENTER_LEFT, EDGE_FUZZ_FACTOR, 0), |
| GeneralLocation.CENTER_RIGHT, Press.FINGER)); |
| } |
| |
| /** |
| * Returns an action that performs a swipe top-to-bottom across the horizontal center of the view. |
| * The swipe doesn't start at the very edge of the view, but has a bit of offset.<br> |
| * <br> |
| * View constraints: |
| * <ul> |
| * <li>must be displayed on screen |
| * <ul> |
| */ |
| public static ViewAction swipeDown() { |
| return actionWithAssertions(new GeneralSwipeAction(Swipe.FAST, |
| GeneralLocation.translate(GeneralLocation.TOP_CENTER, 0, EDGE_FUZZ_FACTOR), |
| GeneralLocation.BOTTOM_CENTER, Press.FINGER)); |
| } |
| |
| /** |
| * Returns an action that performs a swipe bottom-to-top across the horizontal center of the view. |
| * The swipe doesn't start at the very edge of the view, but has a bit of offset.<br> |
| * <br> |
| * View constraints: |
| * <ul> |
| * <li>must be displayed on screen |
| * <ul> |
| */ |
| public static ViewAction swipeUp() { |
| return actionWithAssertions(new GeneralSwipeAction(Swipe.FAST, |
| GeneralLocation.translate(GeneralLocation.BOTTOM_CENTER, 0, -EDGE_FUZZ_FACTOR), |
| GeneralLocation.TOP_CENTER, Press.FINGER)); |
| } |
| |
| /** |
| * Returns an action that closes soft keyboard. If the keyboard is already closed, it is a no-op. |
| */ |
| public static ViewAction closeSoftKeyboard() { |
| return actionWithAssertions(new CloseKeyboardAction()); |
| } |
| |
| /** |
| * Returns an action that presses the current action button (next, done, search, etc) on the IME |
| * (Input Method Editor). The selected view will have its onEditorAction method called. |
| */ |
| public static ViewAction pressImeActionButton() { |
| return actionWithAssertions(new EditorAction()); |
| } |
| |
| /** |
| * Returns an action that clicks the back button. |
| */ |
| public static ViewAction pressBack() { |
| return pressKey(KeyEvent.KEYCODE_BACK); |
| } |
| |
| /** |
| * Returns an action that presses the hardware menu key. |
| */ |
| public static ViewAction pressMenuKey() { |
| return pressKey(KeyEvent.KEYCODE_MENU); |
| } |
| |
| /** |
| * Returns an action that presses the key specified by the keyCode (eg. Keyevent.KEYCODE_BACK). |
| */ |
| public static ViewAction pressKey(int keyCode) { |
| return actionWithAssertions( |
| new KeyEventAction(new EspressoKey.Builder().withKeyCode(keyCode).build())); |
| } |
| |
| /** |
| * Returns an action that presses the specified key with the specified modifiers. |
| */ |
| public static ViewAction pressKey(EspressoKey key) { |
| return actionWithAssertions(new KeyEventAction(key)); |
| } |
| |
| /** |
| * Returns an action that double clicks the view.<br> |
| * <br> |
| * View preconditions: |
| * <ul> |
| * <li>must be displayed on screen |
| * <ul> |
| */ |
| public static ViewAction doubleClick() { |
| return actionWithAssertions( |
| new GeneralClickAction(Tap.DOUBLE, GeneralLocation.CENTER, Press.FINGER)); |
| } |
| |
| /** |
| * Returns an action that long clicks the view.<br> |
| * |
| * <br> |
| * View preconditions: |
| * <ul> |
| * <li>must be displayed on screen |
| * <ul> |
| */ |
| public static ViewAction longClick() { |
| return actionWithAssertions( |
| new GeneralClickAction(Tap.LONG, GeneralLocation.CENTER, Press.FINGER)); |
| } |
| |
| /** |
| * Returns an action that scrolls to the view.<br> |
| * <br> |
| * View preconditions: |
| * <ul> |
| * <li>must be a descendant of ScrollView |
| * <li>must have visibility set to View.VISIBLE |
| * <ul> |
| */ |
| public static ViewAction scrollTo() { |
| return actionWithAssertions(new ScrollToAction()); |
| } |
| |
| /** |
| * Returns an action that types the provided string into the view. |
| * Appending a \n to the end of the string translates to a ENTER key event. Note: this method |
| * does not change cursor position in the focused view - text is inserted at the location where |
| * the cursor is currently pointed.<br> |
| * <br> |
| * View preconditions: |
| * <ul> |
| * <li>must be displayed on screen |
| * <li>must support input methods |
| * <li>must be already focused |
| * <ul> |
| */ |
| public static ViewAction typeTextIntoFocusedView(String stringToBeTyped) { |
| return actionWithAssertions(new TypeTextAction(stringToBeTyped, false /* tapToFocus */)); |
| } |
| |
| /** |
| * Returns an action that selects the view (by clicking on it) and types the provided string into |
| * the view. Appending a \n to the end of the string translates to a ENTER key event. Note: this |
| * method performs a tap on the view before typing to force the view into focus, if the view |
| * already contains text this tap may place the cursor at an arbitrary position within the text. |
| * <br> |
| * <br> |
| * View preconditions: |
| * <ul> |
| * <li>must be displayed on screen |
| * <li>must support input methods |
| * <ul> |
| */ |
| public static ViewAction typeText(String stringToBeTyped) { |
| return actionWithAssertions(new TypeTextAction(stringToBeTyped)); |
| } |
| |
| /** |
| * Returns an action that updates the text attribute of a view. |
| * <br> |
| * <br> |
| * View preconditions: |
| * <ul> |
| * <li>must be displayed on screen |
| * <li>must be assignable from EditText |
| * <ul> |
| */ |
| public static ViewAction replaceText(@Nonnull String stringToBeSet) { |
| return actionWithAssertions(new ReplaceTextAction(stringToBeSet)); |
| } |
| |
| /** |
| * Same as {@code openLinkWithText(Matcher<String> linkTextMatcher)}, but uses |
| * {@code is(linkText)} as the linkTextMatcher. |
| */ |
| public static ViewAction openLinkWithText(String linkText) { |
| return openLinkWithText(is(linkText)); |
| } |
| |
| /** |
| * Same as {@code openLink(Matcher<String> linkTextMatcher, Matcher<Uri> uriMatcher)}, but uses |
| * {@code any(Uri.class)} as the uriMatcher. |
| */ |
| public static ViewAction openLinkWithText(Matcher<String> linkTextMatcher) { |
| return openLink(linkTextMatcher, any(Uri.class)); |
| } |
| |
| /** |
| * Same as {@code openLinkWithUri(Matcher<Uri> uriMatcher)}, but uses {@code is(uri)} as the |
| * uriMatcher. |
| */ |
| public static ViewAction openLinkWithUri(String uri) { |
| return openLinkWithUri(is(Uri.parse(uri))); |
| } |
| |
| /** |
| * Same as {@code openLink(Matcher<String> linkTextMatcher, Matcher<Uri> uriMatcher)}, but uses |
| * {@code any(String.class)} as the linkTextMatcher. |
| */ |
| public static ViewAction openLinkWithUri(Matcher<Uri> uriMatcher) { |
| return openLink(any(String.class), uriMatcher); |
| } |
| |
| /** |
| * Returns an action that opens a link matching the given link text and uri matchers. The action |
| * is performed by invoking the link's onClick method (as opposed to actually issuing a click on |
| * the screen). |
| * <br> |
| * <br> |
| * View preconditions: |
| * <ul> |
| * <li>must be displayed on screen |
| * <li>must be assignable from TextView |
| * <li>must have links |
| * <ul> |
| */ |
| public static ViewAction openLink(Matcher<String> linkTextMatcher, Matcher<Uri> uriMatcher) { |
| checkNotNull(linkTextMatcher); |
| checkNotNull(uriMatcher); |
| return actionWithAssertions(new OpenLinkAction(linkTextMatcher, uriMatcher)); |
| } |
| |
| /** |
| * Returns an action that performs given {@code ViewAction} on the view until view matches the |
| * desired {@code Matcher<View>}. It will repeat the given action until view matches the desired |
| * {@code Matcher<View>} or PerformException will be thrown if given number of unsuccessful |
| * attempts are made. |
| * |
| * @param action action to be performed repeatedly |
| * @param desiredStateMatcher action is performed repeatedly until view matches this view matcher |
| * @param maxAttempts max number of times for which this action to be performed if view doesn't |
| * match the given view matcher |
| */ |
| public static ViewAction repeatedlyUntil(final ViewAction action, |
| final Matcher<View> desiredStateMatcher, |
| final int maxAttempts) { |
| checkNotNull(action); |
| checkNotNull(desiredStateMatcher); |
| return actionWithAssertions( |
| new RepeatActionUntilViewState(action, desiredStateMatcher, maxAttempts)); |
| } |
| } |