// Copyright 2015 The Project Buendia Authors // // 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 distrib- // uted 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 // specific language governing permissions and limitations under the License. package org.projectbuendia.client.ui.matchers; import android.app.Activity; import android.graphics.drawable.Drawable; import android.support.test.InstrumentationRegistry; import android.support.test.espresso.DataInteraction; import android.support.test.espresso.Espresso; import android.support.test.espresso.ViewInteraction; import android.support.test.espresso.action.ViewActions; import android.support.test.espresso.matcher.ViewMatchers; import android.test.ActivityInstrumentationTestCase2; import android.view.View; import com.estimote.sdk.internal.Preconditions; import com.google.common.base.Joiner; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.StringDescription; import org.hamcrest.TypeSafeMatcher; import org.hamcrest.Matchers; import static android.support.test.espresso.assertion.ViewAssertions.matches; import static org.hamcrest.Matchers.allOf; /** Matchers for {@link View}s. */ public class TestCaseWithMatcherMethods<T extends Activity> extends ActivityInstrumentationTestCase2<T> { public TestCaseWithMatcherMethods(Class<T> startingActivity) { super(startingActivity); } // More concise ways of expressing common constructions like // onView(hasId(...)), onView(...).check(matches(...)), onView(...).perform(...). public static ViewInteraction viewWithId(int id) { return Espresso.onView(hasId(id)); } public static Matcher<View> hasId(int id) { return new MatcherWithDescription<>(ViewMatchers.withId(id), "has ID " + id); } public static ViewInteraction viewWithText(String text) { return Espresso.onView(hasText(text)); } public static Matcher<View> hasText(String text) { return new MatcherWithDescription<>(ViewMatchers.withText(text), "has the exact text \"" + text + "\""); } public static ViewInteraction viewWithText(int resourceId) { return Espresso.onView(hasText(resourceId)); } public static Matcher<View> hasText(int resourceId) { return new MatcherWithDescription<>(ViewMatchers.withText(resourceId), "has string resource " + resourceId + " as its text"); } @SafeVarargs public static ViewInteraction viewThat(Matcher<View>... matchers) { return Espresso.onView(matchers.length > 1 ? allOf(matchers) : matchers[0]); } public static DataInteraction dataThat(Matcher... matchers) { return Espresso.onData(matchers.length > 1 ? allOf(matchers) : matchers[0]); } @SafeVarargs public static void expect(ViewInteraction vi, Matcher<View>... matchers) { vi.check(matches(matchers.length == 0 ? isVisible() : matchers.length > 1 ? allOf(matchers) : matchers[0])); } public static Matcher<View> isVisible() { return new MatcherWithDescription<>(ViewMatchers.isDisplayed(), "is visible"); } public static void expectVisible(DataInteraction di) { di.check(matches(isVisible())); } public static void click(DataInteraction di) { di.perform(ViewActions.click()); } public static void openActionBarOptionsMenu() { Espresso.openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getTargetContext()); } public static void scrollToAndClick(ViewInteraction vi) { scrollTo(vi); click(vi); } public static void scrollTo(ViewInteraction vi) { vi.perform(ViewActions.scrollTo()); } public static void click(ViewInteraction vi) { vi.perform(ViewActions.click()); } public static void scrollToAndType(Object obj, ViewInteraction vi) { scrollTo(vi); type(obj, vi); } public static void type(Object obj, ViewInteraction vi) { vi.perform(ViewActions.typeText(obj.toString())); } // Matchers with better descriptions than those in espresso.matcher.ViewMatchers. public static void scrollToAndExpectVisible(ViewInteraction vi) { scrollTo(vi); vi.check(matches(isVisible())); } public static Matcher<View> isA(final Class<? extends View> cls) { String name = cls.getSimpleName(); return new MatcherWithDescription<>( ViewMatchers.isAssignableFrom(cls), (name.matches("^[AEIOU]") ? "is an " : "is a ") + name); } // Names of Espresso matchers form expressions that don't make any grammatical sense, // such as withParent(withSibling(isVisible())). Instead of prepositional // phrases like "withFoo", matcher names should be verb phrases like "hasFoo" or // connecting verb phrases ending in "That", yielding more readable expressions // such as whoseParent(hasSiblingThat(isVisible())). public static Matcher<View> isAnyOf(final Class<? extends View>... classes) { Preconditions.checkArgument(classes.length >= 1); return new TypeSafeMatcher<View>() { @Override public boolean matchesSafely(View obj) { for (Class cls : classes) { if (cls.isInstance(obj)) return true; } return false; } @Override public void describeTo(Description description) { String[] names = new String[classes.length]; for (int i = 0; i < classes.length; i++) { names[i] = classes[i].getSimpleName(); } String list = Joiner.on(", ").join(names); if (names.length == 2) { list = list.replace(", ", " or "); } else if (names.length > 2) { list = list.replaceAll(", ([^,*])$", ", or $1"); } description.appendText( (names[0].matches("^[AEIOU]") ? "is an " : "is a ") + list); } }; } @SafeVarargs public static Matcher<View> whoseParent(Matcher<View>... matchers) { Matcher<View> matcher = matchers.length > 1 ? allOf(matchers) : matchers[0]; return new MatcherWithDescription<>(ViewMatchers.withParent(matcher), "whose parent {1}", matcher); } @SafeVarargs public static Matcher<View> hasChildThat(Matcher<View>... matchers) { Matcher<View> matcher = matchers.length > 1 ? allOf(matchers) : matchers[0]; return new MatcherWithDescription<>(ViewMatchers.withChild(matcher), "has a child that {1}", matcher); } @SafeVarargs public static Matcher<View> hasAncestorThat(Matcher<View>... matchers) { Matcher<View> matcher = matchers.length > 1 ? allOf(matchers) : matchers[0]; return new MatcherWithDescription<>(ViewMatchers.isDescendantOfA(matcher), "has an ancestor that {1}", matcher); } @SafeVarargs public static Matcher<View> hasDescendantThat(Matcher<View>... matchers) { Matcher<View> matcher = matchers.length > 1 ? allOf(matchers) : matchers[0]; return new MatcherWithDescription<>(ViewMatchers.hasDescendant(matcher), "has a descendant that {1}", matcher); } @SafeVarargs public static Matcher<View> hasSiblingThat(Matcher<View>... matchers) { Matcher<View> matcher = matchers.length > 1 ? allOf(matchers) : matchers[0]; return new MatcherWithDescription<>(ViewMatchers.hasSibling(matcher), "has a sibling that {1}", matcher); } public static Matcher<View> isChecked() { return new MatcherWithDescription<>(ViewMatchers.isChecked(), "is checked"); } public static Matcher<View> isNotChecked() { return new MatcherWithDescription<>(ViewMatchers.isNotChecked(), "is unchecked"); } @SafeVarargs public static Matcher<View> hasText(Matcher<String>... matchers) { Matcher<String> matcher = matchers.length > 1 ? allOf(matchers) : matchers[0]; return new MatcherWithDescription<>(ViewMatchers.withText(matcher), "has text {1}", matcher); } public static Matcher<View> hasTextContaining(String text) { return new MatcherWithDescription<>(ViewMatchers.withText(Matchers.containsString(text)), "has text containing \"" + text + "\""); } public static Matcher<View> hasTextMatchingRegex(String regex) { return new MatcherWithDescription<>( ViewMatchers.withText(StringMatchers.matchesRegex(regex)), "has text matching regex /" + regex + "/"); } public static Matcher<View> isAtLeastNPercentVisible(int percentage) { return new MatcherWithDescription<>(ViewMatchers.isDisplayingAtLeast(percentage), "is at least " + percentage + "% visible"); } /** Matcher that matches any view in the given row, assuming all rows have the specified height. */ public static TypeSafeMatcher<View> isInRow(final int rowNumber, final int rowHeight) { return new TypeSafeMatcher<View>() { @Override public boolean matchesSafely(View view) { return view.getY() >= getMinY() && view.getY() < getMaxY(); } private int getMaxY() { return (rowNumber + 1)*rowHeight; } private int getMinY() { return rowNumber*rowHeight; } @Override public void describeTo(Description description) { description.appendText("has " + getMinY() + " <= y < " + getMaxY()); } }; } /** Matcher that matches any view in the given column, assuming all columns have the specified with. */ public static TypeSafeMatcher<View> isInColumn(final int colNumber, final int colWidth) { return new TypeSafeMatcher<View>() { @Override public boolean matchesSafely(View view) { return view.getX() >= getMinX() && view.getX() < getMaxX(); } private int getMaxX() { return (colNumber + 1)*colWidth; } private int getMinX() { return colNumber*colWidth; } @Override public void describeTo(Description description) { description.appendText("has " + getMinX() + " <= x < " + getMaxX()); } }; } /** Matcher that matches any view with the given background drawable. */ public static TypeSafeMatcher<View> hasBackground(final Drawable background) { return new TypeSafeMatcher<View>() { @Override public boolean matchesSafely(View view) { return background != null && view.getBackground() != null && background.getConstantState().equals( view.getBackground().getConstantState()); } @Override public void describeTo(Description description) { description.appendText("has background " + background.toString()); } }; } /** Matcher that matches a view with the background drawable specified by ID. */ public static TypeSafeMatcher<View> hasBackground(final int resourceId) { return new TypeSafeMatcher<View>() { @Override public boolean matchesSafely(View view) { Drawable background = view.getResources().getDrawable(resourceId); return background != null && view.getBackground() != null && background.getConstantState().equals( view.getBackground().getConstantState()); } @Override public void describeTo(Description description) { description.appendText("has drawable resource " + resourceId + " as its background"); } }; } protected static void expectVisibleSoon(ViewInteraction vi) { expectVisibleWithin(30000, vi); } protected static void expectVisibleWithin(int timeoutMs, ViewInteraction vi) { long deadline = System.currentTimeMillis() + timeoutMs; boolean found = false; Throwable throwable = null; while (!found && System.currentTimeMillis() < deadline) { try { expectVisible(vi); found = true; } catch (Throwable t) { throwable = t; try { Thread.sleep(100); } catch (InterruptedException e1) { Thread.yield(); } } } if (!found) { throw new RuntimeException(throwable); } } public static void expectVisible(ViewInteraction vi) { vi.check(matches(isVisible())); } /** Replaces the description of an existing matcher. */ static class MatcherWithDescription<T> extends TypeSafeMatcher<T> { Matcher mMatcher; String mFormat; Matcher[] mArgMatchers; public MatcherWithDescription(Matcher<T> matcher, String format, Matcher... argMatchers) { mMatcher = matcher; mFormat = format; mArgMatchers = argMatchers; } public boolean matchesSafely(T obj) { return mMatcher.matches(obj); } public void describeTo(Description description) { String[] args = new String[mArgMatchers.length]; for (int i = 0; i < mArgMatchers.length; i++) { StringDescription argDescription = new StringDescription(); mArgMatchers[i].describeTo(argDescription); args[i] = argDescription.toString(); } description.appendText(String.format(mFormat, (Object[]) args)); } } }