Home | History | Annotate | Download | only in espresso
      1 /*
      2  * Copyright (C) 2015 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 
     17 package android.widget.espresso;
     18 
     19 import static android.support.test.espresso.action.ViewActions.actionWithAssertions;
     20 
     21 import android.graphics.Rect;
     22 import android.support.test.espresso.PerformException;
     23 import android.support.test.espresso.ViewAction;
     24 import android.support.test.espresso.action.CoordinatesProvider;
     25 import android.support.test.espresso.action.GeneralLocation;
     26 import android.support.test.espresso.action.Press;
     27 import android.support.test.espresso.action.Tap;
     28 import android.support.test.espresso.util.HumanReadables;
     29 import android.text.Layout;
     30 import android.view.MotionEvent;
     31 import android.view.View;
     32 import android.widget.Editor;
     33 import android.widget.Editor.HandleView;
     34 import android.widget.TextView;
     35 
     36 /**
     37  * A collection of actions on a {@link android.widget.TextView}.
     38  */
     39 public final class TextViewActions {
     40 
     41     private TextViewActions() {}
     42 
     43     /**
     44      * Returns an action that clicks on text at an index on the TextView.<br>
     45      * <br>
     46      * View constraints:
     47      * <ul>
     48      * <li>must be a TextView displayed on screen
     49      * <ul>
     50      *
     51      * @param index The index of the TextView's text to click on.
     52      */
     53     public static ViewAction clickOnTextAtIndex(int index) {
     54         return actionWithAssertions(
     55                 new ViewClickAction(Tap.SINGLE, new TextCoordinates(index), Press.FINGER));
     56     }
     57 
     58 
     59     /**
     60      * Returns an action that single-clicks by mouse on the View.<br>
     61      * <br>
     62      * View constraints:
     63      * <ul>
     64      * <li>must be a View displayed on screen
     65      * <ul>
     66      */
     67     public static ViewAction mouseClick() {
     68         return actionWithAssertions(new MouseClickAction(Tap.SINGLE, GeneralLocation.VISIBLE_CENTER,
     69                 MotionEvent.BUTTON_PRIMARY));
     70     }
     71 
     72     /**
     73      * Returns an action that clicks by mouse on text at an index on the TextView.<br>
     74      * <br>
     75      * View constraints:
     76      * <ul>
     77      * <li>must be a TextView displayed on screen
     78      * <ul>
     79      *
     80      * @param index The index of the TextView's text to click on.
     81      */
     82     public static ViewAction mouseClickOnTextAtIndex(int index) {
     83         return mouseClickOnTextAtIndex(index, MotionEvent.BUTTON_PRIMARY);
     84     }
     85 
     86     /**
     87      * Returns an action that clicks by mouse on text at an index on the TextView.<br>
     88      * <br>
     89      * View constraints:
     90      * <ul>
     91      * <li>must be a TextView displayed on screen
     92      * <ul>
     93      *
     94      * @param index The index of the TextView's text to click on.
     95      * @param button the mouse button to use.
     96      */
     97     public static ViewAction mouseClickOnTextAtIndex(int index,
     98             @MouseUiController.MouseButton int button) {
     99         return actionWithAssertions(
    100                 new MouseClickAction(Tap.SINGLE, new TextCoordinates(index), button));
    101     }
    102 
    103     /**
    104      * Returns an action that double-clicks on text at an index on the TextView.<br>
    105      * <br>
    106      * View constraints:
    107      * <ul>
    108      * <li>must be a TextView displayed on screen
    109      * <ul>
    110      *
    111      * @param index The index of the TextView's text to double-click on.
    112      */
    113     public static ViewAction doubleClickOnTextAtIndex(int index) {
    114         return actionWithAssertions(
    115                 new ViewClickAction(Tap.DOUBLE, new TextCoordinates(index), Press.FINGER));
    116     }
    117 
    118     /**
    119      * Returns an action that double-clicks by mouse on text at an index on the TextView.<br>
    120      * <br>
    121      * View constraints:
    122      * <ul>
    123      * <li>must be a TextView displayed on screen
    124      * <ul>
    125      *
    126      * @param index The index of the TextView's text to double-click on.
    127      */
    128     public static ViewAction mouseDoubleClickOnTextAtIndex(int index) {
    129         return actionWithAssertions(
    130                 new MouseClickAction(Tap.DOUBLE, new TextCoordinates(index)));
    131     }
    132 
    133     /**
    134      * Returns an action that long presses on text at an index on the TextView.<br>
    135      * <br>
    136      * View constraints:
    137      * <ul>
    138      * <li>must be a TextView displayed on screen
    139      * <ul>
    140      *
    141      * @param index The index of the TextView's text to long press on.
    142      */
    143     public static ViewAction longPressOnTextAtIndex(int index) {
    144         return actionWithAssertions(
    145                 new ViewClickAction(Tap.LONG, new TextCoordinates(index), Press.FINGER));
    146     }
    147 
    148     /**
    149      * Returns an action that long click by mouse on text at an index on the TextView.<br>
    150      * <br>
    151      * View constraints:
    152      * <ul>
    153      * <li>must be a TextView displayed on screen
    154      * <ul>
    155      *
    156      * @param index The index of the TextView's text to long click on.
    157      */
    158     public static ViewAction mouseLongClickOnTextAtIndex(int index) {
    159         return actionWithAssertions(
    160                 new MouseClickAction(Tap.LONG, new TextCoordinates(index)));
    161     }
    162 
    163     /**
    164      * Returns an action that triple-clicks by mouse on text at an index on the TextView.<br>
    165      * <br>
    166      * View constraints:
    167      * <ul>
    168      * <li>must be a TextView displayed on screen
    169      * <ul>
    170      *
    171      * @param index The index of the TextView's text to triple-click on.
    172      */
    173     public static ViewAction mouseTripleClickOnTextAtIndex(int index) {
    174         return actionWithAssertions(
    175                 new MouseClickAction(MouseClickAction.CLICK.TRIPLE, new TextCoordinates(index)));
    176     }
    177 
    178     /**
    179      * Returns an action that long presses then drags on text from startIndex to endIndex on the
    180      * TextView.<br>
    181      * <br>
    182      * View constraints:
    183      * <ul>
    184      * <li>must be a TextView displayed on screen
    185      * <ul>
    186      *
    187      * @param startIndex The index of the TextView's text to start a drag from
    188      * @param endIndex The index of the TextView's text to end the drag at
    189      */
    190     public static ViewAction longPressAndDragOnText(int startIndex, int endIndex) {
    191         return actionWithAssertions(
    192                 new DragAction(
    193                         DragAction.Drag.LONG_PRESS,
    194                         new TextCoordinates(startIndex),
    195                         new TextCoordinates(endIndex),
    196                         Press.FINGER,
    197                         TextView.class));
    198     }
    199 
    200     /**
    201      * Returns an action that double taps then drags on text from startIndex to endIndex on the
    202      * TextView.<br>
    203      * <br>
    204      * View constraints:
    205      * <ul>
    206      * <li>must be a TextView displayed on screen
    207      * <ul>
    208      *
    209      * @param startIndex The index of the TextView's text to start a drag from
    210      * @param endIndex The index of the TextView's text to end the drag at
    211      */
    212     public static ViewAction doubleTapAndDragOnText(int startIndex, int endIndex) {
    213         return actionWithAssertions(
    214                 new DragAction(
    215                         DragAction.Drag.DOUBLE_TAP,
    216                         new TextCoordinates(startIndex),
    217                         new TextCoordinates(endIndex),
    218                         Press.FINGER,
    219                         TextView.class));
    220     }
    221 
    222     /**
    223      * Returns an action that click then drags by mouse on text from startIndex to endIndex on the
    224      * TextView.<br>
    225      * <br>
    226      * View constraints:
    227      * <ul>
    228      * <li>must be a TextView displayed on screen
    229      * <ul>
    230      *
    231      * @param startIndex The index of the TextView's text to start a drag from
    232      * @param endIndex The index of the TextView's text to end the drag at
    233      */
    234     public static ViewAction mouseDragOnText(int startIndex, int endIndex) {
    235         return actionWithAssertions(
    236                 new DragAction(
    237                         DragAction.Drag.MOUSE_DOWN,
    238                         new TextCoordinates(startIndex),
    239                         new TextCoordinates(endIndex),
    240                         Press.PINPOINT,
    241                         TextView.class));
    242     }
    243 
    244     /**
    245      * Returns an action that double click then drags by mouse on text from startIndex to endIndex
    246      * on the TextView.<br>
    247      * <br>
    248      * View constraints:
    249      * <ul>
    250      * <li>must be a TextView displayed on screen
    251      * <ul>
    252      *
    253      * @param startIndex The index of the TextView's text to start a drag from
    254      * @param endIndex The index of the TextView's text to end the drag at
    255      */
    256     public static ViewAction mouseDoubleClickAndDragOnText(int startIndex, int endIndex) {
    257         return actionWithAssertions(
    258                 new DragAction(
    259                         DragAction.Drag.MOUSE_DOUBLE_CLICK,
    260                         new TextCoordinates(startIndex),
    261                         new TextCoordinates(endIndex),
    262                         Press.PINPOINT,
    263                         TextView.class));
    264     }
    265 
    266     /**
    267      * Returns an action that long click then drags by mouse on text from startIndex to endIndex
    268      * on the TextView.<br>
    269      * <br>
    270      * View constraints:
    271      * <ul>
    272      * <li>must be a TextView displayed on screen
    273      * <ul>
    274      *
    275      * @param startIndex The index of the TextView's text to start a drag from
    276      * @param endIndex The index of the TextView's text to end the drag at
    277      */
    278     public static ViewAction mouseLongClickAndDragOnText(int startIndex, int endIndex) {
    279         return actionWithAssertions(
    280                 new DragAction(
    281                         DragAction.Drag.MOUSE_LONG_CLICK,
    282                         new TextCoordinates(startIndex),
    283                         new TextCoordinates(endIndex),
    284                         Press.PINPOINT,
    285                         TextView.class));
    286     }
    287 
    288     /**
    289     * Returns an action that triple click then drags by mouse on text from startIndex to endIndex
    290     * on the TextView.<br>
    291     * <br>
    292     * View constraints:
    293     * <ul>
    294     * <li>must be a TextView displayed on screen
    295     * <ul>
    296     *
    297     * @param startIndex The index of the TextView's text to start a drag from
    298     * @param endIndex The index of the TextView's text to end the drag at
    299     */
    300    public static ViewAction mouseTripleClickAndDragOnText(int startIndex, int endIndex) {
    301        return actionWithAssertions(
    302                new DragAction(
    303                        DragAction.Drag.MOUSE_TRIPLE_CLICK,
    304                        new TextCoordinates(startIndex),
    305                        new TextCoordinates(endIndex),
    306                        Press.PINPOINT,
    307                        TextView.class));
    308    }
    309 
    310     public enum Handle {
    311         SELECTION_START,
    312         SELECTION_END,
    313         INSERTION
    314     };
    315 
    316     /**
    317      * Returns an action that tap then drags on the handle from the current position to endIndex on
    318      * the TextView.<br>
    319      * <br>
    320      * View constraints:
    321      * <ul>
    322      * <li>must be a TextView's drag-handle displayed on screen
    323      * <ul>
    324      *
    325      * @param textView TextView the handle is on
    326      * @param handleType Type of the handle
    327      * @param endIndex The index of the TextView's text to end the drag at
    328      */
    329     public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex) {
    330         return dragHandle(textView, handleType, endIndex, true);
    331     }
    332 
    333     /**
    334      * Returns an action that tap then drags on the handle from the current position to endIndex on
    335      * the TextView.<br>
    336      * <br>
    337      * View constraints:
    338      * <ul>
    339      * <li>must be a TextView's drag-handle displayed on screen
    340      * <ul>
    341      *
    342      * @param textView TextView the handle is on
    343      * @param handleType Type of the handle
    344      * @param endIndex The index of the TextView's text to end the drag at
    345      * @param primary whether to use primary direction to get coordinate form index when endIndex is
    346      * at a direction boundary.
    347      */
    348     public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex,
    349             boolean primary) {
    350         return actionWithAssertions(
    351                 new DragAction(
    352                         DragAction.Drag.TAP,
    353                         new CurrentHandleCoordinates(textView),
    354                         new HandleCoordinates(textView, handleType, endIndex, primary),
    355                         Press.FINGER,
    356                         Editor.HandleView.class));
    357     }
    358 
    359     /**
    360      * A provider of the x, y coordinates of the handle dragging point.
    361      */
    362     private static final class CurrentHandleCoordinates implements CoordinatesProvider {
    363         // Must be larger than Editor#LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS.
    364         private final TextView mTextView;
    365         private final String mActionDescription;
    366 
    367 
    368         public CurrentHandleCoordinates(TextView textView) {
    369             mTextView = textView;
    370             mActionDescription = "Could not locate handle.";
    371         }
    372 
    373         @Override
    374         public float[] calculateCoordinates(View view) {
    375             try {
    376                 return locateHandle(view);
    377             } catch (StringIndexOutOfBoundsException e) {
    378                 throw new PerformException.Builder()
    379                         .withActionDescription(mActionDescription)
    380                         .withViewDescription(HumanReadables.describe(view))
    381                         .withCause(e)
    382                         .build();
    383             }
    384         }
    385 
    386         private float[] locateHandle(View view) {
    387             final Rect bounds = new Rect();
    388             view.getBoundsOnScreen(bounds);
    389             final Rect visibleDisplayBounds = new Rect();
    390             mTextView.getWindowVisibleDisplayFrame(visibleDisplayBounds);
    391             visibleDisplayBounds.right -= 1;
    392             visibleDisplayBounds.bottom -= 1;
    393             if (!visibleDisplayBounds.intersect(bounds)) {
    394                 throw new PerformException.Builder()
    395                         .withActionDescription(mActionDescription
    396                                 + " The handle is entirely out of the visible display frame of"
    397                                 + "the TextView's window.")
    398                         .withViewDescription(HumanReadables.describe(view))
    399                         .build();
    400             }
    401             final float dragPointX = Math.max(Math.min(bounds.centerX(),
    402                     visibleDisplayBounds.right), visibleDisplayBounds.left);
    403             final float verticalOffset = bounds.height() * 0.7f;
    404             final float dragPointY = Math.max(Math.min(bounds.top + verticalOffset,
    405                     visibleDisplayBounds.bottom), visibleDisplayBounds.top);
    406             return new float[] {dragPointX, dragPointY};
    407         }
    408     }
    409 
    410     /**
    411      * A provider of the x, y coordinates of the handle that points the specified text index in a
    412      * text view.
    413      */
    414     private static final class HandleCoordinates implements CoordinatesProvider {
    415         // Must be larger than Editor#LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS.
    416         private final static float LINE_SLOP_MULTIPLIER = 0.6f;
    417         private final TextView mTextView;
    418         private final Handle mHandleType;
    419         private final int mIndex;
    420         private final boolean mPrimary;
    421         private final String mActionDescription;
    422 
    423         public HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary) {
    424             mTextView = textView;
    425             mHandleType = handleType;
    426             mIndex = index;
    427             mPrimary = primary;
    428             mActionDescription = "Could not locate " + handleType.toString()
    429                     + " handle that points text index: " + index
    430                     + " (" + (primary ? "primary" : "secondary" ) + ")";
    431         }
    432 
    433         @Override
    434         public float[] calculateCoordinates(View view) {
    435             try {
    436                 return locateHandlePointsTextIndex(view);
    437             } catch (StringIndexOutOfBoundsException e) {
    438                 throw new PerformException.Builder()
    439                         .withActionDescription(mActionDescription)
    440                         .withViewDescription(HumanReadables.describe(view))
    441                         .withCause(e)
    442                         .build();
    443             }
    444         }
    445 
    446         private float[] locateHandlePointsTextIndex(View view) {
    447             if (!(view instanceof HandleView)) {
    448                 throw new PerformException.Builder()
    449                         .withActionDescription(mActionDescription + " The view is not a HandleView")
    450                         .withViewDescription(HumanReadables.describe(view))
    451                         .build();
    452             }
    453             final HandleView handleView = (HandleView) view;
    454             final int currentOffset = mHandleType == Handle.SELECTION_START ?
    455                     mTextView.getSelectionStart() : mTextView.getSelectionEnd();
    456 
    457             final Layout layout = mTextView.getLayout();
    458 
    459             final int currentLine = layout.getLineForOffset(currentOffset);
    460             final int targetLine = layout.getLineForOffset(mIndex);
    461             final float currentX = handleView.getHorizontal(layout, currentOffset);
    462             final float currentY = layout.getLineTop(currentLine);
    463             final float[] currentCoordinates =
    464                     TextCoordinates.convertToScreenCoordinates(mTextView, currentX, currentY);
    465             final float[] targetCoordinates =
    466                     (new TextCoordinates(mIndex, mPrimary)).calculateCoordinates(mTextView);
    467             final Rect bounds = new Rect();
    468             view.getBoundsOnScreen(bounds);
    469             final Rect visibleDisplayBounds = new Rect();
    470             mTextView.getWindowVisibleDisplayFrame(visibleDisplayBounds);
    471             visibleDisplayBounds.right -= 1;
    472             visibleDisplayBounds.bottom -= 1;
    473             if (!visibleDisplayBounds.intersect(bounds)) {
    474                 throw new PerformException.Builder()
    475                         .withActionDescription(mActionDescription
    476                                 + " The handle is entirely out of the visible display frame of"
    477                                 + "the TextView's window.")
    478                         .withViewDescription(HumanReadables.describe(view))
    479                         .build();
    480             }
    481             final float dragPointX = Math.max(Math.min(bounds.centerX(),
    482                     visibleDisplayBounds.right), visibleDisplayBounds.left);
    483             final float diffX = dragPointX - currentCoordinates[0];
    484             final float verticalOffset = bounds.height() * 0.7f;
    485             final float dragPointY = Math.max(Math.min(bounds.top + verticalOffset,
    486                     visibleDisplayBounds.bottom), visibleDisplayBounds.top);
    487             float diffY = dragPointY - currentCoordinates[1];
    488             if (currentLine > targetLine) {
    489                 diffY -= mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER;
    490             } else if (currentLine < targetLine) {
    491                 diffY += mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER;
    492             }
    493             return new float[] {targetCoordinates[0] + diffX, targetCoordinates[1] + diffY};
    494         }
    495     }
    496 
    497     /**
    498      * A provider of the x, y coordinates of the text at the specified index in a text view.
    499      */
    500     private static final class TextCoordinates implements CoordinatesProvider {
    501 
    502         private final int mIndex;
    503         private final boolean mPrimary;
    504         private final String mActionDescription;
    505 
    506         public TextCoordinates(int index) {
    507             this(index, true);
    508         }
    509 
    510         public TextCoordinates(int index, boolean primary) {
    511             mIndex = index;
    512             mPrimary = primary;
    513             mActionDescription = "Could not locate text at index: " + mIndex
    514                     + " (" + (primary ? "primary" : "secondary" ) + ")";
    515         }
    516 
    517         @Override
    518         public float[] calculateCoordinates(View view) {
    519             try {
    520                 return locateTextAtIndex((TextView) view, mIndex, mPrimary);
    521             } catch (ClassCastException e) {
    522                 throw new PerformException.Builder()
    523                         .withActionDescription(mActionDescription)
    524                         .withViewDescription(HumanReadables.describe(view))
    525                         .withCause(e)
    526                         .build();
    527             } catch (StringIndexOutOfBoundsException e) {
    528                 throw new PerformException.Builder()
    529                         .withActionDescription(mActionDescription)
    530                         .withViewDescription(HumanReadables.describe(view))
    531                         .withCause(e)
    532                         .build();
    533             }
    534         }
    535 
    536         /**
    537          * @throws StringIndexOutOfBoundsException
    538          */
    539         private float[] locateTextAtIndex(TextView textView, int index, boolean primary) {
    540             if (index < 0 || index > textView.getText().length()) {
    541                 throw new StringIndexOutOfBoundsException(index);
    542             }
    543             final Layout layout = textView.getLayout();
    544             final int line = layout.getLineForOffset(index);
    545             return convertToScreenCoordinates(textView,
    546                     (primary ? layout.getPrimaryHorizontal(index)
    547                             : layout.getSecondaryHorizontal(index)),
    548                     layout.getLineTop(line));
    549         }
    550 
    551         /**
    552          * Convert TextView's local coordinates to on screen coordinates.
    553          * @param textView the TextView
    554          * @param x local horizontal coordinate
    555          * @param y local vertical coordinate
    556          * @return
    557          */
    558         public static float[] convertToScreenCoordinates(TextView textView, float x, float y) {
    559             final int[] xy = new int[2];
    560             textView.getLocationOnScreen(xy);
    561             return new float[]{ x + textView.getTotalPaddingLeft() - textView.getScrollX() + xy[0],
    562                     y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] };
    563         }
    564     }
    565 }
    566