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