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