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 android.widget.espresso;
18 
19 import static android.support.test.espresso.Espresso.onView;
20 import static android.support.test.espresso.action.ViewActions.click;
21 import static android.support.test.espresso.assertion.ViewAssertions.matches;
22 import static android.support.test.espresso.matcher.RootMatchers.isPlatformPopup;
23 import static android.support.test.espresso.matcher.RootMatchers.withDecorView;
24 import static android.support.test.espresso.matcher.ViewMatchers.hasDescendant;
25 import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
26 import static android.support.test.espresso.matcher.ViewMatchers.isRoot;
27 import static android.support.test.espresso.matcher.ViewMatchers.withId;
28 import static android.support.test.espresso.matcher.ViewMatchers.withTagValue;
29 import static android.support.test.espresso.matcher.ViewMatchers.withText;
30 
31 import static org.hamcrest.Matchers.allOf;
32 import static org.hamcrest.Matchers.is;
33 
34 import android.support.test.espresso.NoMatchingRootException;
35 import android.support.test.espresso.NoMatchingViewException;
36 import android.support.test.espresso.UiController;
37 import android.support.test.espresso.ViewAction;
38 import android.support.test.espresso.ViewInteraction;
39 import android.view.MenuItem;
40 import android.view.View;
41 import android.view.ViewGroup;
42 
43 import com.android.internal.widget.FloatingToolbar;
44 
45 import org.hamcrest.Description;
46 import org.hamcrest.Matcher;
47 import org.hamcrest.TypeSafeMatcher;
48 
49 import java.util.ArrayList;
50 import java.util.List;
51 import java.util.function.Predicate;
52 
53 /**
54  * Espresso utility methods for the floating toolbar.
55  */
56 public class FloatingToolbarEspressoUtils {
57     private final static Object TAG = FloatingToolbar.FLOATING_TOOLBAR_TAG;
58 
FloatingToolbarEspressoUtils()59     private FloatingToolbarEspressoUtils() {}
60 
onFloatingToolBar()61     private static ViewInteraction onFloatingToolBar() {
62         return onView(withTagValue(is(TAG)))
63                 .inRoot(allOf(
64                         isPlatformPopup(),
65                         withDecorView(hasDescendant(withTagValue(is(TAG))))));
66     }
67 
68     /**
69      * Creates a {@link ViewInteraction} for the floating bar menu item with the given matcher.
70      *
71      * @param matcher The matcher for the menu item.
72      */
onFloatingToolBarItem(Matcher<View> matcher)73     public static ViewInteraction onFloatingToolBarItem(Matcher<View> matcher) {
74         return onView(matcher)
75                 .inRoot(withDecorView(hasDescendant(withTagValue(is(TAG)))));
76     }
77 
78     /**
79      * Asserts that the floating toolbar is displayed on screen.
80      *
81      * @throws AssertionError if the assertion fails
82      */
assertFloatingToolbarIsDisplayed()83     public static void assertFloatingToolbarIsDisplayed() {
84         onFloatingToolBar().check(matches(isDisplayed()));
85     }
86 
87     /**
88      * Asserts that the floating toolbar is not displayed on screen.
89      *
90      * @throws AssertionError if the assertion fails
91      * @deprecated Negative assertions are taking too long to timeout in Espresso.
92      */
93     @Deprecated
assertFloatingToolbarIsNotDisplayed()94     public static void assertFloatingToolbarIsNotDisplayed() {
95         try {
96             onFloatingToolBar().check(matches(isDisplayed()));
97         } catch (NoMatchingRootException | NoMatchingViewException | AssertionError e) {
98             return;
99         }
100         throw new AssertionError("Floating toolbar is displayed");
101     }
102 
toggleOverflow()103     private static void toggleOverflow() {
104         final int id = com.android.internal.R.id.overflow;
105         onView(allOf(withId(id), isDisplayed()))
106                 .inRoot(withDecorView(hasDescendant(withId(id))))
107                 .perform(click());
108         onView(isRoot()).perform(SLEEP);
109     }
110 
sleepForFloatingToolbarPopup()111     public static void sleepForFloatingToolbarPopup() {
112         onView(isRoot()).perform(SLEEP);
113     }
114 
115     /**
116      * Asserts that the floating toolbar contains the specified item.
117      *
118      * @param itemLabel label of the item.
119      * @throws AssertionError if the assertion fails
120      */
assertFloatingToolbarContainsItem(String itemLabel)121     public static void assertFloatingToolbarContainsItem(String itemLabel) {
122         try{
123             onFloatingToolBar().check(matches(hasDescendant(withText(itemLabel))));
124         } catch (AssertionError e) {
125             try{
126                 toggleOverflow();
127             } catch (NoMatchingViewException | NoMatchingRootException e2) {
128                 // No overflow items.
129                 throw e;
130             }
131             try{
132                 onFloatingToolBar().check(matches(hasDescendant(withText(itemLabel))));
133             } finally {
134                 toggleOverflow();
135             }
136         }
137     }
138 
139     /**
140      * Asserts that the floating toolbar contains a specified item at a specified index.
141      *
142      * @param menuItemId id of the menu item
143      * @param index expected index of the menu item in the floating toolbar
144      * @throws AssertionError if the assertion fails
145      */
assertFloatingToolbarItemIndex(final int menuItemId, final int index)146     public static void assertFloatingToolbarItemIndex(final int menuItemId, final int index) {
147         onFloatingToolBar().check(matches(new TypeSafeMatcher<View>() {
148             private List<Integer> menuItemIds = new ArrayList<>();
149 
150             @Override
151             public boolean matchesSafely(View view) {
152                 collectMenuItemIds(view);
153                 return menuItemIds.size() > index && menuItemIds.get(index) == menuItemId;
154             }
155 
156             @Override
157             public void describeTo(Description description) {}
158 
159             private void collectMenuItemIds(View view) {
160                 if (view.getTag() instanceof MenuItem) {
161                     menuItemIds.add(((MenuItem) view.getTag()).getItemId());
162                 } else if (view instanceof ViewGroup) {
163                     ViewGroup viewGroup = (ViewGroup) view;
164                     for (int i = 0; i < viewGroup.getChildCount(); i++) {
165                         collectMenuItemIds(viewGroup.getChildAt(i));
166                     }
167                 }
168             }
169         }));
170     }
171 
172     /**
173      * Asserts that the floating toolbar doesn't contain the specified item.
174      *
175      * @param itemLabel label of the item.
176      * @throws AssertionError if the assertion fails
177      */
assertFloatingToolbarDoesNotContainItem(String itemLabel)178     public static void assertFloatingToolbarDoesNotContainItem(String itemLabel) {
179         final Predicate<View> hasMenuItemLabel = view ->
180                 view.getTag() instanceof MenuItem
181                         && itemLabel.equals(((MenuItem) view.getTag()).getTitle().toString());
182         assertFloatingToolbarMenuItem(hasMenuItemLabel, false);
183     }
184 
185     /**
186      * Asserts that the floating toolbar does not contain a menu item with the specified id.
187      *
188      * @param menuItemId id of the menu item
189      * @throws AssertionError if the assertion fails
190      */
assertFloatingToolbarDoesNotContainItem(final int menuItemId)191     public static void assertFloatingToolbarDoesNotContainItem(final int menuItemId) {
192         final Predicate<View> hasMenuItemId = view ->
193                 view.getTag() instanceof MenuItem
194                         && ((MenuItem) view.getTag()).getItemId() == menuItemId;
195         assertFloatingToolbarMenuItem(hasMenuItemId, false);
196     }
197 
assertFloatingToolbarMenuItem( final Predicate<View> predicate, final boolean positiveAssertion)198     private static void assertFloatingToolbarMenuItem(
199             final Predicate<View> predicate, final boolean positiveAssertion) {
200         onFloatingToolBar().check(matches(new TypeSafeMatcher<View>() {
201             @Override
202             public boolean matchesSafely(View view) {
203                 return positiveAssertion == containsItem(view);
204             }
205 
206             @Override
207             public void describeTo(Description description) {}
208 
209             private boolean containsItem(View view) {
210                 if (predicate.test(view)) {
211                     return true;
212                 } else if (view instanceof ViewGroup) {
213                     ViewGroup viewGroup = (ViewGroup) view;
214                     for (int i = 0; i < viewGroup.getChildCount(); i++) {
215                         if (containsItem(viewGroup.getChildAt(i))) {
216                             return true;
217                         }
218                     }
219                 }
220                 return false;
221             }
222         }));
223     }
224 
225     /**
226      * Click specified item on the floating tool bar.
227      *
228      * @param itemLabel label of the item.
229      */
clickFloatingToolbarItem(String itemLabel)230     public static void clickFloatingToolbarItem(String itemLabel) {
231         try{
232             onFloatingToolBarItem(withText(itemLabel)).check(matches(isDisplayed()));
233         } catch (AssertionError e) {
234             // Try to find the item in the overflow menu.
235             toggleOverflow();
236         }
237         onFloatingToolBarItem(withText(itemLabel)).perform(click());
238     }
239 
240     /**
241      * ViewAction to sleep to wait floating toolbar's animation.
242      */
243     private static final ViewAction SLEEP = new ViewAction() {
244         private static final long SLEEP_DURATION = 400;
245 
246         @Override
247         public Matcher<View> getConstraints() {
248             return isDisplayed();
249         }
250 
251         @Override
252         public String getDescription() {
253             return "Sleep " + SLEEP_DURATION + " ms.";
254         }
255 
256         @Override
257         public void perform(UiController uiController, View view) {
258             uiController.loopMainThreadForAtLeast(SLEEP_DURATION);
259         }
260     };
261 }
262