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 21 import android.graphics.Rect; 22 import android.support.test.espresso.PerformException; 23 import android.support.test.espresso.ViewAction; 24 import android.support.test.espresso.action.CoordinatesProvider; 25 import android.support.test.espresso.action.GeneralLocation; 26 import android.support.test.espresso.action.Press; 27 import android.support.test.espresso.action.Tap; 28 import android.support.test.espresso.util.HumanReadables; 29 import android.text.Layout; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.widget.Editor; 33 import android.widget.Editor.HandleView; 34 import android.widget.TextView; 35 36 /** 37 * A collection of actions on a {@link android.widget.TextView}. 38 */ 39 public final class TextViewActions { 40 41 private TextViewActions() {} 42 43 /** 44 * Returns an action that clicks on text at an index on the TextView.<br> 45 * <br> 46 * View constraints: 47 * <ul> 48 * <li>must be a TextView displayed on screen 49 * <ul> 50 * 51 * @param index The index of the TextView's text to click on. 52 */ 53 public static ViewAction clickOnTextAtIndex(int index) { 54 return actionWithAssertions( 55 new ViewClickAction(Tap.SINGLE, new TextCoordinates(index), Press.FINGER)); 56 } 57 58 59 /** 60 * Returns an action that single-clicks by mouse on the View.<br> 61 * <br> 62 * View constraints: 63 * <ul> 64 * <li>must be a View displayed on screen 65 * <ul> 66 */ 67 public static ViewAction mouseClick() { 68 return actionWithAssertions(new MouseClickAction(Tap.SINGLE, GeneralLocation.VISIBLE_CENTER, 69 MotionEvent.BUTTON_PRIMARY)); 70 } 71 72 /** 73 * Returns an action that clicks by mouse on text at an index on the TextView.<br> 74 * <br> 75 * View constraints: 76 * <ul> 77 * <li>must be a TextView displayed on screen 78 * <ul> 79 * 80 * @param index The index of the TextView's text to click on. 81 */ 82 public static ViewAction mouseClickOnTextAtIndex(int index) { 83 return mouseClickOnTextAtIndex(index, MotionEvent.BUTTON_PRIMARY); 84 } 85 86 /** 87 * Returns an action that clicks by mouse on text at an index on the TextView.<br> 88 * <br> 89 * View constraints: 90 * <ul> 91 * <li>must be a TextView displayed on screen 92 * <ul> 93 * 94 * @param index The index of the TextView's text to click on. 95 * @param button the mouse button to use. 96 */ 97 public static ViewAction mouseClickOnTextAtIndex(int index, 98 @MouseUiController.MouseButton int button) { 99 return actionWithAssertions( 100 new MouseClickAction(Tap.SINGLE, new TextCoordinates(index), button)); 101 } 102 103 /** 104 * Returns an action that double-clicks on text at an index on the TextView.<br> 105 * <br> 106 * View constraints: 107 * <ul> 108 * <li>must be a TextView displayed on screen 109 * <ul> 110 * 111 * @param index The index of the TextView's text to double-click on. 112 */ 113 public static ViewAction doubleClickOnTextAtIndex(int index) { 114 return actionWithAssertions( 115 new ViewClickAction(Tap.DOUBLE, new TextCoordinates(index), Press.FINGER)); 116 } 117 118 /** 119 * Returns an action that double-clicks by mouse on text at an index on the TextView.<br> 120 * <br> 121 * View constraints: 122 * <ul> 123 * <li>must be a TextView displayed on screen 124 * <ul> 125 * 126 * @param index The index of the TextView's text to double-click on. 127 */ 128 public static ViewAction mouseDoubleClickOnTextAtIndex(int index) { 129 return actionWithAssertions( 130 new MouseClickAction(Tap.DOUBLE, new TextCoordinates(index))); 131 } 132 133 /** 134 * Returns an action that long presses on text at an index on the TextView.<br> 135 * <br> 136 * View constraints: 137 * <ul> 138 * <li>must be a TextView displayed on screen 139 * <ul> 140 * 141 * @param index The index of the TextView's text to long press on. 142 */ 143 public static ViewAction longPressOnTextAtIndex(int index) { 144 return actionWithAssertions( 145 new ViewClickAction(Tap.LONG, new TextCoordinates(index), Press.FINGER)); 146 } 147 148 /** 149 * Returns an action that long click by mouse on text at an index on the TextView.<br> 150 * <br> 151 * View constraints: 152 * <ul> 153 * <li>must be a TextView displayed on screen 154 * <ul> 155 * 156 * @param index The index of the TextView's text to long click on. 157 */ 158 public static ViewAction mouseLongClickOnTextAtIndex(int index) { 159 return actionWithAssertions( 160 new MouseClickAction(Tap.LONG, new TextCoordinates(index))); 161 } 162 163 /** 164 * Returns an action that triple-clicks by mouse on text at an index on the TextView.<br> 165 * <br> 166 * View constraints: 167 * <ul> 168 * <li>must be a TextView displayed on screen 169 * <ul> 170 * 171 * @param index The index of the TextView's text to triple-click on. 172 */ 173 public static ViewAction mouseTripleClickOnTextAtIndex(int index) { 174 return actionWithAssertions( 175 new MouseClickAction(MouseClickAction.CLICK.TRIPLE, new TextCoordinates(index))); 176 } 177 178 /** 179 * Returns an action that long presses then drags on text from startIndex to endIndex on the 180 * TextView.<br> 181 * <br> 182 * View constraints: 183 * <ul> 184 * <li>must be a TextView displayed on screen 185 * <ul> 186 * 187 * @param startIndex The index of the TextView's text to start a drag from 188 * @param endIndex The index of the TextView's text to end the drag at 189 */ 190 public static ViewAction longPressAndDragOnText(int startIndex, int endIndex) { 191 return actionWithAssertions( 192 new DragAction( 193 DragAction.Drag.LONG_PRESS, 194 new TextCoordinates(startIndex), 195 new TextCoordinates(endIndex), 196 Press.FINGER, 197 TextView.class)); 198 } 199 200 /** 201 * Returns an action that double taps then drags on text from startIndex to endIndex on the 202 * TextView.<br> 203 * <br> 204 * View constraints: 205 * <ul> 206 * <li>must be a TextView displayed on screen 207 * <ul> 208 * 209 * @param startIndex The index of the TextView's text to start a drag from 210 * @param endIndex The index of the TextView's text to end the drag at 211 */ 212 public static ViewAction doubleTapAndDragOnText(int startIndex, int endIndex) { 213 return actionWithAssertions( 214 new DragAction( 215 DragAction.Drag.DOUBLE_TAP, 216 new TextCoordinates(startIndex), 217 new TextCoordinates(endIndex), 218 Press.FINGER, 219 TextView.class)); 220 } 221 222 /** 223 * Returns an action that click then drags by mouse on text from startIndex to endIndex on the 224 * TextView.<br> 225 * <br> 226 * View constraints: 227 * <ul> 228 * <li>must be a TextView displayed on screen 229 * <ul> 230 * 231 * @param startIndex The index of the TextView's text to start a drag from 232 * @param endIndex The index of the TextView's text to end the drag at 233 */ 234 public static ViewAction mouseDragOnText(int startIndex, int endIndex) { 235 return actionWithAssertions( 236 new DragAction( 237 DragAction.Drag.MOUSE_DOWN, 238 new TextCoordinates(startIndex), 239 new TextCoordinates(endIndex), 240 Press.PINPOINT, 241 TextView.class)); 242 } 243 244 /** 245 * Returns an action that double click then drags by mouse on text from startIndex to endIndex 246 * on the TextView.<br> 247 * <br> 248 * View constraints: 249 * <ul> 250 * <li>must be a TextView displayed on screen 251 * <ul> 252 * 253 * @param startIndex The index of the TextView's text to start a drag from 254 * @param endIndex The index of the TextView's text to end the drag at 255 */ 256 public static ViewAction mouseDoubleClickAndDragOnText(int startIndex, int endIndex) { 257 return actionWithAssertions( 258 new DragAction( 259 DragAction.Drag.MOUSE_DOUBLE_CLICK, 260 new TextCoordinates(startIndex), 261 new TextCoordinates(endIndex), 262 Press.PINPOINT, 263 TextView.class)); 264 } 265 266 /** 267 * Returns an action that long click then drags by mouse on text from startIndex to endIndex 268 * on the TextView.<br> 269 * <br> 270 * View constraints: 271 * <ul> 272 * <li>must be a TextView displayed on screen 273 * <ul> 274 * 275 * @param startIndex The index of the TextView's text to start a drag from 276 * @param endIndex The index of the TextView's text to end the drag at 277 */ 278 public static ViewAction mouseLongClickAndDragOnText(int startIndex, int endIndex) { 279 return actionWithAssertions( 280 new DragAction( 281 DragAction.Drag.MOUSE_LONG_CLICK, 282 new TextCoordinates(startIndex), 283 new TextCoordinates(endIndex), 284 Press.PINPOINT, 285 TextView.class)); 286 } 287 288 /** 289 * Returns an action that triple click then drags by mouse on text from startIndex to endIndex 290 * on the TextView.<br> 291 * <br> 292 * View constraints: 293 * <ul> 294 * <li>must be a TextView displayed on screen 295 * <ul> 296 * 297 * @param startIndex The index of the TextView's text to start a drag from 298 * @param endIndex The index of the TextView's text to end the drag at 299 */ 300 public static ViewAction mouseTripleClickAndDragOnText(int startIndex, int endIndex) { 301 return actionWithAssertions( 302 new DragAction( 303 DragAction.Drag.MOUSE_TRIPLE_CLICK, 304 new TextCoordinates(startIndex), 305 new TextCoordinates(endIndex), 306 Press.PINPOINT, 307 TextView.class)); 308 } 309 310 public enum Handle { 311 SELECTION_START, 312 SELECTION_END, 313 INSERTION 314 }; 315 316 /** 317 * Returns an action that tap then drags on the handle from the current position to endIndex on 318 * the TextView.<br> 319 * <br> 320 * View constraints: 321 * <ul> 322 * <li>must be a TextView's drag-handle displayed on screen 323 * <ul> 324 * 325 * @param textView TextView the handle is on 326 * @param handleType Type of the handle 327 * @param endIndex The index of the TextView's text to end the drag at 328 */ 329 public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex) { 330 return dragHandle(textView, handleType, endIndex, true); 331 } 332 333 /** 334 * Returns an action that tap then drags on the handle from the current position to endIndex on 335 * the TextView.<br> 336 * <br> 337 * View constraints: 338 * <ul> 339 * <li>must be a TextView's drag-handle displayed on screen 340 * <ul> 341 * 342 * @param textView TextView the handle is on 343 * @param handleType Type of the handle 344 * @param endIndex The index of the TextView's text to end the drag at 345 * @param primary whether to use primary direction to get coordinate form index when endIndex is 346 * at a direction boundary. 347 */ 348 public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex, 349 boolean primary) { 350 return actionWithAssertions( 351 new DragAction( 352 DragAction.Drag.TAP, 353 new CurrentHandleCoordinates(textView), 354 new HandleCoordinates(textView, handleType, endIndex, primary), 355 Press.FINGER, 356 Editor.HandleView.class)); 357 } 358 359 /** 360 * A provider of the x, y coordinates of the handle dragging point. 361 */ 362 private static final class CurrentHandleCoordinates implements CoordinatesProvider { 363 // Must be larger than Editor#LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS. 364 private final TextView mTextView; 365 private final String mActionDescription; 366 367 368 public CurrentHandleCoordinates(TextView textView) { 369 mTextView = textView; 370 mActionDescription = "Could not locate handle."; 371 } 372 373 @Override 374 public float[] calculateCoordinates(View view) { 375 try { 376 return locateHandle(view); 377 } catch (StringIndexOutOfBoundsException e) { 378 throw new PerformException.Builder() 379 .withActionDescription(mActionDescription) 380 .withViewDescription(HumanReadables.describe(view)) 381 .withCause(e) 382 .build(); 383 } 384 } 385 386 private float[] locateHandle(View view) { 387 final Rect bounds = new Rect(); 388 view.getBoundsOnScreen(bounds); 389 final Rect visibleDisplayBounds = new Rect(); 390 mTextView.getWindowVisibleDisplayFrame(visibleDisplayBounds); 391 visibleDisplayBounds.right -= 1; 392 visibleDisplayBounds.bottom -= 1; 393 if (!visibleDisplayBounds.intersect(bounds)) { 394 throw new PerformException.Builder() 395 .withActionDescription(mActionDescription 396 + " The handle is entirely out of the visible display frame of" 397 + "the TextView's window.") 398 .withViewDescription(HumanReadables.describe(view)) 399 .build(); 400 } 401 final float dragPointX = Math.max(Math.min(bounds.centerX(), 402 visibleDisplayBounds.right), visibleDisplayBounds.left); 403 final float verticalOffset = bounds.height() * 0.7f; 404 final float dragPointY = Math.max(Math.min(bounds.top + verticalOffset, 405 visibleDisplayBounds.bottom), visibleDisplayBounds.top); 406 return new float[] {dragPointX, dragPointY}; 407 } 408 } 409 410 /** 411 * A provider of the x, y coordinates of the handle that points the specified text index in a 412 * text view. 413 */ 414 private static final class HandleCoordinates implements CoordinatesProvider { 415 // Must be larger than Editor#LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS. 416 private final static float LINE_SLOP_MULTIPLIER = 0.6f; 417 private final TextView mTextView; 418 private final Handle mHandleType; 419 private final int mIndex; 420 private final boolean mPrimary; 421 private final String mActionDescription; 422 423 public HandleCoordinates(TextView textView, Handle handleType, int index, boolean primary) { 424 mTextView = textView; 425 mHandleType = handleType; 426 mIndex = index; 427 mPrimary = primary; 428 mActionDescription = "Could not locate " + handleType.toString() 429 + " handle that points text index: " + index 430 + " (" + (primary ? "primary" : "secondary" ) + ")"; 431 } 432 433 @Override 434 public float[] calculateCoordinates(View view) { 435 try { 436 return locateHandlePointsTextIndex(view); 437 } catch (StringIndexOutOfBoundsException e) { 438 throw new PerformException.Builder() 439 .withActionDescription(mActionDescription) 440 .withViewDescription(HumanReadables.describe(view)) 441 .withCause(e) 442 .build(); 443 } 444 } 445 446 private float[] locateHandlePointsTextIndex(View view) { 447 if (!(view instanceof HandleView)) { 448 throw new PerformException.Builder() 449 .withActionDescription(mActionDescription + " The view is not a HandleView") 450 .withViewDescription(HumanReadables.describe(view)) 451 .build(); 452 } 453 final HandleView handleView = (HandleView) view; 454 final int currentOffset = mHandleType == Handle.SELECTION_START ? 455 mTextView.getSelectionStart() : mTextView.getSelectionEnd(); 456 457 final Layout layout = mTextView.getLayout(); 458 459 final int currentLine = layout.getLineForOffset(currentOffset); 460 final int targetLine = layout.getLineForOffset(mIndex); 461 final float currentX = handleView.getHorizontal(layout, currentOffset); 462 final float currentY = layout.getLineTop(currentLine); 463 final float[] currentCoordinates = 464 TextCoordinates.convertToScreenCoordinates(mTextView, currentX, currentY); 465 final float[] targetCoordinates = 466 (new TextCoordinates(mIndex, mPrimary)).calculateCoordinates(mTextView); 467 final Rect bounds = new Rect(); 468 view.getBoundsOnScreen(bounds); 469 final Rect visibleDisplayBounds = new Rect(); 470 mTextView.getWindowVisibleDisplayFrame(visibleDisplayBounds); 471 visibleDisplayBounds.right -= 1; 472 visibleDisplayBounds.bottom -= 1; 473 if (!visibleDisplayBounds.intersect(bounds)) { 474 throw new PerformException.Builder() 475 .withActionDescription(mActionDescription 476 + " The handle is entirely out of the visible display frame of" 477 + "the TextView's window.") 478 .withViewDescription(HumanReadables.describe(view)) 479 .build(); 480 } 481 final float dragPointX = Math.max(Math.min(bounds.centerX(), 482 visibleDisplayBounds.right), visibleDisplayBounds.left); 483 final float diffX = dragPointX - currentCoordinates[0]; 484 final float verticalOffset = bounds.height() * 0.7f; 485 final float dragPointY = Math.max(Math.min(bounds.top + verticalOffset, 486 visibleDisplayBounds.bottom), visibleDisplayBounds.top); 487 float diffY = dragPointY - currentCoordinates[1]; 488 if (currentLine > targetLine) { 489 diffY -= mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER; 490 } else if (currentLine < targetLine) { 491 diffY += mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER; 492 } 493 return new float[] {targetCoordinates[0] + diffX, targetCoordinates[1] + diffY}; 494 } 495 } 496 497 /** 498 * A provider of the x, y coordinates of the text at the specified index in a text view. 499 */ 500 private static final class TextCoordinates implements CoordinatesProvider { 501 502 private final int mIndex; 503 private final boolean mPrimary; 504 private final String mActionDescription; 505 506 public TextCoordinates(int index) { 507 this(index, true); 508 } 509 510 public TextCoordinates(int index, boolean primary) { 511 mIndex = index; 512 mPrimary = primary; 513 mActionDescription = "Could not locate text at index: " + mIndex 514 + " (" + (primary ? "primary" : "secondary" ) + ")"; 515 } 516 517 @Override 518 public float[] calculateCoordinates(View view) { 519 try { 520 return locateTextAtIndex((TextView) view, mIndex, mPrimary); 521 } catch (ClassCastException e) { 522 throw new PerformException.Builder() 523 .withActionDescription(mActionDescription) 524 .withViewDescription(HumanReadables.describe(view)) 525 .withCause(e) 526 .build(); 527 } catch (StringIndexOutOfBoundsException e) { 528 throw new PerformException.Builder() 529 .withActionDescription(mActionDescription) 530 .withViewDescription(HumanReadables.describe(view)) 531 .withCause(e) 532 .build(); 533 } 534 } 535 536 /** 537 * @throws StringIndexOutOfBoundsException 538 */ 539 private float[] locateTextAtIndex(TextView textView, int index, boolean primary) { 540 if (index < 0 || index > textView.getText().length()) { 541 throw new StringIndexOutOfBoundsException(index); 542 } 543 final Layout layout = textView.getLayout(); 544 final int line = layout.getLineForOffset(index); 545 return convertToScreenCoordinates(textView, 546 (primary ? layout.getPrimaryHorizontal(index) 547 : layout.getSecondaryHorizontal(index)), 548 layout.getLineTop(line)); 549 } 550 551 /** 552 * Convert TextView's local coordinates to on screen coordinates. 553 * @param textView the TextView 554 * @param x local horizontal coordinate 555 * @param y local vertical coordinate 556 * @return 557 */ 558 public static float[] convertToScreenCoordinates(TextView textView, float x, float y) { 559 final int[] xy = new int[2]; 560 textView.getLocationOnScreen(xy); 561 return new float[]{ x + textView.getTotalPaddingLeft() - textView.getScrollX() + xy[0], 562 y + textView.getTotalPaddingTop() - textView.getScrollY() + xy[1] }; 563 } 564 } 565 } 566