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.
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;
30 public 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," +
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." +
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>" +
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**";
67 public void launchWithUrl(String url) throws Throwable {
68 launchContentShellWithUrl(url);
69 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
70 assertWaitForPageScaleFactorMatch(1.0f);
72 // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never
74 getImeAdapter().setInputMethodManagerWrapper(
75 new TestInputMethodManagerWrapper(getContentViewCore()));
79 @Feature({"TextSelection", "TextInput", "Main"})
80 public void testUnselectHidesHandle() throws Throwable {
81 launchWithUrl(TEXTAREA_DATA_URL);
82 clickNodeToShowInsertionHandle(TEXTAREA_ID);
84 // Unselecting should cause the handle to disappear.
85 getImeAdapter().unselect();
86 assertTrue(waitForHandleShowingEquals(false));
92 * @Feature({"TextSelection", "TextInput", "Main"})
93 * http://crbug.com/169648
96 public void testKeyEventHidesHandle() throws Throwable {
97 launchWithUrl(TEXTAREA_DATA_URL);
98 clickNodeToShowInsertionHandle(TEXTAREA_ID);
100 getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_X));
101 getInstrumentation().sendKeySync(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_X));
102 assertTrue(waitForHandleShowingEquals(false));
107 * @Feature({"TextSelection", "TextInput", "Main"})
108 * http://crbug.com/169648
111 public void testDragInsertionHandle() throws Throwable {
112 launchWithUrl(TEXTAREA_DATA_URL);
114 clickNodeToShowInsertionHandle(TEXTAREA_ID);
116 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
117 HandleView handle = ihc.getHandleViewForTest();
119 int initialX = handle.getPositionX();
120 int initialY = handle.getPositionY();
121 int dragToX = initialX + 120;
122 int dragToY = initialY + 120;
124 dragHandleTo(dragToX, dragToY);
125 assertWaitForHandleNear(dragToX, dragToY);
130 @Feature({"TextSelection", "TextInput", "Main"})
131 public void testPasteAtInsertionHandle() throws Throwable {
132 launchWithUrl(TEXTAREA_DATA_URL);
134 clickNodeToShowInsertionHandle(TEXTAREA_ID);
136 int offset = getSelectionStart();
137 String initialText = getEditableText();
139 saveToClipboard(PASTE_TEXT);
142 String expectedText =
143 initialText.substring(0, offset) + PASTE_TEXT + initialText.substring(offset);
144 assertTrue(waitForEditableTextEquals(expectedText));
145 assertTrue(waitForHandleShowingEquals(false));
150 * @Feature({"TextSelection", "TextInput", "Main"})
151 * http://crbug.com/169648
154 public void testDragInsertionHandleInputText() throws Throwable {
155 launchWithUrl(INPUT_TEXT_DATA_URL);
157 clickNodeToShowInsertionHandle(INPUT_TEXT_ID);
159 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
160 HandleView handle = ihc.getHandleViewForTest();
162 int initialX = handle.getPositionX();
163 int initialY = handle.getPositionY();
164 int dragToX = initialX + 120;
165 int dragToY = initialY;
166 dragHandleTo(dragToX, dragToY);
167 assertWaitForHandleNear(dragToX, initialY);
169 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
171 initialX = handle.getPositionX();
172 initialY = handle.getPositionY();
173 dragToY = initialY + 120;
174 dragHandleTo(initialX, dragToY);
175 // Vertical drag should not change the y-position.
176 assertWaitForHandleNear(initialX, initialY);
181 * @Feature({"TextSelection", "TextInput", "Main"})
182 * http://crbug.com/169648
185 public void testDragInsertionHandleInputTextOutsideBounds() throws Throwable {
186 launchWithUrl(INPUT_TEXT_DATA_URL);
188 clickNodeToShowInsertionHandle(INPUT_TEXT_ID);
190 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
191 HandleView handle = ihc.getHandleViewForTest();
193 int initialX = handle.getPositionX();
194 int initialY = handle.getPositionY();
195 int dragToX = initialX;
196 int dragToY = initialY + 150;
198 // A vertical drag should not move the insertion handle.
199 dragHandleTo(dragToX, dragToY);
200 assertWaitForHandleNear(initialX, initialY);
202 // The input box does not go to the edge of the screen, and neither should the insertion
204 dragToX = getContentView().getWidth();
205 dragHandleTo(dragToX, dragToY);
206 assertTrue(handle.getPositionX() < dragToX - 100);
210 protected void tearDown() throws Exception {
212 // No way to just clear clipboard, so setting to empty string instead.
216 private void clickNodeToShowInsertionHandle(String nodeId) throws Throwable {
217 // On the first click the keyboard will be displayed but no insertion handles. On the second
218 // click (only if it changes the selection), the insertion handle is displayed. So that the
219 // second click changes the selection, the two clicks should be in sufficiently different
221 Rect nodeBounds = DOMUtils.getNodeBounds(getContentView(),
222 new TestCallbackHelperContainer(getContentView()), nodeId);
224 RenderCoordinates renderCoordinates = getContentView().getRenderCoordinates();
225 int offsetX = getContentView().getContentViewCore().getViewportSizeOffsetWidthPix();
226 int offsetY = getContentView().getContentViewCore().getViewportSizeOffsetHeightPix();
227 float left = renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX;
228 float right = renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX;
229 float top = renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY;
230 float bottom = renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY;
232 TouchCommon touchCommon = new TouchCommon(this);
233 touchCommon.singleClickView(getContentView(),
234 (int)(left + 3 * (right - left) / 4), (int)(top + (bottom - top) / 2));
237 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
238 assertTrue(waitForHasSelectionPosition());
240 // TODO(cjhopman): Wait for keyboard display finished?
241 touchCommon.singleClickView(getContentView(),
242 (int)(left + (right - left) / 4), (int)(top + (bottom - top) / 2));
243 assertTrue(waitForHandleShowingEquals(true));
244 assertTrue(waitForHandleViewStopped());
247 private boolean waitForHandleViewStopped() throws Throwable {
248 // If the polling interval is too short, slowly moving may be detected as not moving.
249 final int POLLING_INTERVAL = 200;
250 return CriteriaHelper.pollForCriteria(new Criteria() {
254 public boolean isSatisfied() {
255 int lastPositionX = mPositionX;
256 int lastPositionY = mPositionY;
257 InsertionHandleController ihc =
258 getContentViewCore().getInsertionHandleControllerForTest();
259 HandleView handle = ihc.getHandleViewForTest();
260 mPositionX = handle.getPositionX();
261 mPositionY = handle.getPositionY();
262 return !handle.isDragging() &&
263 mPositionX == lastPositionX && mPositionY == lastPositionY;
265 }, CriteriaHelper.DEFAULT_MAX_TIME_TO_POLL, POLLING_INTERVAL);
268 private boolean waitForEditableTextEquals(final String expectedText)
270 return CriteriaHelper.pollForCriteria(new Criteria() {
272 public boolean isSatisfied() {
273 return getEditableText().trim().equals(expectedText.trim());
278 private boolean waitForHasSelectionPosition()
280 return CriteriaHelper.pollForCriteria(new Criteria() {
282 public boolean isSatisfied() {
283 int start = getSelectionStart();
284 int end = getSelectionEnd();
285 return start > 0 && start == end;
290 private void dragHandleTo(int dragToX, int dragToY, int steps) throws Throwable {
291 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
292 HandleView handle = ihc.getHandleViewForTest();
293 int initialX = handle.getPositionX();
294 int initialY = handle.getPositionY();
295 ContentView view = getContentView();
297 int fromLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, initialX,
298 initialY + VERTICAL_OFFSET);
299 int toLocation[] = TestTouchUtils.getAbsoluteLocationFromRelative(view, dragToX,
300 dragToY + VERTICAL_OFFSET);
302 long downTime = TestTouchUtils.dragStart(getInstrumentation(), fromLocation[0],
304 assertWaitForHandleDraggingEquals(true);
305 TestTouchUtils.dragTo(getInstrumentation(), fromLocation[0], toLocation[0],
306 fromLocation[1], toLocation[1], steps, downTime);
307 TestTouchUtils.dragEnd(getInstrumentation(), toLocation[0], toLocation[1], downTime);
308 assertWaitForHandleDraggingEquals(false);
309 assertTrue(waitForHandleViewStopped());
312 private void dragHandleTo(int dragToX, int dragToY) throws Throwable {
313 dragHandleTo(dragToX, dragToY, 5);
316 private void assertWaitForHandleDraggingEquals(final boolean expected) throws Throwable {
317 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
318 final HandleView handle = ihc.getHandleViewForTest();
319 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
321 public boolean isSatisfied() {
322 return handle.isDragging() == expected;
327 private static boolean isHandleNear(HandleView handle, int x, int y) {
328 return (Math.abs(handle.getPositionX() - x) < HANDLE_POSITION_TOLERANCE) &&
329 (Math.abs(handle.getPositionY() - VERTICAL_OFFSET - y) < HANDLE_POSITION_TOLERANCE);
332 private void assertWaitForHandleNear(final int x, final int y) throws Throwable {
333 InsertionHandleController ihc = getContentViewCore().getInsertionHandleControllerForTest();
334 final HandleView handle = ihc.getHandleViewForTest();
335 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
337 public boolean isSatisfied() {
338 return isHandleNear(handle, x, y);
343 private boolean waitForHandleShowingEquals(final boolean shouldBeShowing)
345 return CriteriaHelper.pollForCriteria(new Criteria() {
347 public boolean isSatisfied() {
348 InsertionHandleController ihc =
349 getContentViewCore().getInsertionHandleControllerForTest();
350 boolean isShowing = ihc != null && ihc.isShowing();
351 return isShowing == shouldBeShowing;
356 private void pasteOnMainSync() {
357 getInstrumentation().runOnMainSync(new Runnable() {
360 getContentViewCore().getInsertionHandleControllerForTest().paste();
365 private int getSelectionStart() {
366 return Selection.getSelectionStart(getEditable());
369 private int getSelectionEnd() {
370 return Selection.getSelectionEnd(getEditable());
373 private Editable getEditable() {
374 return getContentViewCore().getEditableForTest();
377 private String getEditableText() {
378 return getEditable().toString();
381 private void saveToClipboard(String text) {
382 ClipboardManager clipMgr =
383 (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
384 clipMgr.setPrimaryClip(ClipData.newPlainText(null, text));
387 private ImeAdapter getImeAdapter() {
388 return getContentViewCore().getImeAdapterForTest();