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.FlakyTest;
11 import android.test.suitebuilder.annotation.MediumTest;
12 import android.text.Editable;
13 import android.text.Selection;
14 import android.view.MotionEvent;
16 import org.chromium.base.ThreadUtils;
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 import java.util.concurrent.Callable;
32 public class SelectionHandleTest 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 // For these we use a tiny font-size so that we can be more strict on the expected handle
44 private static final String TEXTAREA_ID = "textarea";
45 private static final String TEXTAREA_DATA_URL = UrlUtils.encodeHtmlDataUri(
46 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
47 "<textarea id=\"" + TEXTAREA_ID +
48 "\" cols=\"40\" rows=\"20\" style=\"font-size:6px\">" +
49 "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 " +
50 "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 " +
51 "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 " +
52 "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 " +
53 "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 " +
54 "o f c a e e u t o l t n m d s l b r m." +
55 "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 " +
56 "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 " +
57 "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 " +
58 "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 " +
59 "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 " +
60 "o f c a e e u t o l t n m d s l b r m." +
64 private static final String NONEDITABLE_DIV_ID = "noneditable";
65 private static final String NONEDITABLE_DATA_URL = UrlUtils.encodeHtmlDataUri(
66 "<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
67 "<div id=\"" + NONEDITABLE_DIV_ID + "\" style=\"width:200; font-size:6px\">" +
68 "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 " +
69 "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 " +
70 "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 " +
71 "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 " +
72 "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 " +
73 "o f c a e e u t o l t n m d s l b r m." +
74 "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 " +
75 "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 " +
76 "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 " +
77 "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 " +
78 "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 " +
79 "o f c a e e u t o l t n m d s l b r m." +
83 // TODO(cjhopman): These tolerances should be based on the actual width/height of a
85 private static final int HANDLE_POSITION_X_TOLERANCE_PIX = 20;
86 private static final int HANDLE_POSITION_Y_TOLERANCE_PIX = 30;
88 private enum TestPageType {
89 EDITABLE(TEXTAREA_ID, TEXTAREA_DATA_URL, true),
90 NONEDITABLE(NONEDITABLE_DIV_ID, NONEDITABLE_DATA_URL, false);
94 final boolean selectionShouldBeEditable;
96 TestPageType(String nodeId, String dataUrl, boolean selectionShouldBeEditable) {
98 this.dataUrl = dataUrl;
99 this.selectionShouldBeEditable = selectionShouldBeEditable;
103 private void launchWithUrl(String url) throws Throwable {
104 launchContentShellWithUrl(url);
105 assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
106 assertWaitForPageScaleFactorMatch(1.0f);
108 // The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never
110 getImeAdapter().setInputMethodManagerWrapper(
111 new TestInputMethodManagerWrapper(getContentViewCore()));
114 private void assertWaitForHasSelectionPosition()
116 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
118 public boolean isSatisfied() {
119 int start = getSelectionStart();
120 int end = getSelectionEnd();
121 return start > 0 && start == end;
127 * Verifies that when a long-press is performed on static page text,
128 * selection handles appear and that handles can be dragged to extend the
129 * selection. Does not check exact handle position as this will depend on
130 * screen size; instead, position is expected to be correct within
131 * HANDLE_POSITION_TOLERANCE_PIX.
133 * Test is flaky: crbug.com/290375
135 * @Feature({ "TextSelection", "Main" })
138 public void testNoneditableSelectionHandles() throws Throwable {
139 doSelectionHandleTest(TestPageType.NONEDITABLE);
143 * Verifies that when a long-press is performed on editable text (within a
144 * textarea), selection handles appear and that handles can be dragged to
145 * extend the selection. Does not check exact handle position as this will
146 * depend on screen size; instead, position is expected to be correct within
147 * HANDLE_POSITION_TOLERANCE_PIX.
150 @Feature({ "TextSelection" })
151 public void testEditableSelectionHandles() throws Throwable {
152 doSelectionHandleTest(TestPageType.EDITABLE);
155 private void doSelectionHandleTest(TestPageType pageType) throws Throwable {
156 launchWithUrl(pageType.dataUrl);
158 clickNodeToShowSelectionHandles(pageType.nodeId);
159 assertWaitForSelectionEditableEquals(pageType.selectionShouldBeEditable);
161 HandleView startHandle = getStartHandle();
162 HandleView endHandle = getEndHandle();
164 Rect nodeWindowBounds = getNodeBoundsPix(pageType.nodeId);
166 int leftX = (nodeWindowBounds.left + nodeWindowBounds.centerX()) / 2;
167 int centerX = nodeWindowBounds.centerX();
168 int rightX = (nodeWindowBounds.right + nodeWindowBounds.centerX()) / 2;
170 int topY = (nodeWindowBounds.top + nodeWindowBounds.centerY()) / 2;
171 int centerY = nodeWindowBounds.centerY();
172 int bottomY = (nodeWindowBounds.bottom + nodeWindowBounds.centerY()) / 2;
174 // Drag start handle up and to the left. The selection start should decrease.
175 dragHandleAndCheckSelectionChange(startHandle, leftX, topY, -1, 0);
176 // Drag end handle down and to the right. The selection end should increase.
177 dragHandleAndCheckSelectionChange(endHandle, rightX, bottomY, 0, 1);
178 // Drag start handle back to the middle. The selection start should increase.
179 dragHandleAndCheckSelectionChange(startHandle, centerX, centerY, 1, 0);
180 // Drag end handle up and to the left past the start handle. Both selection start and end
182 dragHandleAndCheckSelectionChange(endHandle, leftX, topY, -1, -1);
183 // Drag start handle down and to the right past the end handle. Both selection start and end
185 dragHandleAndCheckSelectionChange(startHandle, rightX, bottomY, 1, 1);
187 clickToDismissHandles();
190 private void dragHandleAndCheckSelectionChange(HandleView handle, int dragToX, int dragToY,
191 final int expectedStartChange, final int expectedEndChange) throws Throwable {
192 String initialText = getContentViewCore().getSelectedText();
193 final int initialSelectionEnd = getSelectionEnd();
194 final int initialSelectionStart = getSelectionStart();
196 dragHandleTo(handle, dragToX, dragToY, 10);
197 assertWaitForEitherHandleNear(dragToX, dragToY);
199 if (getContentViewCore().isSelectionEditable()) {
200 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
202 public boolean isSatisfied() {
203 int startChange = getSelectionStart() - initialSelectionStart;
204 // TODO(cjhopman): Due to http://crbug.com/244633 we can't really assert that
205 // there is no change when we expect to be able to.
206 if (expectedStartChange != 0) {
207 if ((int) Math.signum(startChange) != expectedStartChange) return false;
210 int endChange = getSelectionEnd() - initialSelectionEnd;
211 if (expectedEndChange != 0) {
212 if ((int) Math.signum(endChange) != expectedEndChange) return false;
220 assertWaitForHandleViewStopped(getStartHandle());
221 assertWaitForHandleViewStopped(getEndHandle());
224 private void assertWaitForSelectionEditableEquals(final boolean expected) throws Throwable {
225 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
227 public boolean isSatisfied() {
228 return getContentViewCore().isSelectionEditable() == expected;
233 private void assertWaitForHandleViewStopped(final HandleView handle) throws Throwable {
234 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
235 private Point position = new Point(-1, -1);
237 public boolean isSatisfied() {
238 Point lastPosition = position;
239 position = getHandlePosition(handle);
240 return !handle.isDragging() &&
241 position.equals(lastPosition);
247 * Verifies that when a selection is made within static page text, that the
248 * contextual action bar of the correct type is displayed. Also verified
249 * that the bar disappears upon deselection.
252 @Feature({ "TextSelection" })
253 public void testNoneditableSelectionActionBar() throws Throwable {
254 doSelectionActionBarTest(TestPageType.NONEDITABLE);
258 * Verifies that when a selection is made within editable text, that the
259 * contextual action bar of the correct type is displayed. Also verified
260 * that the bar disappears upon deselection.
263 @Feature({ "TextSelection" })
264 public void testEditableSelectionActionBar() throws Throwable {
265 doSelectionActionBarTest(TestPageType.EDITABLE);
268 private void doSelectionActionBarTest(TestPageType pageType) throws Throwable {
269 launchWithUrl(pageType.dataUrl);
270 assertFalse(getContentViewCore().isSelectActionBarShowing());
271 clickNodeToShowSelectionHandles(pageType.nodeId);
272 assertWaitForSelectActionBarShowingEquals(true);
273 clickToDismissHandles();
274 assertWaitForSelectActionBarShowingEquals(false);
277 private static Point getHandlePosition(final HandleView handle) {
278 return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Point>() {
280 public Point call() {
281 return new Point(handle.getAdjustedPositionX(), handle.getAdjustedPositionY());
286 private static boolean isHandleNear(HandleView handle, int x, int y) {
287 Point position = getHandlePosition(handle);
288 return (Math.abs(position.x - x) < HANDLE_POSITION_X_TOLERANCE_PIX) &&
289 (Math.abs(position.y - y) < HANDLE_POSITION_Y_TOLERANCE_PIX);
292 private void assertWaitForHandleNear(final HandleView handle, final int x, final int y)
294 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
296 public boolean isSatisfied() {
297 return isHandleNear(handle, x, y);
302 private void assertWaitForEitherHandleNear(final int x, final int y) throws Throwable {
303 final HandleView startHandle = getStartHandle();
304 final HandleView endHandle = getEndHandle();
305 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
307 public boolean isSatisfied() {
308 return isHandleNear(startHandle, x, y) || isHandleNear(endHandle, x, y);
313 private void assertWaitForHandlesShowingEquals(final boolean shouldBeShowing) throws Throwable {
314 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
316 public boolean isSatisfied() {
317 SelectionHandleController shc =
318 getContentViewCore().getSelectionHandleControllerForTest();
319 boolean isShowing = shc != null && shc.isShowing();
320 return shouldBeShowing == isShowing;
326 private void dragHandleTo(final HandleView handle, final int dragToX, final int dragToY,
327 final int steps) throws Throwable {
328 ContentView view = getContentView();
329 assertTrue(ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() {
331 public Boolean call() {
332 int adjustedX = handle.getAdjustedPositionX();
333 int adjustedY = handle.getAdjustedPositionY();
334 int realX = handle.getPositionX();
335 int realY = handle.getPositionY();
337 int realDragToX = dragToX + (realX - adjustedX);
338 int realDragToY = dragToY + (realY - adjustedY);
340 ContentView view = getContentView();
341 int[] fromLocation = TestTouchUtils.getAbsoluteLocationFromRelative(
343 int[] toLocation = TestTouchUtils.getAbsoluteLocationFromRelative(
344 view, realDragToX, realDragToY);
346 long downTime = SystemClock.uptimeMillis();
347 MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
348 fromLocation[0], fromLocation[1], 0);
349 handle.dispatchTouchEvent(event);
351 if (!handle.isDragging()) return false;
353 for (int i = 0; i < steps; i++) {
354 float scale = (float) (i + 1) / steps;
355 int x = fromLocation[0] + (int) (scale * (toLocation[0] - fromLocation[0]));
356 int y = fromLocation[1] + (int) (scale * (toLocation[1] - fromLocation[1]));
357 long eventTime = SystemClock.uptimeMillis();
358 event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE,
360 handle.dispatchTouchEvent(event);
362 long upTime = SystemClock.uptimeMillis();
363 event = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP,
364 toLocation[0], toLocation[1], 0);
365 handle.dispatchTouchEvent(event);
367 return !handle.isDragging();
372 private Rect getNodeBoundsPix(String nodeId) throws Throwable {
373 Rect nodeBounds = DOMUtils.getNodeBounds(getContentView(),
374 new TestCallbackHelperContainer(getContentView()), nodeId);
376 RenderCoordinates renderCoordinates = getContentView().getRenderCoordinates();
377 int offsetX = getContentView().getContentViewCore().getViewportSizeOffsetWidthPix();
378 int offsetY = getContentView().getContentViewCore().getViewportSizeOffsetHeightPix();
380 int left = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX;
381 int right = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX;
382 int top = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY;
383 int bottom = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY;
385 return new Rect(left, top, right, bottom);
388 private void clickNodeToShowSelectionHandles(String nodeId) throws Throwable {
389 Rect nodeWindowBounds = getNodeBoundsPix(nodeId);
391 TouchCommon touchCommon = new TouchCommon(this);
392 int centerX = nodeWindowBounds.centerX();
393 int centerY = nodeWindowBounds.centerY();
394 touchCommon.longPressView(getContentView(), centerX, centerY);
396 assertWaitForHandlesShowingEquals(true);
397 assertWaitForHandleViewStopped(getStartHandle());
399 // No words wrap in the sample text so handles should be at the same y
401 assertEquals(getStartHandle().getPositionY(), getEndHandle().getPositionY());
404 private void clickToDismissHandles() throws Throwable {
405 TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
406 new TouchCommon(this).singleClickView(getContentView(), 0, 0);
407 assertWaitForHandlesShowingEquals(false);
410 private void assertWaitForSelectActionBarShowingEquals(final boolean shouldBeShowing)
411 throws InterruptedException {
412 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
414 public boolean isSatisfied() {
415 return shouldBeShowing == getContentViewCore().isSelectActionBarShowing();
420 public void assertWaitForHasInputConnection() {
422 assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
424 public boolean isSatisfied() {
425 return getContentViewCore().getInputConnectionForTest() != null;
428 } catch (InterruptedException e) {
433 private ImeAdapter getImeAdapter() {
434 return getContentViewCore().getImeAdapterForTest();
437 private int getSelectionStart() {
438 return Selection.getSelectionStart(getEditable());
441 private int getSelectionEnd() {
442 return Selection.getSelectionEnd(getEditable());
445 private Editable getEditable() {
446 // We have to wait for the input connection (with the IME) to be created before accessing
447 // the ContentViewCore's editable.
448 assertWaitForHasInputConnection();
449 return getContentViewCore().getEditableForTest();
452 private HandleView getStartHandle() {
453 SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest();
454 return shc.getStartHandleViewForTest();
457 private HandleView getEndHandle() {
458 SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest();
459 return shc.getEndHandleViewForTest();