1 // Copyright 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.
5 package org.chromium.content.browser.input;
7 import android.content.ClipData;
8 import android.content.ClipboardManager;
9 import android.content.Context;
10 import android.graphics.Rect;
11 import android.test.suitebuilder.annotation.MediumTest;
12 import android.text.Editable;
13 import android.text.Selection;
14 import android.view.KeyEvent;
16 import org.chromium.base.test.util.DisabledTest;
17 import org.chromium.base.test.util.Feature;
18 import org.chromium.base.test.util.UrlUtils;
19 import org.chromium.content.browser.ContentView;
20 import org.chromium.content.browser.RenderCoordinates;
21 import org.chromium.content.browser.test.util.Criteria;
22 import org.chromium.content.browser.test.util.CriteriaHelper;
23 import org.chromium.content.browser.test.util.DOMUtils;
24 import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
25 import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
26 import org.chromium.content.browser.test.util.TestTouchUtils;
27 import org.chromium.content.browser.test.util.TouchCommon;
28 import org.chromium.content_shell_apk.ContentShellTestBase;
31 * Tests the insertion handle that allows users to paste text.
33 public class InsertionHandleTest 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," +
43 private static final String TEXTAREA_ID = "textarea";
44 private static final String TEXTAREA_DATA_URL = UrlUtils.encodeHtmlDataUri(
45 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
46 "<textarea id=\"" + TEXTAREA_ID + "\" cols=\"20\" rows=\"10\">" +
47 "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor " +
48 "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud " +
49 "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute " +
50 "irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " +
51 "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
52 "officia deserunt mollit anim id est laborum." +
56 private static final String INPUT_TEXT_ID = "input_text";
57 private static final String INPUT_TEXT_DATA_URL = UrlUtils.encodeHtmlDataUri(
58 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
59 "<input id=\"input_text\" type=\"text\" value=\"" +
60 "T0D0(cjhopman): put amusing sample text here. Make sure it is at least " +
61 "100 characters. 123456789012345678901234567890\" size=20></input>" +
64 // Offset to compensate for the fact that the handle is below the text.
65 private static final int VERTICAL_OFFSET = 10;
66 private static final int HANDLE_POSITION_TOLERANCE = 20;
67 private static final String PASTE_TEXT = "**test text to paste**";
70 public void launchWithUrl(String url) throws Throwable {
71 launchContentShellWithUrl(url);
72 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
73 assertWaitForPageScaleFactorMatch(1.0f);
75 // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never
77 getContentViewCore().getImeAdapterForTest().setInputMethodManagerWrapper(
78 new TestInputMethodManagerWrapper(getContentViewCore()));
82 @Feature({"TextSelection", "TextInput", "Main"})
83 public void testUnselectHidesHandle() throws Throwable {
84 launchWithUrl(TEXTAREA_DATA_URL);
85 clickNodeToShowInsertionHandle(TEXTAREA_ID);
87 // Unselecting should cause the handle to disappear.
89 assertTrue(waitForHandleShowingEquals(false));
95 * @Feature({"TextSelection", "TextInput", "Main"})
96 * http://crbug.com/169648
99 public void testKeyEventHidesHandle() throws Throwable {
100 launchWithUrl(TEXTAREA_DATA_URL);
101 clickNodeToShowInsertionHandle(TEXTAREA_ID);
103 getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X));
104 getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_X));
105 assertTrue(waitForHandleShowingEquals(false));
110 * @Feature({"TextSelection", "TextInput", "Main"})
111 * http://crbug.com/169648
114 public void testDragInsertionHandle() throws Throwable {
115 launchWithUrl(TEXTAREA_DATA_URL);
117 clickNodeToShowInsertionHandle(TEXTAREA_ID);
119 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
120 HandleView handle = ihc.getHandleViewForTest();
122 int initialX = handle.getPositionX();
123 int initialY = handle.getPositionY();
124 int dragToX = initialX + 120;
125 int dragToY = initialY + 120;
127 dragHandleTo(dragToX, dragToY);
128 assertWaitForHandleNear(dragToX, dragToY);
133 @Feature({"TextSelection", "TextInput", "Main"})
134 public void testPasteAtInsertionHandle() throws Throwable {
135 launchWithUrl(TEXTAREA_DATA_URL);
137 clickNodeToShowInsertionHandle(TEXTAREA_ID);
139 int offset = getSelectionStart();
140 String initialText = getEditableText();
142 saveToClipboard(PASTE_TEXT);
145 String expectedText =
146 initialText.substring(0, offset) + PASTE_TEXT + initialText.substring(offset);
147 assertTrue(waitForEditableTextEquals(expectedText));
148 assertTrue(waitForHandleShowingEquals(false));
153 * @Feature({"TextSelection", "TextInput", "Main"})
154 * http://crbug.com/169648
157 public void testDragInsertionHandleInputText() throws Throwable {
158 launchWithUrl(INPUT_TEXT_DATA_URL);
160 clickNodeToShowInsertionHandle(INPUT_TEXT_ID);
162 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
163 HandleView handle = ihc.getHandleViewForTest();
165 int initialX = handle.getPositionX();
166 int initialY = handle.getPositionY();
167 int dragToX = initialX + 120;
168 int dragToY = initialY;
169 dragHandleTo(dragToX, dragToY);
170 assertWaitForHandleNear(dragToX, initialY);
172 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
174 initialX = handle.getPositionX();
175 initialY = handle.getPositionY();
176 dragToY = initialY + 120;
177 dragHandleTo(initialX, dragToY);
178 // Vertical drag should not change the y-position.
179 assertWaitForHandleNear(initialX, initialY);
184 * @Feature({"TextSelection", "TextInput", "Main"})
185 * http://crbug.com/169648
188 public void testDragInsertionHandleInputTextOutsideBounds() throws Throwable {
189 launchWithUrl(INPUT_TEXT_DATA_URL);
191 clickNodeToShowInsertionHandle(INPUT_TEXT_ID);
193 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
194 HandleView handle = ihc.getHandleViewForTest();
196 int initialX = handle.getPositionX();
197 int initialY = handle.getPositionY();
198 int dragToX = initialX;
199 int dragToY = initialY + 150;
201 // A vertical drag should not move the insertion handle.
202 dragHandleTo(dragToX, dragToY);
203 assertWaitForHandleNear(initialX, initialY);
205 // The input box does not go to the edge of the screen, and neither should the insertion
207 dragToX = getContentView().getWidth();
208 dragHandleTo(dragToX, dragToY);
209 assertTrue(handle.getPositionX() < dragToX - 100);
213 * Tests insertion handle visibility relative to the clipping rectangle.
214 * This is currently not implemented using dragHandleTo, because of issues with
215 * http://crbug.com/169648.
218 @Feature({"TextSelection", "TextInput", "Main"})
219 public void testInsertionHandleVisiblity() throws Throwable {
220 launchWithUrl(TEXTAREA_DATA_URL);
221 clickNodeToShowInsertionHandle(TEXTAREA_ID);
223 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
224 HandleView handle = ihc.getHandleViewForTest();
226 assertTrue(handle.isPositionVisible());
228 ihc.setVisibleClippingRectangle(
229 handle.getAdjustedPositionX() + 1, handle.getAdjustedPositionY() + 1,
230 handle.getAdjustedPositionX() + 100, handle.getAdjustedPositionY() + 100);
232 assertFalse(handle.isPositionVisible());
234 ihc.setVisibleClippingRectangle(
235 handle.getAdjustedPositionX() - 1, handle.getAdjustedPositionY() - 1,
236 handle.getAdjustedPositionX() + 1, handle.getAdjustedPositionY() + 1);
238 assertTrue(handle.isPositionVisible());
242 protected void tearDown() throws Exception {
244 // No way to just clear clipboard, so setting to empty string instead.
248 private void clickNodeToShowInsertionHandle(String nodeId) throws Throwable {
249 // On the first click the keyboard will be displayed but no insertion handles. On the second
250 // click (only if it changes the selection), the insertion handle is displayed. So that the
251 // second click changes the selection, the two clicks should be in sufficiently different
253 Rect nodeBounds = DOMUtils.getNodeBounds(getContentView(),
254 new TestCallbackHelperContainer(getContentView()), nodeId);
256 RenderCoordinates renderCoordinates = getContentView().getRenderCoordinates();
257 int offsetX = getContentView().getContentViewCore().getViewportSizeOffsetWidthPix();
258 int offsetY = getContentView().getContentViewCore().getViewportSizeOffsetHeightPix();
259 float left = renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX;
260 float right = renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX;
261 float top = renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY;
262 float bottom = renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY;
264 TouchCommon touchCommon = new TouchCommon(this);
265 touchCommon.singleClickView(getContentView(),
266 (int)(left + 3 * (right - left) / 4), (int)(top + (bottom - top) / 2));
269 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
270 assertTrue(waitForHasSelectionPosition());
272 // TODO(cjhopman): Wait for keyboard display finished?
273 touchCommon.singleClickView(getContentView(),
274 (int)(left + (right - left) / 4), (int)(top + (bottom - top) / 2));
275 assertTrue(waitForHandleShowingEquals(true));
276 assertTrue(waitForHandleViewStopped());
279 private boolean waitForHandleViewStopped() throws Throwable {
280 // If the polling interval is too short, slowly moving may be detected as not moving.
281 final int POLLING_INTERVAL = 200;
282 return CriteriaHelper.pollForCriteria(new Criteria() {
286 public boolean isSatisfied() {
287 int lastPositionX = mPositionX;
288 int lastPositionY = mPositionY;
289 InsertionHandleController ihc =
290 getContentViewCore().getInsertionHandleControllerForTest();
291 HandleView handle = ihc.getHandleViewForTest();
292 mPositionX = handle.getPositionX();
293 mPositionY = handle.getPositionY();
294 return !handle.isDragging() &&
295 mPositionX == lastPositionX && mPositionY == lastPositionY;
297 }, CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL, POLLING_INTERVAL);
300 private boolean waitForEditableTextEquals(final String expectedText)
302 return CriteriaHelper.pollForCriteria(new Criteria() {
304 public boolean isSatisfied() {
305 return getEditableText().trim().equals(expectedText.trim());
310 private boolean waitForHasSelectionPosition()
312 return CriteriaHelper.pollForCriteria(new Criteria() {
314 public boolean isSatisfied() {
315 int start = getSelectionStart();
316 int end = getSelectionEnd();
317 return start > 0 && start == end;
322 private void dragHandleTo(int dragToX, int dragToY, int steps) throws Throwable {
323 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
324 HandleView handle = ihc.getHandleViewForTest();
325 int initialX = handle.getPositionX();
326 int initialY = handle.getPositionY();
327 ContentView view = getContentView();
329 int fromLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, initialX,
330 initialY + VERTICAL_OFFSET);
331 int toLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, dragToX,
332 dragToY + VERTICAL_OFFSET);
334 long downTime = TestTouchUtils.dragStart(getInstrumentation(), fromLocation[0],
336 assertWaitForHandleDraggingEquals(true);
337 TestTouchUtils.dragTo(getInstrumentation(), fromLocation[0], toLocation[0],
338 fromLocation[1], toLocation[1], steps, downTime);
339 TestTouchUtils.dragEnd(getInstrumentation(), toLocation[0], toLocation[1], downTime);
340 assertWaitForHandleDraggingEquals(false);
341 assertTrue(waitForHandleViewStopped());
344 private void dragHandleTo(int dragToX, int dragToY) throws Throwable {
345 dragHandleTo(dragToX, dragToY, 5);
348 private void assertWaitForHandleDraggingEquals(final boolean expected) throws Throwable {
349 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
350 final HandleView handle = ihc.getHandleViewForTest();
351 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
353 public boolean isSatisfied() {
354 return handle.isDragging() == expected;
359 private static boolean isHandleNear(HandleView handle, int x, int y) {
360 return (Math.abs(handle.getPositionX() - x) < HANDLE_POSITION_TOLERANCE) &&
361 (Math.abs(handle.getPositionY() - VERTICAL_OFFSET - y) < HANDLE_POSITION_TOLERANCE);
364 private void assertWaitForHandleNear(final int x, final int y) throws Throwable {
365 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
366 final HandleView handle = ihc.getHandleViewForTest();
367 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
369 public boolean isSatisfied() {
370 return isHandleNear(handle, x, y);
375 private boolean waitForHandleShowingEquals(final boolean shouldBeShowing)
377 return CriteriaHelper.pollForCriteria(new Criteria() {
379 public boolean isSatisfied() {
380 InsertionHandleController ihc =
381 getContentViewCore().getInsertionHandleControllerForTest();
382 boolean isShowing = ihc != null && ihc.isShowing();
383 return isShowing == shouldBeShowing;
388 private void pasteOnMainSync() {
389 getInstrumentation().runOnMainSync(new Runnable() {
392 getContentViewCore().getInsertionHandleControllerForTest().paste();
397 private void unselectOnMainSync() {
398 getInstrumentation().runOnMainSync(new Runnable() {
401 getContentViewCore().getImeAdapterForTest().unselect();
406 private int getSelectionStart() {
407 return Selection.getSelectionStart(getEditable());
410 private int getSelectionEnd() {
411 return Selection.getSelectionEnd(getEditable());
414 private Editable getEditable() {
415 return getContentViewCore().getEditableForTest();
418 private String getEditableText() {
419 return getEditable().toString();
422 private void saveToClipboard(String text) {
423 ClipboardManager clipMgr =
424 (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
425 clipMgr.setPrimaryClip(ClipData.newPlainText(null, text));