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 com.android.documentsui.bots;
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.ViewMatchers.hasFocus;
23 import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom;
24 import static android.support.test.espresso.matcher.ViewMatchers.withClassName;
25 import static android.support.test.espresso.matcher.ViewMatchers.withId;
26 import static android.support.test.espresso.matcher.ViewMatchers.withText;
27 import static junit.framework.Assert.assertEquals;
28 import static junit.framework.Assert.assertNotNull;
29 import static org.hamcrest.CoreMatchers.allOf;
30 import static org.hamcrest.CoreMatchers.is;
31 import static org.hamcrest.Matchers.endsWith;
32 
33 import android.content.Context;
34 import android.support.test.espresso.Espresso;
35 import android.support.test.espresso.NoMatchingViewException;
36 import android.support.test.espresso.action.ViewActions;
37 import android.support.test.espresso.matcher.BoundedMatcher;
38 import android.support.test.espresso.matcher.ViewMatchers;
39 import android.support.test.uiautomator.By;
40 import android.support.test.uiautomator.UiDevice;
41 import android.support.test.uiautomator.UiObject;
42 import android.support.test.uiautomator.UiObject2;
43 import android.support.test.uiautomator.UiObjectNotFoundException;
44 import android.support.test.uiautomator.UiSelector;
45 import android.support.test.uiautomator.Until;
46 import android.util.TypedValue;
47 import android.view.View;
48 import android.widget.Toolbar;
49 
50 import com.android.documentsui.R;
51 
52 import org.hamcrest.Description;
53 import org.hamcrest.Matcher;
54 
55 import java.util.Iterator;
56 import java.util.List;
57 
58 /**
59  * A test helper class that provides support for controlling DocumentsUI activities
60  * programmatically, and making assertions against the state of the UI.
61  * <p>
62  * Support for working directly with Roots and Directory view can be found in the respective bots.
63  */
64 public class UiBot extends Bots.BaseBot {
65 
66     public static final String TARGET_PKG = "com.android.documentsui";
67 
68     @SuppressWarnings("unchecked")
69     private static final Matcher<View> TOOLBAR = allOf(
70             isAssignableFrom(Toolbar.class),
71             withId(R.id.toolbar));
72 
73     @SuppressWarnings("unchecked")
74     private static final Matcher<View> ACTIONBAR = allOf(
75             withClassName(endsWith("ActionBarContextView")));
76 
77     @SuppressWarnings("unchecked")
78     private static final Matcher<View> TEXT_ENTRY = allOf(
79             withClassName(endsWith("EditText")));
80 
81     @SuppressWarnings("unchecked")
82     private static final Matcher<View> TOOLBAR_OVERFLOW = allOf(
83             withClassName(endsWith("OverflowMenuButton")),
84             ViewMatchers.isDescendantOfA(TOOLBAR));
85 
86     @SuppressWarnings("unchecked")
87     private static final Matcher<View> ACTIONBAR_OVERFLOW = allOf(
88             withClassName(endsWith("OverflowMenuButton")),
89             ViewMatchers.isDescendantOfA(ACTIONBAR));
90 
UiBot(UiDevice device, Context context, int timeout)91     public UiBot(UiDevice device, Context context, int timeout) {
92         super(device, context, timeout);
93     }
94 
assertWindowTitle(String expected)95     public void assertWindowTitle(String expected) {
96         onView(TOOLBAR)
97                 .check(matches(withToolbarTitle(is(expected))));
98     }
99 
assertMenuEnabled(int id, boolean enabled)100     public void assertMenuEnabled(int id, boolean enabled) {
101         UiObject2 menu = findMenuWithName(mContext.getString(id));
102         assertNotNull(menu);
103         assertEquals(enabled, menu.isEnabled());
104     }
105 
assertInActionMode(boolean inActionMode)106     public void assertInActionMode(boolean inActionMode) {
107         assertEquals(inActionMode, waitForActionModeBarToAppear());
108     }
109 
openOverflowMenu()110     public UiObject openOverflowMenu() throws UiObjectNotFoundException {
111         UiObject obj = findMenuMoreOptions();
112         obj.click();
113         mDevice.waitForIdle(mTimeout);
114         return obj;
115     }
116 
setDialogText(String text)117     public void setDialogText(String text) throws UiObjectNotFoundException {
118         onView(TEXT_ENTRY)
119                 .perform(ViewActions.replaceText(text));
120     }
121 
assertDialogText(String expected)122     public void assertDialogText(String expected) throws UiObjectNotFoundException {
123         onView(TEXT_ENTRY)
124                 .check(matches(withText(is(expected))));
125     }
126 
inFixedLayout()127     public boolean inFixedLayout() {
128         TypedValue val = new TypedValue();
129         // We alias files_activity to either fixed or drawer layouts based
130         // on screen dimensions. In order to determine which layout
131         // has been selected, we check the resolved value.
132         mContext.getResources().getValue(R.layout.files_activity, val, true);
133         return val.resourceId == R.layout.fixed_layout;
134     }
135 
inDrawerLayout()136     public boolean inDrawerLayout() {
137         return !inFixedLayout();
138     }
139 
switchToListMode()140     public void switchToListMode() {
141         final UiObject2 listMode = menuListMode();
142         if (listMode != null) {
143             listMode.click();
144         }
145     }
146 
switchToGridMode()147     public void switchToGridMode() {
148         final UiObject2 gridMode = menuGridMode();
149         if (gridMode != null) {
150             gridMode.click();
151         }
152     }
153 
menuGridMode()154     UiObject2 menuGridMode() {
155         // Note that we're using By.desc rather than By.res, because of b/25285770
156         return find(By.desc("Grid view"));
157     }
158 
menuListMode()159     UiObject2 menuListMode() {
160         // Note that we're using By.desc rather than By.res, because of b/25285770
161         return find(By.desc("List view"));
162     }
163 
clickToolbarItem(int id)164     public void clickToolbarItem(int id) {
165         onView(withId(id)).perform(click());
166     }
167 
clickNewFolder()168     public void clickNewFolder() {
169         onView(ACTIONBAR_OVERFLOW).perform(click());
170 
171         // Click the item by label, since Espresso doesn't support lookup by id on overflow.
172         onView(withText("New folder")).perform(click());
173     }
174 
clickActionbarOverflowItem(String label)175     public void clickActionbarOverflowItem(String label) {
176         onView(ACTIONBAR_OVERFLOW).perform(click());
177         // Click the item by label, since Espresso doesn't support lookup by id on overflow.
178         onView(withText(label)).perform(click());
179     }
180 
clickToolbarOverflowItem(String label)181     public void clickToolbarOverflowItem(String label) {
182         onView(TOOLBAR_OVERFLOW).perform(click());
183         // Click the item by label, since Espresso doesn't support lookup by id on overflow.
184         onView(withText(label)).perform(click());
185     }
186 
waitForActionModeBarToAppear()187     public boolean waitForActionModeBarToAppear() {
188         UiObject2 bar =
189                 mDevice.wait(Until.findObject(By.res("android:id/action_mode_bar")), mTimeout);
190         return (bar != null);
191     }
192 
findDownloadRetryDialog()193     public UiObject findDownloadRetryDialog() {
194         UiSelector selector = new UiSelector().text("Couldn't download");
195         UiObject title = mDevice.findObject(selector);
196         title.waitForExists(mTimeout);
197         return title;
198     }
199 
findFileRenameDialog()200     public UiObject findFileRenameDialog() {
201         UiSelector selector = new UiSelector().text("Rename");
202         UiObject title = mDevice.findObject(selector);
203         title.waitForExists(mTimeout);
204         return title;
205     }
206 
findRenameErrorMessage()207     public UiObject findRenameErrorMessage() {
208         UiSelector selector = new UiSelector().text(mContext.getString(R.string.name_conflict));
209         UiObject title = mDevice.findObject(selector);
210         title.waitForExists(mTimeout);
211         return title;
212     }
213 
214     @SuppressWarnings("unchecked")
assertDialogOkButtonFocused()215     public void assertDialogOkButtonFocused() {
216         onView(withId(android.R.id.button1)).check(matches(hasFocus()));
217     }
218 
clickDialogOkButton()219     public void clickDialogOkButton() {
220         // Espresso has flaky results when keyboard shows up, so hiding it for now
221         // before trying to click on any dialog button
222         Espresso.closeSoftKeyboard();
223         onView(withId(android.R.id.button1)).perform(click());
224     }
225 
clickDialogCancelButton()226     public void clickDialogCancelButton() throws UiObjectNotFoundException {
227         // Espresso has flaky results when keyboard shows up, so hiding it for now
228         // before trying to click on any dialog button
229         Espresso.closeSoftKeyboard();
230         onView(withId(android.R.id.button2)).perform(click());
231     }
232 
findMenuLabelWithName(String label)233     UiObject findMenuLabelWithName(String label) {
234         UiSelector selector = new UiSelector().text(label);
235         return mDevice.findObject(selector);
236     }
237 
findMenuWithName(String label)238     UiObject2 findMenuWithName(String label) {
239         List<UiObject2> menuItems = mDevice.findObjects(By.clazz("android.widget.LinearLayout"));
240         Iterator<UiObject2> it = menuItems.iterator();
241 
242         UiObject2 menuItem = null;
243         while (it.hasNext()) {
244             menuItem = it.next();
245             UiObject2 text = menuItem.findObject(By.text(label));
246             if (text != null) {
247                 break;
248             }
249         }
250         return menuItem;
251     }
252 
hasMenuWithName(String label)253     boolean hasMenuWithName(String label) {
254         return findMenuWithName(label) != null;
255     }
256 
findMenuMoreOptions()257     UiObject findMenuMoreOptions() {
258         UiSelector selector = new UiSelector().className("android.widget.ImageButton")
259                 .descriptionContains("More options");
260         // TODO: use the system string ? android.R.string.action_menu_overflow_description
261         return mDevice.findObject(selector);
262     }
263 
withToolbarTitle( final Matcher<CharSequence> textMatcher)264     private static Matcher<Object> withToolbarTitle(
265             final Matcher<CharSequence> textMatcher) {
266         return new BoundedMatcher<Object, Toolbar>(Toolbar.class) {
267             @Override
268             public boolean matchesSafely(Toolbar toolbar) {
269                 return textMatcher.matches(toolbar.getTitle());
270             }
271 
272             @Override
273             public void describeTo(Description description) {
274                 description.appendText("with toolbar title: ");
275                 textMatcher.describeTo(description);
276             }
277         };
278     }
279 }
280