1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5package org.chromium.content.browser.input; 6 7import android.graphics.Point; 8import android.graphics.Rect; 9import android.os.SystemClock; 10import android.text.Editable; 11import android.text.Selection; 12import android.test.suitebuilder.annotation.MediumTest; 13import android.view.MotionEvent; 14import android.view.View; 15import android.view.inputmethod.EditorInfo; 16 17import java.util.concurrent.Callable; 18 19import org.chromium.base.test.util.Feature; 20import org.chromium.base.test.util.UrlUtils; 21import org.chromium.base.ThreadUtils; 22import org.chromium.content.browser.ContentView; 23import org.chromium.content.browser.RenderCoordinates; 24import org.chromium.content.browser.test.util.CriteriaHelper; 25import org.chromium.content.browser.test.util.Criteria; 26import org.chromium.content.browser.test.util.DOMUtils; 27import org.chromium.content.browser.test.util.TestCallbackHelperContainer; 28import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper; 29import org.chromium.content.browser.test.util.TestTouchUtils; 30import org.chromium.content.browser.test.util.TouchCommon; 31import org.chromium.content_shell_apk.ContentShellTestBase; 32 33public class SelectionHandleTest extends ContentShellTestBase { 34 private static final String META_DISABLE_ZOOM = 35 "<meta name=\"viewport\" content=\"" + 36 "height=device-height," + 37 "width=device-width," + 38 "initial-scale=1.0," + 39 "minimum-scale=1.0," + 40 "maximum-scale=1.0," + 41 "\" />"; 42 43 // For these we use a tiny font-size so that we can be more strict on the expected handle 44 // positions. 45 private static final String TEXTAREA_ID = "textarea"; 46 private static final String TEXTAREA_DATA_URL = UrlUtils.encodeHtmlDataUri( 47 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" + 48 "<textarea id=\"" + TEXTAREA_ID + 49 "\" cols=\"40\" rows=\"20\" style=\"font-size:6px\">" + 50 "L r m i s m d l r s t a e , c n e t t r a i i i i g e i , s d d e u m d t m o " + 51 "i c d d n u l b r e d l r m g a l q a U e i a m n m e i m q i n s r d " + 52 "e e c t t o u l m o a o i n s u a i u p x a o m d c n e u t D i a t " + 53 "i u e o o i r p e e d r t n o u t t v l t s e i l m o o e u u i t u l " + 54 "p r a u . x e t u s n o c e a c p d t t o p o d n , u t n u p q i " + 55 "o f c a e e u t o l t n m d s l b r m." + 56 "L r m i s m d l r s t a e , c n e t t r a i i i i g e i , s d d e u m d t m o " + 57 "i c d d n u l b r e d l r m g a l q a U e i a m n m e i m q i n s r d " + 58 "e e c t t o u l m o a o i n s u a i u p x a o m d c n e u t D i a t " + 59 "i u e o o i r p e e d r t n o u t t v l t s e i l m o o e u u i t u l " + 60 "p r a u . x e t u s n o c e a c p d t t o p o d n , u t n u p q i " + 61 "o f c a e e u t o l t n m d s l b r m." + 62 "</textarea>" + 63 "</body></html>"); 64 65 private static final String NONEDITABLE_DIV_ID = "noneditable"; 66 private static final String NONEDITABLE_DATA_URL = UrlUtils.encodeHtmlDataUri( 67 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" + 68 "<div id=\"" + NONEDITABLE_DIV_ID + "\" style=\"width:200; font-size:6px\">" + 69 "L r m i s m d l r s t a e , c n e t t r a i i i i g e i , s d d e u m d t m o " + 70 "i c d d n u l b r e d l r m g a l q a U e i a m n m e i m q i n s r d " + 71 "e e c t t o u l m o a o i n s u a i u p x a o m d c n e u t D i a t " + 72 "i u e o o i r p e e d r t n o u t t v l t s e i l m o o e u u i t u l " + 73 "p r a u . x e t u s n o c e a c p d t t o p o d n , u t n u p q i " + 74 "o f c a e e u t o l t n m d s l b r m." + 75 "L r m i s m d l r s t a e , c n e t t r a i i i i g e i , s d d e u m d t m o " + 76 "i c d d n u l b r e d l r m g a l q a U e i a m n m e i m q i n s r d " + 77 "e e c t t o u l m o a o i n s u a i u p x a o m d c n e u t D i a t " + 78 "i u e o o i r p e e d r t n o u t t v l t s e i l m o o e u u i t u l " + 79 "p r a u . x e t u s n o c e a c p d t t o p o d n , u t n u p q i " + 80 "o f c a e e u t o l t n m d s l b r m." + 81 "</div>" + 82 "</body></html>"); 83 84 // TODO(cjhopman): These tolerances should be based on the actual width/height of a 85 // character/line. 86 private static final int HANDLE_POSITION_X_TOLERANCE_PIX = 20; 87 private static final int HANDLE_POSITION_Y_TOLERANCE_PIX = 30; 88 89 private enum TestPageType { 90 EDITABLE(TEXTAREA_ID, TEXTAREA_DATA_URL, true), 91 NONEDITABLE(NONEDITABLE_DIV_ID, NONEDITABLE_DATA_URL, false); 92 93 final String nodeId; 94 final String dataUrl; 95 final boolean selectionShouldBeEditable; 96 97 TestPageType(String nodeId, String dataUrl, boolean selectionShouldBeEditable) { 98 this.nodeId = nodeId; 99 this.dataUrl = dataUrl; 100 this.selectionShouldBeEditable = selectionShouldBeEditable; 101 } 102 } 103 104 private void launchWithUrl(String url) throws Throwable { 105 launchContentShellWithUrl(url); 106 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading()); 107 assertWaitForPageScaleFactorMatch(1.0f); 108 109 // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never 110 // brought up. 111 getImeAdapter().setInputMethodManagerWrapper( 112 new TestInputMethodManagerWrapper(getContentViewCore())); 113 } 114 115 private void assertWaitForHasSelectionPosition() 116 throws Throwable { 117 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 118 @Override 119 public boolean isSatisfied() { 120 int start = getSelectionStart(); 121 int end = getSelectionEnd(); 122 return start > 0 && start == end; 123 } 124 })); 125 } 126 127 /** 128 * Verifies that when a long-press is performed on static page text, 129 * selection handles appear and that handles can be dragged to extend the 130 * selection. Does not check exact handle position as this will depend on 131 * screen size; instead, position is expected to be correct within 132 * HANDLE_POSITION_TOLERANCE_PIX. 133 */ 134 @MediumTest 135 @Feature({ "TextSelection", "Main" }) 136 public void testNoneditableSelectionHandles() throws Throwable { 137 doSelectionHandleTest(TestPageType.NONEDITABLE); 138 } 139 140 /** 141 * Verifies that when a long-press is performed on editable text (within a 142 * textarea), selection handles appear and that handles can be dragged to 143 * extend the selection. Does not check exact handle position as this will 144 * depend on screen size; instead, position is expected to be correct within 145 * HANDLE_POSITION_TOLERANCE_PIX. 146 */ 147 @MediumTest 148 @Feature({ "TextSelection" }) 149 public void testEditableSelectionHandles() throws Throwable { 150 doSelectionHandleTest(TestPageType.EDITABLE); 151 } 152 153 private void doSelectionHandleTest(TestPageType pageType) throws Throwable { 154 launchWithUrl(pageType.dataUrl); 155 156 clickNodeToShowSelectionHandles(pageType.nodeId); 157 assertWaitForSelectionEditableEquals(pageType.selectionShouldBeEditable); 158 159 HandleView startHandle = getStartHandle(); 160 HandleView endHandle = getEndHandle(); 161 162 Rect nodeWindowBounds = getNodeBoundsPix(pageType.nodeId); 163 164 int leftX = (nodeWindowBounds.left + nodeWindowBounds.centerX()) / 2; 165 int centerX = nodeWindowBounds.centerX(); 166 int rightX = (nodeWindowBounds.right + nodeWindowBounds.centerX()) / 2; 167 168 int topY = (nodeWindowBounds.top + nodeWindowBounds.centerY()) / 2; 169 int centerY = nodeWindowBounds.centerY(); 170 int bottomY = (nodeWindowBounds.bottom + nodeWindowBounds.centerY()) / 2; 171 172 // Drag start handle up and to the left. The selection start should decrease. 173 dragHandleAndCheckSelectionChange(startHandle, leftX, topY, -1, 0); 174 // Drag end handle down and to the right. The selection end should increase. 175 dragHandleAndCheckSelectionChange(endHandle, rightX, bottomY, 0, 1); 176 // Drag start handle back to the middle. The selection start should increase. 177 dragHandleAndCheckSelectionChange(startHandle, centerX, centerY, 1, 0); 178 // Drag end handle up and to the left past the start handle. Both selection start and end 179 // should decrease. 180 dragHandleAndCheckSelectionChange(endHandle, leftX, topY, -1, -1); 181 // Drag start handle down and to the right past the end handle. Both selection start and end 182 // should increase. 183 dragHandleAndCheckSelectionChange(startHandle, rightX, bottomY, 1, 1); 184 185 clickToDismissHandles(); 186 } 187 188 private void dragHandleAndCheckSelectionChange(HandleView handle, int dragToX, int dragToY, 189 final int expectedStartChange, final int expectedEndChange) throws Throwable { 190 String initialText = getContentViewCore().getSelectedText(); 191 final int initialSelectionEnd = getSelectionEnd(); 192 final int initialSelectionStart = getSelectionStart(); 193 194 dragHandleTo(handle, dragToX, dragToY, 10); 195 assertWaitForEitherHandleNear(dragToX, dragToY); 196 197 if (getContentViewCore().isSelectionEditable()) { 198 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 199 @Override 200 public boolean isSatisfied() { 201 int startChange = getSelectionStart() - initialSelectionStart; 202 // TODO(cjhopman): Due to http://crbug.com/244633 we can't really assert that 203 // there is no change when we expect to be able to. 204 if (expectedStartChange != 0) { 205 if ((int) Math.signum(startChange) != expectedStartChange) return false; 206 } 207 208 int endChange = getSelectionEnd() - initialSelectionEnd; 209 if (expectedEndChange != 0) { 210 if ((int) Math.signum(endChange) != expectedEndChange) return false; 211 } 212 213 return true; 214 } 215 })); 216 } 217 218 assertWaitForHandleViewStopped(getStartHandle()); 219 assertWaitForHandleViewStopped(getEndHandle()); 220 } 221 222 private void assertWaitForSelectionEditableEquals(final boolean expected) throws Throwable { 223 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 224 @Override 225 public boolean isSatisfied() { 226 return getContentViewCore().isSelectionEditable() == expected; 227 } 228 })); 229 } 230 231 private void assertWaitForHandleViewStopped(final HandleView handle) throws Throwable { 232 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 233 private Point position = new Point(-1, -1); 234 @Override 235 public boolean isSatisfied() { 236 Point lastPosition = position; 237 position = getHandlePosition(handle); 238 return !handle.isDragging() && 239 position.equals(lastPosition); 240 } 241 })); 242 } 243 244 /** 245 * Verifies that when a selection is made within static page text, that the 246 * contextual action bar of the correct type is displayed. Also verified 247 * that the bar disappears upon deselection. 248 */ 249 @MediumTest 250 @Feature({ "TextSelection" }) 251 public void testNoneditableSelectionActionBar() throws Throwable { 252 doSelectionActionBarTest(TestPageType.NONEDITABLE); 253 } 254 255 /** 256 * Verifies that when a selection is made within editable text, that the 257 * contextual action bar of the correct type is displayed. Also verified 258 * that the bar disappears upon deselection. 259 */ 260 @MediumTest 261 @Feature({ "TextSelection" }) 262 public void testEditableSelectionActionBar() throws Throwable { 263 doSelectionActionBarTest(TestPageType.EDITABLE); 264 } 265 266 private void doSelectionActionBarTest(TestPageType pageType) throws Throwable { 267 launchWithUrl(pageType.dataUrl); 268 assertFalse(getContentViewCore().isSelectActionBarShowing()); 269 clickNodeToShowSelectionHandles(pageType.nodeId); 270 assertWaitForSelectActionBarShowingEquals(true); 271 clickToDismissHandles(); 272 assertWaitForSelectActionBarShowingEquals(false); 273 } 274 275 private static Point getHandlePosition(final HandleView handle) { 276 return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Point>() { 277 @Override 278 public Point call() { 279 return new Point(handle.getAdjustedPositionX(), handle.getAdjustedPositionY()); 280 } 281 }); 282 } 283 284 private static boolean isHandleNear(HandleView handle, int x, int y) { 285 Point position = getHandlePosition(handle); 286 return (Math.abs(position.x - x) < HANDLE_POSITION_X_TOLERANCE_PIX) && 287 (Math.abs(position.y - y) < HANDLE_POSITION_Y_TOLERANCE_PIX); 288 } 289 290 private void assertWaitForHandleNear(final HandleView handle, final int x, final int y) 291 throws Throwable { 292 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 293 @Override 294 public boolean isSatisfied() { 295 return isHandleNear(handle, x, y); 296 } 297 })); 298 } 299 300 private void assertWaitForEitherHandleNear(final int x, final int y) throws Throwable { 301 final HandleView startHandle = getStartHandle(); 302 final HandleView endHandle = getEndHandle(); 303 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 304 @Override 305 public boolean isSatisfied() { 306 return isHandleNear(startHandle, x, y) || isHandleNear(endHandle, x, y); 307 } 308 })); 309 } 310 311 private void assertWaitForHandlesShowingEquals(final boolean shouldBeShowing) throws Throwable { 312 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 313 @Override 314 public boolean isSatisfied() { 315 SelectionHandleController shc = 316 getContentViewCore().getSelectionHandleControllerForTest(); 317 boolean isShowing = shc != null && shc.isShowing(); 318 return shouldBeShowing == isShowing; 319 } 320 })); 321 } 322 323 324 private void dragHandleTo(final HandleView handle, final int dragToX, final int dragToY, 325 final int steps) throws Throwable { 326 ContentView view = getContentView(); 327 assertTrue(ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() { 328 @Override 329 public Boolean call() { 330 int adjustedX = handle.getAdjustedPositionX(); 331 int adjustedY = handle.getAdjustedPositionY(); 332 int realX = handle.getPositionX(); 333 int realY = handle.getPositionY(); 334 335 int realDragToX = dragToX + (realX - adjustedX); 336 int realDragToY = dragToY + (realY - adjustedY); 337 338 ContentView view = getContentView(); 339 int[] fromLocation = TestTouchUtils.getAbsoluteLocationFromRelative( 340 view, realX, realY); 341 int[] toLocation = TestTouchUtils.getAbsoluteLocationFromRelative( 342 view, realDragToX, realDragToY); 343 344 long downTime = SystemClock.uptimeMillis(); 345 MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 346 fromLocation[0], fromLocation[1], 0); 347 handle.dispatchTouchEvent(event); 348 349 if (!handle.isDragging()) return false; 350 351 for (int i = 0; i < steps; i++) { 352 float scale = (float) (i + 1) / steps; 353 int x = fromLocation[0] + (int) (scale * (toLocation[0] - fromLocation[0])); 354 int y = fromLocation[1] + (int) (scale * (toLocation[1] - fromLocation[1])); 355 long eventTime = SystemClock.uptimeMillis(); 356 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, 357 x, y, 0); 358 handle.dispatchTouchEvent(event); 359 } 360 long upTime = SystemClock.uptimeMillis(); 361 event = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP, 362 toLocation[0], toLocation[1], 0); 363 handle.dispatchTouchEvent(event); 364 365 return !handle.isDragging(); 366 } 367 })); 368 } 369 370 private Rect getNodeBoundsPix(String nodeId) throws Throwable { 371 Rect nodeBounds = DOMUtils.getNodeBounds(getContentView(), 372 new TestCallbackHelperContainer(getContentView()), nodeId); 373 374 RenderCoordinates renderCoordinates = getContentView().getRenderCoordinates(); 375 int offsetX = getContentView().getContentViewCore().getViewportSizeOffsetWidthPix(); 376 int offsetY = getContentView().getContentViewCore().getViewportSizeOffsetHeightPix(); 377 378 int left = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX; 379 int right = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX; 380 int top = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY; 381 int bottom = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY; 382 383 return new Rect(left, top, right, bottom); 384 } 385 386 private void clickNodeToShowSelectionHandles(String nodeId) throws Throwable { 387 Rect nodeWindowBounds = getNodeBoundsPix(nodeId); 388 389 TouchCommon touchCommon = new TouchCommon(this); 390 int centerX = nodeWindowBounds.centerX(); 391 int centerY = nodeWindowBounds.centerY(); 392 touchCommon.longPressView(getContentView(), centerX, centerY); 393 394 assertWaitForHandlesShowingEquals(true); 395 396 // No words wrap in the sample text so handles should be at the same y 397 // position. 398 assertEquals(getStartHandle().getPositionY(), getEndHandle().getPositionY()); 399 400 // In ContentShell, the handles are initially misplaced when they first appear. This is 401 // fixed after the first time they are dragged (or the page is scrolled). 402 // TODO(cjhopman): Fix this problem in ContentShell: http://crbug.com/243836 403 dragHandleTo(getStartHandle(), centerX - 40, centerY - 40, 1); 404 assertWaitForHandleViewStopped(getStartHandle()); 405 } 406 407 private void clickToDismissHandles() throws Throwable { 408 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation()); 409 new TouchCommon(this).singleClickView(getContentView(), 0, 0); 410 assertWaitForHandlesShowingEquals(false); 411 } 412 413 private void assertWaitForSelectActionBarShowingEquals(final boolean shouldBeShowing) 414 throws InterruptedException { 415 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 416 @Override 417 public boolean isSatisfied() { 418 return shouldBeShowing == getContentViewCore().isSelectActionBarShowing(); 419 } 420 })); 421 } 422 423 private ImeAdapter getImeAdapter() { 424 return getContentViewCore().getImeAdapterForTest(); 425 } 426 427 private int getSelectionStart() { 428 return Selection.getSelectionStart(getEditable()); 429 } 430 431 private int getSelectionEnd() { 432 return Selection.getSelectionEnd(getEditable()); 433 } 434 435 private Editable getEditable() { 436 return getContentViewCore().getEditableForTest(); 437 } 438 439 private HandleView getStartHandle() { 440 SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest(); 441 return shc.getStartHandleViewForTest(); 442 } 443 444 private HandleView getEndHandle() { 445 SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest(); 446 return shc.getEndHandleViewForTest(); 447 } 448} 449