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