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 android.support.test.espresso.action.ViewActions.actionWithAssertions; 20 import android.graphics.Rect; 21 import android.support.test.espresso.PerformException; 22 import android.support.test.espresso.ViewAction; 23 import android.support.test.espresso.action.CoordinatesProvider; 24 import android.support.test.espresso.action.Press; 25 import android.support.test.espresso.action.Tap; 26 import android.support.test.espresso.util.HumanReadables; 27 import android.text.Layout; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.widget.Editor; 31 import android.widget.Editor.HandleView; 32 import android.widget.TextView; 33 34 /** 35 * A collection of actions on a {@link android.widget.TextView}. 36 */ 37 public 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