[go: nahoru, domu]

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.Press;
25import android.support.test.espresso.action.Tap;
26import android.support.test.espresso.util.HumanReadables;
27import android.text.Layout;
28import android.view.MotionEvent;
29import android.view.View;
30import android.widget.Editor;
31import android.widget.Editor.HandleView;
32import android.widget.TextView;
33
34/**
35 * A collection of actions on a {@link android.widget.TextView}.
36 */
37public final class TextViewActions {
38
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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    */
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     */
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     */
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
352        public CurrentHandleCoordinates(TextView textView) {
353            mTextView = textView;
354            mActionDescription = "Could not locate handle.";
355        }
356
357        @Override
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
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
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
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
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
490        public TextCoordinates(int index) {
491            this(index, true);
492        }
493
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
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         */
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         */
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