PositionAssertions.java
/*
* Copyright (C) 2014 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 androidx.test.espresso.assertion;
import static androidx.test.espresso.matcher.ViewMatchers.assertThat;
import static androidx.test.espresso.util.TreeIterables.breadthFirstViewTraversal;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.hamcrest.Matchers.is;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import androidx.test.espresso.AmbiguousViewMatcherException;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.espresso.ViewAssertion;
import androidx.test.espresso.util.HumanReadables;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import java.util.Iterator;
import java.util.Locale;
import org.hamcrest.Matcher;
import org.hamcrest.StringDescription;
/**
* A collection of {@link ViewAssertion}s for checking relative position of elements on the screen.
*
* <p>These comparisons are on the x,y plane; they ignore the z plane.
*/
public final class PositionAssertions {
private static final String TAG = "PositionAssertions";
private PositionAssertions() {}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is completely left of the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is any horizontal overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isCompletelyLeftOf(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.COMPLETELY_LEFT_OF);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is completely right of the
* view matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is any horizontal overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isCompletelyRightOf(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.COMPLETELY_RIGHT_OF);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is completely left of the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is any horizontal overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
* @deprecated Use {@link #isCompletelyLeftOf(Matcher)} instead.
*/
@Deprecated
public static ViewAssertion isLeftOf(Matcher<View> matcher) {
return isCompletelyLeftOf(matcher);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is completely right of the
* view matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is any horizontal overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
* @deprecated Use {@link #isCompletelyRightOf(Matcher)} instead.
*/
@Deprecated
public static ViewAssertion isRightOf(Matcher<View> matcher) {
return isCompletelyRightOf(matcher);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is partially left of the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is no horizontal overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isPartiallyLeftOf(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.PARTIALLY_LEFT_OF);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is partially right of the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is no horizontal overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isPartiallyRightOf(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.PARTIALLY_RIGHT_OF);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is left aligned with the view
* matching the given matcher.
*
* <p>The left 'x' coordinate of the view displayed must equal the left 'x' coordinate of the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if the views are not aligned to the left.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isLeftAlignedWith(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.LEFT_ALIGNED);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is right aligned with the view
* matching the given matcher.
*
* <p>The right 'x' coordinate of the view displayed must equal the right 'x' coordinate of the
* view matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if the views are not aligned to the right.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isRightAlignedWith(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.RIGHT_ALIGNED);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is completely above the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is any vertical overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isCompletelyAbove(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.COMPLETELY_ABOVE);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is completely below the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is any vertical overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isCompletelyBelow(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.COMPLETELY_BELOW);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is partially above the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is no vertical overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isPartiallyAbove(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.PARTIALLY_ABOVE);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is partially below the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is no vertical overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isPartiallyBelow(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.PARTIALLY_BELOW);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is completely above the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there is any vertical overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
* @deprecated Use {@link #isCompletelyAbove(Matcher)} instead.
*/
@Deprecated
public static ViewAssertion isAbove(Matcher<View> matcher) {
return isCompletelyAbove(matcher);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is completely below the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if there any vertical overlap.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
* @deprecated Use {@link #isCompletelyBelow(Matcher)} instead.
*/
@Deprecated
public static ViewAssertion isBelow(Matcher<View> matcher) {
return isCompletelyBelow(matcher);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is bottom aligned with the
* view matching the given matcher.
*
* <p>The bottom 'y' coordinate of the view displayed must equal the bottom 'y' coordinate of the
* view matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if the views are not aligned bottom.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isBottomAlignedWith(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.BOTTOM_ALIGNED);
}
/**
* Returns a {@link ViewAssertion} that asserts that view displayed is top aligned with the view
* matching the given matcher.
*
* <p>The top 'y' coordinate of the view displayed must equal the top 'y' coordinate of the view
* matching the given matcher.
*
* @throws junit.framework.AssertionFailedError if the views are not aligned top.
* @throws AmbiguousViewMatcherException if more than one view matches the given matcher.
* @throws NoMatchingViewException if no views match the given matcher.
*/
public static ViewAssertion isTopAlignedWith(Matcher<View> matcher) {
return relativePositionOf(matcher, Position.TOP_ALIGNED);
}
static ViewAssertion relativePositionOf(
final Matcher<View> viewMatcher, final Position position) {
checkNotNull(viewMatcher);
return new ViewAssertion() {
@Override
public void check(final View foundView, NoMatchingViewException noViewException) {
StringDescription description = new StringDescription();
if (noViewException != null) {
description.appendText(
String.format(
Locale.ROOT,
"' check could not be performed because view '%s' was not found.\n",
noViewException.getViewMatcherDescription()));
Log.e(TAG, description.toString());
throw noViewException;
} else {
// TODO: describe the foundView matcher instead of the foundView itself.
description
.appendText("View:")
.appendText(HumanReadables.describe(foundView))
.appendText(" is not ")
.appendText(position.toString())
.appendText(" view ")
.appendText(viewMatcher.toString());
assertThat(
description.toString(),
isRelativePosition(
foundView, findView(viewMatcher, getTopViewGroup(foundView)), position),
is(true));
}
}
};
}
// Helper methods
static View findView(final Matcher<View> toView, View root) {
Preconditions.checkNotNull(toView);
Preconditions.checkNotNull(root);
final Predicate<View> viewPredicate =
new Predicate<View>() {
@Override
public boolean apply(View input) {
return toView.matches(input);
}
};
Iterator<View> matchedViewIterator =
Iterables.filter(breadthFirstViewTraversal(root), viewPredicate).iterator();
View matchedView = null;
while (matchedViewIterator.hasNext()) {
if (matchedView != null) {
// Ambiguous!
throw new AmbiguousViewMatcherException.Builder()
.withRootView(root)
.withViewMatcher(toView)
.withView1(matchedView)
.withView2(matchedViewIterator.next())
.withOtherAmbiguousViews(Iterators.toArray(matchedViewIterator, View.class))
.build();
} else {
matchedView = matchedViewIterator.next();
}
}
if (matchedView == null) {
throw new NoMatchingViewException.Builder()
.withViewMatcher(toView)
.withRootView(root)
.build();
}
return matchedView;
}
private static ViewGroup getTopViewGroup(View view) {
ViewParent currentParent = view.getParent();
ViewGroup topView = null;
while (currentParent != null) {
if (currentParent instanceof ViewGroup) {
topView = (ViewGroup) currentParent;
}
currentParent = currentParent.getParent();
}
return topView;
}
static boolean isRelativePosition(View view1, View view2, Position position) {
int[] location1 = new int[2];
int[] location2 = new int[2];
view1.getLocationOnScreen(location1);
view2.getLocationOnScreen(location2);
switch (position) {
case COMPLETELY_LEFT_OF:
return location1[0] + view1.getWidth() <= location2[0];
case COMPLETELY_RIGHT_OF:
return location2[0] + view2.getWidth() <= location1[0];
case COMPLETELY_ABOVE:
return location1[1] + view1.getHeight() <= location2[1];
case COMPLETELY_BELOW:
return location2[1] + view2.getHeight() <= location1[1];
case PARTIALLY_LEFT_OF:
return location1[0] < location2[0] && location2[0] < location1[0] + view1.getWidth();
case PARTIALLY_RIGHT_OF:
return location2[0] < location1[0] && location1[0] < location2[0] + view2.getWidth();
case PARTIALLY_ABOVE:
return location1[1] < location2[1] && location2[1] < location1[1] + view1.getHeight();
case PARTIALLY_BELOW:
return location2[1] < location1[1] && location1[1] < location2[1] + view2.getHeight();
case LEFT_ALIGNED:
return location1[0] == location2[0];
case RIGHT_ALIGNED:
return location1[0] + view1.getWidth() == location2[0] + view2.getWidth();
case TOP_ALIGNED:
return location1[1] == location2[1];
case BOTTOM_ALIGNED:
return location1[1] + view1.getHeight() == location2[1] + view2.getHeight();
default:
return false;
}
}
enum Position {
COMPLETELY_LEFT_OF("completely left of"),
COMPLETELY_RIGHT_OF("completely right of"),
COMPLETELY_ABOVE("completely above"),
COMPLETELY_BELOW("completely below"),
PARTIALLY_LEFT_OF("partially left of"),
PARTIALLY_RIGHT_OF("partially right of"),
PARTIALLY_ABOVE("partially above"),
PARTIALLY_BELOW("partially below"),
LEFT_ALIGNED("aligned left with"),
RIGHT_ALIGNED("aligned right with"),
TOP_ALIGNED("aligned top with"),
BOTTOM_ALIGNED("aligned bottom with");
private final String positionValue;
private Position(String value) {
positionValue = value;
}
@Override
public String toString() {
return positionValue;
}
}
}