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