Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add test case for MainDialog Activity #30

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package de.dotwee.micropinner;

import static de.dotwee.micropinner.VisibleViewMatcher.isVisible;
import static org.hamcrest.Matchers.allOf;

import android.util.Log;
import android.view.View;
import android.view.ViewConfiguration;
import android.webkit.WebView;
import android.support.test.espresso.PerformException;
import android.support.test.espresso.UiController;
import android.support.test.espresso.ViewAction;
import android.support.test.espresso.action.CoordinatesProvider;
import android.support.test.espresso.action.PrecisionDescriber;
import android.support.test.espresso.action.Tap;
import android.support.test.espresso.action.Tapper;
import android.support.test.espresso.util.HumanReadables;
import java.util.Locale;
import java.util.Optional;

import org.hamcrest.Matcher;

/**
* Custom click action similar to the GeneralClickAction provided by Espresso.
*
* The only difference is that it does not force the target view to be displayed at least 90% on
* screen (i.e., 90% of the view in sight of the user).
* In this custom class, the only constraint is that the view needs to have "Visible" visibility and
* positive height and width. A typical example is when a long form has a visible view at the
* bottom, but the UI needs to be scrolled to reach it.
*/
public final class ClickWithoutDisplayConstraint implements ViewAction {
private static final String TAG = "ClickWithoutDisplayConstraint";

final CoordinatesProvider coordinatesProvider;
final Tapper tapper;
final PrecisionDescriber precisionDescriber;
private final Optional<ViewAction> rollbackAction;
private final int inputDevice;
private final int buttonState;


@Deprecated
public ClickWithoutDisplayConstraint(
Tapper tapper,
CoordinatesProvider coordinatesProvider,
PrecisionDescriber precisionDescriber) {
this(tapper, coordinatesProvider, precisionDescriber, 0, 0, null);
}

public ClickWithoutDisplayConstraint(
Tapper tapper,
CoordinatesProvider coordinatesProvider,
PrecisionDescriber precisionDescriber,
int inputDevice,
int buttonState) {
this(tapper, coordinatesProvider, precisionDescriber, inputDevice, buttonState, null);
}

@Deprecated
public ClickWithoutDisplayConstraint(
Tapper tapper,
CoordinatesProvider coordinatesProvider,
PrecisionDescriber precisionDescriber,
ViewAction rollbackAction) {
this(tapper, coordinatesProvider, precisionDescriber, 0, 0, rollbackAction);
}

public ClickWithoutDisplayConstraint(
Tapper tapper,
CoordinatesProvider coordinatesProvider,
PrecisionDescriber precisionDescriber,
int inputDevice,
int buttonState,
ViewAction rollbackAction) {
this.coordinatesProvider = coordinatesProvider;
this.tapper = tapper;
this.precisionDescriber = precisionDescriber;
this.inputDevice = inputDevice;
this.buttonState = buttonState;
this.rollbackAction = Optional.ofNullable(rollbackAction);
}

@Override
@SuppressWarnings("unchecked")
public Matcher<View> getConstraints() {
Matcher<View> standardConstraint = isVisible();
if (rollbackAction.isPresent()) {
return allOf(standardConstraint, rollbackAction.get().getConstraints());
} else {
return standardConstraint;
}
}

@Override
public void perform(UiController uiController, View view) {
float[] coordinates = coordinatesProvider.calculateCoordinates(view);
float[] precision = precisionDescriber.describePrecision();

Tapper.Status status = Tapper.Status.FAILURE;
int loopCount = 0;
// Native event injection is quite a tricky process. A tap is actually 2
// seperate motion events which need to get injected into the system. Injection
// makes an RPC call from our app under test to the Android system server, the
// system server decides which window layer to deliver the event to, the system
// server makes an RPC to that window layer, that window layer delivers the event
// to the correct UI element, activity, or window object. Now we need to repeat
// that 2x. for a simple down and up. Oh and the down event triggers timers to
// detect whether or not the event is a long vs. short press. The timers are
// removed the moment the up event is received (NOTE: the possibility of eventTime
// being in the future is totally ignored by most motion event processors).
//
// Phew.
//
// The net result of this is sometimes we'll want to do a regular tap, and for
// whatever reason the up event (last half) of the tap is delivered after long
// press timeout (depending on system load) and the long press behaviour is
// displayed (EG: show a context menu). There is no way to avoid or handle this more
// gracefully. Also the longpress behavour is app/widget specific. So if you have
// a seperate long press behaviour from your short press, you can pass in a
// 'RollBack' ViewAction which when executed will undo the effects of long press.

while (status != Tapper.Status.SUCCESS && loopCount < 3) {
try {
status = tapper.sendTap(uiController, coordinates, precision, inputDevice, buttonState);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(
TAG,
"perform: "
+ String.format(
Locale.ROOT,
"%s - At Coordinates: %d, %d and precision: %d, %d",
this.getDescription(),
(int) coordinates[0],
(int) coordinates[1],
(int) precision[0],
(int) precision[1]));
}
} catch (RuntimeException re) {
throw new PerformException.Builder()
.withActionDescription(
String.format(
Locale.ROOT,
"%s - At Coordinates: %d, %d and precision: %d, %d",
this.getDescription(),
(int) coordinates[0],
(int) coordinates[1],
(int) precision[0],
(int) precision[1]))
.withViewDescription(HumanReadables.describe(view))
.withCause(re)
.build();
}

int duration = ViewConfiguration.getPressedStateDuration();
// ensures that all work enqueued to process the tap has been run.
if (duration > 0) {
uiController.loopMainThreadForAtLeast(duration);
}
if (status == Tapper.Status.WARNING) {
if (rollbackAction.isPresent()) {
rollbackAction.get().perform(uiController, view);
} else {
break;
}
}
loopCount++;
}
if (status == Tapper.Status.FAILURE) {
throw new PerformException.Builder()
.withActionDescription(this.getDescription())
.withViewDescription(HumanReadables.describe(view))
.withCause(
new RuntimeException(
String.format(
Locale.ROOT,
"Couldn't click at: %s,%s precision: %s, %s . Tapper: %s coordinate"
+ " provider: %s precision describer: %s. Tried %s times. With Rollback?"
+ " %s",
coordinates[0],
coordinates[1],
precision[0],
precision[1],
tapper,
coordinatesProvider,
precisionDescriber,
loopCount,
rollbackAction.isPresent())))
.build();
}

if (tapper == Tap.SINGLE && view instanceof WebView) {
// WebViews will not process click events until double tap
// timeout. Not the best place for this - but good for now.
uiController.loopMainThreadForAtLeast(ViewConfiguration.getDoubleTapTimeout());
}
}

@Override
public String getDescription() {
return tapper.toString().toLowerCase() + " click";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package de.dotwee.micropinner;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;

/**
* Custom BaseMatcher to match strings ignoring case as well as leading and trailing spaces
*/
public class IsEqualTrimmingAndIgnoringCase extends BaseMatcher<String> {

private final String string;

public IsEqualTrimmingAndIgnoringCase(String string) {
if (string == null) {
throw new IllegalArgumentException("Non-null value required by IsEqualTrimmingAndIgnoringCase()");
}
this.string = string;
}

public boolean matchesSafely(String item) {
return string.trim().equalsIgnoreCase(item.trim());
}

private void describeMismatchSafely(String item, Description mismatchDescription) {
mismatchDescription.appendText("was ").appendText(item);
}

@Override
public void describeTo(Description description) {
description.appendText("equalToTrimmingAndIgnoringCase(")
.appendValue(string)
.appendText(")");
}

public static IsEqualTrimmingAndIgnoringCase equalToTrimmingAndIgnoringCase(String string) {
return new IsEqualTrimmingAndIgnoringCase(string);
}

@Override
public boolean matches(Object item) {
return item != null && matchesSafely(item.toString());
}

@Override
final public void describeMismatch(Object item, Description description) {
if (item == null) {
super.describeMismatch(item, description);
} else {
describeMismatchSafely(item.toString(), description);
}
}
}
137 changes: 137 additions & 0 deletions app/src/androidTest/java/de/dotwee/micropinner/MainDialogTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package de.dotwee.micropinner;

import android.support.test.espresso.Espresso;
import android.support.test.espresso.ViewInteraction;
import android.support.test.espresso.action.GeneralLocation;
import android.support.test.espresso.action.Press;
import android.support.test.espresso.action.Tap;
import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;

import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import de.dotwee.micropinner.view.MainDialog;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.action.ViewActions.replaceText;
import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
import static android.support.test.espresso.matcher.ViewMatchers.isDescendantOfA;
import static android.support.test.espresso.matcher.ViewMatchers.withHint;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static de.dotwee.micropinner.IsEqualTrimmingAndIgnoringCase.equalToTrimmingAndIgnoringCase;
import static de.dotwee.micropinner.VisibleViewMatcher.isVisible;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;

@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainDialogTest {

@Rule
public ActivityTestRule<MainDialog> mActivityTestRule =
new ActivityTestRule<>(MainDialog.class);

@Test
public void mainDialogTest() {
System.out.println("Starting run of ETGTestCaseForPR");
ViewInteraction android_widget_Spinner =
onView(
allOf(
withId(R.id.spinnerVisibility),
isVisible(),
isDescendantOfA(withId(R.id.dialogContentView))));
android_widget_Spinner.perform(getLongClickAction());

Espresso.pressBackUnconditionally();

ViewInteraction android_widget_EditText =
onView(
allOf(
withId(R.id.editTextContent),
withTextOrHint(equalToTrimmingAndIgnoringCase("Content")),
isVisible(),
isDescendantOfA(withId(R.id.dialogContentView))));
android_widget_EditText.perform(replaceText("holoptychiid"));

ViewInteraction android_widget_Switch =
onView(
allOf(
withId(R.id.switchAdvanced),
isVisible(),
isDescendantOfA(
allOf(
withId(R.id.linearLayoutHeader),
isDescendantOfA(withId(R.id.dialogHeaderView))))));
android_widget_Switch.perform(getClickAction());

ViewInteraction android_widget_LinearLayout =
onView(
allOf(
withId(R.id.linearLayoutHeader),
isVisible(),
hasDescendant(
allOf(
withId(R.id.dialogTitle),
withTextOrHint(equalToTrimmingAndIgnoringCase("MicroPinner")))),
hasDescendant(withId(R.id.switchAdvanced)),
isDescendantOfA(withId(R.id.dialogHeaderView))));
android_widget_LinearLayout.perform(getLongClickAction());

ViewInteraction android_widget_CheckBox =
onView(
allOf(
withId(R.id.checkBoxShowActions),
withTextOrHint(equalToTrimmingAndIgnoringCase("Show notification actions")),
isVisible(),
isDescendantOfA(withId(R.id.dialogContentView))));
android_widget_CheckBox.perform(getClickAction());

ViewInteraction android_widget_EditText2 =
onView(
allOf(
withId(R.id.editTextTitle),
withTextOrHint(equalToTrimmingAndIgnoringCase("Title")),
isVisible(),
isDescendantOfA(withId(R.id.dialogContentView))));
android_widget_EditText2.perform(replaceText("tetraspgia"));

ViewInteraction android_widget_Button =
onView(
allOf(
withId(R.id.buttonPin),
withTextOrHint(equalToTrimmingAndIgnoringCase("PIN")),
isVisible(),
isDescendantOfA(withId(R.id.dialogFooterView))));
android_widget_Button.perform(getClickAction());
}

private static Matcher<View> withTextOrHint(final Matcher<String> stringMatcher) {
return anyOf(withText(stringMatcher), withHint(stringMatcher));
}

private ClickWithoutDisplayConstraint getClickAction() {
return new ClickWithoutDisplayConstraint(
Tap.SINGLE,
GeneralLocation.VISIBLE_CENTER,
Press.FINGER,
InputDevice.SOURCE_UNKNOWN,
MotionEvent.BUTTON_PRIMARY);
}

private ClickWithoutDisplayConstraint getLongClickAction() {
return new ClickWithoutDisplayConstraint(
Tap.LONG,
GeneralLocation.CENTER,
Press.FINGER,
InputDevice.SOURCE_UNKNOWN,
MotionEvent.BUTTON_PRIMARY);
}
}
Loading