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