• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.traceur.uitest;
18 
19 import static org.junit.Assert.assertNotNull;
20 import static org.junit.Assert.assertTrue;
21 
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.os.RemoteException;
26 import android.os.SystemClock;
27 import android.platform.test.annotations.Presubmit;
28 import androidx.test.uiautomator.By;
29 import androidx.test.uiautomator.UiDevice;
30 import androidx.test.uiautomator.UiObject2;
31 import androidx.test.uiautomator.UiObjectNotFoundException;
32 import androidx.test.uiautomator.UiSelector;
33 import androidx.test.uiautomator.UiScrollable;
34 import androidx.test.uiautomator.Until;
35 
36 import androidx.test.InstrumentationRegistry;
37 import androidx.test.runner.AndroidJUnit4;
38 
39 import org.junit.After;
40 import org.junit.Before;
41 import org.junit.Test;
42 import org.junit.runner.RunWith;
43 
44 import java.util.List;
45 import java.util.regex.Pattern;
46 
47 @RunWith(AndroidJUnit4.class)
48 public class TraceurAppTests {
49 
50     private static final String TRACEUR_PACKAGE = "com.android.traceur";
51     private static final String RECYCLERVIEW_ID = "com.android.traceur:id/recycler_view";
52     private static final int LAUNCH_TIMEOUT_MS = 10000;
53     private static final int UI_TIMEOUT_MS = 7500;
54     private static final int SHORT_PAUSE_MS = 1000;
55     private static final int MAX_SCROLL_SWIPES = 10;
56 
57     private UiDevice mDevice;
58     private UiScrollable mScrollableMainScreen;
59 
60     @Before
setUp()61     public void setUp() throws Exception {
62         mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
63 
64         try {
65             if (!mDevice.isScreenOn()) {
66                 mDevice.wakeUp();
67             }
68 
69             // Press Menu to skip the lock screen.
70             // In case we weren't on the lock screen, press Home to return to a clean launcher.
71             mDevice.pressMenu();
72             mDevice.pressHome();
73 
74             mDevice.setOrientationNatural();
75         } catch (RemoteException e) {
76             throw new RuntimeException("Failed to freeze device orientation.", e);
77         }
78 
79         mDevice.waitForIdle();
80 
81         Context context = InstrumentationRegistry.getContext();
82         Intent intent = context.getPackageManager().getLaunchIntentForPackage(TRACEUR_PACKAGE);
83         intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);    // Clear out any previous instances
84         context.startActivity(intent);
85 
86         // Wait for the app to appear.
87         assertTrue(mDevice.wait(Until.hasObject(By.pkg(TRACEUR_PACKAGE).depth(0)),
88                   LAUNCH_TIMEOUT_MS));
89 
90         // The ID for the scrollable RecyclerView is used to find the specific view that we want,
91         // because scrollable views may exist higher in the view hierarchy.
92         mScrollableMainScreen =
93                 new UiScrollable(new UiSelector().scrollable(true).resourceId(RECYCLERVIEW_ID));
94         if (mScrollableMainScreen.exists()) {
95             mScrollableMainScreen.setAsVerticalList();
96             mScrollableMainScreen.setMaxSearchSwipes(MAX_SCROLL_SWIPES);
97         }
98 
99         // Default trace categories are restored in case a previous test modified them and
100         // terminated early.
101         restoreDefaultCategories();
102 
103         // Ensure that the test begins at the top of the main screen.
104         returnToTopOfMainScreen();
105     }
106 
107     @After
tearDown()108     public void tearDown() throws Exception {
109         mDevice.unfreezeRotation();
110         // Finish Traceur activity.
111         mDevice.pressBack();
112         mDevice.pressHome();
113     }
114 
115     /**
116      * Verifies that the main page contains the correct UI elements.
117      * If the main page is scrollable, the test checks that all expected elements are found while
118      * scrolling. Otherwise, it checks that the expected elements are already on the page.
119      */
120     @Presubmit
121     @Test
testElementsOnMainScreen()122     public void testElementsOnMainScreen() throws Exception {
123         String[] elementTitles = {
124             "Record trace",
125             "Record CPU profile",
126             "Trace debuggable applications",
127             "Categories",
128             "Restore default categories",
129             "Per-CPU buffer size",
130             "Long traces",
131             "Maximum long trace size",
132             "Maximum long trace duration",
133             "View saved files",
134             "Clear saved files",
135             // This is intentionally disabled because it can differ between internal and AOSP.
136             // "Stop recording for bug reports",
137             "Show Quick Settings tile",
138         };
139         for (String title : elementTitles) {
140             assertNotNull(title + " element not found.", findObjectOnMainScreenByText(title));
141         }
142     }
143 
144     /**
145      * Checks that a trace can be recorded and shared.
146      * This test records a trace by toggling 'Record trace' in the UI, taps on the share
147      * notification once the trace is saved, then (on non-AOSP) verifies that a share dialog
148      * appears.
149      */
150     @Presubmit
151     @Test
testSuccessfulTracing()152     public void testSuccessfulTracing() throws Exception {
153         UiObject2 recordTraceSwitch = findObjectOnMainScreenByText("Record trace");
154         assertNotNull("Record trace switch not found.", recordTraceSwitch);
155         recordTraceSwitch.click();
156 
157         mDevice.waitForIdle();
158 
159         mDevice.wait(Until.hasObject(By.text("Trace is being recorded")), UI_TIMEOUT_MS);
160         mDevice.wait(Until.gone(By.text("Trace is being recorded")), UI_TIMEOUT_MS);
161 
162         recordTraceSwitch = findObjectOnMainScreenByText("Record trace");
163         assertNotNull("Record trace switch not found.", recordTraceSwitch);
164         recordTraceSwitch.click();
165 
166         mDevice.waitForIdle();
167 
168         waitForShareHUN();
169         tapShareNotification();
170         clickThroughShareSteps();
171     }
172 
173     /**
174      * Checks that stack samples can be recorded and shared.
175      * This test records stack samples by toggling 'Record CPU profile' in the UI, taps on the share
176      * notification once the trace is saved, then (on non-AOSP) verifies that a share dialog
177      * appears.
178      */
179     @Presubmit
180     @Test
testSuccessfulCpuProfiling()181     public void testSuccessfulCpuProfiling() throws Exception {
182         UiObject2 recordCpuProfileSwitch = findObjectOnMainScreenByText("Record CPU profile");
183         assertNotNull("Record CPU profile switch not found.", recordCpuProfileSwitch);
184         recordCpuProfileSwitch.click();
185 
186         mDevice.waitForIdle();
187 
188         // The full "Stack samples are being recorded" text may be cut off.
189         mDevice.wait(Until.hasObject(By.textContains("Stack samples are")), UI_TIMEOUT_MS);
190         mDevice.wait(Until.gone(By.textContains("Stack samples are")), UI_TIMEOUT_MS);
191 
192         recordCpuProfileSwitch = findObjectOnMainScreenByText("Record CPU profile");
193         assertNotNull("Record CPU profile switch not found.", recordCpuProfileSwitch);
194         recordCpuProfileSwitch.click();
195 
196         mDevice.waitForIdle();
197 
198         waitForShareHUN();
199         tapShareNotification();
200         clickThroughShareSteps();
201     }
202 
203     /**
204      * Checks that trace categories are displayed after tapping on the 'Categories' button.
205      */
206     @Presubmit
207     @Test
testTraceCategoriesExist()208     public void testTraceCategoriesExist() throws Exception {
209         openTraceCategories();
210         List<UiObject2> categories = getTraceCategories();
211         assertNotNull("List of categories not found.", categories);
212         assertTrue("No available trace categories.", categories.size() > 0);
213     }
214 
215     /**
216      * Checks that the 'Categories' summary updates when trace categories are selected.
217      * This test checks that the summary for the 'Categories' button changes from 'Default' to 'N
218      * selected' when a trace category is clicked, then back to 'Default' when the same category is
219      * clicked again.
220      */
221     @Presubmit
222     @Test
testCorrectCategoriesSummary()223     public void testCorrectCategoriesSummary() throws Exception {
224         UiObject2 summary = getCategoriesSummary();
225         assertTrue("Expected 'Default' summary not found on startup.",
226                 summary.getText().contains("Default"));
227 
228         openTraceCategories();
229         toggleFirstTraceCategory();
230 
231         // The summary must be reset after each toggle because the reference will be stale.
232         summary = getCategoriesSummary();
233         assertTrue("Expected 'N selected' summary not found.",
234                 summary.getText().contains("selected"));
235 
236         openTraceCategories();
237         toggleFirstTraceCategory();
238 
239         summary = getCategoriesSummary();
240         assertTrue("Expected 'Default' summary not found after changing categories.",
241                 summary.getText().contains("Default"));
242     }
243 
244     /**
245      * Checks that the 'Restore default categories' button resets the trace categories summary.
246      * This test changes the set of selected trace categories from the default, then checks that the
247      * 'Categories' summary resets to 'Default' when the restore button is clicked.
248      */
249     @Presubmit
250     @Test
testRestoreDefaultCategories()251     public void testRestoreDefaultCategories() throws Exception {
252         openTraceCategories();
253         toggleFirstTraceCategory();
254 
255         UiObject2 summary = getCategoriesSummary();
256         assertTrue("Expected 'N selected' summary not found.",
257                 summary.getText().contains("selected"));
258 
259         restoreDefaultCategories();
260         returnToTopOfMainScreen();
261 
262         // The summary must be reset after the toggle because the reference will be stale.
263         summary = getCategoriesSummary();
264         assertTrue("Expected 'Default' summary not found after restoring categories.",
265                 summary.getText().contains("Default"));
266     }
267 
268     /**
269      * Returns to the top of the main Traceur screen if it is scrollable.
270      */
returnToTopOfMainScreen()271     private void returnToTopOfMainScreen() throws Exception {
272         if (mScrollableMainScreen.exists()) {
273             mScrollableMainScreen.setAsVerticalList();
274             mScrollableMainScreen.scrollToBeginning(10);
275         }
276     }
277 
278     /**
279      * Finds and returns the specified element by text, scrolling down if needed.
280      * This method makes the assumption that Traceur's main screen is open, and shouldn't be used as
281      * a general way to find UI elements elsewhere.
282      */
findObjectOnMainScreenByText(String text)283     private UiObject2 findObjectOnMainScreenByText(String text)
284             throws Exception {
285         if (mScrollableMainScreen.exists()) {
286             mScrollableMainScreen.scrollTextIntoView(text);
287         }
288         return mDevice.wait(Until.findObject(By.text(text)), UI_TIMEOUT_MS);
289     }
290 
291     /**
292      * This method waits for the share heads-up notification to appear and disappear.
293      * This is intended to allow for the notification in the shade to be reliably clicked, and is
294      * only used in testSuccessfulTracing and testSuccessfulCpuProfiling.
295      */
waitForShareHUN()296     private void waitForShareHUN() throws Exception {
297         mDevice.wait(Until.hasObject(By.text("Tap to share your recording")), UI_TIMEOUT_MS);
298         mDevice.wait(Until.gone(By.text("Tap to share your recording")), UI_TIMEOUT_MS);
299     }
300 
301     /**
302      * This method opens the notification shade and taps on the share notification.
303      * This is only used in testSuccessfulTracing and testSuccessfulCpuProfiling.
304      */
tapShareNotification()305     private void tapShareNotification() throws Exception {
306         mDevice.openNotification();
307         UiObject2 shareNotification = mDevice.wait(Until.findObject(
308                 By.text("Tap to share your recording")),
309                 UI_TIMEOUT_MS);
310         assertNotNull("Share notification not found.", shareNotification);
311         shareNotification.click();
312 
313         mDevice.waitForIdle();
314     }
315 
316     /**
317      * This method clicks through the share dialog steps.
318      * This is only used in testSuccessfulTracing and testSuccessfulCpuProfiling.
319      */
clickThroughShareSteps()320     private void clickThroughShareSteps() throws Exception {
321         UiObject2 shareDialog = mDevice.wait(Until.findObject(
322                 By.textContains("Only share system traces with people and apps you trust.")),
323                 UI_TIMEOUT_MS);
324         assertNotNull("Share dialog not found.", shareDialog);
325 
326         // The buttons on dialogs sometimes have their capitalization manipulated by themes.
327         UiObject2 shareButton = mDevice.wait(Until.findObject(
328                 By.text(Pattern.compile("share", Pattern.CASE_INSENSITIVE))), UI_TIMEOUT_MS);
329         assertNotNull("Share button not found.", shareButton);
330         shareButton.click();
331 
332         // The share sheet will not appear on AOSP builds, as there are no apps available to share
333         // traces with. This checks if Gmail is installed (i.e. if the build is non-AOSP) before
334         // verifying that the share sheet exists.
335         try {
336             Context context = InstrumentationRegistry.getContext();
337             context.getPackageManager().getApplicationInfo("com.google.android.gm", 0);
338             UiObject2 shareSheet = mDevice.wait(Until.findObject(
339                     By.res("android:id/profile_tabhost")), UI_TIMEOUT_MS);
340             assertNotNull("Share sheet not found.", shareSheet);
341         } catch (PackageManager.NameNotFoundException e) {
342             // Gmail is not installed, so the device is on an AOSP build.
343         }
344     }
345 
346     /**
347      * Taps on the 'Categories' button.
348      */
openTraceCategories()349     private void openTraceCategories() throws Exception {
350         UiObject2 categoriesButton = findObjectOnMainScreenByText("Categories");
351         assertNotNull("Categories button not found.", categoriesButton);
352         categoriesButton.click();
353 
354         mDevice.waitForIdle();
355     }
356 
357     /**
358      * Taps on the 'Restore default categories' button.
359      */
restoreDefaultCategories()360     private void restoreDefaultCategories() throws Exception {
361         UiObject2 restoreButton = findObjectOnMainScreenByText("Restore default categories");
362         assertNotNull("Restore default categories button not found.", restoreButton);
363         restoreButton.click();
364 
365         mDevice.waitForIdle();
366         // This pause is necessary because the trace category restoration takes time to propagate to
367         // the main page.
368         SystemClock.sleep(SHORT_PAUSE_MS);
369     }
370 
371     /**
372      * Returns the UiObject2 of the summary for 'Categories'.
373      * This must only be used on Traceur's main page.
374      */
getCategoriesSummary()375     private UiObject2 getCategoriesSummary() throws Exception {
376         UiObject2 categoriesButton = findObjectOnMainScreenByText("Categories");
377         assertNotNull("Categories button not found.", categoriesButton);
378 
379         // The summary text is a sibling view of 'Categories' and can be found through their parent.
380         UiObject2 categoriesSummary = categoriesButton.getParent().wait(Until.findObject(
381                 By.res("android:id/summary")), UI_TIMEOUT_MS);
382         assertNotNull("Categories summary not found.", categoriesSummary);
383         return categoriesSummary;
384     }
385 
386     /**
387      * Returns the list of available trace categories.
388      * This must only be used after openTraceCategories() has been called.
389      */
getTraceCategories()390     private List<UiObject2> getTraceCategories() {
391         UiObject2 categoriesListView = mDevice.wait(Until.findObject(
392                 By.res("android:id/select_dialog_listview")), UI_TIMEOUT_MS);
393         assertNotNull("List of categories not found.", categoriesListView);
394         return categoriesListView.getChildren();
395     }
396 
397     /**
398      * Toggles the first checkbox in the list of trace categories.
399      * This must only be used after openTraceCategories() has been called.
400      */
toggleFirstTraceCategory()401     private void toggleFirstTraceCategory() throws Exception {
402         getTraceCategories().get(0).click();
403 
404         mDevice.waitForIdle();
405 
406         UiObject2 confirmButton = mDevice.wait(Until.findObject(
407                 By.res("android:id/button1")), UI_TIMEOUT_MS);
408         assertNotNull("'OK' button not found under trace categories list.", confirmButton);
409         confirmButton.click();
410 
411         mDevice.waitForIdle();
412         // This pause is necessary because the trace category selection takes time to propagate to
413         // the main page.
414         SystemClock.sleep(SHORT_PAUSE_MS);
415     }
416 
417 }
418