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