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