• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
TextViewActions()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      */
clickOnTextAtIndex(int index)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      */
mouseClickOnTextAtIndex(int index)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      */
mouseClickOnTextAtIndex(int index, @MouseUiController.MouseButton int button)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      */
doubleClickOnTextAtIndex(int index)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      */
mouseDoubleClickOnTextAtIndex(int index)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      */
longPressOnTextAtIndex(int index)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      */
mouseLongClickOnTextAtIndex(int index)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      */
mouseTripleClickOnTextAtIndex(int index)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      */
longPressAndDragOnText(int startIndex, int endIndex)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      */
doubleTapAndDragOnText(int startIndex, int endIndex)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      */
mouseDragOnText(int startIndex, int endIndex)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      */
mouseDoubleClickAndDragOnText(int startIndex, int endIndex)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      */
mouseLongClickAndDragOnText(int startIndex, int endIndex)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     */
mouseTripleClickAndDragOnText(int startIndex, int endIndex)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      */
dragHandle(TextView textView, Handle handleType, int endIndex)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      */
dragHandle(TextView textView, Handle handleType, int endIndex, boolean primary)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 
CurrentHandleCoordinates(TextView textView)352         public CurrentHandleCoordinates(TextView textView) {
353             mTextView = textView;
354             mActionDescription = "Could not locate handle.";
355         }
356 
357         @Override
calculateCoordinates(View view)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 
locateHandle(View view)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 
HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary)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
calculateCoordinates(View view)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 
locateHandlePointsTextIndex(View view)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 
TextCoordinates(int index)490         public TextCoordinates(int index) {
491             this(index, true);
492         }
493 
TextCoordinates(int index, boolean primary)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
calculateCoordinates(View view)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          */
locateTextAtIndex(TextView textView, int index, boolean primary)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          */
convertToScreenCoordinates(TextView textView, float x, float y)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