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