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