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