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