• 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             "Collect Winscope traces",
127             "Trace debuggable applications",
128             "Categories",
129             "Restore default categories",
130             "Per-CPU buffer size",
131             "Long traces",
132             "Maximum long trace size",
133             "Maximum long trace duration",
134             "View saved files",
135             "Clear saved files",
136             // This is intentionally disabled because it can differ between internal and AOSP.
137             // "Stop recording for bug reports",
138             "Show CPU profiling Quick Settings tile",
139             "Show tracing Quick Settings tile",
140         };
141         for (String title : elementTitles) {
142             assertNotNull(title + " element not found.", findObjectOnMainScreenByText(title));
143         }
144     }
145 
146     /**
147      * Checks that a trace can be recorded and shared.
148      * This test records a trace by toggling 'Record trace' in the UI, taps on the share
149      * notification once the trace is saved, then (on non-AOSP) verifies that a share dialog
150      * appears.
151      */
152     @Presubmit
153     @Test
testSuccessfulTracing()154     public void testSuccessfulTracing() throws Exception {
155         UiObject2 recordTraceSwitch = findObjectOnMainScreenByText("Record trace");
156         assertNotNull("Record trace switch not found.", recordTraceSwitch);
157         recordTraceSwitch.click();
158 
159         mDevice.waitForIdle();
160 
161         mDevice.wait(Until.hasObject(By.text("Trace is being recorded")), UI_TIMEOUT_MS);
162         mDevice.wait(Until.gone(By.text("Trace is being recorded")), UI_TIMEOUT_MS);
163 
164         recordTraceSwitch = findObjectOnMainScreenByText("Record trace");
165         assertNotNull("Record trace switch not found.", recordTraceSwitch);
166         recordTraceSwitch.click();
167 
168         mDevice.waitForIdle();
169 
170         waitForShareHUN();
171         tapShareNotification();
172         clickThroughShareSteps();
173     }
174 
175     /**
176      * Checks that a trace with Winscope data can be recorded and shared.
177      * This test enables Winscope data collection by toggling 'Collect Winscope traces' in the UI,
178      * then records a trace by toggling 'Record trace', taps on the share notification once
179      * the trace is saved, then (on non-AOSP) verifies that a share dialog appears.
180      */
181     @Presubmit
182     @Test
testSuccessfulWinscopeTracing()183     public void testSuccessfulWinscopeTracing() throws Exception {
184         UiObject2 collectWinscopeTracesSwitch =
185             findObjectOnMainScreenByText("Collect Winscope traces");
186         assertNotNull("Collect Winscope traces switch not found.", collectWinscopeTracesSwitch);
187         collectWinscopeTracesSwitch.click();
188 
189         UiObject2 recordTraceSwitch = findObjectOnMainScreenByText("Record trace");
190         assertNotNull("Record trace switch not found.", recordTraceSwitch);
191         recordTraceSwitch.click();
192 
193         mDevice.waitForIdle();
194 
195         mDevice.wait(Until.hasObject(By.text("Trace is being recorded")), UI_TIMEOUT_MS);
196         mDevice.wait(Until.gone(By.text("Trace is being recorded")), UI_TIMEOUT_MS);
197 
198         recordTraceSwitch = findObjectOnMainScreenByText("Record trace");
199         assertNotNull("Record trace switch not found.", recordTraceSwitch);
200         recordTraceSwitch.click();
201 
202         mDevice.waitForIdle();
203 
204         waitForShareHUN();
205         tapShareNotification();
206         clickThroughShareSteps();
207     }
208 
209     /**
210      * Checks that stack samples can be recorded and shared.
211      * This test records stack samples by toggling 'Record CPU profile' in the UI, taps on the share
212      * notification once the trace is saved, then (on non-AOSP) verifies that a share dialog
213      * appears.
214      */
215     @Presubmit
216     @Test
testSuccessfulCpuProfiling()217     public void testSuccessfulCpuProfiling() throws Exception {
218         UiObject2 recordCpuProfileSwitch = findObjectOnMainScreenByText("Record CPU profile");
219         assertNotNull("Record CPU profile switch not found.", recordCpuProfileSwitch);
220         recordCpuProfileSwitch.click();
221 
222         mDevice.waitForIdle();
223 
224         // The full "Stack samples are being recorded" text may be cut off.
225         mDevice.wait(Until.hasObject(By.textContains("Stack samples are")), UI_TIMEOUT_MS);
226         mDevice.wait(Until.gone(By.textContains("Stack samples are")), UI_TIMEOUT_MS);
227 
228         recordCpuProfileSwitch = findObjectOnMainScreenByText("Record CPU profile");
229         assertNotNull("Record CPU profile switch not found.", recordCpuProfileSwitch);
230         recordCpuProfileSwitch.click();
231 
232         mDevice.waitForIdle();
233 
234         waitForShareHUN();
235         tapShareNotification();
236         clickThroughShareSteps();
237     }
238 
239     /**
240      * Checks that trace categories are displayed after tapping on the 'Categories' button.
241      */
242     @Presubmit
243     @Test
testTraceCategoriesExist()244     public void testTraceCategoriesExist() throws Exception {
245         openTraceCategories();
246         List<UiObject2> categories = getTraceCategories();
247         assertNotNull("List of categories not found.", categories);
248         assertTrue("No available trace categories.", categories.size() > 0);
249     }
250 
251     /**
252      * Checks that the 'Categories' summary updates when trace categories are selected.
253      * This test checks that the summary for the 'Categories' button changes from 'Default' to 'N
254      * selected' when a trace category is clicked, then back to 'Default' when the same category is
255      * clicked again.
256      */
257     @Presubmit
258     @Test
testCorrectCategoriesSummary()259     public void testCorrectCategoriesSummary() throws Exception {
260         UiObject2 summary = getCategoriesSummary();
261         assertTrue("Expected 'Default' summary not found on startup.",
262                 summary.getText().contains("Default"));
263 
264         openTraceCategories();
265         toggleFirstTraceCategory();
266 
267         // The summary must be reset after each toggle because the reference will be stale.
268         summary = getCategoriesSummary();
269         assertTrue("Expected 'N selected' summary not found.",
270                 summary.getText().contains("selected"));
271 
272         openTraceCategories();
273         toggleFirstTraceCategory();
274 
275         summary = getCategoriesSummary();
276         assertTrue("Expected 'Default' summary not found after changing categories.",
277                 summary.getText().contains("Default"));
278     }
279 
280     /**
281      * Checks that the 'Restore default categories' button resets the trace categories summary.
282      * This test changes the set of selected trace categories from the default, then checks that the
283      * 'Categories' summary resets to 'Default' when the restore button is clicked.
284      */
285     @Presubmit
286     @Test
testRestoreDefaultCategories()287     public void testRestoreDefaultCategories() throws Exception {
288         openTraceCategories();
289         toggleFirstTraceCategory();
290 
291         UiObject2 summary = getCategoriesSummary();
292         assertTrue("Expected 'N selected' summary not found.",
293                 summary.getText().contains("selected"));
294 
295         restoreDefaultCategories();
296         returnToTopOfMainScreen();
297 
298         // The summary must be reset after the toggle because the reference will be stale.
299         summary = getCategoriesSummary();
300         assertTrue("Expected 'Default' summary not found after restoring categories.",
301                 summary.getText().contains("Default"));
302     }
303 
304     /**
305      * Returns to the top of the main Traceur screen if it is scrollable.
306      */
returnToTopOfMainScreen()307     private void returnToTopOfMainScreen() throws Exception {
308         if (mScrollableMainScreen.exists()) {
309             mScrollableMainScreen.setAsVerticalList();
310             mScrollableMainScreen.scrollToBeginning(10);
311         }
312     }
313 
314     /**
315      * Finds and returns the specified element by text, scrolling down if needed.
316      * This method makes the assumption that Traceur's main screen is open, and shouldn't be used as
317      * a general way to find UI elements elsewhere.
318      */
findObjectOnMainScreenByText(String text)319     private UiObject2 findObjectOnMainScreenByText(String text)
320             throws Exception {
321         if (mScrollableMainScreen.exists()) {
322             mScrollableMainScreen.scrollTextIntoView(text);
323         }
324         return mDevice.wait(Until.findObject(By.text(text)), UI_TIMEOUT_MS);
325     }
326 
327     /**
328      * This method waits for the share heads-up notification to appear and disappear.
329      * This is intended to allow for the notification in the shade to be reliably clicked, and is
330      * only used in testSuccessfulTracing and testSuccessfulCpuProfiling.
331      */
waitForShareHUN()332     private void waitForShareHUN() throws Exception {
333         mDevice.wait(Until.hasObject(By.text("Tap to share your recording")), UI_TIMEOUT_MS);
334         mDevice.wait(Until.gone(By.text("Tap to share your recording")), UI_TIMEOUT_MS);
335     }
336 
337     /**
338      * This method opens the notification shade and taps on the share notification.
339      * This is only used in testSuccessfulTracing and testSuccessfulCpuProfiling.
340      */
tapShareNotification()341     private void tapShareNotification() throws Exception {
342         mDevice.openNotification();
343         UiObject2 shareNotification = mDevice.wait(Until.findObject(
344                 By.text("Tap to share your recording")),
345                 UI_TIMEOUT_MS);
346         assertNotNull("Share notification not found.", shareNotification);
347         shareNotification.click();
348 
349         mDevice.waitForIdle();
350     }
351 
352     /**
353      * This method clicks through the share dialog steps.
354      * This is only used in testSuccessfulTracing and testSuccessfulCpuProfiling.
355      */
clickThroughShareSteps()356     private void clickThroughShareSteps() throws Exception {
357         UiObject2 shareDialog = mDevice.wait(Until.findObject(
358                 By.textContains("Only share system traces with people and apps you trust.")),
359                 UI_TIMEOUT_MS);
360         assertNotNull("Share dialog not found.", shareDialog);
361 
362         // The buttons on dialogs sometimes have their capitalization manipulated by themes.
363         UiObject2 shareButton = mDevice.wait(Until.findObject(
364                 By.text(Pattern.compile("share", Pattern.CASE_INSENSITIVE))), UI_TIMEOUT_MS);
365         assertNotNull("Share button not found.", shareButton);
366         shareButton.click();
367 
368         // The share sheet will not appear on AOSP builds, as there are no apps available to share
369         // traces with. This checks if Gmail is installed (i.e. if the build is non-AOSP) before
370         // verifying that the share sheet exists.
371         try {
372             Context context = InstrumentationRegistry.getContext();
373             context.getPackageManager().getApplicationInfo("com.google.android.gm", 0);
374             UiObject2 shareSheet = mDevice.wait(Until.findObject(
375                     By.res("android:id/profile_tabhost")), UI_TIMEOUT_MS);
376             assertNotNull("Share sheet not found.", shareSheet);
377         } catch (PackageManager.NameNotFoundException e) {
378             // Gmail is not installed, so the device is on an AOSP build.
379         }
380     }
381 
382     /**
383      * Taps on the 'Categories' button.
384      */
openTraceCategories()385     private void openTraceCategories() throws Exception {
386         UiObject2 categoriesButton = findObjectOnMainScreenByText("Categories");
387         assertNotNull("Categories button not found.", categoriesButton);
388         categoriesButton.click();
389 
390         mDevice.waitForIdle();
391     }
392 
393     /**
394      * Taps on the 'Restore default categories' button.
395      */
restoreDefaultCategories()396     private void restoreDefaultCategories() throws Exception {
397         UiObject2 restoreButton = findObjectOnMainScreenByText("Restore default categories");
398         assertNotNull("Restore default categories button not found.", restoreButton);
399         restoreButton.click();
400 
401         mDevice.waitForIdle();
402         // This pause is necessary because the trace category restoration takes time to propagate to
403         // the main page.
404         SystemClock.sleep(SHORT_PAUSE_MS);
405     }
406 
407     /**
408      * Returns the UiObject2 of the summary for 'Categories'.
409      * This must only be used on Traceur's main page.
410      */
getCategoriesSummary()411     private UiObject2 getCategoriesSummary() throws Exception {
412         UiObject2 categoriesButton = findObjectOnMainScreenByText("Categories");
413         assertNotNull("Categories button not found.", categoriesButton);
414 
415         // The summary text is a sibling view of 'Categories' and can be found through their parent.
416         UiObject2 categoriesSummary = categoriesButton.getParent().wait(Until.findObject(
417                 By.res("android:id/summary")), UI_TIMEOUT_MS);
418         assertNotNull("Categories summary not found.", categoriesSummary);
419         return categoriesSummary;
420     }
421 
422     /**
423      * Returns the list of available trace categories.
424      * This must only be used after openTraceCategories() has been called.
425      */
getTraceCategories()426     private List<UiObject2> getTraceCategories() {
427         UiObject2 categoriesListView = mDevice.wait(Until.findObject(
428                 By.res("android:id/select_dialog_listview")), UI_TIMEOUT_MS);
429         assertNotNull("List of categories not found.", categoriesListView);
430         return categoriesListView.getChildren();
431     }
432 
433     /**
434      * Toggles the first checkbox in the list of trace categories.
435      * This must only be used after openTraceCategories() has been called.
436      */
toggleFirstTraceCategory()437     private void toggleFirstTraceCategory() throws Exception {
438         getTraceCategories().get(0).click();
439 
440         mDevice.waitForIdle();
441 
442         UiObject2 confirmButton = mDevice.wait(Until.findObject(
443                 By.res("android:id/button1")), UI_TIMEOUT_MS);
444         assertNotNull("'OK' button not found under trace categories list.", confirmButton);
445         confirmButton.click();
446 
447         mDevice.waitForIdle();
448         // This pause is necessary because the trace category selection takes time to propagate to
449         // the main page.
450         SystemClock.sleep(SHORT_PAUSE_MS);
451     }
452 
453 }
454