/* * 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.support.design.testutils; import static org.junit.Assert.assertEquals; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.annotation.ColorInt; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.design.widget.FloatingActionButton; import android.support.test.espresso.matcher.BoundedMatcher; import android.support.v4.view.GravityCompat; import android.support.v4.view.ViewCompat; import android.support.v4.widget.TextViewCompat; import android.support.v7.view.menu.MenuItemImpl; import android.view.Gravity; import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; import android.widget.ImageView; import android.widget.TextView; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; public class TestUtilsMatchers { /** * Returns a matcher that matches Views that are not narrower than specified width in pixels. */ public static Matcher<View> isNotNarrowerThan(final int minWidth) { return new BoundedMatcher<View, View>(View.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final View view) { final int viewWidth = view.getWidth(); if (viewWidth < minWidth) { failedCheckDescription = "width " + viewWidth + " is less than minimum " + minWidth; return false; } return true; } }; } /** * Returns a matcher that matches Views that are not wider than specified width in pixels. */ public static Matcher<View> isNotWiderThan(final int maxWidth) { return new BoundedMatcher<View, View>(View.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final View view) { final int viewWidth = view.getWidth(); if (viewWidth > maxWidth) { failedCheckDescription = "width " + viewWidth + " is more than maximum " + maxWidth; return false; } return true; } }; } /** * Returns a matcher that matches TextViews with the specified text size. */ public static Matcher withTextSize(final float textSize) { return new BoundedMatcher<View, TextView>(TextView.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final TextView view) { final float ourTextSize = view.getTextSize(); if (Math.abs(textSize - ourTextSize) > 1.0f) { failedCheckDescription = "text size " + ourTextSize + " is different than expected " + textSize; return false; } return true; } }; } /** * Returns a matcher that matches TextViews with the specified text color. */ public static Matcher withTextColor(final @ColorInt int textColor) { return new BoundedMatcher<View, TextView>(TextView.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final TextView view) { final @ColorInt int ourTextColor = view.getCurrentTextColor(); if (ourTextColor != textColor) { int ourAlpha = Color.alpha(ourTextColor); int ourRed = Color.red(ourTextColor); int ourGreen = Color.green(ourTextColor); int ourBlue = Color.blue(ourTextColor); int expectedAlpha = Color.alpha(textColor); int expectedRed = Color.red(textColor); int expectedGreen = Color.green(textColor); int expectedBlue = Color.blue(textColor); failedCheckDescription = "expected color to be [" + expectedAlpha + "," + expectedRed + "," + expectedGreen + "," + expectedBlue + "] but found [" + ourAlpha + "," + ourRed + "," + ourGreen + "," + ourBlue + "]"; return false; } return true; } }; } /** * Returns a matcher that matches TextViews whose start drawable is filled with the specified * fill color. */ public static Matcher withStartDrawableFilledWith(final @ColorInt int fillColor, final int allowedComponentVariance) { return new BoundedMatcher<View, TextView>(TextView.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final TextView view) { final Drawable[] compoundDrawables = view.getCompoundDrawables(); final boolean isRtl = (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL); final Drawable startDrawable = isRtl ? compoundDrawables[2] : compoundDrawables[0]; if (startDrawable == null) { failedCheckDescription = "no start drawable"; return false; } try { final Rect bounds = startDrawable.getBounds(); TestUtils.assertAllPixelsOfColor("", startDrawable, bounds.width(), bounds.height(), true, fillColor, allowedComponentVariance, true); } catch (Throwable t) { failedCheckDescription = t.getMessage(); return false; } return true; } }; } /** * Returns a matcher that matches <code>ImageView</code>s which have drawable flat-filled * with the specific color. */ public static Matcher drawable(@ColorInt final int color, final int allowedComponentVariance) { return new BoundedMatcher<View, ImageView>(ImageView.class) { private String mFailedComparisonDescription; @Override public void describeTo(final Description description) { description.appendText("with drawable of color: "); description.appendText(mFailedComparisonDescription); } @Override public boolean matchesSafely(final ImageView view) { Drawable drawable = view.getDrawable(); if (drawable == null) { return false; } // One option is to check if we have a ColorDrawable and then call getColor // but that API is v11+. Instead, we call our helper method that checks whether // all pixels in a Drawable are of the same specified color. try { TestUtils.assertAllPixelsOfColor("", drawable, view.getWidth(), view.getHeight(), true, color, allowedComponentVariance, true); // If we are here, the color comparison has passed. mFailedComparisonDescription = null; return true; } catch (Throwable t) { // If we are here, the color comparison has failed. mFailedComparisonDescription = t.getMessage(); return false; } } }; } /** * Returns a matcher that matches Views with the specified background fill color. */ public static Matcher withBackgroundFill(final @ColorInt int fillColor) { return new BoundedMatcher<View, View>(View.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final View view) { Drawable background = view.getBackground(); try { TestUtils.assertAllPixelsOfColor("", background, view.getWidth(), view.getHeight(), true, fillColor, 0, true); } catch (Throwable t) { failedCheckDescription = t.getMessage(); return false; } return true; } }; } /** * Returns a matcher that matches FloatingActionButtons with the specified background * fill color. */ public static Matcher withFabBackgroundFill(final @ColorInt int fillColor) { return new BoundedMatcher<View, View>(View.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final View view) { if (!(view instanceof FloatingActionButton)) { return false; } final FloatingActionButton fab = (FloatingActionButton) view; // Since the FAB background is round, and may contain the shadow, we'll look at // just the center half rect of the content area final Rect area = new Rect(); fab.getContentRect(area); final int rectHeightQuarter = area.height() / 4; final int rectWidthQuarter = area.width() / 4; area.left += rectWidthQuarter; area.top += rectHeightQuarter; area.right -= rectWidthQuarter; area.bottom -= rectHeightQuarter; try { TestUtils.assertAllPixelsOfColor("", fab.getBackground(), view.getWidth(), view.getHeight(), false, fillColor, area, 0, true); } catch (Throwable t) { failedCheckDescription = t.getMessage(); return false; } return true; } }; } /** * Returns a matcher that matches {@link View}s based on the given parent type. * * @param parentMatcher the type of the parent to match on */ public static Matcher<View> isChildOfA(final Matcher<View> parentMatcher) { return new TypeSafeMatcher<View>() { @Override public void describeTo(Description description) { description.appendText("is child of a: "); parentMatcher.describeTo(description); } @Override public boolean matchesSafely(View view) { final ViewParent viewParent = view.getParent(); if (!(viewParent instanceof View)) { return false; } if (parentMatcher.matches(viewParent)) { return true; } return false; } }; } /** * Returns a matcher that matches FloatingActionButtons with the specified content height */ public static Matcher withFabContentHeight(final int size) { return new BoundedMatcher<View, View>(View.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final View view) { if (!(view instanceof FloatingActionButton)) { return false; } final FloatingActionButton fab = (FloatingActionButton) view; final Rect area = new Rect(); fab.getContentRect(area); return area.height() == size; } }; } /** * Returns a matcher that matches FloatingActionButtons with the specified gravity. */ public static Matcher withFabContentAreaOnMargins(final int gravity) { return new BoundedMatcher<View, View>(View.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final View view) { if (!(view instanceof FloatingActionButton)) { return false; } final FloatingActionButton fab = (FloatingActionButton) view; final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) fab.getLayoutParams(); final ViewGroup parent = (ViewGroup) view.getParent(); final Rect area = new Rect(); fab.getContentRect(area); final int absGravity = GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(view)); try { switch (absGravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.TOP: assertEquals(lp.topMargin, fab.getTop() + area.top); break; case Gravity.BOTTOM: assertEquals(parent.getHeight() - lp.bottomMargin, fab.getTop() + area.bottom); break; } switch (absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { case Gravity.LEFT: assertEquals(lp.leftMargin, fab.getLeft() + area.left); break; case Gravity.RIGHT: assertEquals(parent.getWidth() - lp.rightMargin, fab.getLeft() + area.right); break; } return true; } catch (Throwable t) { failedCheckDescription = t.getMessage(); return false; } } }; } /** * Returns a matcher that matches FloatingActionButtons with the specified content height */ public static Matcher withCompoundDrawable(final int index, final Drawable expected) { return new BoundedMatcher<View, View>(View.class) { private String failedCheckDescription; @Override public void describeTo(final Description description) { description.appendText(failedCheckDescription); } @Override public boolean matchesSafely(final View view) { if (!(view instanceof TextView)) { return false; } final TextView textView = (TextView) view; return expected == TextViewCompat.getCompoundDrawablesRelative(textView)[index]; } }; } /** * Returns a matcher that matches {@link View}s that are pressed. */ public static Matcher<View> isPressed() { return new TypeSafeMatcher<View>() { @Override public void describeTo(Description description) { description.appendText("is pressed"); } @Override public boolean matchesSafely(View view) { return view.isPressed(); } }; } /** * Returns a matcher that matches views which have a z-value greater than 0. Also matches if * the platform we're running on does not support z-values. */ public static Matcher<View> hasZ() { return new TypeSafeMatcher<View>() { @Override public void describeTo(Description description) { description.appendText("has a z value greater than 0"); } @Override public boolean matchesSafely(View view) { return Build.VERSION.SDK_INT < 21 || ViewCompat.getZ(view) > 0f; } }; } /** * Returns a matcher that matches TextViews with the specified typeface. */ public static Matcher withTypeface(@NonNull final Typeface typeface) { return new TypeSafeMatcher<TextView>(TextView.class) { @Override public void describeTo(final Description description) { description.appendText("view with typeface: " + typeface); } @Override public boolean matchesSafely(final TextView view) { return typeface.equals(view.getTypeface()); } }; } /** * Returns a matcher that matches the action view of the specified menu item. * * @param menu The menu * @param id The ID of the menu item */ public static Matcher<View> isActionViewOf(@NonNull final Menu menu, @IdRes final int id) { return new TypeSafeMatcher<View>() { private Resources mResources; @Override protected boolean matchesSafely(View view) { mResources = view.getResources(); MenuItemImpl item = (MenuItemImpl) menu.findItem(id); return item != null && item.getActionView() == view; } @Override public void describeTo(Description description) { String name; if (mResources != null) { name = mResources.getResourceName(id); } else { name = Integer.toString(id); } description.appendText("is action view of menu item " + name); } }; } }