/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.documentsui.bots; import static androidx.test.espresso.Espresso.onView; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasFocus; import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom; import static androidx.test.espresso.matcher.ViewMatchers.withClassName; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.endsWith; import android.content.Context; import android.support.test.uiautomator.By; import android.support.test.uiautomator.UiDevice; import android.support.test.uiautomator.UiObject; import android.support.test.uiautomator.UiObject2; import android.support.test.uiautomator.UiObjectNotFoundException; import android.support.test.uiautomator.UiSelector; import android.support.test.uiautomator.Until; import android.util.TypedValue; import android.view.View; import androidx.appcompat.widget.Toolbar; import androidx.test.InstrumentationRegistry; import androidx.test.espresso.Espresso; import androidx.test.espresso.action.ViewActions; import androidx.test.espresso.matcher.BoundedMatcher; import androidx.test.espresso.matcher.ViewMatchers; import com.android.documentsui.R; import org.hamcrest.Description; import org.hamcrest.Matcher; import java.util.Iterator; import java.util.List; /** * A test helper class that provides support for controlling DocumentsUI activities * programmatically, and making assertions against the state of the UI. *

* Support for working directly with Roots and Directory view can be found in the respective bots. */ public class UiBot extends Bots.BaseBot { public static String targetPackageName; @SuppressWarnings("unchecked") private static final Matcher TOOLBAR = allOf( isAssignableFrom(Toolbar.class), withId(R.id.toolbar)); @SuppressWarnings("unchecked") private static final Matcher ACTIONBAR = allOf( withClassName(endsWith("ActionBarContextView"))); @SuppressWarnings("unchecked") private static final Matcher TEXT_ENTRY = allOf( withClassName(endsWith("EditText"))); @SuppressWarnings("unchecked") private static final Matcher TOOLBAR_OVERFLOW = allOf( withClassName(endsWith("OverflowMenuButton")), ViewMatchers.isDescendantOfA(TOOLBAR)); @SuppressWarnings("unchecked") private static final Matcher ACTIONBAR_OVERFLOW = allOf( withClassName(endsWith("OverflowMenuButton")), ViewMatchers.isDescendantOfA(ACTIONBAR)); public UiBot(UiDevice device, Context context, int timeout) { super(device, context, timeout); targetPackageName = InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName(); } public void assertWindowTitle(String expected) { onView(TOOLBAR) .check(matches(withToolbarTitle(is(expected)))); } public void assertSearchBarShow() { UiSelector selector = new UiSelector().text(mContext.getString(R.string.search_bar_hint)); UiObject searchHint = mDevice.findObject(selector); assertTrue(searchHint.exists()); } public void assertMenuEnabled(int id, boolean enabled) { UiObject2 menu = findMenuWithName(mContext.getString(id)); if (enabled) { assertNotNull(menu); assertEquals(enabled, menu.isEnabled()); } else { assertNull(menu); } } public void assertInActionMode(boolean inActionMode) { assertEquals(inActionMode, waitForActionModeBarToAppear()); } public UiObject openOverflowMenu() throws UiObjectNotFoundException { UiObject obj = findMenuMoreOptions(); obj.click(); mDevice.waitForIdle(mTimeout); return obj; } public void setDialogText(String text) throws UiObjectNotFoundException { onView(TEXT_ENTRY) .perform(ViewActions.replaceText(text)); } public void assertDialogText(String expected) throws UiObjectNotFoundException { onView(TEXT_ENTRY) .check(matches(withText(is(expected)))); } public boolean inFixedLayout() { TypedValue val = new TypedValue(); // We alias files_activity to either fixed or drawer layouts based // on screen dimensions. In order to determine which layout // has been selected, we check the resolved value. mContext.getResources().getValue(R.layout.files_activity, val, true); return val.resourceId == R.layout.fixed_layout; } public boolean inDrawerLayout() { return !inFixedLayout(); } public void switchToListMode() { final UiObject2 listMode = menuListMode(); if (listMode != null) { listMode.click(); } } public void clickActionItem(String label) throws UiObjectNotFoundException { if (!waitForActionModeBarToAppear()) { throw new UiObjectNotFoundException("ActionMode bar not found"); } clickActionbarOverflowItem(label); mDevice.waitForIdle(); } public void switchToGridMode() { final UiObject2 gridMode = menuGridMode(); if (gridMode != null) { gridMode.click(); } } UiObject2 menuGridMode() { // Note that we're using By.desc rather than By.res, because of b/25285770 return find(By.desc("Grid view")); } UiObject2 menuListMode() { // Note that we're using By.desc rather than By.res, because of b/25285770 return find(By.desc("List view")); } public void clickToolbarItem(int id) { onView(withId(id)).perform(click()); } public void clickNewFolder() { onView(ACTIONBAR_OVERFLOW).perform(click()); // Click the item by label, since Espresso doesn't support lookup by id on overflow. onView(withText("New folder")).perform(click()); } public void clickActionbarOverflowItem(String label) { onView(ACTIONBAR_OVERFLOW).perform(click()); // Click the item by label, since Espresso doesn't support lookup by id on overflow. onView(withText(label)).perform(click()); } public void clickToolbarOverflowItem(String label) { onView(TOOLBAR_OVERFLOW).perform(click()); // Click the item by label, since Espresso doesn't support lookup by id on overflow. onView(withText(label)).perform(click()); } public void clickSaveButton() { onView(withId(android.R.id.button1)).perform(click()); } public boolean waitForActionModeBarToAppear() { UiObject2 bar = mDevice.wait(Until.findObject( By.res(mTargetPackage + ":id/action_mode_bar")), mTimeout); return (bar != null); } public void clickRename() throws UiObjectNotFoundException { if (!waitForActionModeBarToAppear()) { throw new UiObjectNotFoundException("ActionMode bar not found"); } clickActionbarOverflowItem(mContext.getString(R.string.menu_rename)); mDevice.waitForIdle(); } public void clickDelete() throws UiObjectNotFoundException { if (!waitForActionModeBarToAppear()) { throw new UiObjectNotFoundException("ActionMode bar not found"); } clickToolbarItem(R.id.action_menu_delete); mDevice.waitForIdle(); } public UiObject findDownloadRetryDialog() { UiSelector selector = new UiSelector().text("Couldn't download"); UiObject title = mDevice.findObject(selector); title.waitForExists(mTimeout); return title; } public UiObject findFileRenameDialog() { UiSelector selector = new UiSelector().text("Rename"); UiObject title = mDevice.findObject(selector); title.waitForExists(mTimeout); return title; } public UiObject findRenameErrorMessage() { UiSelector selector = new UiSelector().text(mContext.getString(R.string.name_conflict)); UiObject title = mDevice.findObject(selector); title.waitForExists(mTimeout); return title; } @SuppressWarnings("unchecked") public void assertDialogOkButtonFocused() { onView(withId(android.R.id.button1)).check(matches(hasFocus())); } public void clickDialogOkButton() { // Espresso has flaky results when keyboard shows up, so hiding it for now // before trying to click on any dialog button Espresso.closeSoftKeyboard(); onView(withId(android.R.id.button1)).perform(click()); } public void clickDialogCancelButton() throws UiObjectNotFoundException { // Espresso has flaky results when keyboard shows up, so hiding it for now // before trying to click on any dialog button Espresso.closeSoftKeyboard(); onView(withId(android.R.id.button2)).perform(click()); } public UiObject findMenuLabelWithName(String label) { UiSelector selector = new UiSelector().text(label); return mDevice.findObject(selector); } UiObject2 findMenuWithName(String label) { UiObject2 list = mDevice.findObject(By.clazz("android.widget.ListView")); List menuItems = list.getChildren(); Iterator it = menuItems.iterator(); UiObject2 menuItem = null; while (it.hasNext()) { menuItem = it.next(); UiObject2 text = menuItem.findObject(By.text(label)); if (text != null) { return menuItem; } } return null; } boolean hasMenuWithName(String label) { return findMenuWithName(label) != null; } UiObject findMenuMoreOptions() { UiSelector selector = new UiSelector().className("android.widget.ImageView") .descriptionContains("More options"); // TODO: use the system string ? android.R.string.action_menu_overflow_description return mDevice.findObject(selector); } private static Matcher withToolbarTitle( final Matcher textMatcher) { return new BoundedMatcher(Toolbar.class) { @Override public boolean matchesSafely(Toolbar toolbar) { return textMatcher.matches(toolbar.getTitle()); } @Override public void describeTo(Description description) { description.appendText("with toolbar title: "); textMatcher.describeTo(description); } }; } }