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 
TextViewActions()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      */
clickOnTextAtIndex(int index)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      */
mouseClick()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      */
mouseClickOnTextAtIndex(int index)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      */
mouseClickOnTextAtIndex(int index, @MouseUiController.MouseButton int button)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      */
doubleClickOnTextAtIndex(int index)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      */
mouseDoubleClickOnTextAtIndex(int index)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      */
longPressOnTextAtIndex(int index)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      */
mouseLongClickOnTextAtIndex(int index)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      */
mouseTripleClickOnTextAtIndex(int index)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      */
longPressAndDragOnText(int startIndex, int endIndex)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      */
doubleTapAndDragOnText(int startIndex, int endIndex)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      */
mouseDragOnText(int startIndex, int endIndex)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      */
mouseDoubleClickAndDragOnText(int startIndex, int endIndex)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      */
mouseLongClickAndDragOnText(int startIndex, int endIndex)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     */
mouseTripleClickAndDragOnText(int startIndex, int endIndex)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      */
dragHandle(TextView textView, Handle handleType, int endIndex)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      */
dragHandle(TextView textView, Handle handleType, int endIndex, boolean primary)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 
CurrentHandleCoordinates(TextView textView)368         public CurrentHandleCoordinates(TextView textView) {
369             mTextView = textView;
370             mActionDescription = "Could not locate handle.";
371         }
372 
373         @Override
calculateCoordinates(View view)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 
locateHandle(View view)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 
HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary)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
calculateCoordinates(View view)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 
locateHandlePointsTextIndex(View view)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 
TextCoordinates(int index)506         public TextCoordinates(int index) {
507             this(index, true);
508         }
509 
TextCoordinates(int index, boolean primary)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
calculateCoordinates(View view)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          */
locateTextAtIndex(TextView textView, int index, boolean primary)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          */
convertToScreenCoordinates(TextView textView, float x, float y)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