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