TextViewActions.java revision da79ee683d74f2ad7d29147a30f3da17dd0ea6e2
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 double-clicks 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 double-click on.
64     */
65    public static ViewAction doubleClickOnTextAtIndex(int index) {
66        return actionWithAssertions(
67                new GeneralClickAction(Tap.DOUBLE, new TextCoordinates(index), Press.FINGER));
68    }
69
70    /**
71     * Returns an action that long presses 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 long press on.
79     */
80    public static ViewAction longPressOnTextAtIndex(int index) {
81        return actionWithAssertions(
82                new GeneralClickAction(Tap.LONG, new TextCoordinates(index), Press.FINGER));
83    }
84
85    /**
86     * Returns an action that long presses then drags on text from startIndex to endIndex on the
87     * TextView.<br>
88     * <br>
89     * View constraints:
90     * <ul>
91     * <li>must be a TextView displayed on screen
92     * <ul>
93     *
94     * @param startIndex The index of the TextView's text to start a drag from
95     * @param endIndex The index of the TextView's text to end the drag at
96     */
97    public static ViewAction longPressAndDragOnText(int startIndex, int endIndex) {
98        return actionWithAssertions(
99                new DragAction(
100                        DragAction.Drag.LONG_PRESS,
101                        new TextCoordinates(startIndex),
102                        new TextCoordinates(endIndex),
103                        Press.FINGER,
104                        TextView.class));
105    }
106
107    /**
108     * Returns an action that double taps then drags on text from startIndex to endIndex on the
109     * TextView.<br>
110     * <br>
111     * View constraints:
112     * <ul>
113     * <li>must be a TextView displayed on screen
114     * <ul>
115     *
116     * @param startIndex The index of the TextView's text to start a drag from
117     * @param endIndex The index of the TextView's text to end the drag at
118     */
119    public static ViewAction doubleTapAndDragOnText(int startIndex, int endIndex) {
120        return actionWithAssertions(
121                new DragAction(
122                        DragAction.Drag.DOUBLE_TAP,
123                        new TextCoordinates(startIndex),
124                        new TextCoordinates(endIndex),
125                        Press.FINGER,
126                        TextView.class));
127    }
128
129    /**
130     * Returns an action that click then drags by mouse on text from startIndex to endIndex on the
131     * TextView.<br>
132     * <br>
133     * View constraints:
134     * <ul>
135     * <li>must be a TextView displayed on screen
136     * <ul>
137     *
138     * @param startIndex The index of the TextView's text to start a drag from
139     * @param endIndex The index of the TextView's text to end the drag at
140     */
141    public static ViewAction mouseDragOnText(int startIndex, int endIndex) {
142        return actionWithAssertions(
143                new DragAction(
144                        DragAction.Drag.MOUSE_DOWN,
145                        new TextCoordinates(startIndex),
146                        new TextCoordinates(endIndex),
147                        Press.PINPOINT,
148                        TextView.class));
149    }
150
151    public enum Handle {
152        SELECTION_START,
153        SELECTION_END,
154        INSERTION
155    };
156
157    /**
158     * Returns an action that tap then drags on the handle from the current position to endIndex on
159     * the TextView.<br>
160     * <br>
161     * View constraints:
162     * <ul>
163     * <li>must be a TextView's drag-handle displayed on screen
164     * <ul>
165     *
166     * @param textView TextView the handle is on
167     * @param handleType Type of the handle
168     * @param endIndex The index of the TextView's text to end the drag at
169     */
170    public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex) {
171        final int currentOffset = handleType == Handle.SELECTION_START ?
172                textView.getSelectionStart() : textView.getSelectionEnd();
173        return actionWithAssertions(
174                new DragAction(
175                        DragAction.Drag.TAP,
176                        new HandleCoordinates(textView, handleType, currentOffset),
177                        new HandleCoordinates(textView, handleType, endIndex),
178                        Press.FINGER,
179                        Editor.HandleView.class));
180    }
181
182    /**
183     * A provider of the x, y coordinates of the handle that points the specified text index in a
184     * text view.
185     */
186    private static final class HandleCoordinates implements CoordinatesProvider {
187        private final TextView mTextView;
188        private final Handle mHandleType;
189        private final int mIndex;
190        private final String mActionDescription;
191
192        public HandleCoordinates(TextView textView, Handle handleType, int index) {
193            mTextView = textView;
194            mHandleType = handleType;
195            mIndex = index;
196            mActionDescription = "Could not locate " + handleType.toString()
197                    + " handle that points text index: " + index;
198        }
199
200        @Override
201        public float[] calculateCoordinates(View view) {
202            try {
203                return locateHandlePointsTextIndex(view);
204            } catch (StringIndexOutOfBoundsException e) {
205                throw new PerformException.Builder()
206                        .withActionDescription(mActionDescription)
207                        .withViewDescription(HumanReadables.describe(view))
208                        .withCause(e)
209                        .build();
210            }
211        }
212
213        private float[] locateHandlePointsTextIndex(View view) {
214            final int currentOffset = mHandleType == Handle.SELECTION_START ?
215                    mTextView.getSelectionStart() : mTextView.getSelectionEnd();
216            final float[] currentCoordinates =
217                    (new TextCoordinates(currentOffset)).calculateCoordinates(mTextView);
218            final float[] targetCoordinates =
219                    (new TextCoordinates(mIndex)).calculateCoordinates(mTextView);
220            final Rect bounds = new Rect();
221            view.getBoundsOnScreen(bounds);
222            final float diffX = bounds.centerX() - currentCoordinates[0];
223            final float diffY = bounds.centerY() - currentCoordinates[1];
224            return new float[] {targetCoordinates[0] + diffX, targetCoordinates[1] + diffY};
225        }
226    }
227
228    /**
229     * A provider of the x, y coordinates of the text at the specified index in a text view.
230     */
231    private static final class TextCoordinates implements CoordinatesProvider {
232
233        private final int mIndex;
234        private final String mActionDescription;
235
236        public TextCoordinates(int index) {
237            mIndex = index;
238            mActionDescription = "Could not locate text at index: " + mIndex;
239        }
240
241        @Override
242        public float[] calculateCoordinates(View view) {
243            try {
244                return locateTextAtIndex((TextView) view, mIndex);
245            } catch (ClassCastException e) {
246                throw new PerformException.Builder()
247                        .withActionDescription(mActionDescription)
248                        .withViewDescription(HumanReadables.describe(view))
249                        .withCause(e)
250                        .build();
251            } catch (StringIndexOutOfBoundsException e) {
252                throw new PerformException.Builder()
253                        .withActionDescription(mActionDescription)
254                        .withViewDescription(HumanReadables.describe(view))
255                        .withCause(e)
256                        .build();
257            }
258        }
259
260        /**
261         * @throws StringIndexOutOfBoundsException
262         */
263        private float[] locateTextAtIndex(TextView textView, int index) {
264            if (index < 0 || index > textView.getText().length()) {
265                throw new StringIndexOutOfBoundsException(index);
266            }
267            final int[] xy = new int[2];
268            textView.getLocationOnScreen(xy);
269            final Layout layout = textView.getLayout();
270            final int line = layout.getLineForOffset(index);
271            final float x = textView.getTotalPaddingLeft() - textView.getScrollX()
272                    + layout.getPrimaryHorizontal(index);
273            final float y = textView.getTotalPaddingTop() - textView.getScrollY()
274                    + layout.getLineTop(line);
275            return new float[]{x + xy[0], y + xy[1]};
276        }
277    }
278}
279