Add more TextView selection (by touch) tests.
This change adds simple tests for: - longpress and drag to select - double-tap and drag to select Bug: 24102650 Change-Id: I8614c8ec430dfc204159a03be409a2361cd7d844
This commit is contained in:
@@ -17,6 +17,9 @@
|
||||
package android.widget;
|
||||
|
||||
import static android.widget.espresso.TextViewActions.clickOnTextAtIndex;
|
||||
import static android.widget.espresso.TextViewActions.doubleTapAndDragOnText;
|
||||
import static android.widget.espresso.TextViewActions.longPressAndDragOnText;
|
||||
import static android.widget.espresso.TextViewAssertions.hasSelection;
|
||||
import static android.support.test.espresso.Espresso.onView;
|
||||
import static android.support.test.espresso.action.ViewActions.click;
|
||||
import static android.support.test.espresso.action.ViewActions.pressKey;
|
||||
@@ -63,4 +66,28 @@ public class TextViewActivityTest extends ActivityInstrumentationTestCase2<TextV
|
||||
onView(withId(R.id.textview)).perform(pressKey(KeyEvent.KEYCODE_FORWARD_DEL));
|
||||
onView(withId(R.id.textview)).check(matches(withText("Hello orld!")));
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testLongPressAndDragToSelect() throws Exception {
|
||||
getActivity();
|
||||
|
||||
final String helloWorld = "Hello little handsome boy!";
|
||||
onView(withId(R.id.textview)).perform(typeTextIntoFocusedView(helloWorld));
|
||||
onView(withId(R.id.textview)).perform(
|
||||
longPressAndDragOnText(helloWorld.indexOf("little"), helloWorld.indexOf(" boy!")));
|
||||
|
||||
onView(withId(R.id.textview)).check(hasSelection("little handsome"));
|
||||
}
|
||||
|
||||
@SmallTest
|
||||
public void testDoubleTapAndDragToSelect() throws Exception {
|
||||
getActivity();
|
||||
|
||||
final String helloWorld = "Hello young beautiful girl!";
|
||||
onView(withId(R.id.textview)).perform(typeTextIntoFocusedView(helloWorld));
|
||||
onView(withId(R.id.textview)).perform(
|
||||
doubleTapAndDragOnText(helloWorld.indexOf("young"), helloWorld.indexOf(" girl!")));
|
||||
|
||||
onView(withId(R.id.textview)).check(hasSelection("young beautiful"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
|
||||
package android.widget.espresso;
|
||||
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
|
||||
import static com.android.internal.util.Preconditions.checkNotNull;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.os.SystemClock;
|
||||
import android.support.test.espresso.UiController;
|
||||
import android.support.test.espresso.PerformException;
|
||||
import android.support.test.espresso.ViewAction;
|
||||
import android.support.test.espresso.action.CoordinatesProvider;
|
||||
import android.support.test.espresso.action.GeneralClickAction;
|
||||
import android.support.test.espresso.action.MotionEvents;
|
||||
import android.support.test.espresso.action.PrecisionDescriber;
|
||||
import android.support.test.espresso.action.Press;
|
||||
import android.support.test.espresso.action.Swiper;
|
||||
import android.support.test.espresso.action.Tap;
|
||||
import android.support.test.espresso.util.HumanReadables;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.widget.TextView;
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
|
||||
/**
|
||||
* Drags on text in a TextView using touch events.<br>
|
||||
* <br>
|
||||
* View constraints:
|
||||
* <ul>
|
||||
* <li>must be a TextView displayed on screen
|
||||
* <ul>
|
||||
*/
|
||||
public final class DragOnTextViewActions implements ViewAction {
|
||||
|
||||
/**
|
||||
* Executes different "drag on text" types to given positions.
|
||||
*/
|
||||
public enum Drag implements Swiper {
|
||||
|
||||
/**
|
||||
* Starts a drag with a long-press.
|
||||
*/
|
||||
LONG_PRESS {
|
||||
private DownMotionPerformer downMotion = new DownMotionPerformer() {
|
||||
@Override
|
||||
public MotionEvent perform(
|
||||
UiController uiController, float[] coordinates, float[] precision) {
|
||||
MotionEvent downEvent = MotionEvents.sendDown(
|
||||
uiController, coordinates, precision)
|
||||
.down;
|
||||
// Duration before a press turns into a long press.
|
||||
// Factor 1.5 is needed, otherwise a long press is not safely detected.
|
||||
// See android.test.TouchUtils longClickView
|
||||
long longPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f);
|
||||
uiController.loopMainThreadForAtLeast(longPressTimeout);
|
||||
return downEvent;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public Status sendSwipe(
|
||||
UiController uiController,
|
||||
float[] startCoordinates, float[] endCoordinates, float[] precision) {
|
||||
return sendLinearDrag(
|
||||
uiController, downMotion, startCoordinates, endCoordinates, precision);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "long press and drag to select";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts a drag with a double-tap.
|
||||
*/
|
||||
DOUBLE_TAP {
|
||||
private DownMotionPerformer downMotion = new DownMotionPerformer() {
|
||||
@Override
|
||||
@Nullable
|
||||
public MotionEvent perform(
|
||||
UiController uiController, float[] coordinates, float[] precision) {
|
||||
MotionEvent downEvent = MotionEvents.sendDown(
|
||||
uiController, coordinates, precision)
|
||||
.down;
|
||||
try {
|
||||
if (!MotionEvents.sendUp(uiController, downEvent)) {
|
||||
String logMessage = "Injection of up event as part of the double tap " +
|
||||
"failed. Sending cancel event.";
|
||||
Log.d(TAG, logMessage);
|
||||
MotionEvents.sendCancel(uiController, downEvent);
|
||||
return null;
|
||||
}
|
||||
|
||||
long doubleTapMinimumTimeout = ViewConfiguration.getDoubleTapMinTime();
|
||||
uiController.loopMainThreadForAtLeast(doubleTapMinimumTimeout);
|
||||
|
||||
return MotionEvents.sendDown(uiController, coordinates, precision).down;
|
||||
} finally {
|
||||
downEvent.recycle();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public Status sendSwipe(
|
||||
UiController uiController,
|
||||
float[] startCoordinates, float[] endCoordinates, float[] precision) {
|
||||
return sendLinearDrag(
|
||||
uiController, downMotion, startCoordinates, endCoordinates, precision);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "double-tap and drag to select";
|
||||
}
|
||||
};
|
||||
|
||||
private static final String TAG = Drag.class.getSimpleName();
|
||||
|
||||
/** The number of move events to send for each drag. */
|
||||
private static final int DRAG_STEP_COUNT = 10;
|
||||
|
||||
/** Length of time a drag should last for, in milliseconds. */
|
||||
private static final int DRAG_DURATION = 1500;
|
||||
|
||||
private static Status sendLinearDrag(
|
||||
UiController uiController, DownMotionPerformer downMotion,
|
||||
float[] startCoordinates, float[] endCoordinates, float[] precision) {
|
||||
float[][] steps = interpolate(startCoordinates, endCoordinates);
|
||||
final int delayBetweenMovements = DRAG_DURATION / steps.length;
|
||||
|
||||
MotionEvent downEvent = downMotion.perform(uiController, startCoordinates, precision);
|
||||
if (downEvent == null) {
|
||||
return Status.FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
for (int i = 0; i < steps.length; i++) {
|
||||
if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) {
|
||||
String logMessage = "Injection of move event as part of the drag failed. " +
|
||||
"Sending cancel event.";
|
||||
Log.e(TAG, logMessage);
|
||||
MotionEvents.sendCancel(uiController, downEvent);
|
||||
return Status.FAILURE;
|
||||
}
|
||||
|
||||
long desiredTime = downEvent.getDownTime() + delayBetweenMovements * i;
|
||||
long timeUntilDesired = desiredTime - SystemClock.uptimeMillis();
|
||||
if (timeUntilDesired > 10) {
|
||||
// If the wait time until the next event isn't long enough, skip the wait
|
||||
// and execute the next event.
|
||||
uiController.loopMainThreadForAtLeast(timeUntilDesired);
|
||||
}
|
||||
}
|
||||
|
||||
if (!MotionEvents.sendUp(uiController, downEvent, endCoordinates)) {
|
||||
String logMessage = "Injection of up event as part of the drag failed. " +
|
||||
"Sending cancel event.";
|
||||
Log.e(TAG, logMessage);
|
||||
MotionEvents.sendCancel(uiController, downEvent);
|
||||
return Status.FAILURE;
|
||||
}
|
||||
} finally {
|
||||
downEvent.recycle();
|
||||
}
|
||||
return Status.SUCCESS;
|
||||
}
|
||||
|
||||
private static float[][] interpolate(float[] start, float[] end) {
|
||||
float[][] res = new float[DRAG_STEP_COUNT][2];
|
||||
|
||||
for (int i = 1; i < DRAG_STEP_COUNT + 1; i++) {
|
||||
res[i - 1][0] = start[0] + (end[0] - start[0]) * i / (DRAG_STEP_COUNT + 2f);
|
||||
res[i - 1][1] = start[1] + (end[1] - start[1]) * i / (DRAG_STEP_COUNT + 2f);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to implement different "down motion" types.
|
||||
*/
|
||||
private interface DownMotionPerformer {
|
||||
/**
|
||||
* Performs and returns a down motion.
|
||||
*
|
||||
* @param uiController a UiController to use to send MotionEvents to the screen.
|
||||
* @param coordinates a float[] with x and y values of center of the tap.
|
||||
* @param precision a float[] with x and y values of precision of the tap.
|
||||
* @return the down motion event or null if the down motion event failed.
|
||||
*/
|
||||
@Nullable
|
||||
MotionEvent perform(UiController uiController, float[] coordinates, float[] precision);
|
||||
}
|
||||
|
||||
private final Swiper mDragger;
|
||||
private final CoordinatesProvider mStartCoordinatesProvider;
|
||||
private final CoordinatesProvider mEndCoordinatesProvider;
|
||||
private final PrecisionDescriber mPrecisionDescriber;
|
||||
|
||||
public DragOnTextViewActions(
|
||||
Swiper dragger,
|
||||
CoordinatesProvider startCoordinatesProvider,
|
||||
CoordinatesProvider endCoordinatesProvider,
|
||||
PrecisionDescriber precisionDescriber) {
|
||||
mDragger = checkNotNull(dragger);
|
||||
mStartCoordinatesProvider = checkNotNull(startCoordinatesProvider);
|
||||
mEndCoordinatesProvider = checkNotNull(endCoordinatesProvider);
|
||||
mPrecisionDescriber = checkNotNull(precisionDescriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Matcher<View> getConstraints() {
|
||||
return allOf(isCompletelyDisplayed(), isAssignableFrom(TextView.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void perform(UiController uiController, View view) {
|
||||
checkNotNull(uiController);
|
||||
checkNotNull(view);
|
||||
|
||||
float[] startCoordinates = mStartCoordinatesProvider.calculateCoordinates(view);
|
||||
float[] endCoordinates = mEndCoordinatesProvider.calculateCoordinates(view);
|
||||
float[] precision = mPrecisionDescriber.describePrecision();
|
||||
|
||||
Swiper.Status status;
|
||||
|
||||
try {
|
||||
status = mDragger.sendSwipe(
|
||||
uiController, startCoordinates, endCoordinates, precision);
|
||||
} catch (RuntimeException re) {
|
||||
throw new PerformException.Builder()
|
||||
.withActionDescription(this.getDescription())
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.withCause(re)
|
||||
.build();
|
||||
}
|
||||
|
||||
int duration = ViewConfiguration.getPressedStateDuration();
|
||||
// ensures that all work enqueued to process the swipe has been run.
|
||||
if (duration > 0) {
|
||||
uiController.loopMainThreadForAtLeast(duration);
|
||||
}
|
||||
|
||||
if (status == Swiper.Status.FAILURE) {
|
||||
throw new PerformException.Builder()
|
||||
.withActionDescription(getDescription())
|
||||
.withViewDescription(HumanReadables.describe(view))
|
||||
.withCause(new RuntimeException(getDescription() + " failed"))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return mDragger.toString();
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ package android.widget.espresso;
|
||||
|
||||
import static android.support.test.espresso.action.ViewActions.actionWithAssertions;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.support.test.espresso.PerformException;
|
||||
import android.support.test.espresso.ViewAction;
|
||||
import android.support.test.espresso.action.CoordinatesProvider;
|
||||
@@ -27,8 +26,6 @@ import android.support.test.espresso.action.Press;
|
||||
import android.support.test.espresso.action.Tap;
|
||||
import android.support.test.espresso.util.HumanReadables;
|
||||
import android.text.Layout;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -40,18 +37,62 @@ public final class TextViewActions {
|
||||
private TextViewActions() {}
|
||||
|
||||
/**
|
||||
* Returns an action that clicks on text at an index on the text view.<br>
|
||||
* Returns an action that clicks on text at an index on the TextView.<br>
|
||||
* <br>
|
||||
* View constraints:
|
||||
* <ul>
|
||||
* <li>must be a text view displayed on screen
|
||||
* <li>must be a TextView displayed on screen
|
||||
* <ul>
|
||||
*
|
||||
* @param index The index of the TextView's text to click on.
|
||||
*/
|
||||
public static ViewAction clickOnTextAtIndex(int index) {
|
||||
return actionWithAssertions(
|
||||
new GeneralClickAction(Tap.SINGLE, new TextCoordinates(index), Press.FINGER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an action that long presses then drags on text from startIndex to endIndex on the
|
||||
* TextView.<br>
|
||||
* <br>
|
||||
* View constraints:
|
||||
* <ul>
|
||||
* <li>must be a TextView displayed on screen
|
||||
* <ul>
|
||||
*
|
||||
* @param startIndex The index of the TextView's text to start a drag from
|
||||
* @param endIndex The index of the TextView's text to end the drag at
|
||||
*/
|
||||
public static ViewAction longPressAndDragOnText(int startIndex, int endIndex) {
|
||||
return actionWithAssertions(
|
||||
new DragOnTextViewActions(
|
||||
DragOnTextViewActions.Drag.LONG_PRESS,
|
||||
new TextCoordinates(startIndex),
|
||||
new TextCoordinates(endIndex),
|
||||
Press.FINGER));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an action that double taps then drags on text from startIndex to endIndex on the
|
||||
* TextView.<br>
|
||||
* <br>
|
||||
* View constraints:
|
||||
* <ul>
|
||||
* <li>must be a TextView displayed on screen
|
||||
* <ul>
|
||||
*
|
||||
* @param startIndex The index of the TextView's text to start a drag from
|
||||
* @param endIndex The index of the TextView's text to end the drag at
|
||||
*/
|
||||
public static ViewAction doubleTapAndDragOnText(int startIndex, int endIndex) {
|
||||
return actionWithAssertions(
|
||||
new DragOnTextViewActions(
|
||||
DragOnTextViewActions.Drag.DOUBLE_TAP,
|
||||
new TextCoordinates(startIndex),
|
||||
new TextCoordinates(endIndex),
|
||||
Press.FINGER));
|
||||
}
|
||||
|
||||
/**
|
||||
* A provider of the x, y coordinates of the text at the specified index in a text view.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
|
||||
package android.widget.espresso;
|
||||
|
||||
import static android.support.test.espresso.matcher.ViewMatchers.assertThat;
|
||||
import static com.android.internal.util.Preconditions.checkNotNull;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
|
||||
import android.support.test.espresso.NoMatchingViewException;
|
||||
import android.support.test.espresso.ViewAssertion;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import junit.framework.AssertionFailedError;
|
||||
import org.hamcrest.Matcher;
|
||||
|
||||
/**
|
||||
* A collection of assertions on a {@link android.widget.TextView}.
|
||||
*/
|
||||
public final class TextViewAssertions {
|
||||
|
||||
private TextViewAssertions() {}
|
||||
|
||||
/**
|
||||
* Returns a {@link ViewAssertion} that asserts that the text view has a specified
|
||||
* selection.<br>
|
||||
* <br>
|
||||
* View constraints:
|
||||
* <ul>
|
||||
* <li>must be a text view displayed on screen
|
||||
* <ul>
|
||||
*
|
||||
* @param selection The expected selection.
|
||||
*/
|
||||
public static ViewAssertion hasSelection(String selection) {
|
||||
return new TextSelectionAssertion(is(selection));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link ViewAssertion} that asserts that the text view has a specified
|
||||
* selection.<br>
|
||||
* <br>
|
||||
* View constraints:
|
||||
* <ul>
|
||||
* <li>must be a text view displayed on screen
|
||||
* <ul>
|
||||
*
|
||||
* @param selection A matcher representing the expected selection.
|
||||
*/
|
||||
public static ViewAssertion hasSelection(Matcher<String> selection) {
|
||||
return new TextSelectionAssertion(selection);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link ViewAssertion} to check the selected text in a {@link TextView}.
|
||||
*/
|
||||
private static final class TextSelectionAssertion implements ViewAssertion {
|
||||
|
||||
private final Matcher<String> mSelection;
|
||||
|
||||
public TextSelectionAssertion(Matcher<String> selection) {
|
||||
mSelection = checkNotNull(selection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void check(View view, NoMatchingViewException exception) {
|
||||
if (view instanceof TextView) {
|
||||
TextView textView = (TextView) view;
|
||||
int selectionStart = textView.getSelectionStart();
|
||||
int selectionEnd = textView.getSelectionEnd();
|
||||
try {
|
||||
String selectedText = textView.getText()
|
||||
.subSequence(selectionStart, selectionEnd)
|
||||
.toString();
|
||||
assertThat(selectedText, mSelection);
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
throw new AssertionFailedError(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
throw new AssertionFailedError("TextView not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user