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 junit.framework.Assert.assertEquals;
20 import static junit.framework.Assert.assertFalse;
21 import static junit.framework.Assert.assertNotNull;
22 import static junit.framework.Assert.assertTrue;
23 import static junit.framework.Assert.fail;
24 
25 import android.app.UiAutomation;
26 import android.content.Context;
27 import android.graphics.Point;
28 import android.graphics.Rect;
29 import android.os.SystemClock;
30 import android.view.InputDevice;
31 import android.view.KeyEvent;
32 import android.view.MotionEvent;
33 import android.view.View;
34 
35 import androidx.test.uiautomator.By;
36 import androidx.test.uiautomator.BySelector;
37 import androidx.test.uiautomator.Configurator;
38 import androidx.test.uiautomator.UiDevice;
39 import androidx.test.uiautomator.UiObject;
40 import androidx.test.uiautomator.UiObject2;
41 import androidx.test.uiautomator.UiObjectNotFoundException;
42 import androidx.test.uiautomator.UiScrollable;
43 import androidx.test.uiautomator.UiSelector;
44 import androidx.test.uiautomator.Until;
45 
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.List;
49 import java.util.regex.Pattern;
50 
51 /**
52  * A test helper class that provides support for controlling directory list
53  * and making assertions against the state of it.
54  */
55 public class DirectoryListBot extends Bots.BaseBot {
56 
57     private static final int MAX_LAYOUT_LEVEL = 10;
58 
59     private static final BySelector SNACK_DELETE =
60             By.text(Pattern.compile("^Deleting [0-9]+ item.+"));
61 
62     private final String mDirContainerId;
63     private final String mDirListId;
64     private final String mItemRootId;
65     private final String mPreviewId;
66     private final String mIconId;
67 
68     private UiAutomation mAutomation;
69 
DirectoryListBot( UiDevice device, UiAutomation automation, Context context, int timeout)70     public DirectoryListBot(
71             UiDevice device, UiAutomation automation, Context context, int timeout) {
72         super(device, context, timeout);
73         mAutomation = automation;
74         mDirContainerId = mTargetPackage + ":id/container_directory";
75         mDirListId = mTargetPackage + ":id/dir_list";
76         mItemRootId = mTargetPackage + ":id/item_root";
77         mPreviewId = mTargetPackage + ":id/preview_icon";
78         mIconId = mTargetPackage + ":id/icon";
79     }
80 
assertDocumentsCount(int count)81     public void assertDocumentsCount(int count) throws UiObjectNotFoundException {
82         UiObject docsList = findDocumentsList();
83         assertEquals(count, docsList.getChildCount());
84     }
85 
assertDocumentsPresent(String... labels)86     public void assertDocumentsPresent(String... labels) throws UiObjectNotFoundException {
87         List<String> absent = new ArrayList<>();
88         for (String label : labels) {
89             if (!findDocument(label).exists()) {
90                 absent.add(label);
91             }
92         }
93         if (!absent.isEmpty()) {
94             fail("Expected documents " + Arrays.asList(labels)
95                     + ", but missing " + absent);
96         }
97     }
98 
assertDocumentsAbsent(String... labels)99     public void assertDocumentsAbsent(String... labels) throws UiObjectNotFoundException {
100         List<String> found = new ArrayList<>();
101         for (String label : labels) {
102             if (findDocument(label).exists()) {
103                 found.add(label);
104             }
105         }
106         if (!found.isEmpty()) {
107             fail("Expected documents not present" + Arrays.asList(labels)
108                     + ", but present " + found);
109         }
110     }
111 
assertDocumentsCountOnList(boolean exists, int count)112     public void assertDocumentsCountOnList(boolean exists, int count) throws UiObjectNotFoundException {
113         UiObject docsList = findDocumentsList();
114         assertEquals(exists, docsList.exists());
115         if(docsList.exists()) {
116             assertEquals(count, docsList.getChildCount());
117         }
118     }
119 
assertHasMessage(String expected)120     public void assertHasMessage(String expected) throws UiObjectNotFoundException {
121         UiObject messageTextView = findHeaderMessageTextView();
122         String msg = String.valueOf(expected);
123         assertEquals(msg, messageTextView.getText());
124     }
125 
assertHasMessage(boolean expected)126     public void assertHasMessage(boolean expected) throws UiObjectNotFoundException {
127         UiObject messageTextView = findHeaderMessageTextView();
128         if (expected) {
129             assertTrue(messageTextView.exists());
130         } else {
131             assertFalse(messageTextView.exists());
132         }
133     }
134 
assertHasMessageButtonText(String expected)135     public void assertHasMessageButtonText(String expected) throws UiObjectNotFoundException {
136         UiObject button = findHeaderMessageButton();
137         String msg = String.valueOf(expected);
138         assertEquals(msg.toUpperCase(), button.getText().toUpperCase());
139     }
140 
clickMessageButton()141     public void clickMessageButton() throws UiObjectNotFoundException {
142         UiObject button = findHeaderMessageButton();
143         button.click();
144     }
145 
146     /**
147      * Checks against placeholder text. Placeholder can be Empty page, No results page, or the
148      * "Hourglass" page (ie. something-went-wrong page).
149      */
assertPlaceholderMessageText(String message)150     public void assertPlaceholderMessageText(String message) throws UiObjectNotFoundException {
151         UiObject messageTextView = findPlaceholderMessageTextView();
152         assertTrue(messageTextView.exists());
153 
154         String msg = String.valueOf(message);
155         assertEquals(msg, messageTextView.getText());
156 
157     }
158 
findHeaderMessageTextView()159     private UiObject findHeaderMessageTextView() {
160         return findObject(
161                 mDirContainerId,
162                 mTargetPackage + ":id/message_textview");
163     }
164 
findHeaderMessageButton()165     private UiObject findHeaderMessageButton() {
166         return findObject(
167                 mDirContainerId,
168                 mTargetPackage + ":id/dismiss_button");
169     }
170 
findPlaceholderMessageTextView()171     private UiObject findPlaceholderMessageTextView() {
172         return findObject(
173                 mDirContainerId,
174                 mTargetPackage + ":id/message");
175     }
176 
waitForHolderMessage()177     public void waitForHolderMessage() {
178         findPlaceholderMessageTextView().waitForExists(mTimeout);
179     }
180 
assertSnackbar(int id)181     public void assertSnackbar(int id) {
182         assertNotNull(getSnackbar(mContext.getString(id)));
183     }
184 
openDocument(String label)185     public void openDocument(String label) throws UiObjectNotFoundException {
186         int toolType = Configurator.getInstance().getToolType();
187         Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_FINGER);
188         UiObject doc = findDocument(label, true);
189         doc.click();
190         Configurator.getInstance().setToolType(toolType);
191     }
192 
selectDocument(String label)193     public void selectDocument(String label) throws UiObjectNotFoundException {
194         waitForDocument(label);
195         UiObject2 selectionHotspot = findSelectionHotspot(label);
196         selectionHotspot.click();
197     }
198 
199     /**
200      * @param label The filename of the document
201      * @param number Which nth document it is. The number corresponding to "n selected"
202      */
selectDocument(String label, int number)203     public void selectDocument(String label, int number) throws UiObjectNotFoundException {
204         selectDocument(label);
205 
206         // wait until selection is fully done to avoid future click being registered as double
207         // clicking
208         assertSelection(number);
209     }
210 
isDocumentSelected(String label)211     public boolean isDocumentSelected(String label) throws UiObjectNotFoundException {
212         waitForDocument(label);
213         UiObject2 selectionHotspot = findSelectionHotspot(label);
214         return selectionHotspot.getResourceName()
215                 .equals(mTargetPackage + ":id/icon_check");
216     }
217 
findSelectionHotspot(String label)218     public UiObject2 findSelectionHotspot(String label) throws UiObjectNotFoundException {
219         final BySelector list = By.res(mDirListId);
220 
221         BySelector selector = By.hasChild(By.text(label));
222 
223         final UiSelector docList = findDocumentsListSelector();
224         new UiScrollable(docList).scrollIntoView(new UiSelector().text(label));
225 
226         UiObject2 parent = mDevice.findObject(list).findObject(selector);
227         for (int i = 1; i <= MAX_LAYOUT_LEVEL; i++) {
228             parent = parent.getParent();
229             if (mItemRootId.equals(parent.getResourceName())) {
230                 break;
231             }
232         }
233         return parent.findObject(By.res(mIconId));
234     }
235 
copyFilesToClipboard(String...labels)236     public void copyFilesToClipboard(String...labels) throws UiObjectNotFoundException {
237         for (String label: labels) {
238             selectDocument(label);
239         }
240         mDevice.pressKeyCode(KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_ON);
241     }
242 
pasteFilesFromClipboard()243     public void pasteFilesFromClipboard() {
244         mDevice.pressKeyCode(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON);
245     }
246 
getSnackbar(String message)247     public UiObject2 getSnackbar(String message) {
248         return mDevice.wait(Until.findObject(By.text(message)), mTimeout);
249     }
250 
clickSnackbarAction()251     public void clickSnackbarAction() throws UiObjectNotFoundException {
252         UiObject snackbarAction =
253                 findObject(mTargetPackage + ":id/snackbar_action");
254         snackbarAction.click();
255     }
256 
waitForDeleteSnackbar()257     public void waitForDeleteSnackbar() {
258         mDevice.wait(Until.findObject(SNACK_DELETE), mTimeout);
259     }
260 
waitForDeleteSnackbarGone()261     public void waitForDeleteSnackbarGone() {
262         // wait a little longer for snackbar to go away, as it disappears after a timeout.
263         mDevice.wait(Until.gone(SNACK_DELETE), mTimeout * 2);
264     }
265 
waitForDocument(String label)266     public void waitForDocument(String label) throws UiObjectNotFoundException {
267         findDocument(label).waitForExists(mTimeout);
268     }
269 
findDocument(String label)270     public UiObject findDocument(String label) throws UiObjectNotFoundException {
271         return findDocument(label, false);
272     }
273 
findDocument(String label, boolean withScroll)274     public UiObject findDocument(String label, boolean withScroll)
275             throws UiObjectNotFoundException {
276         final UiSelector docList = findDocumentsListSelector();
277 
278         // Wait for the first list item to appear
279         new UiObject(docList.childSelector(new UiSelector())).waitForExists(mTimeout);
280 
281         if (withScroll) {
282             new UiScrollable(docList).scrollIntoView(new UiSelector().text(label));
283         }
284         return mDevice.findObject(docList.childSelector(new UiSelector().text(label)));
285     }
286 
hasDocuments(String... labels)287     public boolean hasDocuments(String... labels) throws UiObjectNotFoundException {
288         for (String label : labels) {
289             if (!findDocument(label).exists()) {
290                 return false;
291             }
292         }
293         return true;
294     }
295 
hasDocumentPreview(String label)296     public boolean hasDocumentPreview(String label) {
297         final BySelector list = By.res(mDirListId);
298         final UiObject2 text = mDevice.findObject(list).findObject(By.text(label));
299 
300         UiObject2 parent = text;
301         for (int i = 1; i <= MAX_LAYOUT_LEVEL; i++) {
302             parent = parent.getParent();
303             if (mItemRootId.equals(parent.getResourceName())) {
304                 break;
305             }
306         }
307 
308         return parent.hasObject(By.res(mPreviewId));
309     }
310 
assertFirstDocumentHasFocus()311     public void assertFirstDocumentHasFocus() throws UiObjectNotFoundException {
312         final UiSelector docList = findDocumentsListSelector();
313 
314         // Wait for the first list item to appear
315         UiObject doc = new UiObject(docList.childSelector(new UiSelector()));
316         doc.waitForExists(mTimeout);
317 
318         assertTrue(doc.isFocused());
319     }
320 
findDocumentsList()321     public UiObject findDocumentsList() {
322         return findObject(
323                 mDirContainerId,
324                 mDirListId);
325     }
326 
findDocumentsListSelector()327     private UiSelector findDocumentsListSelector() {
328         return new UiSelector().resourceId(
329                 mDirContainerId).childSelector(
330                 new UiSelector().resourceId(mDirListId));
331     }
332 
assertHasFocus()333     public void assertHasFocus() {
334         assertHasFocus(mDirListId);
335     }
336 
assertSelection(int numSelected)337     public void assertSelection(int numSelected) {
338         String assertSelectionText = numSelected + " selected";
339         UiObject2 selectionText = mDevice.wait(
340                 Until.findObject(By.text(assertSelectionText)), mTimeout);
341         assertTrue(selectionText != null);
342     }
343 
assertOrder(String[] dirs, String[] files)344     public void assertOrder(String[] dirs, String[] files) throws UiObjectNotFoundException {
345         for (int i = 0; i < dirs.length - 1; ++i) {
346             assertOrder(dirs[i], dirs[i + 1]);
347         }
348 
349         if (dirs.length > 0 && files.length > 0) {
350             assertOrder(dirs[dirs.length - 1], files[0]);
351         }
352 
353         for (int i = 0; i < files.length - 1; ++i) {
354             assertOrder(files[i], files[i + 1]);
355         }
356     }
357 
rightClickDocument(String label)358     public void rightClickDocument(String label) throws UiObjectNotFoundException {
359         Rect startCoord = findDocument(label, true).getBounds();
360         rightClickDocument(new Point(startCoord.centerX(), startCoord.centerY()));
361     }
362 
rightClickDocument(Point point)363     public void rightClickDocument(Point point) throws UiObjectNotFoundException {
364         //TODO: Use Espresso instead of doing the events mock ourselves
365         MotionEvent motionDown = getTestMotionEvent(
366                 MotionEvent.ACTION_DOWN,
367                 MotionEvent.BUTTON_SECONDARY,
368                 MotionEvent.TOOL_TYPE_MOUSE,
369                 InputDevice.SOURCE_MOUSE,
370                 point.x,
371                 point.y);
372         mAutomation.injectInputEvent(motionDown, true);
373         SystemClock.sleep(100);
374 
375         MotionEvent motionUp = getTestMotionEvent(
376                 MotionEvent.ACTION_UP,
377                 MotionEvent.BUTTON_SECONDARY,
378                 MotionEvent.TOOL_TYPE_MOUSE,
379                 InputDevice.SOURCE_MOUSE,
380                 point.x,
381                 point.y);
382 
383         mAutomation.injectInputEvent(motionUp, true);
384     }
385 
getTestMotionEvent( int action, int buttonState, int toolType, int source, int x, int y)386     private MotionEvent getTestMotionEvent(
387             int action, int buttonState, int toolType, int source, int x, int y) {
388         long eventTime = SystemClock.uptimeMillis();
389 
390         MotionEvent.PointerProperties[] pp = {new MotionEvent.PointerProperties()};
391         pp[0].clear();
392         pp[0].id = 0;
393         pp[0].toolType = toolType;
394 
395         MotionEvent.PointerCoords[] pointerCoords = {new MotionEvent.PointerCoords()};
396         pointerCoords[0].clear();
397         pointerCoords[0].x = x;
398         pointerCoords[0].y = y;
399         pointerCoords[0].pressure = 0;
400         pointerCoords[0].size = 1;
401 
402         return MotionEvent.obtain(
403                 eventTime,
404                 eventTime,
405                 action,
406                 1,
407                 pp,
408                 pointerCoords,
409                 0,
410                 buttonState,
411                 1f,
412                 1f,
413                 0,
414                 0,
415                 source,
416                 0);
417     }
418 
assertOrder(String first, String second)419     private void assertOrder(String first, String second) throws UiObjectNotFoundException {
420 
421         final UiObject firstObj = findDocument(first);
422         final UiObject secondObj = findDocument(second);
423 
424         final int layoutDirection = mContext.getResources().getConfiguration().getLayoutDirection();
425         final Rect firstBound = firstObj.getVisibleBounds();
426         final Rect secondBound = secondObj.getVisibleBounds();
427         if (layoutDirection == View.LAYOUT_DIRECTION_LTR) {
428             assertTrue(
429                     "\"" + first + "\" is not located above or to the left of \"" + second
430                             + "\" in LTR",
431                     firstBound.bottom < secondBound.top || firstBound.right < secondBound.left);
432         } else {
433             assertTrue(
434                     "\"" + first + "\" is not located above or to the right of \"" + second +
435                             "\" in RTL",
436                     firstBound.bottom < secondBound.top || firstBound.left > secondBound.right);
437         }
438     }
439 }
440