/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.traceur.uitest; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.os.RemoteException; import android.os.SystemClock; import android.platform.test.annotations.Presubmit; import androidx.test.uiautomator.By; import androidx.test.uiautomator.UiDevice; import androidx.test.uiautomator.UiObject2; import androidx.test.uiautomator.UiObjectNotFoundException; import androidx.test.uiautomator.UiSelector; import androidx.test.uiautomator.UiScrollable; import androidx.test.uiautomator.Until; import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import java.util.List; import java.util.regex.Pattern; @RunWith(AndroidJUnit4.class) public class TraceurAppTests { private static final String TRACEUR_PACKAGE = "com.android.traceur"; private static final String RECYCLERVIEW_ID = "com.android.traceur:id/recycler_view"; private static final int LAUNCH_TIMEOUT_MS = 10000; private static final int UI_TIMEOUT_MS = 7500; private static final int SHORT_PAUSE_MS = 1000; private static final int MAX_SCROLL_SWIPES = 10; private UiDevice mDevice; private UiScrollable mScrollableMainScreen; @Before public void setUp() throws Exception { mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); try { if (!mDevice.isScreenOn()) { mDevice.wakeUp(); } // Press Menu to skip the lock screen. // In case we weren't on the lock screen, press Home to return to a clean launcher. mDevice.pressMenu(); mDevice.pressHome(); mDevice.setOrientationNatural(); } catch (RemoteException e) { throw new RuntimeException("Failed to freeze device orientation.", e); } mDevice.waitForIdle(); Context context = InstrumentationRegistry.getContext(); Intent intent = context.getPackageManager().getLaunchIntentForPackage(TRACEUR_PACKAGE); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // Clear out any previous instances context.startActivity(intent); // Wait for the app to appear. assertTrue(mDevice.wait(Until.hasObject(By.pkg(TRACEUR_PACKAGE).depth(0)), LAUNCH_TIMEOUT_MS)); // The ID for the scrollable RecyclerView is used to find the specific view that we want, // because scrollable views may exist higher in the view hierarchy. mScrollableMainScreen = new UiScrollable(new UiSelector().scrollable(true).resourceId(RECYCLERVIEW_ID)); if (mScrollableMainScreen.exists()) { mScrollableMainScreen.setAsVerticalList(); mScrollableMainScreen.setMaxSearchSwipes(MAX_SCROLL_SWIPES); } // Default trace categories are restored in case a previous test modified them and // terminated early. restoreDefaultCategories(); // Ensure that the test begins at the top of the main screen. returnToTopOfMainScreen(); } @After public void tearDown() throws Exception { mDevice.unfreezeRotation(); // Finish Traceur activity. mDevice.pressBack(); mDevice.pressHome(); } /** * Verifies that the main page contains the correct UI elements. * If the main page is scrollable, the test checks that all expected elements are found while * scrolling. Otherwise, it checks that the expected elements are already on the page. */ @Presubmit @Test public void testElementsOnMainScreen() throws Exception { String[] elementTitles = { "Record trace", "Record CPU profile", "Collect Winscope traces", "Trace debuggable applications", "Categories", "Restore default categories", "Per-CPU buffer size", "Long traces", "Maximum long trace size", "Maximum long trace duration", "View saved files", "Clear saved files", // This is intentionally disabled because it can differ between internal and AOSP. // "Stop recording for bug reports", "Show CPU profiling Quick Settings tile", "Show tracing Quick Settings tile", }; for (String title : elementTitles) { assertNotNull(title + " element not found.", findObjectOnMainScreenByText(title)); } } /** * Checks that a trace can be recorded and shared. * This test records a trace by toggling 'Record trace' in the UI, taps on the share * notification once the trace is saved, then (on non-AOSP) verifies that a share dialog * appears. */ @Presubmit @Test public void testSuccessfulTracing() throws Exception { UiObject2 recordTraceSwitch = findObjectOnMainScreenByText("Record trace"); assertNotNull("Record trace switch not found.", recordTraceSwitch); recordTraceSwitch.click(); mDevice.waitForIdle(); mDevice.wait(Until.hasObject(By.text("Trace is being recorded")), UI_TIMEOUT_MS); mDevice.wait(Until.gone(By.text("Trace is being recorded")), UI_TIMEOUT_MS); recordTraceSwitch = findObjectOnMainScreenByText("Record trace"); assertNotNull("Record trace switch not found.", recordTraceSwitch); recordTraceSwitch.click(); mDevice.waitForIdle(); waitForShareHUN(); tapShareNotification(); clickThroughShareSteps(); } /** * Checks that a trace with Winscope data can be recorded and shared. * This test enables Winscope data collection by toggling 'Collect Winscope traces' in the UI, * then records a trace by toggling 'Record trace', taps on the share notification once * the trace is saved, then (on non-AOSP) verifies that a share dialog appears. */ @Presubmit @Test public void testSuccessfulWinscopeTracing() throws Exception { UiObject2 collectWinscopeTracesSwitch = findObjectOnMainScreenByText("Collect Winscope traces"); assertNotNull("Collect Winscope traces switch not found.", collectWinscopeTracesSwitch); collectWinscopeTracesSwitch.click(); UiObject2 recordTraceSwitch = findObjectOnMainScreenByText("Record trace"); assertNotNull("Record trace switch not found.", recordTraceSwitch); recordTraceSwitch.click(); mDevice.waitForIdle(); mDevice.wait(Until.hasObject(By.text("Trace is being recorded")), UI_TIMEOUT_MS); mDevice.wait(Until.gone(By.text("Trace is being recorded")), UI_TIMEOUT_MS); recordTraceSwitch = findObjectOnMainScreenByText("Record trace"); assertNotNull("Record trace switch not found.", recordTraceSwitch); recordTraceSwitch.click(); mDevice.waitForIdle(); waitForShareHUN(); tapShareNotification(); clickThroughShareSteps(); } /** * Checks that stack samples can be recorded and shared. * This test records stack samples by toggling 'Record CPU profile' in the UI, taps on the share * notification once the trace is saved, then (on non-AOSP) verifies that a share dialog * appears. */ @Presubmit @Test public void testSuccessfulCpuProfiling() throws Exception { UiObject2 recordCpuProfileSwitch = findObjectOnMainScreenByText("Record CPU profile"); assertNotNull("Record CPU profile switch not found.", recordCpuProfileSwitch); recordCpuProfileSwitch.click(); mDevice.waitForIdle(); // The full "Stack samples are being recorded" text may be cut off. mDevice.wait(Until.hasObject(By.textContains("Stack samples are")), UI_TIMEOUT_MS); mDevice.wait(Until.gone(By.textContains("Stack samples are")), UI_TIMEOUT_MS); recordCpuProfileSwitch = findObjectOnMainScreenByText("Record CPU profile"); assertNotNull("Record CPU profile switch not found.", recordCpuProfileSwitch); recordCpuProfileSwitch.click(); mDevice.waitForIdle(); waitForShareHUN(); tapShareNotification(); clickThroughShareSteps(); } /** * Checks that trace categories are displayed after tapping on the 'Categories' button. */ @Presubmit @Test public void testTraceCategoriesExist() throws Exception { openTraceCategories(); List categories = getTraceCategories(); assertNotNull("List of categories not found.", categories); assertTrue("No available trace categories.", categories.size() > 0); } /** * Checks that the 'Categories' summary updates when trace categories are selected. * This test checks that the summary for the 'Categories' button changes from 'Default' to 'N * selected' when a trace category is clicked, then back to 'Default' when the same category is * clicked again. */ @Presubmit @Test public void testCorrectCategoriesSummary() throws Exception { UiObject2 summary = getCategoriesSummary(); assertTrue("Expected 'Default' summary not found on startup.", summary.getText().contains("Default")); openTraceCategories(); toggleFirstTraceCategory(); // The summary must be reset after each toggle because the reference will be stale. summary = getCategoriesSummary(); assertTrue("Expected 'N selected' summary not found.", summary.getText().contains("selected")); openTraceCategories(); toggleFirstTraceCategory(); summary = getCategoriesSummary(); assertTrue("Expected 'Default' summary not found after changing categories.", summary.getText().contains("Default")); } /** * Checks that the 'Restore default categories' button resets the trace categories summary. * This test changes the set of selected trace categories from the default, then checks that the * 'Categories' summary resets to 'Default' when the restore button is clicked. */ @Presubmit @Test public void testRestoreDefaultCategories() throws Exception { openTraceCategories(); toggleFirstTraceCategory(); UiObject2 summary = getCategoriesSummary(); assertTrue("Expected 'N selected' summary not found.", summary.getText().contains("selected")); restoreDefaultCategories(); returnToTopOfMainScreen(); // The summary must be reset after the toggle because the reference will be stale. summary = getCategoriesSummary(); assertTrue("Expected 'Default' summary not found after restoring categories.", summary.getText().contains("Default")); } /** * Returns to the top of the main Traceur screen if it is scrollable. */ private void returnToTopOfMainScreen() throws Exception { if (mScrollableMainScreen.exists()) { mScrollableMainScreen.setAsVerticalList(); mScrollableMainScreen.scrollToBeginning(10); } } /** * Finds and returns the specified element by text, scrolling down if needed. * This method makes the assumption that Traceur's main screen is open, and shouldn't be used as * a general way to find UI elements elsewhere. */ private UiObject2 findObjectOnMainScreenByText(String text) throws Exception { if (mScrollableMainScreen.exists()) { mScrollableMainScreen.scrollTextIntoView(text); } return mDevice.wait(Until.findObject(By.text(text)), UI_TIMEOUT_MS); } /** * This method waits for the share heads-up notification to appear and disappear. * This is intended to allow for the notification in the shade to be reliably clicked, and is * only used in testSuccessfulTracing and testSuccessfulCpuProfiling. */ private void waitForShareHUN() throws Exception { mDevice.wait(Until.hasObject(By.text("Tap to share your recording")), UI_TIMEOUT_MS); mDevice.wait(Until.gone(By.text("Tap to share your recording")), UI_TIMEOUT_MS); } /** * This method opens the notification shade and taps on the share notification. * This is only used in testSuccessfulTracing and testSuccessfulCpuProfiling. */ private void tapShareNotification() throws Exception { mDevice.openNotification(); UiObject2 shareNotification = mDevice.wait(Until.findObject( By.text("Tap to share your recording")), UI_TIMEOUT_MS); assertNotNull("Share notification not found.", shareNotification); shareNotification.click(); mDevice.waitForIdle(); } /** * This method clicks through the share dialog steps. * This is only used in testSuccessfulTracing and testSuccessfulCpuProfiling. */ private void clickThroughShareSteps() throws Exception { UiObject2 shareDialog = mDevice.wait(Until.findObject( By.textContains("Only share system traces with people and apps you trust.")), UI_TIMEOUT_MS); assertNotNull("Share dialog not found.", shareDialog); // The buttons on dialogs sometimes have their capitalization manipulated by themes. UiObject2 shareButton = mDevice.wait(Until.findObject( By.text(Pattern.compile("share", Pattern.CASE_INSENSITIVE))), UI_TIMEOUT_MS); assertNotNull("Share button not found.", shareButton); shareButton.click(); // The share sheet will not appear on AOSP builds, as there are no apps available to share // traces with. This checks if Gmail is installed (i.e. if the build is non-AOSP) before // verifying that the share sheet exists. try { Context context = InstrumentationRegistry.getContext(); context.getPackageManager().getApplicationInfo("com.google.android.gm", 0); UiObject2 shareSheet = mDevice.wait(Until.findObject( By.res("android:id/profile_tabhost")), UI_TIMEOUT_MS); assertNotNull("Share sheet not found.", shareSheet); } catch (PackageManager.NameNotFoundException e) { // Gmail is not installed, so the device is on an AOSP build. } } /** * Taps on the 'Categories' button. */ private void openTraceCategories() throws Exception { UiObject2 categoriesButton = findObjectOnMainScreenByText("Categories"); assertNotNull("Categories button not found.", categoriesButton); categoriesButton.click(); mDevice.waitForIdle(); } /** * Taps on the 'Restore default categories' button. */ private void restoreDefaultCategories() throws Exception { UiObject2 restoreButton = findObjectOnMainScreenByText("Restore default categories"); assertNotNull("Restore default categories button not found.", restoreButton); restoreButton.click(); mDevice.waitForIdle(); // This pause is necessary because the trace category restoration takes time to propagate to // the main page. SystemClock.sleep(SHORT_PAUSE_MS); } /** * Returns the UiObject2 of the summary for 'Categories'. * This must only be used on Traceur's main page. */ private UiObject2 getCategoriesSummary() throws Exception { UiObject2 categoriesButton = findObjectOnMainScreenByText("Categories"); assertNotNull("Categories button not found.", categoriesButton); // The summary text is a sibling view of 'Categories' and can be found through their parent. UiObject2 categoriesSummary = categoriesButton.getParent().wait(Until.findObject( By.res("android:id/summary")), UI_TIMEOUT_MS); assertNotNull("Categories summary not found.", categoriesSummary); return categoriesSummary; } /** * Returns the list of available trace categories. * This must only be used after openTraceCategories() has been called. */ private List getTraceCategories() { UiObject2 categoriesListView = mDevice.wait(Until.findObject( By.res("android:id/select_dialog_listview")), UI_TIMEOUT_MS); assertNotNull("List of categories not found.", categoriesListView); return categoriesListView.getChildren(); } /** * Toggles the first checkbox in the list of trace categories. * This must only be used after openTraceCategories() has been called. */ private void toggleFirstTraceCategory() throws Exception { getTraceCategories().get(0).click(); mDevice.waitForIdle(); UiObject2 confirmButton = mDevice.wait(Until.findObject( By.res("android:id/button1")), UI_TIMEOUT_MS); assertNotNull("'OK' button not found under trace categories list.", confirmButton); confirmButton.click(); mDevice.waitForIdle(); // This pause is necessary because the trace category selection takes time to propagate to // the main page. SystemClock.sleep(SHORT_PAUSE_MS); } }