blob: 730e2f172d546f37b3eb36ca9c3d805891b8fd7b [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser.input;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.SystemClock;
import android.test.FlakyTest;
import android.test.suitebuilder.annotation.MediumTest;
import android.text.Editable;
import android.text.Selection;
import android.view.MotionEvent;
import android.view.ViewGroup;
import org.chromium.base.ThreadUtils;
import org.chromium.base.test.util.Feature;
import org.chromium.base.test.util.UrlUtils;
import org.chromium.content.browser.RenderCoordinates;
import org.chromium.content.browser.test.util.Criteria;
import org.chromium.content.browser.test.util.CriteriaHelper;
import org.chromium.content.browser.test.util.DOMUtils;
import org.chromium.content.browser.test.util.TestInputMethodManagerWrapper;
import org.chromium.content.browser.test.util.TestTouchUtils;
import org.chromium.content.browser.test.util.TouchCommon;
import org.chromium.content_shell_apk.ContentShellTestBase;
import java.util.concurrent.Callable;
public class SelectionHandleTest extends ContentShellTestBase {
private static final String META_DISABLE_ZOOM =
"<meta name=\"viewport\" content=\"" +
"height=device-height," +
"width=device-width," +
"initial-scale=1.0," +
"minimum-scale=1.0," +
"maximum-scale=1.0," +
"\" />";
// For these we use a tiny font-size so that we can be more strict on the expected handle
// positions.
private static final String TEXTAREA_ID = "textarea";
private static final String TEXTAREA_DATA_URL = UrlUtils.encodeHtmlDataUri(
"<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
"<textarea id=\"" + TEXTAREA_ID +
"\" cols=\"40\" rows=\"20\" style=\"font-size:6px\">" +
"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 " +
"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 " +
"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 " +
"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 " +
"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 " +
"o f c a e e u t o l t n m d s l b r m." +
"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 " +
"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 " +
"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 " +
"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 " +
"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 " +
"o f c a e e u t o l t n m d s l b r m." +
"</textarea>" +
"</body></html>");
private static final String NONEDITABLE_DIV_ID = "noneditable";
private static final String NONEDITABLE_DATA_URL = UrlUtils.encodeHtmlDataUri(
"<html><head>" + META_DISABLE_ZOOM + "</head><body>" +
"<div id=\"" + NONEDITABLE_DIV_ID + "\" style=\"width:200; font-size:6px\">" +
"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 " +
"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 " +
"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 " +
"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 " +
"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 " +
"o f c a e e u t o l t n m d s l b r m." +
"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 " +
"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 " +
"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 " +
"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 " +
"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 " +
"o f c a e e u t o l t n m d s l b r m." +
"</div>" +
"</body></html>");
// TODO(cjhopman): These tolerances should be based on the actual width/height of a
// character/line.
private static final int HANDLE_POSITION_X_TOLERANCE_PIX = 20;
private static final int HANDLE_POSITION_Y_TOLERANCE_PIX = 30;
private enum TestPageType {
EDITABLE(TEXTAREA_ID, TEXTAREA_DATA_URL, true),
NONEDITABLE(NONEDITABLE_DIV_ID, NONEDITABLE_DATA_URL, false);
final String nodeId;
final String dataUrl;
final boolean selectionShouldBeEditable;
TestPageType(String nodeId, String dataUrl, boolean selectionShouldBeEditable) {
this.nodeId = nodeId;
this.dataUrl = dataUrl;
this.selectionShouldBeEditable = selectionShouldBeEditable;
}
}
private void launchWithUrl(String url) throws Throwable {
launchContentShellWithUrl(url);
assertTrue("Page failed to load", waitForActiveShellToBeDoneLoading());
assertWaitForPageScaleFactorMatch(1.0f);
// The TestInputMethodManagerWrapper intercepts showSoftInput so that a keyboard is never
// brought up.
getImeAdapter().setInputMethodManagerWrapper(
new TestInputMethodManagerWrapper(getContentViewCore()));
}
private void assertWaitForHasSelectionPosition()
throws Throwable {
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
int start = getSelectionStart();
int end = getSelectionEnd();
return start > 0 && start == end;
}
}));
}
/**
* Verifies that when a long-press is performed on static page text,
* selection handles appear and that handles can be dragged to extend the
* selection. Does not check exact handle position as this will depend on
* screen size; instead, position is expected to be correct within
* HANDLE_POSITION_TOLERANCE_PIX.
*
* Test is flaky: crbug.com/290375
* @MediumTest
* @Feature({ "TextSelection", "Main" })
*/
@FlakyTest
public void testNoneditableSelectionHandles() throws Throwable {
doSelectionHandleTest(TestPageType.NONEDITABLE);
}
/**
* Test is flaky: crbug.com/290375
* @MediumTest
* @Feature({ "TextSelection", "Main" })
*/
@FlakyTest
public void testUpdateContainerViewAndNoneditableSelectionHandles() throws Throwable {
launchWithUrl(TestPageType.NONEDITABLE.dataUrl);
replaceContainerView();
doSelectionHandleTestUrlLaunched(TestPageType.NONEDITABLE);
}
/**
* Verifies that when a long-press is performed on editable text (within a
* textarea), selection handles appear and that handles can be dragged to
* extend the selection. Does not check exact handle position as this will
* depend on screen size; instead, position is expected to be correct within
* HANDLE_POSITION_TOLERANCE_PIX.
*/
@MediumTest
@Feature({ "TextSelection" })
public void testEditableSelectionHandles() throws Throwable {
doSelectionHandleTest(TestPageType.EDITABLE);
}
@MediumTest
@Feature({ "TextSelection" })
public void testUpdateContainerViewAndEditableSelectionHandles() throws Throwable {
launchWithUrl(TestPageType.EDITABLE.dataUrl);
replaceContainerView();
doSelectionHandleTestUrlLaunched(TestPageType.EDITABLE);
}
private void doSelectionHandleTest(TestPageType pageType) throws Throwable {
launchWithUrl(pageType.dataUrl);
doSelectionHandleTestUrlLaunched(pageType);
}
private void doSelectionHandleTestUrlLaunched(TestPageType pageType) throws Throwable {
clickNodeToShowSelectionHandles(pageType.nodeId);
assertWaitForSelectionEditableEquals(pageType.selectionShouldBeEditable);
HandleView startHandle = getStartHandle();
HandleView endHandle = getEndHandle();
Rect nodeWindowBounds = getNodeBoundsPix(pageType.nodeId);
int leftX = (nodeWindowBounds.left + nodeWindowBounds.centerX()) / 2;
int centerX = nodeWindowBounds.centerX();
int rightX = (nodeWindowBounds.right + nodeWindowBounds.centerX()) / 2;
int topY = (nodeWindowBounds.top + nodeWindowBounds.centerY()) / 2;
int centerY = nodeWindowBounds.centerY();
int bottomY = (nodeWindowBounds.bottom + nodeWindowBounds.centerY()) / 2;
// Drag start handle up and to the left. The selection start should decrease.
dragHandleAndCheckSelectionChange(startHandle, leftX, topY, -1, 0);
// Drag end handle down and to the right. The selection end should increase.
dragHandleAndCheckSelectionChange(endHandle, rightX, bottomY, 0, 1);
// Drag start handle back to the middle. The selection start should increase.
dragHandleAndCheckSelectionChange(startHandle, centerX, centerY, 1, 0);
// Drag end handle up and to the left past the start handle. Both selection start and end
// should decrease.
dragHandleAndCheckSelectionChange(endHandle, leftX, topY, -1, -1);
// Drag start handle down and to the right past the end handle. Both selection start and end
// should increase.
dragHandleAndCheckSelectionChange(startHandle, rightX, bottomY, 1, 1);
clickToDismissHandles();
}
private void dragHandleAndCheckSelectionChange(HandleView handle, int dragToX, int dragToY,
final int expectedStartChange, final int expectedEndChange) throws Throwable {
String initialText = getContentViewCore().getSelectedText();
final int initialSelectionEnd = getSelectionEnd();
final int initialSelectionStart = getSelectionStart();
dragHandleTo(handle, dragToX, dragToY, 10);
assertWaitForEitherHandleNear(dragToX, dragToY);
if (getContentViewCore().isSelectionEditable()) {
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
int startChange = getSelectionStart() - initialSelectionStart;
// TODO(cjhopman): Due to http://crbug.com/244633 we can't really assert that
// there is no change when we expect to be able to.
if (expectedStartChange != 0) {
if ((int) Math.signum(startChange) != expectedStartChange) return false;
}
int endChange = getSelectionEnd() - initialSelectionEnd;
if (expectedEndChange != 0) {
if ((int) Math.signum(endChange) != expectedEndChange) return false;
}
return true;
}
}));
}
assertWaitForHandleViewStopped(getStartHandle());
assertWaitForHandleViewStopped(getEndHandle());
}
private void assertWaitForSelectionEditableEquals(final boolean expected) throws Throwable {
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return getContentViewCore().isSelectionEditable() == expected;
}
}));
}
private void assertWaitForHandleViewStopped(final HandleView handle) throws Throwable {
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
private Point position = new Point(-1, -1);
@Override
public boolean isSatisfied() {
Point lastPosition = position;
position = getHandlePosition(handle);
return !handle.isDragging() &&
position.equals(lastPosition);
}
}));
}
/**
* Verifies that when a selection is made within static page text, that the
* contextual action bar of the correct type is displayed. Also verified
* that the bar disappears upon deselection.
*/
@MediumTest
@Feature({ "TextSelection" })
public void testNoneditableSelectionActionBar() throws Throwable {
doSelectionActionBarTest(TestPageType.NONEDITABLE);
}
/**
* Verifies that when a selection is made within editable text, that the
* contextual action bar of the correct type is displayed. Also verified
* that the bar disappears upon deselection.
*/
@MediumTest
@Feature({ "TextSelection" })
public void testEditableSelectionActionBar() throws Throwable {
doSelectionActionBarTest(TestPageType.EDITABLE);
}
private void doSelectionActionBarTest(TestPageType pageType) throws Throwable {
launchWithUrl(pageType.dataUrl);
assertFalse(getContentViewCore().isSelectActionBarShowing());
clickNodeToShowSelectionHandles(pageType.nodeId);
assertWaitForSelectActionBarShowingEquals(true);
clickToDismissHandles();
assertWaitForSelectActionBarShowingEquals(false);
}
private static Point getHandlePosition(final HandleView handle) {
return ThreadUtils.runOnUiThreadBlockingNoException(new Callable<Point>() {
@Override
public Point call() {
return new Point(handle.getAdjustedPositionX(), handle.getAdjustedPositionY());
}
});
}
private static boolean isHandleNear(HandleView handle, int x, int y) {
Point position = getHandlePosition(handle);
return (Math.abs(position.x - x) < HANDLE_POSITION_X_TOLERANCE_PIX) &&
(Math.abs(position.y - y) < HANDLE_POSITION_Y_TOLERANCE_PIX);
}
private void assertWaitForHandleNear(final HandleView handle, final int x, final int y)
throws Throwable {
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return isHandleNear(handle, x, y);
}
}));
}
private void assertWaitForEitherHandleNear(final int x, final int y) throws Throwable {
final HandleView startHandle = getStartHandle();
final HandleView endHandle = getEndHandle();
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return isHandleNear(startHandle, x, y) || isHandleNear(endHandle, x, y);
}
}));
}
private void assertWaitForHandlesShowingEquals(final boolean shouldBeShowing) throws Throwable {
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
SelectionHandleController shc =
getContentViewCore().getSelectionHandleControllerForTest();
boolean isShowing = shc != null && shc.isShowing();
return shouldBeShowing == isShowing;
}
}));
}
private void dragHandleTo(final HandleView handle, final int dragToX, final int dragToY,
final int steps) throws Throwable {
assertTrue(ThreadUtils.runOnUiThreadBlocking(new Callable<Boolean>() {
@Override
public Boolean call() {
int adjustedX = handle.getAdjustedPositionX();
int adjustedY = handle.getAdjustedPositionY();
int realX = handle.getPositionX();
int realY = handle.getPositionY();
int realDragToX = dragToX + (realX - adjustedX);
int realDragToY = dragToY + (realY - adjustedY);
ViewGroup view = getContentViewCore().getContainerView();
int[] fromLocation = TestTouchUtils.getAbsoluteLocationFromRelative(
view, realX, realY);
int[] toLocation = TestTouchUtils.getAbsoluteLocationFromRelative(
view, realDragToX, realDragToY);
long downTime = SystemClock.uptimeMillis();
MotionEvent event = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN,
fromLocation[0], fromLocation[1], 0);
handle.dispatchTouchEvent(event);
if (!handle.isDragging()) return false;
for (int i = 0; i < steps; i++) {
float scale = (float) (i + 1) / steps;
int x = fromLocation[0] + (int) (scale * (toLocation[0] - fromLocation[0]));
int y = fromLocation[1] + (int) (scale * (toLocation[1] - fromLocation[1]));
long eventTime = SystemClock.uptimeMillis();
event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE,
x, y, 0);
handle.dispatchTouchEvent(event);
}
long upTime = SystemClock.uptimeMillis();
event = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP,
toLocation[0], toLocation[1], 0);
handle.dispatchTouchEvent(event);
return !handle.isDragging();
}
}));
}
private Rect getNodeBoundsPix(String nodeId) throws Throwable {
Rect nodeBounds = DOMUtils.getNodeBounds(getContentViewCore(), nodeId);
RenderCoordinates renderCoordinates = getContentViewCore().getRenderCoordinates();
int offsetX = getContentViewCore().getViewportSizeOffsetWidthPix();
int offsetY = getContentViewCore().getViewportSizeOffsetHeightPix();
int left = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.left) + offsetX;
int right = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.right) + offsetX;
int top = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.top) + offsetY;
int bottom = (int) renderCoordinates.fromLocalCssToPix(nodeBounds.bottom) + offsetY;
return new Rect(left, top, right, bottom);
}
private void clickNodeToShowSelectionHandles(String nodeId) throws Throwable {
Rect nodeWindowBounds = getNodeBoundsPix(nodeId);
TouchCommon touchCommon = new TouchCommon(this);
int centerX = nodeWindowBounds.centerX();
int centerY = nodeWindowBounds.centerY();
touchCommon.longPressView(getContentViewCore().getContainerView(), centerX, centerY);
assertWaitForHandlesShowingEquals(true);
assertWaitForHandleViewStopped(getStartHandle());
// No words wrap in the sample text so handles should be at the same y
// position.
assertEquals(getStartHandle().getPositionY(), getEndHandle().getPositionY());
}
private void clickToDismissHandles() throws Throwable {
TestTouchUtils.sleepForDoubleTapTimeout(getInstrumentation());
new TouchCommon(this).singleClickView(getContentViewCore().getContainerView(), 0, 0);
assertWaitForHandlesShowingEquals(false);
}
private void assertWaitForSelectActionBarShowingEquals(final boolean shouldBeShowing)
throws InterruptedException {
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return shouldBeShowing == getContentViewCore().isSelectActionBarShowing();
}
}));
}
public void assertWaitForHasInputConnection() {
try {
assertTrue(CriteriaHelper.pollForCriteria(new Criteria() {
@Override
public boolean isSatisfied() {
return getContentViewCore().getInputConnectionForTest() != null;
}
}));
} catch (InterruptedException e) {
fail();
}
}
private ImeAdapter getImeAdapter() {
return getContentViewCore().getImeAdapterForTest();
}
private int getSelectionStart() {
return Selection.getSelectionStart(getEditable());
}
private int getSelectionEnd() {
return Selection.getSelectionEnd(getEditable());
}
private Editable getEditable() {
// We have to wait for the input connection (with the IME) to be created before accessing
// the ContentViewCore's editable.
assertWaitForHasInputConnection();
return getContentViewCore().getEditableForTest();
}
private HandleView getStartHandle() {
SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest();
return shc.getStartHandleViewForTest();
}
private HandleView getEndHandle() {
SelectionHandleController shc = getContentViewCore().getSelectionHandleControllerForTest();
return shc.getEndHandleViewForTest();
}
}