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.
5 package org.chromium.content.browser.input;
7 import android.graphics.Point;
8 import android.graphics.Rect;
9 import android.os.SystemClock;
10 import android.test.suitebuilder.annotation.MediumTest;
11 import android.test.FlakyTest;
12 import android.text.Editable;
13 import android.text.Selection;
14 import android.view.MotionEvent;
15 import android.view.View;
16 import android.view.inputmethod.EditorInfo;
18 import java.util.concurrent.Callable;
20 import org.chromium.base.test.util.Feature;
21 import org.chromium.base.test.util.UrlUtils;
22 import org.chromium.base.ThreadUtils;
23 import org.chromium.content.browser.ContentView;
24 import org.chromium.content.browser.RenderCoordinates;
25 import org.chromium.content.browser.test.util.CriteriaHelper;
26 import org.chromium.content.browser.test.util.Criteria;
27 import org.chromium.content.browser.test.util.DOMUtils;
28 import org.chromium.content.browser.test.util.TestCallbackHelperContainer;
29 import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
30 import org.chromium.content.browser.test.util.TestTouchUtils;
31 import org.chromium.content.browser.test.util.TouchCommon;
32 import org.chromium.content_shell_apk.ContentShellTestBase;
34 public class SelectionHandleTest extends ContentShellTestBase {
35 private static final String META_DISABLE_ZOOM =
36 "<meta name=\"viewport\" content=\"" +
37 "height=device-height," +
38 "width=device-width," +
39 "initial-scale=1.0," +
40 "minimum-scale=1.0," +
41 "maximum-scale=1.0," +
44 // For these we use a tiny font-size so that we can be more strict on the expected handle
46 private static final String TEXTAREA_ID = "textarea";
47 private static final String TEXTAREA_DATA_URL = UrlUtils.encodeHtmlDataUri(
48 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
49 "<textarea id=\"" + TEXTAREA_ID +
50 "\" cols=\"40\" rows=\"20\" style=\"font-size:6px\">" +
51 "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 " +
52 "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 " +
53 "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 " +
54 "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 " +
55 "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 " +
56 "o f c a e e u t o l t n m d s l b r m." +
57 "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 " +
58 "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 " +
59 "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 " +
60 "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 " +
61 "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 " +
62 "o f c a e e u t o l t n m d s l b r m." +
66 private static final String NONEDITABLE_DIV_ID = "noneditable";
67 private static final String NONEDITABLE_DATA_URL = UrlUtils.encodeHtmlDataUri(
68 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
69 "<div id=\"" + NONEDITABLE_DIV_ID + "\" style=\"width:200; font-size:6px\">" +
70 "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 " +
71 "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 " +
72 "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 " +
73 "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 " +
74 "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 " +
75 "o f c a e e u t o l t n m d s l b r m." +
76 "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 " +
77 "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 " +
78 "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 " +
79 "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 " +
80 "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 " +
81 "o f c a e e u t o l t n m d s l b r m." +
85 // TODO(cjhopman): These tolerances should be based on the actual width/height of a
87 private static final int HANDLE_POSITION_X_TOLERANCE_PIX = 20;
88 private static final int HANDLE_POSITION_Y_TOLERANCE_PIX = 30;
90 private enum TestPageType {
91 EDITABLE(TEXTAREA_ID, TEXTAREA_DATA_URL, true),
92 NONEDITABLE(NONEDITABLE_DIV_ID, NONEDITABLE_DATA_URL, false);
96 final boolean selectionShouldBeEditable;
98 TestPageType(String nodeId, String dataUrl, boolean selectionShouldBeEditable) {
100 this.dataUrl = dataUrl;
101 this.selectionShouldBeEditable = selectionShouldBeEditable;
105 private void launchWithUrl(String url) throws Throwable {
106 launchContentShellWithUrl(url);
107 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
108 assertWaitForPageScaleFactorMatch(1.0f);
110 // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never
112 getImeAdapter().setInputMethodManagerWrapper(
113 new TestInputMethodManagerWrapper(getContentViewCore()));
116 private void assertWaitForHasSelectionPosition()
118 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
120 public boolean isSatisfied() {
121 int start = getSelectionStart();
122 int end = getSelectionEnd();
123 return start > 0 && start == end;
129 * Verifies that when a long-press is performed on static page text,
130 * selection handles appear and that handles can be dragged to extend the
131 * selection. Does not check exact handle position as this will depend on
132 * screen size; instead, position is expected to be correct within
133 * HANDLE_POSITION_TOLERANCE_PIX.
135 * Test is flaky: crbug.com/290375
137 * @Feature({ "TextSelection", "Main" })
140 public void testNoneditableSelectionHandles() throws Throwable {
141 doSelectionHandleTest(TestPageType.NONEDITABLE);
145 * Verifies that when a long-press is performed on editable text (within a
146 * textarea), selection handles appear and that handles can be dragged to
147 * extend the selection. Does not check exact handle position as this will
148 * depend on screen size; instead, position is expected to be correct within
149 * HANDLE_POSITION_TOLERANCE_PIX.
152 @Feature({ "TextSelection" })
153 public void testEditableSelectionHandles() throws Throwable {
154 doSelectionHandleTest(TestPageType.EDITABLE);
157 private void doSelectionHandleTest(TestPageType pageType) throws Throwable {
158 launchWithUrl(pageType.dataUrl);
160 clickNodeToShowSelectionHandles(pageType.nodeId);
161 assertWaitForSelectionEditableEquals(pageType.selectionShouldBeEditable);
163 HandleView startHandle = getStartHandle();
164 HandleView endHandle = getEndHandle();
166 Rect nodeWindowBounds = getNodeBoundsPix(pageType.nodeId);
168 int leftX = (nodeWindowBounds.left + nodeWindowBounds.centerX()) / 2;
169 int centerX = nodeWindowBounds.centerX();
170 int rightX = (nodeWindowBounds.right + nodeWindowBounds.centerX()) / 2;
172 int topY = (nodeWindowBounds.top + nodeWindowBounds.centerY()) / 2;
173 int centerY = nodeWindowBounds.centerY();
174 int bottomY = (nodeWindowBounds.bottom + nodeWindowBounds.centerY()) / 2;
176 // Drag start handle up and to the left. The selection start should decrease.
177 dragHandleAndCheckSelectionChange(startHandle, leftX, topY, -1, 0);
178 // Drag end handle down and to the right. The selection end should increase.
179 dragHandleAndCheckSelectionChange(endHandle, rightX, bottomY, 0, 1);
180 // Drag start handle back to the middle. The selection start should increase.
181 dragHandleAndCheckSelectionChange(startHandle, centerX, centerY, 1, 0);
182 // Drag end handle up and to the left past the start handle. Both selection start and end
184 dragHandleAndCheckSelectionChange(endHandle, leftX, topY, -1, -1);
185 // Drag start handle down and to the right past the end handle. Both selection start and end
187 dragHandleAndCheckSelectionChange(startHandle, rightX, bottomY, 1, 1);
189 clickToDismissHandles();
192 private void dragHandleAndCheckSelectionChange(HandleView handle, int dragToX, int dragToY,
193 final int expectedStartChange, final int expectedEndChange) throws Throwable {
194 String initialText = getContentViewCore().getSelectedText();
195 final int initialSelectionEnd = getSelectionEnd();
196 final int initialSelectionStart = getSelectionStart();
198 dragHandleTo(handle, dragToX, dragToY, 10);
199 assertWaitForEitherHandleNear(dragToX, dragToY);
201 if (getContentViewCore().isSelectionEditable()) {
202 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
204 public boolean isSatisfied() {
205 int startChange = getSelectionStart() - initialSelectionStart;
206 // TODO(cjhopman): Due to http://crbug.com/244633 we can't really assert that
207 // there is no change when we expect to be able to.
208 if (expectedStartChange != 0) {
209 if ((int) Math.signum(startChange) != expectedStartChange) return false;
212 int endChange = getSelectionEnd() - initialSelectionEnd;
213 if (expectedEndChange != 0) {
214 if ((int) Math.signum(endChange) != expectedEndChange) return false;
222 assertWaitForHandleViewStopped(getStartHandle());
223 assertWaitForHandleViewStopped(getEndHandle());
226 private void assertWaitForSelectionEditableEquals(final boolean expected) throws Throwable {
227 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
229 public boolean isSatisfied() {
230 return getContentViewCore().isSelectionEditable() == expected;
235 private void assertWaitForHandleViewStopped(final HandleView handle) throws Throwable {
236 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
237 private Point position = new Point(-1, -1);
239 public boolean isSatisfied() {
240 Point lastPosition = position;
241 position = getHandlePosition(handle);
242 return !handle.isDragging() &&
243 position.equals(lastPosition);
249 * Verifies that when a selection is made within static page text, that the
250 * contextual action bar of the correct type is displayed. Also verified
251 * that the bar disappears upon deselection.
254 @Feature({ "TextSelection" })
255 public void testNoneditableSelectionActionBar() throws Throwable {
256 doSelectionActionBarTest(TestPageType.NONEDITABLE);
260 * Verifies that when a selection is made within editable text, that the
261 * contextual action bar of the correct type is displayed. Also verified
262 * that the bar disappears upon deselection.
265 @Feature({ "TextSelection" })
266 public void testEditableSelectionActionBar() throws Throwable {
267 doSelectionActionBarTest(TestPageType.EDITABLE);
270 private void doSelectionActionBarTest(TestPageType pageType) throws Throwable {
271 launchWithUrl(pageType.dataUrl);
272 assertFalse(getContentViewCore().isSelectActionBarShowing());
273 clickNodeToShowSelectionHandles(pageType.nodeId);
274 assertWaitForSelectActionBarShowingEquals(true);
275 clickToDismissHandles();
276 assertWaitForSelectActionBarShowingEquals(false);
279 private static Point getHandlePosition(final HandleView handle) {
280 return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Point>() {
282 public Point call() {
283 return new Point(handle.getAdjustedPositionX(), handle.getAdjustedPositionY());
288 private static boolean isHandleNear(HandleView handle, int x, int y) {
289 Point position = getHandlePosition(handle);
290 return (Math.abs(position.x - x) < HANDLE_POSITION_X_TOLERANCE_PIX) &&
291 (Math.abs(position.y - y) < HANDLE_POSITION_Y_TOLERANCE_PIX);
294 private void assertWaitForHandleNear(final HandleView handle, final int x, final int y)
296 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
298 public boolean isSatisfied() {
299 return isHandleNear(handle, x, y);
304 private void assertWaitForEitherHandleNear(final int x, final int y) throws Throwable {
305 final HandleView startHandle = getStartHandle();
306 final HandleView endHandle = getEndHandle();
307 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
309 public boolean isSatisfied() {
310 return isHandleNear(startHandle, x, y) || isHandleNear(endHandle, x, y);
315 private void assertWaitForHandlesShowingEquals(final boolean shouldBeShowing) throws Throwable {
316 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
318 public boolean isSatisfied() {
319 SelectionHandleController shc =
320 getContentViewCore().getSelectionHandleControllerForTest();
321 boolean isShowing = shc != null && shc.isShowing();
322 return shouldBeShowing == isShowing;
328 private void dragHandleTo(final HandleView handle, final int dragToX, final int dragToY,
329 final int steps) throws Throwable {
330 ContentView view = getContentView();
331 assertTrue(ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() {
333 public Boolean call() {
334 int adjustedX = handle.getAdjustedPositionX();
335 int adjustedY = handle.getAdjustedPositionY();
336 int realX = handle.getPositionX();
337 int realY = handle.getPositionY();
339 int realDragToX = dragToX + (realX - adjustedX);
340 int realDragToY = dragToY + (realY - adjustedY);
342 ContentView view = getContentView();
343 int[] fromLocation = TestTouchUtils.getAbsoluteLocationFromRelative(
345 int[] toLocation = TestTouchUtils.getAbsoluteLocationFromRelative(
346 view, realDragToX, realDragToY);
348 long downTime = SystemClock.uptimeMillis();
349 MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
350 fromLocation[0], fromLocation[1], 0);
351 handle.dispatchTouchEvent(event);
353 if (!handle.isDragging()) return false;
355 for (int i = 0; i < steps; i++) {
356 float scale = (float) (i + 1) / steps;
357 int x = fromLocation[0] + (int) (scale * (toLocation[0] - fromLocation[0]));
358 int y = fromLocation[1] + (int) (scale * (toLocation[1] - fromLocation[1]));
359 long eventTime = SystemClock.uptimeMillis();
360 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE,
362 handle.dispatchTouchEvent(event);
364 long upTime = SystemClock.uptimeMillis();
365 event = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP,
366 toLocation[0], toLocation[1], 0);
367 handle.dispatchTouchEvent(event);
369 return !handle.isDragging();
374 private Rect getNodeBoundsPix(String nodeId) throws Throwable {
375 Rect nodeBounds = DOMUtils.getNodeBounds(getContentView(),
376 new TestCallbackHelperContainer(getContentView()), nodeId);
378 RenderCoordinates renderCoordinates = getContentView().getRenderCoordinates();
379 int offsetX = getContentView().getContentViewCore().getViewportSizeOffsetWidthPix();
380 int offsetY = getContentView().getContentViewCore().getViewportSizeOffsetHeightPix();
382 int left = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX;
383 int right = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX;
384 int top = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY;
385 int bottom = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY;
387 return new Rect(left, top, right, bottom);
390 private void clickNodeToShowSelectionHandles(String nodeId) throws Throwable {
391 Rect nodeWindowBounds = getNodeBoundsPix(nodeId);
393 TouchCommon touchCommon = new TouchCommon(this);
394 int centerX = nodeWindowBounds.centerX();
395 int centerY = nodeWindowBounds.centerY();
396 touchCommon.longPressView(getContentView(), centerX, centerY);
398 assertWaitForHandlesShowingEquals(true);
399 assertWaitForHandleViewStopped(getStartHandle());
401 // No words wrap in the sample text so handles should be at the same y
403 assertEquals(getStartHandle().getPositionY(), getEndHandle().getPositionY());
406 private void clickToDismissHandles() throws Throwable {
407 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
408 new TouchCommon(this).singleClickView(getContentView(), 0, 0);
409 assertWaitForHandlesShowingEquals(false);
412 private void assertWaitForSelectActionBarShowingEquals(final boolean shouldBeShowing)
413 throws InterruptedException {
414 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
416 public boolean isSatisfied() {
417 return shouldBeShowing == getContentViewCore().isSelectActionBarShowing();
422 public void assertWaitForHasInputConnection() {
424 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
426 public boolean isSatisfied() {
427 return getContentViewCore().getInputConnectionForTest() != null;
430 } catch (InterruptedException e) {
435 private ImeAdapter getImeAdapter() {
436 return getContentViewCore().getImeAdapterForTest();
439 private int getSelectionStart() {
440 return Selection.getSelectionStart(getEditable());
443 private int getSelectionEnd() {
444 return Selection.getSelectionEnd(getEditable());
447 private Editable getEditable() {
448 // We have to wait for the input connection (with the IME) to be created before accessing
449 // the ContentViewCore's editable.
450 assertWaitForHasInputConnection();
451 return getContentViewCore().getEditableForTest();
454 private HandleView getStartHandle() {
455 SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest();
456 return shc.getStartHandleViewForTest();
459 private HandleView getEndHandle() {
460 SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest();
461 return shc.getEndHandleViewForTest();