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;
15 import android.view.View;
17 import org.chromium.base.test.util.DisabledTest;
18 import org.chromium.base.test.util.Feature;
19 import org.chromium.base.test.util.UrlUtils;
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.TestInputMethodManagerWrapper;
25 import org.chromium.content.browser.test.util.TestTouchUtils;
26 import org.chromium.content.browser.test.util.TouchCommon;
27 import org.chromium.content_shell_apk.ContentShellTestBase;
30 * Tests the insertion handle that allows users to paste text.
32 public class InsertionHandleTest extends ContentShellTestBase {
33 private static final String META_DISABLE_ZOOM =
34 "<meta name=\"viewport\" content=\"" +
35 "height=device-height," +
36 "width=device-width," +
37 "initial-scale=1.0," +
38 "minimum-scale=1.0," +
39 "maximum-scale=1.0," +
42 private static final String TEXTAREA_ID = "textarea";
43 private static final String TEXTAREA_DATA_URL = UrlUtils.encodeHtmlDataUri(
44 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
45 "<textarea id=\"" + TEXTAREA_ID + "\" cols=\"20\" rows=\"10\">" +
46 "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor " +
47 "incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud " +
48 "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute " +
49 "irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla " +
50 "pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " +
51 "officia deserunt mollit anim id est laborum." +
55 private static final String INPUT_TEXT_ID = "input_text";
56 private static final String INPUT_TEXT_DATA_URL = UrlUtils.encodeHtmlDataUri(
57 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
58 "<input id=\"input_text\" type=\"text\" value=\"" +
59 "T0D0(cjhopman): put amusing sample text here. Make sure it is at least " +
60 "100 characters. 123456789012345678901234567890\" size=20></input>" +
63 // Offset to compensate for the fact that the handle is below the text.
64 private static final int VERTICAL_OFFSET = 10;
65 private static final int HANDLE_POSITION_TOLERANCE = 20;
66 private static final String PASTE_TEXT = "**test text to paste**";
69 public void launchWithUrl(String url) throws Throwable {
70 launchContentShellWithUrl(url);
71 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
72 assertWaitForPageScaleFactorMatch(1.0f);
74 // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never
76 getContentViewCore().getImeAdapterForTest().setInputMethodManagerWrapper(
77 new TestInputMethodManagerWrapper(getContentViewCore()));
81 @Feature({"TextSelection", "TextInput", "Main"})
82 public void testUnselectHidesHandle() throws Throwable {
83 launchWithUrl(TEXTAREA_DATA_URL);
84 clickNodeToShowInsertionHandle(TEXTAREA_ID);
86 // Unselecting should cause the handle to disappear.
88 assertTrue(waitForHandleShowingEquals(false));
94 * @Feature({"TextSelection", "TextInput", "Main"})
95 * http://crbug.com/169648
98 public void testKeyEventHidesHandle() throws Throwable {
99 launchWithUrl(TEXTAREA_DATA_URL);
100 clickNodeToShowInsertionHandle(TEXTAREA_ID);
102 getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X));
103 getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_X));
104 assertTrue(waitForHandleShowingEquals(false));
109 * @Feature({"TextSelection", "TextInput", "Main"})
110 * http://crbug.com/169648
113 public void testDragInsertionHandle() throws Throwable {
114 launchWithUrl(TEXTAREA_DATA_URL);
116 clickNodeToShowInsertionHandle(TEXTAREA_ID);
118 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
119 HandleView handle = ihc.getHandleViewForTest();
121 int initialX = handle.getPositionX();
122 int initialY = handle.getPositionY();
123 int dragToX = initialX + 120;
124 int dragToY = initialY + 120;
126 dragHandleTo(dragToX, dragToY);
127 assertWaitForHandleNear(dragToX, dragToY);
132 @Feature({"TextSelection", "TextInput", "Main"})
133 public void testPasteAtInsertionHandle() throws Throwable {
134 launchWithUrl(TEXTAREA_DATA_URL);
136 clickNodeToShowInsertionHandle(TEXTAREA_ID);
138 int offset = getSelectionStart();
139 String initialText = getEditableText();
141 saveToClipboard(PASTE_TEXT);
144 String expectedText =
145 initialText.substring(0, offset) + PASTE_TEXT + initialText.substring(offset);
146 assertTrue(waitForEditableTextEquals(expectedText));
147 assertTrue(waitForHandleShowingEquals(false));
152 * @Feature({"TextSelection", "TextInput", "Main"})
153 * http://crbug.com/169648
156 public void testDragInsertionHandleInputText() throws Throwable {
157 launchWithUrl(INPUT_TEXT_DATA_URL);
159 clickNodeToShowInsertionHandle(INPUT_TEXT_ID);
161 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
162 HandleView handle = ihc.getHandleViewForTest();
164 int initialX = handle.getPositionX();
165 int initialY = handle.getPositionY();
166 int dragToX = initialX + 120;
167 int dragToY = initialY;
168 dragHandleTo(dragToX, dragToY);
169 assertWaitForHandleNear(dragToX, initialY);
171 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
173 initialX = handle.getPositionX();
174 initialY = handle.getPositionY();
175 dragToY = initialY + 120;
176 dragHandleTo(initialX, dragToY);
177 // Vertical drag should not change the y-position.
178 assertWaitForHandleNear(initialX, initialY);
183 * @Feature({"TextSelection", "TextInput", "Main"})
184 * http://crbug.com/169648
187 public void testDragInsertionHandleInputTextOutsideBounds() throws Throwable {
188 launchWithUrl(INPUT_TEXT_DATA_URL);
190 clickNodeToShowInsertionHandle(INPUT_TEXT_ID);
192 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
193 HandleView handle = ihc.getHandleViewForTest();
195 int initialX = handle.getPositionX();
196 int initialY = handle.getPositionY();
197 int dragToX = initialX;
198 int dragToY = initialY + 150;
200 // A vertical drag should not move the insertion handle.
201 dragHandleTo(dragToX, dragToY);
202 assertWaitForHandleNear(initialX, initialY);
204 // The input box does not go to the edge of the screen, and neither should the insertion
206 dragToX = getContentViewCore().getContainerView().getWidth();
207 dragHandleTo(dragToX, dragToY);
208 assertTrue(handle.getPositionX() < dragToX - 100);
212 protected void tearDown() throws Exception {
214 // No way to just clear clipboard, so setting to empty string instead.
218 private void clickNodeToShowInsertionHandle(String nodeId) throws Throwable {
219 // On the first click the keyboard will be displayed but no insertion handles. On the second
220 // click (only if it changes the selection), the insertion handle is displayed. So that the
221 // second click changes the selection, the two clicks should be in sufficiently different
223 Rect nodeBounds = DOMUtils.getNodeBounds(getContentViewCore(), nodeId);
225 RenderCoordinates renderCoordinates = getContentViewCore().getRenderCoordinates();
226 int offsetX = getContentViewCore().getViewportSizeOffsetWidthPix();
227 int offsetY = getContentViewCore().getViewportSizeOffsetHeightPix();
228 float left = renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX;
229 float right = renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX;
230 float top = renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY;
231 float bottom = renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY;
233 TouchCommon touchCommon = new TouchCommon(this);
234 touchCommon.singleClickView(getContentViewCore().getContainerView(),
235 (int)(left + 3 * (right - left) / 4), (int)(top + (bottom - top) / 2));
238 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
239 assertTrue(waitForHasSelectionPosition());
241 // TODO(cjhopman): Wait for keyboard display finished?
242 touchCommon.singleClickView(getContentViewCore().getContainerView(),
243 (int)(left + (right - left) / 4), (int)(top + (bottom - top) / 2));
244 assertTrue(waitForHandleShowingEquals(true));
245 assertTrue(waitForHandleViewStopped());
248 private boolean waitForHandleViewStopped() throws Throwable {
249 // If the polling interval is too short, slowly moving may be detected as not moving.
250 final int POLLING_INTERVAL = 200;
251 return CriteriaHelper.pollForCriteria(new Criteria() {
255 public boolean isSatisfied() {
256 int lastPositionX = mPositionX;
257 int lastPositionY = mPositionY;
258 InsertionHandleController ihc =
259 getContentViewCore().getInsertionHandleControllerForTest();
260 HandleView handle = ihc.getHandleViewForTest();
261 mPositionX = handle.getPositionX();
262 mPositionY = handle.getPositionY();
263 return !handle.isDragging() &&
264 mPositionX == lastPositionX && mPositionY == lastPositionY;
266 }, CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL, POLLING_INTERVAL);
269 private boolean waitForEditableTextEquals(final String expectedText)
271 return CriteriaHelper.pollForCriteria(new Criteria() {
273 public boolean isSatisfied() {
274 return getEditableText().trim().equals(expectedText.trim());
279 private boolean waitForHasSelectionPosition()
281 return CriteriaHelper.pollForCriteria(new Criteria() {
283 public boolean isSatisfied() {
284 int start = getSelectionStart();
285 int end = getSelectionEnd();
286 return start > 0 && start == end;
291 private void dragHandleTo(int dragToX, int dragToY, int steps) throws Throwable {
292 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
293 HandleView handle = ihc.getHandleViewForTest();
294 int initialX = handle.getPositionX();
295 int initialY = handle.getPositionY();
296 View view = getContentViewCore().getContainerView();
298 int fromLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, initialX,
299 initialY + VERTICAL_OFFSET);
300 int toLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, dragToX,
301 dragToY + VERTICAL_OFFSET);
303 long downTime = TestTouchUtils.dragStart(getInstrumentation(), fromLocation[0],
305 assertWaitForHandleDraggingEquals(true);
306 TestTouchUtils.dragTo(getInstrumentation(), fromLocation[0], toLocation[0],
307 fromLocation[1], toLocation[1], steps, downTime);
308 TestTouchUtils.dragEnd(getInstrumentation(), toLocation[0], toLocation[1], downTime);
309 assertWaitForHandleDraggingEquals(false);
310 assertTrue(waitForHandleViewStopped());
313 private void dragHandleTo(int dragToX, int dragToY) throws Throwable {
314 dragHandleTo(dragToX, dragToY, 5);
317 private void assertWaitForHandleDraggingEquals(final boolean expected) throws Throwable {
318 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
319 final HandleView handle = ihc.getHandleViewForTest();
320 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
322 public boolean isSatisfied() {
323 return handle.isDragging() == expected;
328 private static boolean isHandleNear(HandleView handle, int x, int y) {
329 return (Math.abs(handle.getPositionX() - x) < HANDLE_POSITION_TOLERANCE) &&
330 (Math.abs(handle.getPositionY() - VERTICAL_OFFSET - y) < HANDLE_POSITION_TOLERANCE);
333 private void assertWaitForHandleNear(final int x, final int y) throws Throwable {
334 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
335 final HandleView handle = ihc.getHandleViewForTest();
336 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
338 public boolean isSatisfied() {
339 return isHandleNear(handle, x, y);
344 private boolean waitForHandleShowingEquals(final boolean shouldBeShowing)
346 return CriteriaHelper.pollForCriteria(new Criteria() {
348 public boolean isSatisfied() {
349 InsertionHandleController ihc =
350 getContentViewCore().getInsertionHandleControllerForTest();
351 boolean isShowing = ihc != null && ihc.isShowing();
352 return isShowing == shouldBeShowing;
357 private void pasteOnMainSync() {
358 getInstrumentation().runOnMainSync(new Runnable() {
361 getContentViewCore().getInsertionHandleControllerForTest().paste();
366 private void unselectOnMainSync() {
367 getInstrumentation().runOnMainSync(new Runnable() {
370 getContentViewCore().getImeAdapterForTest().unselect();
375 private int getSelectionStart() {
376 return Selection.getSelectionStart(getEditable());
379 private int getSelectionEnd() {
380 return Selection.getSelectionEnd(getEditable());
383 private Editable getEditable() {
384 return getContentViewCore().getEditableForTest();
387 private String getEditableText() {
388 return getEditable().toString();
391 private void saveToClipboard(String text) {
392 ClipboardManager clipMgr =
393 (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
394 clipMgr.setPrimaryClip(ClipData.newPlainText(null, text));