TextViewActions.java revision 46faad60230ade76b6a4944a2b9fae274698ab91
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
17package android.widget.espresso;
18
19import static android.support.test.espresso.action.ViewActions.actionWithAssertions;
20import android.graphics.Rect;
21import android.support.test.espresso.PerformException;
22import android.support.test.espresso.ViewAction;
23import android.support.test.espresso.action.CoordinatesProvider;
24import android.support.test.espresso.action.GeneralClickAction;
25import android.support.test.espresso.action.Press;
26import android.support.test.espresso.action.Tap;
27import android.support.test.espresso.util.HumanReadables;
28import android.text.Layout;
29import android.view.View;
30import android.widget.Editor;
31import android.widget.TextView;
32
33/**
34 * A collection of actions on a {@link android.widget.TextView}.
35 */
36public final class TextViewActions {
37
38    private TextViewActions() {}
39
40    /**
41     * Returns an action that clicks on text at an index on the TextView.<br>
42     * <br>
43     * View constraints:
44     * <ul>
45     * <li>must be a TextView displayed on screen
46     * <ul>
47     *
48     * @param index The index of the TextView's text to click on.
49     */
50    public static ViewAction clickOnTextAtIndex(int index) {
51        return actionWithAssertions(
52                new GeneralClickAction(Tap.SINGLE, new TextCoordinates(index), Press.FINGER));
53    }
54
55    /**
56     * Returns an action that clicks by mouse on text at an index on the TextView.<br>
57     * <br>
58     * View constraints:
59     * <ul>
60     * <li>must be a TextView displayed on screen
61     * <ul>
62     *
63     * @param index The index of the TextView's text to click on.
64     */
65    public static ViewAction mouseClickOnTextAtIndex(int index) {
66        return actionWithAssertions(
67                new MouseClickAction(Tap.SINGLE, new TextCoordinates(index)));
68    }
69
70    /**
71     * Returns an action that double-clicks 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 double-click on.
79     */
80    public static ViewAction doubleClickOnTextAtIndex(int index) {
81        return actionWithAssertions(
82                new GeneralClickAction(Tap.DOUBLE, new TextCoordinates(index), Press.FINGER));
83    }
84
85    /**
86     * Returns an action that double-clicks by mouse on text at an index on the TextView.<br>
87     * <br>
88     * View constraints:
89     * <ul>
90     * <li>must be a TextView displayed on screen
91     * <ul>
92     *
93     * @param index The index of the TextView's text to double-click on.
94     */
95    public static ViewAction mouseDoubleClickOnTextAtIndex(int index) {
96        return actionWithAssertions(
97                new MouseClickAction(Tap.DOUBLE, new TextCoordinates(index)));
98    }
99
100    /**
101     * Returns an action that long presses on text at an index on the TextView.<br>
102     * <br>
103     * View constraints:
104     * <ul>
105     * <li>must be a TextView displayed on screen
106     * <ul>
107     *
108     * @param index The index of the TextView's text to long press on.
109     */
110    public static ViewAction longPressOnTextAtIndex(int index) {
111        return actionWithAssertions(
112                new GeneralClickAction(Tap.LONG, new TextCoordinates(index), Press.FINGER));
113    }
114
115    /**
116     * Returns an action that long click by mouse on text at an index on the TextView.<br>
117     * <br>
118     * View constraints:
119     * <ul>
120     * <li>must be a TextView displayed on screen
121     * <ul>
122     *
123     * @param index The index of the TextView's text to long click on.
124     */
125    public static ViewAction mouseLongClickOnTextAtIndex(int index) {
126        return actionWithAssertions(
127                new MouseClickAction(Tap.LONG, new TextCoordinates(index)));
128    }
129
130    /**
131     * Returns an action that triple-clicks by mouse on text at an index on the TextView.<br>
132     * <br>
133     * View constraints:
134     * <ul>
135     * <li>must be a TextView displayed on screen
136     * <ul>
137     *
138     * @param index The index of the TextView's text to triple-click on.
139     */
140    public static ViewAction mouseTripleClickOnTextAtIndex(int index) {
141        return actionWithAssertions(
142                new MouseClickAction(MouseClickAction.CLICK.TRIPLE, new TextCoordinates(index)));
143    }
144
145    /**
146     * Returns an action that long presses then drags on text from startIndex to endIndex on the
147     * TextView.<br>
148     * <br>
149     * View constraints:
150     * <ul>
151     * <li>must be a TextView displayed on screen
152     * <ul>
153     *
154     * @param startIndex The index of the TextView's text to start a drag from
155     * @param endIndex The index of the TextView's text to end the drag at
156     */
157    public static ViewAction longPressAndDragOnText(int startIndex, int endIndex) {
158        return actionWithAssertions(
159                new DragAction(
160                        DragAction.Drag.LONG_PRESS,
161                        new TextCoordinates(startIndex),
162                        new TextCoordinates(endIndex),
163                        Press.FINGER,
164                        TextView.class));
165    }
166
167    /**
168     * Returns an action that double taps then drags on text from startIndex to endIndex on the
169     * TextView.<br>
170     * <br>
171     * View constraints:
172     * <ul>
173     * <li>must be a TextView displayed on screen
174     * <ul>
175     *
176     * @param startIndex The index of the TextView's text to start a drag from
177     * @param endIndex The index of the TextView's text to end the drag at
178     */
179    public static ViewAction doubleTapAndDragOnText(int startIndex, int endIndex) {
180        return actionWithAssertions(
181                new DragAction(
182                        DragAction.Drag.DOUBLE_TAP,
183                        new TextCoordinates(startIndex),
184                        new TextCoordinates(endIndex),
185                        Press.FINGER,
186                        TextView.class));
187    }
188
189    /**
190     * Returns an action that click then drags by mouse on text from startIndex to endIndex on the
191     * TextView.<br>
192     * <br>
193     * View constraints:
194     * <ul>
195     * <li>must be a TextView displayed on screen
196     * <ul>
197     *
198     * @param startIndex The index of the TextView's text to start a drag from
199     * @param endIndex The index of the TextView's text to end the drag at
200     */
201    public static ViewAction mouseDragOnText(int startIndex, int endIndex) {
202        return actionWithAssertions(
203                new DragAction(
204                        DragAction.Drag.MOUSE_DOWN,
205                        new TextCoordinates(startIndex),
206                        new TextCoordinates(endIndex),
207                        Press.PINPOINT,
208                        TextView.class));
209    }
210
211    /**
212     * Returns an action that double click then drags by mouse on text from startIndex to endIndex
213     * on the TextView.<br>
214     * <br>
215     * View constraints:
216     * <ul>
217     * <li>must be a TextView displayed on screen
218     * <ul>
219     *
220     * @param startIndex The index of the TextView's text to start a drag from
221     * @param endIndex The index of the TextView's text to end the drag at
222     */
223    public static ViewAction mouseDoubleClickAndDragOnText(int startIndex, int endIndex) {
224        return actionWithAssertions(
225                new DragAction(
226                        DragAction.Drag.MOUSE_DOUBLE_CLICK,
227                        new TextCoordinates(startIndex),
228                        new TextCoordinates(endIndex),
229                        Press.PINPOINT,
230                        TextView.class));
231    }
232
233    /**
234     * Returns an action that long click then drags by mouse on text from startIndex to endIndex
235     * on the TextView.<br>
236     * <br>
237     * View constraints:
238     * <ul>
239     * <li>must be a TextView displayed on screen
240     * <ul>
241     *
242     * @param startIndex The index of the TextView's text to start a drag from
243     * @param endIndex The index of the TextView's text to end the drag at
244     */
245    public static ViewAction mouseLongClickAndDragOnText(int startIndex, int endIndex) {
246        return actionWithAssertions(
247                new DragAction(
248                        DragAction.Drag.MOUSE_LONG_CLICK,
249                        new TextCoordinates(startIndex),
250                        new TextCoordinates(endIndex),
251                        Press.PINPOINT,
252                        TextView.class));
253    }
254
255    /**
256    * Returns an action that triple click then drags by mouse on text from startIndex to endIndex
257    * on the TextView.<br>
258    * <br>
259    * View constraints:
260    * <ul>
261    * <li>must be a TextView displayed on screen
262    * <ul>
263    *
264    * @param startIndex The index of the TextView's text to start a drag from
265    * @param endIndex The index of the TextView's text to end the drag at
266    */
267   public static ViewAction mouseTripleClickAndDragOnText(int startIndex, int endIndex) {
268       return actionWithAssertions(
269               new DragAction(
270                       DragAction.Drag.MOUSE_TRIPLE_CLICK,
271                       new TextCoordinates(startIndex),
272                       new TextCoordinates(endIndex),
273                       Press.PINPOINT,
274                       TextView.class));
275   }
276
277    public enum Handle {
278        SELECTION_START,
279        SELECTION_END,
280        INSERTION
281    };
282
283    /**
284     * Returns an action that tap then drags on the handle from the current position to endIndex on
285     * the TextView.<br>
286     * <br>
287     * View constraints:
288     * <ul>
289     * <li>must be a TextView's drag-handle displayed on screen
290     * <ul>
291     *
292     * @param textView TextView the handle is on
293     * @param handleType Type of the handle
294     * @param endIndex The index of the TextView's text to end the drag at
295     */
296    public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex) {
297        final int currentOffset = handleType == Handle.SELECTION_START ?
298                textView.getSelectionStart() : textView.getSelectionEnd();
299        return actionWithAssertions(
300                new DragAction(
301                        DragAction.Drag.TAP,
302                        new HandleCoordinates(textView, handleType, currentOffset),
303                        new HandleCoordinates(textView, handleType, endIndex),
304                        Press.FINGER,
305                        Editor.HandleView.class));
306    }
307
308    /**
309     * A provider of the x, y coordinates of the handle that points the specified text index in a
310     * text view.
311     */
312    private static final class HandleCoordinates implements CoordinatesProvider {
313        // Must be larger than Editor#LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS.
314        private final static float LINE_SLOP_MULTIPLIER = 0.6f;
315        private final TextView mTextView;
316        private final Handle mHandleType;
317        private final int mIndex;
318        private final String mActionDescription;
319
320        public HandleCoordinates(TextView textView, Handle handleType, int index) {
321            mTextView = textView;
322            mHandleType = handleType;
323            mIndex = index;
324            mActionDescription = "Could not locate " + handleType.toString()
325                    + " handle that points text index: " + index;
326        }
327
328        @Override
329        public float[] calculateCoordinates(View view) {
330            try {
331                return locateHandlePointsTextIndex(view);
332            } catch (StringIndexOutOfBoundsException e) {
333                throw new PerformException.Builder()
334                        .withActionDescription(mActionDescription)
335                        .withViewDescription(HumanReadables.describe(view))
336                        .withCause(e)
337                        .build();
338            }
339        }
340
341        private float[] locateHandlePointsTextIndex(View view) {
342            final int currentOffset = mHandleType == Handle.SELECTION_START ?
343                    mTextView.getSelectionStart() : mTextView.getSelectionEnd();
344
345            final Layout layout = mTextView.getLayout();
346            final int currentLine = layout.getLineForOffset(currentOffset);
347            final int targetLine = layout.getLineForOffset(mIndex);
348
349            final float[] currentCoordinates =
350                    (new TextCoordinates(currentOffset)).calculateCoordinates(mTextView);
351            final float[] targetCoordinates =
352                    (new TextCoordinates(mIndex)).calculateCoordinates(mTextView);
353            final Rect bounds = new Rect();
354            view.getBoundsOnScreen(bounds);
355            final Rect visibleDisplayBounds = new Rect();
356            mTextView.getWindowVisibleDisplayFrame(visibleDisplayBounds);
357            visibleDisplayBounds.right -= 1;
358            visibleDisplayBounds.bottom -= 1;
359            if (!visibleDisplayBounds.intersect(bounds)) {
360                throw new PerformException.Builder()
361                        .withActionDescription(mActionDescription
362                                + " The handle is entirely out of the visible display frame of"
363                                + "the TextView's window.")
364                        .withViewDescription(HumanReadables.describe(view))
365                        .build();
366            }
367            final float dragPointX = Math.max(Math.min(bounds.centerX(),
368                    visibleDisplayBounds.right), visibleDisplayBounds.left);
369            final float diffX = dragPointX - currentCoordinates[0];
370            final float verticalOffset = bounds.height() * 0.7f;
371            final float dragPointY = Math.max(Math.min(bounds.top + verticalOffset,
372                    visibleDisplayBounds.bottom), visibleDisplayBounds.top);
373            float diffY = dragPointY - currentCoordinates[1];
374            if (currentLine > targetLine) {
375                diffY -= mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER;
376            } else if (currentLine < targetLine) {
377                diffY += mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER;
378            }
379            return new float[] {targetCoordinates[0] + diffX, targetCoordinates[1] + diffY};
380        }
381    }
382
383    /**
384     * A provider of the x, y coordinates of the text at the specified index in a text view.
385     */
386    private static final class TextCoordinates implements CoordinatesProvider {
387
388        private final int mIndex;
389        private final String mActionDescription;
390
391        public TextCoordinates(int index) {
392            mIndex = index;
393            mActionDescription = "Could not locate text at index: " + mIndex;
394        }
395
396        @Override
397        public float[] calculateCoordinates(View view) {
398            try {
399                return locateTextAtIndex((TextView) view, mIndex);
400            } catch (ClassCastException e) {
401                throw new PerformException.Builder()
402                        .withActionDescription(mActionDescription)
403                        .withViewDescription(HumanReadables.describe(view))
404                        .withCause(e)
405                        .build();
406            } catch (StringIndexOutOfBoundsException e) {
407                throw new PerformException.Builder()
408                        .withActionDescription(mActionDescription)
409                        .withViewDescription(HumanReadables.describe(view))
410                        .withCause(e)
411                        .build();
412            }
413        }
414
415        /**
416         * @throws StringIndexOutOfBoundsException
417         */
418        private float[] locateTextAtIndex(TextView textView, int index) {
419            if (index < 0 || index > textView.getText().length()) {
420                throw new StringIndexOutOfBoundsException(index);
421            }
422            final int[] xy = new int[2];
423            textView.getLocationOnScreen(xy);
424            final Layout layout = textView.getLayout();
425            final int line = layout.getLineForOffset(index);
426            final float x = textView.getTotalPaddingLeft() - textView.getScrollX()
427                    + layout.getPrimaryHorizontal(index);
428            final float y = textView.getTotalPaddingTop() - textView.getScrollY()
429                    + layout.getLineTop(line);
430            return new float[]{x + xy[0], y + xy[1]};
431        }
432    }
433}
434