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