1// Copyright (c) 2012 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.content.ClipData; 8import android.content.ClipboardManager; 9import android.content.Context; 10import android.graphics.Rect; 11import android.test.suitebuilder.annotation.MediumTest; 12import android.text.Editable; 13import android.text.Selection; 14import android.view.KeyEvent; 15 16import org.chromium.base.test.util.DisabledTest; 17import org.chromium.base.test.util.Feature; 18import org.chromium.base.test.util.UrlUtils; 19import org.chromium.content.browser.ContentView; 20import org.chromium.content.browser.RenderCoordinates; 21import org.chromium.content.browser.test.util.Criteria; 22import org.chromium.content.browser.test.util.CriteriaHelper; 23import org.chromium.content.browser.test.util.DOMUtils; 24import org.chromium.content.browser.test.util.TestCallbackHelperContainer; 25import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper; 26import org.chromium.content.browser.test.util.TestTouchUtils; 27import org.chromium.content.browser.test.util.TouchCommon; 28import org.chromium.content_shell_apk.ContentShellTestBase; 29 30public class InsertionHandleTest extends ContentShellTestBase { 31 private static final String META_DISABLE_ZOOM = 32 "<meta name=\"viewport\" content=\"" + 33 "height=device-height," + 34 "width=device-width," + 35 "initial-scale=1.0," + 36 "minimum-scale=1.0," + 37 "maximum-scale=1.0," + 38 "\" />"; 39 40 private static final String TEXTAREA_ID = "textarea"; 41 private static final String TEXTAREA_DATA_URL = UrlUtils.encodeHtmlDataUri( 42 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" + 43 "<textarea id=\"" + TEXTAREA_ID + "\" cols=\"20\" rows=\"10\">" + 44 "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor " + 45 "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud " + 46 "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute " + 47 "irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " + 48 "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " + 49 "officia deserunt mollit anim id est laborum." + 50 "</textarea>" + 51 "</body></html>"); 52 53 private static final String INPUT_TEXT_ID = "input_text"; 54 private static final String INPUT_TEXT_DATA_URL = UrlUtils.encodeHtmlDataUri( 55 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" + 56 "<input id=\"input_text\" type=\"text\" value=\"" + 57 "T0D0(cjhopman): put amusing sample text here. Make sure it is at least " + 58 "100 characters. 123456789012345678901234567890\" size=20></input>" + 59 "</body></html>"); 60 61 // Offset to compensate for the fact that the handle is below the text. 62 private static final int VERTICAL_OFFSET = 10; 63 private static final int HANDLE_POSITION_TOLERANCE = 20; 64 private static final String PASTE_TEXT = "**test text to paste**"; 65 66 67 public void launchWithUrl(String url) throws Throwable { 68 launchContentShellWithUrl(url); 69 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading()); 70 assertWaitForPageScaleFactorMatch(1.0f); 71 72 // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never 73 // brought up. 74 getImeAdapter().setInputMethodManagerWrapper( 75 new TestInputMethodManagerWrapper(getContentViewCore())); 76 } 77 78 @MediumTest 79 @Feature({"TextSelection", "TextInput", "Main"}) 80 public void testUnselectHidesHandle() throws Throwable { 81 launchWithUrl(TEXTAREA_DATA_URL); 82 clickNodeToShowInsertionHandle(TEXTAREA_ID); 83 84 // Unselecting should cause the handle to disappear. 85 getImeAdapter().unselect(); 86 assertTrue(waitForHandleShowingEquals(false)); 87 } 88 89 90 @MediumTest 91 @Feature({"TextSelection", "TextInput", "Main"}) 92 public void testKeyEventHidesHandle() throws Throwable { 93 launchWithUrl(TEXTAREA_DATA_URL); 94 clickNodeToShowInsertionHandle(TEXTAREA_ID); 95 96 getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X)); 97 getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_X)); 98 assertTrue(waitForHandleShowingEquals(false)); 99 } 100 101 /** 102 * @MediumTest 103 * @Feature({"TextSelection", "TextInput", "Main"}) 104 * http://crbug.com/169648 105 */ 106 @DisabledTest 107 public void testDragInsertionHandle() throws Throwable { 108 launchWithUrl(TEXTAREA_DATA_URL); 109 110 clickNodeToShowInsertionHandle(TEXTAREA_ID); 111 112 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest(); 113 HandleView handle = ihc.getHandleViewForTest(); 114 115 int initialX = handle.getPositionX(); 116 int initialY = handle.getPositionY(); 117 int dragToX = initialX + 120; 118 int dragToY = initialY + 120; 119 120 dragHandleTo(dragToX, dragToY); 121 assertWaitForHandleNear(dragToX, dragToY); 122 } 123 124 125 @MediumTest 126 @Feature({"TextSelection", "TextInput", "Main"}) 127 public void testPasteAtInsertionHandle() throws Throwable { 128 launchWithUrl(TEXTAREA_DATA_URL); 129 130 clickNodeToShowInsertionHandle(TEXTAREA_ID); 131 132 int offset = getSelectionStart(); 133 String initialText = getEditableText(); 134 135 saveToClipboard(PASTE_TEXT); 136 pasteOnMainSync(); 137 138 String expectedText = 139 initialText.substring(0, offset) + PASTE_TEXT + initialText.substring(offset); 140 assertTrue(waitForEditableTextEquals(expectedText)); 141 assertTrue(waitForHandleShowingEquals(false)); 142 } 143 144 /** 145 * @MediumTest 146 * @Feature({"TextSelection", "TextInput", "Main"}) 147 * http://crbug.com/169648 148 */ 149 @DisabledTest 150 public void testDragInsertionHandleInputText() throws Throwable { 151 launchWithUrl(INPUT_TEXT_DATA_URL); 152 153 clickNodeToShowInsertionHandle(INPUT_TEXT_ID); 154 155 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest(); 156 HandleView handle = ihc.getHandleViewForTest(); 157 158 int initialX = handle.getPositionX(); 159 int initialY = handle.getPositionY(); 160 int dragToX = initialX + 120; 161 int dragToY = initialY; 162 dragHandleTo(dragToX, dragToY); 163 assertWaitForHandleNear(dragToX, initialY); 164 165 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation()); 166 167 initialX = handle.getPositionX(); 168 initialY = handle.getPositionY(); 169 dragToY = initialY + 120; 170 dragHandleTo(initialX, dragToY); 171 // Vertical drag should not change the y-position. 172 assertWaitForHandleNear(initialX, initialY); 173 } 174 175 /** 176 * @MediumTest 177 * @Feature({"TextSelection", "TextInput", "Main"}) 178 * http://crbug.com/169648 179 */ 180 @DisabledTest 181 public void testDragInsertionHandleInputTextOutsideBounds() throws Throwable { 182 launchWithUrl(INPUT_TEXT_DATA_URL); 183 184 clickNodeToShowInsertionHandle(INPUT_TEXT_ID); 185 186 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest(); 187 HandleView handle = ihc.getHandleViewForTest(); 188 189 int initialX = handle.getPositionX(); 190 int initialY = handle.getPositionY(); 191 int dragToX = initialX; 192 int dragToY = initialY + 150; 193 194 // A vertical drag should not move the insertion handle. 195 dragHandleTo(dragToX, dragToY); 196 assertWaitForHandleNear(initialX, initialY); 197 198 // The input box does not go to the edge of the screen, and neither should the insertion 199 // handle. 200 dragToX = getContentView().getWidth(); 201 dragHandleTo(dragToX, dragToY); 202 assertTrue(handle.getPositionX() < dragToX - 100); 203 } 204 205 @Override 206 protected void tearDown() throws Exception { 207 super.tearDown(); 208 // No way to just clear clipboard, so setting to empty string instead. 209 saveToClipboard(""); 210 } 211 212 private void clickNodeToShowInsertionHandle(String nodeId) throws Throwable { 213 // On the first click the keyboard will be displayed but no insertion handles. On the second 214 // click (only if it changes the selection), the insertion handle is displayed. So that the 215 // second click changes the selection, the two clicks should be in sufficiently different 216 // locations. 217 Rect nodeBounds = DOMUtils.getNodeBounds(getContentView(), 218 new TestCallbackHelperContainer(getContentView()), nodeId); 219 220 RenderCoordinates renderCoordinates = getContentView().getRenderCoordinates(); 221 int offsetX = getContentView().getContentViewCore().getViewportSizeOffsetWidthPix(); 222 int offsetY = getContentView().getContentViewCore().getViewportSizeOffsetHeightPix(); 223 float left = renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX; 224 float right = renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX; 225 float top = renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY; 226 float bottom = renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY; 227 228 TouchCommon touchCommon = new TouchCommon(this); 229 touchCommon.singleClickView(getContentView(), 230 (int)(left + 3 * (right - left) / 4), (int)(top + (bottom - top) / 2)); 231 232 233 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation()); 234 assertTrue(waitForHasSelectionPosition()); 235 236 // TODO(cjhopman): Wait for keyboard display finished? 237 touchCommon.singleClickView(getContentView(), 238 (int)(left + (right - left) / 4), (int)(top + (bottom - top) / 2)); 239 assertTrue(waitForHandleShowingEquals(true)); 240 assertTrue(waitForHandleViewStopped()); 241 } 242 243 private boolean waitForHandleViewStopped() throws Throwable { 244 // If the polling interval is too short, slowly moving may be detected as not moving. 245 final int POLLING_INTERVAL = 200; 246 return CriteriaHelper.pollForCriteria(new Criteria() { 247 int mPositionX = -1; 248 int mPositionY = -1; 249 @Override 250 public boolean isSatisfied() { 251 int lastPositionX = mPositionX; 252 int lastPositionY = mPositionY; 253 InsertionHandleController ihc = 254 getContentViewCore().getInsertionHandleControllerForTest(); 255 HandleView handle = ihc.getHandleViewForTest(); 256 mPositionX = handle.getPositionX(); 257 mPositionY = handle.getPositionY(); 258 return !handle.isDragging() && 259 mPositionX == lastPositionX && mPositionY == lastPositionY; 260 } 261 }, CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL, POLLING_INTERVAL); 262 } 263 264 private boolean waitForEditableTextEquals(final String expectedText) 265 throws Throwable { 266 return CriteriaHelper.pollForCriteria(new Criteria() { 267 @Override 268 public boolean isSatisfied() { 269 return getEditableText().trim().equals(expectedText.trim()); 270 } 271 }); 272 } 273 274 private boolean waitForHasSelectionPosition() 275 throws Throwable { 276 return CriteriaHelper.pollForCriteria(new Criteria() { 277 @Override 278 public boolean isSatisfied() { 279 int start = getSelectionStart(); 280 int end = getSelectionEnd(); 281 return start > 0 && start == end; 282 } 283 }); 284 } 285 286 private void dragHandleTo(int dragToX, int dragToY, int steps) throws Throwable { 287 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest(); 288 HandleView handle = ihc.getHandleViewForTest(); 289 int initialX = handle.getPositionX(); 290 int initialY = handle.getPositionY(); 291 ContentView view = getContentView(); 292 293 int fromLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, initialX, 294 initialY + VERTICAL_OFFSET); 295 int toLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, dragToX, 296 dragToY + VERTICAL_OFFSET); 297 298 long downTime = TestTouchUtils.dragStart(getInstrumentation(), fromLocation[0], 299 fromLocation[1]); 300 assertWaitForHandleDraggingEquals(true); 301 TestTouchUtils.dragTo(getInstrumentation(), fromLocation[0], toLocation[0], 302 fromLocation[1], toLocation[1], steps, downTime); 303 TestTouchUtils.dragEnd(getInstrumentation(), toLocation[0], toLocation[1], downTime); 304 assertWaitForHandleDraggingEquals(false); 305 assertTrue(waitForHandleViewStopped()); 306 } 307 308 private void dragHandleTo(int dragToX, int dragToY) throws Throwable { 309 dragHandleTo(dragToX, dragToY, 5); 310 } 311 312 private void assertWaitForHandleDraggingEquals(final boolean expected) throws Throwable { 313 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest(); 314 final HandleView handle = ihc.getHandleViewForTest(); 315 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 316 @Override 317 public boolean isSatisfied() { 318 return handle.isDragging() == expected; 319 } 320 })); 321 } 322 323 private static boolean isHandleNear(HandleView handle, int x, int y) { 324 return (Math.abs(handle.getPositionX() - x) < HANDLE_POSITION_TOLERANCE) && 325 (Math.abs(handle.getPositionY() - VERTICAL_OFFSET - y) < HANDLE_POSITION_TOLERANCE); 326 } 327 328 private void assertWaitForHandleNear(final int x, final int y) throws Throwable { 329 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest(); 330 final HandleView handle = ihc.getHandleViewForTest(); 331 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() { 332 @Override 333 public boolean isSatisfied() { 334 return isHandleNear(handle, x, y); 335 } 336 })); 337 } 338 339 private boolean waitForHandleShowingEquals(final boolean shouldBeShowing) 340 throws Throwable { 341 return CriteriaHelper.pollForCriteria(new Criteria() { 342 @Override 343 public boolean isSatisfied() { 344 InsertionHandleController ihc = 345 getContentViewCore().getInsertionHandleControllerForTest(); 346 boolean isShowing = ihc != null && ihc.isShowing(); 347 return isShowing == shouldBeShowing; 348 } 349 }); 350 } 351 352 private void pasteOnMainSync() { 353 getInstrumentation().runOnMainSync(new Runnable() { 354 @Override 355 public void run() { 356 getContentViewCore().getInsertionHandleControllerForTest().paste(); 357 } 358 }); 359 } 360 361 private int getSelectionStart() { 362 return Selection.getSelectionStart(getEditable()); 363 } 364 365 private int getSelectionEnd() { 366 return Selection.getSelectionEnd(getEditable()); 367 } 368 369 private Editable getEditable() { 370 return getContentViewCore().getEditableForTest(); 371 } 372 373 private String getEditableText() { 374 return getEditable().toString(); 375 } 376 377 private void saveToClipboard(String text) { 378 ClipboardManager clipMgr = 379 (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE); 380 clipMgr.setPrimaryClip(ClipData.newPlainText(null, text)); 381 } 382 383 private ImeAdapter getImeAdapter() { 384 return getContentViewCore().getImeAdapterForTest(); 385 } 386} 387