TextViewActions.java revision 5f71b5afe83ea6a183a9a010c05ce4e1453e264b
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), Press.PINPOINT));
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), Press.PINPOINT));
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), Press.PINPOINT));
128    }
129
130    /**
131     * Returns an action that long presses then drags on text from startIndex to endIndex on the
132     * TextView.<br>
133     * <br>
134     * View constraints:
135     * <ul>
136     * <li>must be a TextView displayed on screen
137     * <ul>
138     *
139     * @param startIndex The index of the TextView's text to start a drag from
140     * @param endIndex The index of the TextView's text to end the drag at
141     */
142    public static ViewAction longPressAndDragOnText(int startIndex, int endIndex) {
143        return actionWithAssertions(
144                new DragAction(
145                        DragAction.Drag.LONG_PRESS,
146                        new TextCoordinates(startIndex),
147                        new TextCoordinates(endIndex),
148                        Press.FINGER,
149                        TextView.class));
150    }
151
152    /**
153     * Returns an action that double taps then drags on text from startIndex to endIndex on the
154     * TextView.<br>
155     * <br>
156     * View constraints:
157     * <ul>
158     * <li>must be a TextView displayed on screen
159     * <ul>
160     *
161     * @param startIndex The index of the TextView's text to start a drag from
162     * @param endIndex The index of the TextView's text to end the drag at
163     */
164    public static ViewAction doubleTapAndDragOnText(int startIndex, int endIndex) {
165        return actionWithAssertions(
166                new DragAction(
167                        DragAction.Drag.DOUBLE_TAP,
168                        new TextCoordinates(startIndex),
169                        new TextCoordinates(endIndex),
170                        Press.FINGER,
171                        TextView.class));
172    }
173
174    /**
175     * Returns an action that click then drags by mouse on text from startIndex to endIndex on the
176     * TextView.<br>
177     * <br>
178     * View constraints:
179     * <ul>
180     * <li>must be a TextView displayed on screen
181     * <ul>
182     *
183     * @param startIndex The index of the TextView's text to start a drag from
184     * @param endIndex The index of the TextView's text to end the drag at
185     */
186    public static ViewAction mouseDragOnText(int startIndex, int endIndex) {
187        return actionWithAssertions(
188                new DragAction(
189                        DragAction.Drag.MOUSE_DOWN,
190                        new TextCoordinates(startIndex),
191                        new TextCoordinates(endIndex),
192                        Press.PINPOINT,
193                        TextView.class));
194    }
195
196    /**
197     * Returns an action that double click then drags by mouse on text from startIndex to endIndex
198     * on the TextView.<br>
199     * <br>
200     * View constraints:
201     * <ul>
202     * <li>must be a TextView displayed on screen
203     * <ul>
204     *
205     * @param startIndex The index of the TextView's text to start a drag from
206     * @param endIndex The index of the TextView's text to end the drag at
207     */
208    public static ViewAction mouseDoubleClickAndDragOnText(int startIndex, int endIndex) {
209        return actionWithAssertions(
210                new DragAction(
211                        DragAction.Drag.MOUSE_DOUBLE_CLICK,
212                        new TextCoordinates(startIndex),
213                        new TextCoordinates(endIndex),
214                        Press.PINPOINT,
215                        TextView.class));
216    }
217
218    /**
219     * Returns an action that long click then drags by mouse on text from startIndex to endIndex
220     * on the TextView.<br>
221     * <br>
222     * View constraints:
223     * <ul>
224     * <li>must be a TextView displayed on screen
225     * <ul>
226     *
227     * @param startIndex The index of the TextView's text to start a drag from
228     * @param endIndex The index of the TextView's text to end the drag at
229     */
230    public static ViewAction mouseLongClickAndDragOnText(int startIndex, int endIndex) {
231        return actionWithAssertions(
232                new DragAction(
233                        DragAction.Drag.MOUSE_LONG_CLICK,
234                        new TextCoordinates(startIndex),
235                        new TextCoordinates(endIndex),
236                        Press.PINPOINT,
237                        TextView.class));
238    }
239
240    public enum Handle {
241        SELECTION_START,
242        SELECTION_END,
243        INSERTION
244    };
245
246    /**
247     * Returns an action that tap then drags on the handle from the current position to endIndex on
248     * the TextView.<br>
249     * <br>
250     * View constraints:
251     * <ul>
252     * <li>must be a TextView's drag-handle displayed on screen
253     * <ul>
254     *
255     * @param textView TextView the handle is on
256     * @param handleType Type of the handle
257     * @param endIndex The index of the TextView's text to end the drag at
258     */
259    public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex) {
260        final int currentOffset = handleType == Handle.SELECTION_START ?
261                textView.getSelectionStart() : textView.getSelectionEnd();
262        return actionWithAssertions(
263                new DragAction(
264                        DragAction.Drag.TAP,
265                        new HandleCoordinates(textView, handleType, currentOffset),
266                        new HandleCoordinates(textView, handleType, endIndex),
267                        Press.FINGER,
268                        Editor.HandleView.class));
269    }
270
271    /**
272     * A provider of the x, y coordinates of the handle that points the specified text index in a
273     * text view.
274     */
275    private static final class HandleCoordinates implements CoordinatesProvider {
276        // Must be larger than Editor#LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS.
277        private final static float LINE_SLOP_MULTIPLIER = 0.6f;
278        private final TextView mTextView;
279        private final Handle mHandleType;
280        private final int mIndex;
281        private final String mActionDescription;
282
283        public HandleCoordinates(TextView textView, Handle handleType, int index) {
284            mTextView = textView;
285            mHandleType = handleType;
286            mIndex = index;
287            mActionDescription = "Could not locate " + handleType.toString()
288                    + " handle that points text index: " + index;
289        }
290
291        @Override
292        public float[] calculateCoordinates(View view) {
293            try {
294                return locateHandlePointsTextIndex(view);
295            } catch (StringIndexOutOfBoundsException e) {
296                throw new PerformException.Builder()
297                        .withActionDescription(mActionDescription)
298                        .withViewDescription(HumanReadables.describe(view))
299                        .withCause(e)
300                        .build();
301            }
302        }
303
304        private float[] locateHandlePointsTextIndex(View view) {
305            final int currentOffset = mHandleType == Handle.SELECTION_START ?
306                    mTextView.getSelectionStart() : mTextView.getSelectionEnd();
307
308            final Layout layout = mTextView.getLayout();
309            final int currentLine = layout.getLineForOffset(currentOffset);
310            final int targetLine = layout.getLineForOffset(mIndex);
311
312            final float[] currentCoordinates =
313                    (new TextCoordinates(currentOffset)).calculateCoordinates(mTextView);
314            final float[] targetCoordinates =
315                    (new TextCoordinates(mIndex)).calculateCoordinates(mTextView);
316            final Rect bounds = new Rect();
317            view.getBoundsOnScreen(bounds);
318            final Rect visibleDisplayBounds = new Rect();
319            mTextView.getWindowVisibleDisplayFrame(visibleDisplayBounds);
320            visibleDisplayBounds.right -= 1;
321            visibleDisplayBounds.bottom -= 1;
322            if (!visibleDisplayBounds.intersect(bounds)) {
323                throw new PerformException.Builder()
324                        .withActionDescription(mActionDescription
325                                + " The handle is entirely out of the visible display frame of"
326                                + "the TextView's window.")
327                        .withViewDescription(HumanReadables.describe(view))
328                        .build();
329            }
330            final float dragPointX = Math.max(Math.min(bounds.centerX(),
331                    visibleDisplayBounds.right), visibleDisplayBounds.left);
332            final float diffX = dragPointX - currentCoordinates[0];
333            final float verticalOffset = bounds.height() * 0.7f;
334            final float dragPointY = Math.max(Math.min(bounds.top + verticalOffset,
335                    visibleDisplayBounds.bottom), visibleDisplayBounds.top);
336            float diffY = dragPointY - currentCoordinates[1];
337            if (currentLine > targetLine) {
338                diffY -= mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER;
339            } else if (currentLine < targetLine) {
340                diffY += mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER;
341            }
342            return new float[] {targetCoordinates[0] + diffX, targetCoordinates[1] + diffY};
343        }
344    }
345
346    /**
347     * A provider of the x, y coordinates of the text at the specified index in a text view.
348     */
349    private static final class TextCoordinates implements CoordinatesProvider {
350
351        private final int mIndex;
352        private final String mActionDescription;
353
354        public TextCoordinates(int index) {
355            mIndex = index;
356            mActionDescription = "Could not locate text at index: " + mIndex;
357        }
358
359        @Override
360        public float[] calculateCoordinates(View view) {
361            try {
362                return locateTextAtIndex((TextView) view, mIndex);
363            } catch (ClassCastException e) {
364                throw new PerformException.Builder()
365                        .withActionDescription(mActionDescription)
366                        .withViewDescription(HumanReadables.describe(view))
367                        .withCause(e)
368                        .build();
369            } catch (StringIndexOutOfBoundsException e) {
370                throw new PerformException.Builder()
371                        .withActionDescription(mActionDescription)
372                        .withViewDescription(HumanReadables.describe(view))
373                        .withCause(e)
374                        .build();
375            }
376        }
377
378        /**
379         * @throws StringIndexOutOfBoundsException
380         */
381        private float[] locateTextAtIndex(TextView textView, int index) {
382            if (index < 0 || index > textView.getText().length()) {
383                throw new StringIndexOutOfBoundsException(index);
384            }
385            final int[] xy = new int[2];
386            textView.getLocationOnScreen(xy);
387            final Layout layout = textView.getLayout();
388            final int line = layout.getLineForOffset(index);
389            final float x = textView.getTotalPaddingLeft() - textView.getScrollX()
390                    + layout.getPrimaryHorizontal(index);
391            final float y = textView.getTotalPaddingTop() - textView.getScrollY()
392                    + layout.getLineTop(line);
393            return new float[]{x + xy[0], y + xy[1]};
394        }
395    }
396}
397