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 static org.junit.Assert.assertEquals; 20 21import android.graphics.Color; 22import android.graphics.Rect; 23import android.graphics.drawable.Drawable; 24import android.support.annotation.ColorInt; 25import android.support.design.widget.FloatingActionButton; 26import android.support.test.espresso.matcher.BoundedMatcher; 27import android.support.v4.view.GravityCompat; 28import android.support.v4.view.ViewCompat; 29import android.support.v4.widget.TextViewCompat; 30import android.view.Gravity; 31import android.view.View; 32import android.view.ViewGroup; 33import android.view.ViewParent; 34import android.widget.ImageView; 35import android.widget.TextView; 36 37import org.hamcrest.Description; 38import org.hamcrest.Matcher; 39import org.hamcrest.TypeSafeMatcher; 40 41public class TestUtilsMatchers { 42 /** 43 * Returns a matcher that matches Views that are not narrower than specified width in pixels. 44 */ 45 public static Matcher<View> isNotNarrowerThan(final int minWidth) { 46 return new BoundedMatcher<View, View>(View.class) { 47 private String failedCheckDescription; 48 49 @Override 50 public void describeTo(final Description description) { 51 description.appendText(failedCheckDescription); 52 } 53 54 @Override 55 public boolean matchesSafely(final View view) { 56 final int viewWidth = view.getWidth(); 57 if (viewWidth < minWidth) { 58 failedCheckDescription = 59 "width " + viewWidth + " is less than minimum " + minWidth; 60 return false; 61 } 62 return true; 63 } 64 }; 65 } 66 67 /** 68 * Returns a matcher that matches Views that are not wider than specified width in pixels. 69 */ 70 public static Matcher<View> isNotWiderThan(final int maxWidth) { 71 return new BoundedMatcher<View, View>(View.class) { 72 private String failedCheckDescription; 73 74 @Override 75 public void describeTo(final Description description) { 76 description.appendText(failedCheckDescription); 77 } 78 79 @Override 80 public boolean matchesSafely(final View view) { 81 final int viewWidth = view.getWidth(); 82 if (viewWidth > maxWidth) { 83 failedCheckDescription = 84 "width " + viewWidth + " is more than maximum " + maxWidth; 85 return false; 86 } 87 return true; 88 } 89 }; 90 } 91 92 /** 93 * Returns a matcher that matches TextViews with the specified text size. 94 */ 95 public static Matcher withTextSize(final float textSize) { 96 return new BoundedMatcher<View, TextView>(TextView.class) { 97 private String failedCheckDescription; 98 99 @Override 100 public void describeTo(final Description description) { 101 description.appendText(failedCheckDescription); 102 } 103 104 @Override 105 public boolean matchesSafely(final TextView view) { 106 final float ourTextSize = view.getTextSize(); 107 if (Math.abs(textSize - ourTextSize) > 1.0f) { 108 failedCheckDescription = 109 "text size " + ourTextSize + " is different than expected " + textSize; 110 return false; 111 } 112 return true; 113 } 114 }; 115 } 116 117 /** 118 * Returns a matcher that matches TextViews with the specified text color. 119 */ 120 public static Matcher withTextColor(final @ColorInt int textColor) { 121 return new BoundedMatcher<View, TextView>(TextView.class) { 122 private String failedCheckDescription; 123 124 @Override 125 public void describeTo(final Description description) { 126 description.appendText(failedCheckDescription); 127 } 128 129 @Override 130 public boolean matchesSafely(final TextView view) { 131 final @ColorInt int ourTextColor = view.getCurrentTextColor(); 132 if (ourTextColor != textColor) { 133 int ourAlpha = Color.alpha(ourTextColor); 134 int ourRed = Color.red(ourTextColor); 135 int ourGreen = Color.green(ourTextColor); 136 int ourBlue = Color.blue(ourTextColor); 137 138 int expectedAlpha = Color.alpha(textColor); 139 int expectedRed = Color.red(textColor); 140 int expectedGreen = Color.green(textColor); 141 int expectedBlue = Color.blue(textColor); 142 143 failedCheckDescription = 144 "expected color to be [" 145 + expectedAlpha + "," + expectedRed + "," 146 + expectedGreen + "," + expectedBlue 147 + "] but found [" 148 + ourAlpha + "," + ourRed + "," 149 + ourGreen + "," + ourBlue + "]"; 150 return false; 151 } 152 return true; 153 } 154 }; 155 } 156 157 /** 158 * Returns a matcher that matches TextViews whose start drawable is filled with the specified 159 * fill color. 160 */ 161 public static Matcher withStartDrawableFilledWith(final @ColorInt int fillColor, 162 final int allowedComponentVariance) { 163 return new BoundedMatcher<View, TextView>(TextView.class) { 164 private String failedCheckDescription; 165 166 @Override 167 public void describeTo(final Description description) { 168 description.appendText(failedCheckDescription); 169 } 170 171 @Override 172 public boolean matchesSafely(final TextView view) { 173 final Drawable[] compoundDrawables = view.getCompoundDrawables(); 174 final boolean isRtl = 175 (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_RTL); 176 final Drawable startDrawable = isRtl ? compoundDrawables[2] : compoundDrawables[0]; 177 if (startDrawable == null) { 178 failedCheckDescription = "no start drawable"; 179 return false; 180 } 181 try { 182 final Rect bounds = startDrawable.getBounds(); 183 TestUtils.assertAllPixelsOfColor("", 184 startDrawable, bounds.width(), bounds.height(), true, 185 fillColor, allowedComponentVariance, true); 186 } catch (Throwable t) { 187 failedCheckDescription = t.getMessage(); 188 return false; 189 } 190 return true; 191 } 192 }; 193 } 194 195 /** 196 * Returns a matcher that matches <code>ImageView</code>s which have drawable flat-filled 197 * with the specific color. 198 */ 199 public static Matcher drawable(@ColorInt final int color, final int allowedComponentVariance) { 200 return new BoundedMatcher<View, ImageView>(ImageView.class) { 201 private String mFailedComparisonDescription; 202 203 @Override 204 public void describeTo(final Description description) { 205 description.appendText("with drawable of color: "); 206 207 description.appendText(mFailedComparisonDescription); 208 } 209 210 @Override 211 public boolean matchesSafely(final ImageView view) { 212 Drawable drawable = view.getDrawable(); 213 if (drawable == null) { 214 return false; 215 } 216 217 // One option is to check if we have a ColorDrawable and then call getColor 218 // but that API is v11+. Instead, we call our helper method that checks whether 219 // all pixels in a Drawable are of the same specified color. 220 try { 221 TestUtils.assertAllPixelsOfColor("", drawable, view.getWidth(), 222 view.getHeight(), true, color, allowedComponentVariance, true); 223 // If we are here, the color comparison has passed. 224 mFailedComparisonDescription = null; 225 return true; 226 } catch (Throwable t) { 227 // If we are here, the color comparison has failed. 228 mFailedComparisonDescription = t.getMessage(); 229 return false; 230 } 231 } 232 }; 233 } 234 235 /** 236 * Returns a matcher that matches Views with the specified background fill color. 237 */ 238 public static Matcher withBackgroundFill(final @ColorInt int fillColor) { 239 return new BoundedMatcher<View, View>(View.class) { 240 private String failedCheckDescription; 241 242 @Override 243 public void describeTo(final Description description) { 244 description.appendText(failedCheckDescription); 245 } 246 247 @Override 248 public boolean matchesSafely(final View view) { 249 Drawable background = view.getBackground(); 250 try { 251 TestUtils.assertAllPixelsOfColor("", 252 background, view.getWidth(), view.getHeight(), true, 253 fillColor, 0, true); 254 } catch (Throwable t) { 255 failedCheckDescription = t.getMessage(); 256 return false; 257 } 258 return true; 259 } 260 }; 261 } 262 263 /** 264 * Returns a matcher that matches FloatingActionButtons with the specified background 265 * fill color. 266 */ 267 public static Matcher withFabBackgroundFill(final @ColorInt int fillColor) { 268 return new BoundedMatcher<View, View>(View.class) { 269 private String failedCheckDescription; 270 271 @Override 272 public void describeTo(final Description description) { 273 description.appendText(failedCheckDescription); 274 } 275 276 @Override 277 public boolean matchesSafely(final View view) { 278 if (!(view instanceof FloatingActionButton)) { 279 return false; 280 } 281 282 final FloatingActionButton fab = (FloatingActionButton) view; 283 284 // Since the FAB background is round, and may contain the shadow, we'll look at 285 // just the center half rect of the content area 286 final Rect area = new Rect(); 287 fab.getContentRect(area); 288 289 final int rectHeightQuarter = area.height() / 4; 290 final int rectWidthQuarter = area.width() / 4; 291 area.left += rectWidthQuarter; 292 area.top += rectHeightQuarter; 293 area.right -= rectWidthQuarter; 294 area.bottom -= rectHeightQuarter; 295 296 try { 297 TestUtils.assertAllPixelsOfColor("", 298 fab.getBackground(), view.getWidth(), view.getHeight(), false, 299 fillColor, area, 0, true); 300 } catch (Throwable t) { 301 failedCheckDescription = t.getMessage(); 302 return false; 303 } 304 return true; 305 } 306 }; 307 } 308 309 /** 310 * Returns a matcher that matches {@link View}s based on the given parent type. 311 * 312 * @param parentMatcher the type of the parent to match on 313 */ 314 public static Matcher<View> isChildOfA(final Matcher<View> parentMatcher) { 315 return new TypeSafeMatcher<View>() { 316 @Override 317 public void describeTo(Description description) { 318 description.appendText("is child of a: "); 319 parentMatcher.describeTo(description); 320 } 321 322 @Override 323 public boolean matchesSafely(View view) { 324 final ViewParent viewParent = view.getParent(); 325 if (!(viewParent instanceof View)) { 326 return false; 327 } 328 if (parentMatcher.matches(viewParent)) { 329 return true; 330 } 331 return false; 332 } 333 }; 334 } 335 336 /** 337 * Returns a matcher that matches FloatingActionButtons with the specified content height 338 */ 339 public static Matcher withFabContentHeight(final int size) { 340 return new BoundedMatcher<View, View>(View.class) { 341 private String failedCheckDescription; 342 343 @Override 344 public void describeTo(final Description description) { 345 description.appendText(failedCheckDescription); 346 } 347 348 @Override 349 public boolean matchesSafely(final View view) { 350 if (!(view instanceof FloatingActionButton)) { 351 return false; 352 } 353 354 final FloatingActionButton fab = (FloatingActionButton) view; 355 final Rect area = new Rect(); 356 fab.getContentRect(area); 357 358 return area.height() == size; 359 } 360 }; 361 } 362 363 /** 364 * Returns a matcher that matches FloatingActionButtons with the specified gravity. 365 */ 366 public static Matcher withFabContentAreaOnMargins(final int gravity) { 367 return new BoundedMatcher<View, View>(View.class) { 368 private String failedCheckDescription; 369 370 @Override 371 public void describeTo(final Description description) { 372 description.appendText(failedCheckDescription); 373 } 374 375 @Override 376 public boolean matchesSafely(final View view) { 377 if (!(view instanceof FloatingActionButton)) { 378 return false; 379 } 380 381 final FloatingActionButton fab = (FloatingActionButton) view; 382 final ViewGroup.MarginLayoutParams lp = 383 (ViewGroup.MarginLayoutParams) fab.getLayoutParams(); 384 final ViewGroup parent = (ViewGroup) view.getParent(); 385 386 final Rect area = new Rect(); 387 fab.getContentRect(area); 388 389 final int absGravity = GravityCompat.getAbsoluteGravity(gravity, 390 ViewCompat.getLayoutDirection(view)); 391 392 try { 393 switch (absGravity & Gravity.VERTICAL_GRAVITY_MASK) { 394 case Gravity.TOP: 395 assertEquals(lp.topMargin, fab.getTop() + area.top); 396 break; 397 case Gravity.BOTTOM: 398 assertEquals(parent.getHeight() - lp.bottomMargin, 399 fab.getTop() + area.bottom); 400 break; 401 } 402 switch (absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { 403 case Gravity.LEFT: 404 assertEquals(lp.leftMargin, fab.getLeft() + area.left); 405 break; 406 case Gravity.RIGHT: 407 assertEquals(parent.getWidth() - lp.rightMargin, 408 fab.getLeft() + area.right); 409 break; 410 } 411 return true; 412 } catch (Throwable t) { 413 failedCheckDescription = t.getMessage(); 414 return false; 415 } 416 } 417 }; 418 } 419 420 /** 421 * Returns a matcher that matches FloatingActionButtons with the specified content height 422 */ 423 public static Matcher withCompoundDrawable(final int index, final Drawable expected) { 424 return new BoundedMatcher<View, View>(View.class) { 425 private String failedCheckDescription; 426 427 @Override 428 public void describeTo(final Description description) { 429 description.appendText(failedCheckDescription); 430 } 431 432 @Override 433 public boolean matchesSafely(final View view) { 434 if (!(view instanceof TextView)) { 435 return false; 436 } 437 438 final TextView textView = (TextView) view; 439 return expected == TextViewCompat.getCompoundDrawablesRelative(textView)[index]; 440 } 441 }; 442 } 443} 444