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