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