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 17 package androidx.appcompat.testutils; 18 19 import android.database.sqlite.SQLiteCursor; 20 import android.graphics.Bitmap; 21 import android.graphics.drawable.Drawable; 22 import android.support.test.espresso.matcher.BoundedMatcher; 23 import android.text.TextUtils; 24 import android.view.View; 25 import android.view.ViewGroup; 26 import android.widget.CheckedTextView; 27 import android.widget.ImageView; 28 29 import androidx.annotation.ColorInt; 30 import androidx.core.view.TintableBackgroundView; 31 32 import org.hamcrest.Description; 33 import org.hamcrest.Matcher; 34 import org.hamcrest.TypeSafeMatcher; 35 36 import java.util.List; 37 38 public class TestUtilsMatchers { 39 /** 40 * Returns a matcher that matches <code>ImageView</code>s which have drawable flat-filled 41 * with the specific color. 42 */ 43 public static Matcher drawable(@ColorInt final int color) { 44 return new BoundedMatcher<View, ImageView>(ImageView.class) { 45 private String failedComparisonDescription; 46 47 @Override 48 public void describeTo(final Description description) { 49 description.appendText("with drawable of color: "); 50 51 description.appendText(failedComparisonDescription); 52 } 53 54 @Override 55 public boolean matchesSafely(final ImageView view) { 56 Drawable drawable = view.getDrawable(); 57 if (drawable == null) { 58 return false; 59 } 60 61 // One option is to check if we have a ColorDrawable and then call getColor 62 // but that API is v11+. Instead, we call our helper method that checks whether 63 // all pixels in a Drawable are of the same specified color. 64 try { 65 TestUtils.assertAllPixelsOfColor("", drawable, view.getWidth(), 66 view.getHeight(), true, color, 0, true); 67 // If we are here, the color comparison has passed. 68 failedComparisonDescription = null; 69 return true; 70 } catch (Throwable t) { 71 // If we are here, the color comparison has failed. 72 failedComparisonDescription = t.getMessage(); 73 return false; 74 } 75 } 76 }; 77 } 78 79 /** 80 * Returns a matcher that matches <code>View</code>s which have background flat-filled 81 * with the specific color. 82 */ 83 public static Matcher isBackground(@ColorInt final int color) { 84 return isBackground(color, false); 85 } 86 87 /** 88 * Returns a matcher that matches <code>View</code>s which have background flat-filled 89 * with the specific color. 90 */ 91 public static Matcher isBackground(@ColorInt final int color, final boolean onlyTestCenter) { 92 return new BoundedMatcher<View, View>(View.class) { 93 private String failedComparisonDescription; 94 95 @Override 96 public void describeTo(final Description description) { 97 description.appendText("with background of color: "); 98 99 description.appendText(failedComparisonDescription); 100 } 101 102 @Override 103 public boolean matchesSafely(final View view) { 104 Drawable drawable = view.getBackground(); 105 if (drawable == null) { 106 return false; 107 } 108 try { 109 if (onlyTestCenter) { 110 TestUtils.assertCenterPixelOfColor("", drawable, view.getWidth(), 111 view.getHeight(), false, color, 0, true); 112 } else { 113 TestUtils.assertAllPixelsOfColor("", drawable, view.getWidth(), 114 view.getHeight(), false, color, 0, true); 115 } 116 // If we are here, the color comparison has passed. 117 failedComparisonDescription = null; 118 return true; 119 } catch (Throwable t) { 120 // If we are here, the color comparison has failed. 121 failedComparisonDescription = t.getMessage(); 122 return false; 123 } 124 } 125 }; 126 } 127 128 /** 129 * Returns a matcher that matches <code>View</code>s whose combined background starting 130 * from the view and up its ancestor chain matches the specified color. 131 */ 132 public static Matcher isCombinedBackground(@ColorInt final int color, 133 final boolean onlyTestCenterPixel) { 134 return new BoundedMatcher<View, View>(View.class) { 135 private String failedComparisonDescription; 136 137 @Override 138 public void describeTo(final Description description) { 139 description.appendText("with ascendant background of color: "); 140 141 description.appendText(failedComparisonDescription); 142 } 143 144 @Override 145 public boolean matchesSafely(View view) { 146 // Create a bitmap with combined backgrounds of the view and its ancestors. 147 Bitmap combinedBackgroundBitmap = TestUtils.getCombinedBackgroundBitmap(view); 148 try { 149 if (onlyTestCenterPixel) { 150 TestUtils.assertCenterPixelOfColor("", combinedBackgroundBitmap, 151 color, 0, true); 152 } else { 153 TestUtils.assertAllPixelsOfColor("", combinedBackgroundBitmap, 154 combinedBackgroundBitmap.getWidth(), 155 combinedBackgroundBitmap.getHeight(), color, 0, true); 156 } 157 // If we are here, the color comparison has passed. 158 failedComparisonDescription = null; 159 return true; 160 } catch (Throwable t) { 161 failedComparisonDescription = t.getMessage(); 162 return false; 163 } finally { 164 combinedBackgroundBitmap.recycle(); 165 } 166 } 167 }; 168 } 169 170 /** 171 * Returns a matcher that matches <code>CheckedTextView</code>s which are in checked state. 172 */ 173 public static Matcher isCheckedTextView() { 174 return new BoundedMatcher<View, CheckedTextView>(CheckedTextView.class) { 175 private String failedDescription; 176 177 @Override 178 public void describeTo(final Description description) { 179 description.appendText("checked text view: "); 180 181 description.appendText(failedDescription); 182 } 183 184 @Override 185 public boolean matchesSafely(final CheckedTextView view) { 186 if (view.isChecked()) { 187 return true; 188 } 189 190 failedDescription = "not checked"; 191 return false; 192 } 193 }; 194 } 195 196 /** 197 * Returns a matcher that matches <code>CheckedTextView</code>s which are in checked state. 198 */ 199 public static Matcher isNonCheckedTextView() { 200 return new BoundedMatcher<View, CheckedTextView>(CheckedTextView.class) { 201 private String failedDescription; 202 203 @Override 204 public void describeTo(final Description description) { 205 description.appendText("non checked text view: "); 206 207 description.appendText(failedDescription); 208 } 209 210 @Override 211 public boolean matchesSafely(final CheckedTextView view) { 212 if (!view.isChecked()) { 213 return true; 214 } 215 216 failedDescription = "checked"; 217 return false; 218 } 219 }; 220 } 221 222 /** 223 * Returns a matcher that matches data entry in <code>SQLiteCursor</code> that has 224 * the specified text in the specified column. 225 */ 226 public static Matcher<Object> withCursorItemContent(final String columnName, 227 final String expectedText) { 228 return new BoundedMatcher<Object, SQLiteCursor>(SQLiteCursor.class) { 229 @Override 230 public void describeTo(Description description) { 231 description.appendText("doesn't match " + expectedText); 232 } 233 234 @Override 235 protected boolean matchesSafely(SQLiteCursor cursor) { 236 return TextUtils.equals(expectedText, 237 cursor.getString(cursor.getColumnIndex(columnName))); 238 } 239 }; 240 } 241 242 /** 243 * Returns a matcher that matches Views which implement TintableBackgroundView. 244 */ 245 public static Matcher<View> isTintableBackgroundView() { 246 return new TypeSafeMatcher<View>() { 247 @Override 248 public void describeTo(Description description) { 249 description.appendText("is TintableBackgroundView"); 250 } 251 252 @Override 253 public boolean matchesSafely(View view) { 254 return TintableBackgroundView.class.isAssignableFrom(view.getClass()); 255 } 256 }; 257 } 258 259 /** 260 * Returns a matcher that matches lists of float values that fall into the specified range. 261 */ 262 public static Matcher<List<Float>> inRange(final float from, final float to) { 263 return new TypeSafeMatcher<List<Float>>() { 264 private String mFailedDescription; 265 266 @Override 267 public void describeTo(Description description) { 268 description.appendText(mFailedDescription); 269 } 270 271 @Override 272 protected boolean matchesSafely(List<Float> item) { 273 int itemCount = item.size(); 274 275 for (int i = 0; i < itemCount; i++) { 276 float curr = item.get(i); 277 278 if ((curr < from) || (curr > to)) { 279 mFailedDescription = "Value #" + i + ":" + curr + " should be between " + 280 from + " and " + to; 281 return false; 282 } 283 } 284 285 return true; 286 } 287 }; 288 } 289 290 /** 291 * Returns a matcher that matches lists of float values that are in ascending order. 292 */ 293 public static Matcher<List<Float>> inAscendingOrder() { 294 return new TypeSafeMatcher<List<Float>>() { 295 private String mFailedDescription; 296 297 @Override 298 public void describeTo(Description description) { 299 description.appendText(mFailedDescription); 300 } 301 302 @Override 303 protected boolean matchesSafely(List<Float> item) { 304 int itemCount = item.size(); 305 306 if (itemCount >= 2) { 307 for (int i = 0; i < itemCount - 1; i++) { 308 float curr = item.get(i); 309 float next = item.get(i + 1); 310 311 if (curr > next) { 312 mFailedDescription = "Values should increase between #" + i + 313 ":" + curr + " and #" + (i + 1) + ":" + next; 314 return false; 315 } 316 } 317 } 318 319 return true; 320 } 321 }; 322 } 323 324 /** 325 * Returns a matcher that matches lists of float values that are in descending order. 326 */ 327 public static Matcher<List<Float>> inDescendingOrder() { 328 return new TypeSafeMatcher<List<Float>>() { 329 private String mFailedDescription; 330 331 @Override 332 public void describeTo(Description description) { 333 description.appendText(mFailedDescription); 334 } 335 336 @Override 337 protected boolean matchesSafely(List<Float> item) { 338 int itemCount = item.size(); 339 340 if (itemCount >= 2) { 341 for (int i = 0; i < itemCount - 1; i++) { 342 float curr = item.get(i); 343 float next = item.get(i + 1); 344 345 if (curr < next) { 346 mFailedDescription = "Values should decrease between #" + i + 347 ":" + curr + " and #" + (i + 1) + ":" + next; 348 return false; 349 } 350 } 351 } 352 353 return true; 354 } 355 }; 356 } 357 358 359 /** 360 * Returns a matcher that matches {@link View}s based on the given child type. 361 * 362 * @param childMatcher the type of the child to match on 363 */ 364 public static Matcher<ViewGroup> hasChild(final Matcher<View> childMatcher) { 365 return new TypeSafeMatcher<ViewGroup>() { 366 @Override 367 public void describeTo(Description description) { 368 description.appendText("has child: "); 369 childMatcher.describeTo(description); 370 } 371 372 @Override 373 public boolean matchesSafely(ViewGroup view) { 374 final int childCount = view.getChildCount(); 375 for (int i = 0; i < childCount; i++) { 376 if (childMatcher.matches(view.getChildAt(i))) { 377 return true; 378 } 379 } 380 return false; 381 } 382 }; 383 } 384 385 } 386