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