1/* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package android.support.design.testutils; 18 19import android.graphics.Color; 20import android.graphics.Rect; 21import android.graphics.drawable.Drawable; 22import android.support.annotation.ColorInt; 23import android.support.design.widget.FloatingActionButton; 24import android.support.test.espresso.matcher.BoundedMatcher; 25import android.support.v4.view.ViewCompat; 26import android.view.View; 27import android.view.ViewParent; 28import android.widget.TextView; 29 30import org.hamcrest.Description; 31import org.hamcrest.Matcher; 32import org.hamcrest.TypeSafeMatcher; 33 34public class TestUtilsMatchers { 35 /** 36 * Returns a matcher that matches Views that are not narrower than specified width in pixels. 37 */ 38 public static Matcher<View> isNotNarrowerThan(final int minWidth) { 39 return new BoundedMatcher<View, View>(View.class) { 40 private String failedCheckDescription; 41 42 @Override 43 public void describeTo(final Description description) { 44 description.appendText(failedCheckDescription); 45 } 46 47 @Override 48 public boolean matchesSafely(final View view) { 49 final int viewWidth = view.getWidth(); 50 if (viewWidth < minWidth) { 51 failedCheckDescription = 52 "width " + viewWidth + " is less than minimum " + minWidth; 53 return false; 54 } 55 return true; 56 } 57 }; 58 } 59 60 /** 61 * Returns a matcher that matches Views that are not wider than specified width in pixels. 62 */ 63 public static Matcher<View> isNotWiderThan(final int maxWidth) { 64 return new BoundedMatcher<View, View>(View.class) { 65 private String failedCheckDescription; 66 67 @Override 68 public void describeTo(final Description description) { 69 description.appendText(failedCheckDescription); 70 } 71 72 @Override 73 public boolean matchesSafely(final View view) { 74 final int viewWidth = view.getWidth(); 75 if (viewWidth > maxWidth) { 76 failedCheckDescription = 77 "width " + viewWidth + " is more than maximum " + maxWidth; 78 return false; 79 } 80 return true; 81 } 82 }; 83 } 84 85 /** 86 * Returns a matcher that matches TextViews with the specified text size. 87 */ 88 public static Matcher withTextSize(final float textSize) { 89 return new BoundedMatcher<View, TextView>(TextView.class) { 90 private String failedCheckDescription; 91 92 @Override 93 public void describeTo(final Description description) { 94 description.appendText(failedCheckDescription); 95 } 96 97 @Override 98 public boolean matchesSafely(final TextView view) { 99 final float ourTextSize = view.getTextSize(); 100 if (Math.abs(textSize - ourTextSize) > 1.0f) { 101 failedCheckDescription = 102 "text size " + ourTextSize + " is different than expected " + textSize; 103 return false; 104 } 105 return true; 106 } 107 }; 108 } 109 110 /** 111 * Returns a matcher that matches TextViews with the specified text color. 112 */ 113 public static Matcher withTextColor(final @ColorInt int textColor) { 114 return new BoundedMatcher<View, TextView>(TextView.class) { 115 private String failedCheckDescription; 116 117 @Override 118 public void describeTo(final Description description) { 119 description.appendText(failedCheckDescription); 120 } 121 122 @Override 123 public boolean matchesSafely(final TextView view) { 124 final @ColorInt int ourTextColor = view.getCurrentTextColor(); 125 if (ourTextColor != textColor) { 126 int ourAlpha = Color.alpha(ourTextColor); 127 int ourRed = Color.red(ourTextColor); 128 int ourGreen = Color.green(ourTextColor); 129 int ourBlue = Color.blue(ourTextColor); 130 131 int expectedAlpha = Color.alpha(textColor); 132 int expectedRed = Color.red(textColor); 133 int expectedGreen = Color.green(textColor); 134 int expectedBlue = Color.blue(textColor); 135 136 failedCheckDescription = 137 "expected color to be [" 138 + expectedAlpha + "," + expectedRed + "," 139 + expectedGreen + "," + expectedBlue 140 + "] but found [" 141 + ourAlpha + "," + ourRed + "," 142 + ourGreen + "," + ourBlue + "]"; 143 return false; 144 } 145 return true; 146 } 147 }; 148 } 149 150 /** 151 * Returns a matcher that matches TextViews whose start drawable is filled with the specified 152 * fill color. 153 */ 154 public static Matcher withStartDrawableFilledWith(final @ColorInt int fillColor, 155 final int allowedComponentVariance) { 156 return new BoundedMatcher<View, TextView>(TextView.class) { 157 private String failedCheckDescription; 158 159 @Override 160 public void describeTo(final Description description) { 161 description.appendText(failedCheckDescription); 162 } 163 164 @Override 165 public boolean matchesSafely(final TextView view) { 166 final Drawable[] compoundDrawables = view.getCompoundDrawables(); 167 final boolean isRtl = 168 (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL); 169 final Drawable startDrawable = isRtl ? compoundDrawables[2] : compoundDrawables[0]; 170 if (startDrawable == null) { 171 failedCheckDescription = "no start drawable"; 172 return false; 173 } 174 try { 175 final Rect bounds = startDrawable.getBounds(); 176 TestUtils.assertAllPixelsOfColor("", 177 startDrawable, bounds.width(), bounds.height(), true, 178 fillColor, allowedComponentVariance, true); 179 } catch (Throwable t) { 180 failedCheckDescription = t.getMessage(); 181 return false; 182 } 183 return true; 184 } 185 }; 186 } 187 188 /** 189 * Returns a matcher that matches Views with the specified background fill color. 190 */ 191 public static Matcher withBackgroundFill(final @ColorInt int fillColor) { 192 return new BoundedMatcher<View, View>(View.class) { 193 private String failedCheckDescription; 194 195 @Override 196 public void describeTo(final Description description) { 197 description.appendText(failedCheckDescription); 198 } 199 200 @Override 201 public boolean matchesSafely(final View view) { 202 Drawable background = view.getBackground(); 203 try { 204 TestUtils.assertAllPixelsOfColor("", 205 background, view.getWidth(), view.getHeight(), true, 206 fillColor, 0, true); 207 } catch (Throwable t) { 208 failedCheckDescription = t.getMessage(); 209 return false; 210 } 211 return true; 212 } 213 }; 214 } 215 216 /** 217 * Returns a matcher that matches FloatingActionButtons with the specified background 218 * fill color. 219 */ 220 public static Matcher withFabBackgroundFill(final @ColorInt int fillColor) { 221 return new BoundedMatcher<View, View>(View.class) { 222 private String failedCheckDescription; 223 224 @Override 225 public void describeTo(final Description description) { 226 description.appendText(failedCheckDescription); 227 } 228 229 @Override 230 public boolean matchesSafely(final View view) { 231 if (!(view instanceof FloatingActionButton)) { 232 return false; 233 } 234 235 final FloatingActionButton fab = (FloatingActionButton) view; 236 237 // Since the FAB background is round, and may contain the shadow, we'll look at 238 // just the center half rect of the content area 239 final Rect area = new Rect(); 240 fab.getContentRect(area); 241 242 final int rectHeightQuarter = area.height() / 4; 243 final int rectWidthQuarter = area.width() / 4; 244 area.left += rectWidthQuarter; 245 area.top += rectHeightQuarter; 246 area.right -= rectWidthQuarter; 247 area.bottom -= rectHeightQuarter; 248 249 try { 250 TestUtils.assertAllPixelsOfColor("", 251 fab.getBackground(), view.getWidth(), view.getHeight(), false, 252 fillColor, area, 0, true); 253 } catch (Throwable t) { 254 failedCheckDescription = t.getMessage(); 255 return false; 256 } 257 return true; 258 } 259 }; 260 } 261 262 /** 263 * Returns a matcher that matches {@link View}s based on the given parent type. 264 * 265 * @param parentMatcher the type of the parent to match on 266 */ 267 public static Matcher<View> isChildOfA(final Matcher<View> parentMatcher) { 268 return new TypeSafeMatcher<View>() { 269 @Override 270 public void describeTo(Description description) { 271 description.appendText("is child of a: "); 272 parentMatcher.describeTo(description); 273 } 274 275 @Override 276 public boolean matchesSafely(View view) { 277 final ViewParent viewParent = view.getParent(); 278 if (!(viewParent instanceof View)) { 279 return false; 280 } 281 if (parentMatcher.matches(viewParent)) { 282 return true; 283 } 284 return false; 285 } 286 }; 287 } 288 289 /** 290 * Returns a matcher that matches FloatingActionButtons with the specified content height 291 */ 292 public static Matcher withFabContentHeight(final int size) { 293 return new BoundedMatcher<View, View>(View.class) { 294 private String failedCheckDescription; 295 296 @Override 297 public void describeTo(final Description description) { 298 description.appendText(failedCheckDescription); 299 } 300 301 @Override 302 public boolean matchesSafely(final View view) { 303 if (!(view instanceof FloatingActionButton)) { 304 return false; 305 } 306 307 final FloatingActionButton fab = (FloatingActionButton) view; 308 final Rect area = new Rect(); 309 fab.getContentRect(area); 310 311 return area.height() == size; 312 } 313 }; 314 } 315 316} 317