blob: becc43581342d9ac0df9bc17f08e390e8a469ece [file] [log] [blame]
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package testutil contains functionality shared by tast tests for android apps on Chromebooks.
package testutil
import (
"context"
"fmt"
"path/filepath"
"strconv"
"strings"
"time"
"chromiumos/tast/common/android/ui"
"chromiumos/tast/common/testexec"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/apps"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/arc/playstore"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/ash"
"chromiumos/tast/local/chrome/display"
"chromiumos/tast/local/chrome/uiauto"
"chromiumos/tast/local/chrome/uiauto/nodewith"
"chromiumos/tast/local/chrome/uiauto/role"
"chromiumos/tast/local/input"
"chromiumos/tast/local/power"
"chromiumos/tast/local/screenshot"
"chromiumos/tast/testing"
)
// Variables used by other tast tests
const (
AndroidButtonClassName = "android.widget.Button"
notNowText = "Not now"
asphaltPkgName = "com.gameloft.android.ANMP.GloftA9HM"
homescapesPkgName = "com.playrix.homescapes"
skypePkgName = "com.skype.raider"
DefaultUITimeout = 20 * time.Second
ShortUITimeout = 30 * time.Second
LongUITimeout = 90 * time.Second
)
// TestFunc represents the "test" function.
type TestFunc func(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string)
// TestCase represents the name of test, the function to call.
type TestCase struct {
Name string
Fn TestFunc
Timeout time.Duration
}
// TestParams represents the collection of tests to run in tablet mode or clamshell mode.
type TestParams struct {
LaunchTests []TestCase
CommonTests []TestCase
AppSpecificTests []TestCase
}
// ClamshellCommonTests is a list of all tests common to all apps in clamshell mode.
var ClamshellCommonTests = []TestCase{
// TODO(b/166637700): Remove the commented testcases if the proper solution is found for the issue.
//{Name: "Clamshell: Orientation", Fn: OrientationSize},
{Name: "Clamshell: Touchscreen Scroll", Fn: TouchScreenScroll},
//{Name: "Clamshell: Mouse click", Fn: MouseClick},
//{Name: "Clamshell: Mouse Scroll", Fn: MouseScrollAction},
//{Name: "Clamshell: Physical Keyboard", Fn: TouchAndTextInputs},
//{Name: "Clamshell: Keyboard Critical Path", Fn: KeyboardNavigations},
//{Name: "Clamshell: Special keys: ESC key", Fn: EscKey},
// Commented Clamshell: Largescreen Layout testcase since it is handled by Resize lock feature in ARC-VM.
//{Name: "Clamshell: Largescreen Layout", Fn: Largescreenlayout},
{Name: "Clamshell: Fullscreen app", Fn: ClamshellFullscreenApp},
{Name: "Clamshell: Minimise and Restore", Fn: MinimizeRestoreApp},
//{Name: "Clamshell: Resize window", Fn: ClamshellResizeWindow},
{Name: "Clamshell: Reopen app", Fn: ReOpenWindow},
{Name: "Clamshell: Resize Lock", Fn: ResizeLock},
}
// TouchviewCommonTests is a list of all tests common to all apps in touchview mode.
var TouchviewCommonTests = []TestCase{
// TODO(b/166637700): Remove the commented testcases if the proper solution is found for the issue.
{Name: "Touchview: Rotate", Fn: TouchviewRotate},
//{Name: "Touchview: Splitscreen", Fn: SplitScreen},
//{Name: "Touchview: Touchscreen Scroll", Fn: TouchScreenScroll},
//{Name: "Touchview: Virtual Keyboard", Fn: TouchAndTextInputs},
//{Name: "Touchview: Largescreen Layout", Fn: Largescreenlayout},
{Name: "Touchview: Minimise and Restore", Fn: MinimizeRestoreApp},
{Name: "Touchview: Reopen app", Fn: ReOpenWindow},
}
// ClamshellSmokeTests is a list of clamshell tests common to apps in appcompat_smoke suite.
var ClamshellSmokeTests = []TestCase{
{Name: "Clamshell: Touchscreen Scroll", Fn: TouchScreenScroll},
{Name: "Clamshell: Physical Keyboard", Fn: TouchAndTextInputs},
{Name: "Clamshell: Mouse click", Fn: MouseClick},
{Name: "Clamshell: Resize window", Fn: ClamshellResizeWindow},
}
// TouchviewSmokeTests is a list of touchview tests common to apps in appcompat_smoke suite.
var TouchviewSmokeTests = []TestCase{
{Name: "Touchview: Minimise and Restore", Fn: MinimizeRestoreApp},
{Name: "Touchview: Reopen app", Fn: ReOpenWindow},
}
// RunTestCases setups the device and runs all app compat test cases.
func RunTestCases(ctx context.Context, s *testing.State, appPkgName, appActivity string, testCases TestParams) {
// Step up chrome on Chromebook.
cr, tconn, a, appVer := setUpDevice(ctx, s, appPkgName, appActivity)
updatedAppActivity := appActivity
if appPkgName == skypePkgName {
if strings.Compare(appVer, "8.80.0.137") >= 0 {
updatedAppActivity = "com.skype4life.MainActivity"
}
}
s.Log("Updated app activity: ", updatedAppActivity)
// Ensure app launches before test cases.
act, err := arc.NewActivity(a, appPkgName, updatedAppActivity)
if err != nil {
s.Fatal("Failed to create new app activity: ", err)
}
defer act.Close()
// Keep the device awake throughout all tests.
resetKeepAwake, err := power.RequestKeepAwake(ctx, tconn, power.Display)
if err != nil {
s.Fatal("Failed to request the device to keep awake: ", err)
}
defer resetKeepAwake(ctx, tconn)
if err := act.StartWithDefaultOptions(ctx, tconn); err != nil {
s.Fatal("Failed to start app before test cases: ", err)
}
if err := a.Command(ctx, "am", "force-stop", appPkgName).Run(testexec.DumpLogOnError); err != nil {
s.Fatal("Failed to stop app before test cases: ", err)
}
s.Log("Successfully tested launching and closing the app")
// AllTests will have LaunchTests, CommonTests and AppSpecificTests.
var AllTests = []TestCase{}
for _, curTest := range testCases.LaunchTests {
AllTests = append(AllTests, curTest)
}
for _, curTest := range testCases.CommonTests {
AllTests = append(AllTests, curTest)
}
for _, curTest := range testCases.AppSpecificTests {
AllTests = append(AllTests, curTest)
}
// Run the different test cases.
for idx, test := range AllTests {
s.Run(ctx, test.Name, func(cleanupCtx context.Context, s *testing.State) {
// Save time for cleanup and screenshot.
ctx, cancel := ctxutil.Shorten(cleanupCtx, 20*time.Second)
defer cancel()
// Launch the app.
if err := act.StartWithDefaultOptions(ctx, tconn); err != nil {
s.Fatal("Failed to start app: ", err)
}
s.Log("App launched successfully")
d, err := a.NewUIDevice(ctx)
if err != nil {
s.Fatal("Failed initializing UI Automator: ", err)
}
defer d.Close(ctx)
// Close the app between iterations.
defer func(ctx context.Context) {
if appPkgName == asphaltPkgName || appPkgName == homescapesPkgName {
HandleDialogBoxes(ctx, s, d, appPkgName)
}
if err := a.Command(ctx, "am", "force-stop", appPkgName).Run(testexec.DumpLogOnError); err != nil {
s.Fatal("Failed to stop app: ", err)
}
}(cleanupCtx)
// Take screenshot and dump ui info on failure.
defer func(ctx context.Context) {
if s.HasError() {
filename := fmt.Sprintf("screenshot-arcappcompat-failed-test-%d.png", idx)
path := filepath.Join(s.OutDir(), filename)
if err := screenshot.CaptureChrome(ctx, cr, path); err != nil {
s.Log("Failed to capture screenshot: ", err)
}
if err := a.Command(ctx, "uiautomator", "dump").Run(testexec.DumpLogOnError); err != nil {
s.Log("Failed to dump UIAutomator: ", err)
} else {
filename = fmt.Sprintf("ui-dump-arcappcompat-failed-test-%d.xml", idx)
path = filepath.Join(s.OutDir(), filename)
if err := a.PullFile(ctx, "/sdcard/window_dump.xml", path); err != nil {
s.Log("Failed to pull UIAutomator dump: ", err)
}
}
filename = fmt.Sprintf("bugreport-arcappcompat-failed-test-%d.zip", idx)
path = filepath.Join(s.OutDir(), filename)
if err := a.BugReport(ctx, path); err != nil {
s.Log("Failed to get bug report: ", err)
}
}
}(cleanupCtx)
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
// Click on not now button to skip the display profile visible in public.
notNowButton := d.Object(ui.ClassName(AndroidButtonClassName), ui.TextMatches("(?i)"+notNowText))
if err := notNowButton.WaitForExists(ctx, DefaultUITimeout); err == nil {
if err := notNowButton.Click(ctx); err != nil {
s.Fatal("Failed to click on notNow Button: ", err)
}
}
// It is ok if the package is currently equal the installer package.
// It is also ok if the package is currently equal the play service package.
// It is also ok if the package is currently equal the android permission controller package
// It is also ok if the package is currently equal the settings package.
// This happens when you need to accept permissions.
var allowedAppPackage bool
currentAppPkg, err := CurrentAppPackage(ctx, d, s)
if err != nil {
s.Fatal("Failed to get current app package: ", err)
}
if currentAppPkg == appPkgName {
allowedAppPackage = true
} else if currentAppPkg != appPkgName {
for _, perAppPkg := range []struct {
permissionAppPkgNames string
}{
{"com.google.android.packageinstaller"}, {"com.google.android.gms"},
{"com.google.android.permissioncontroller"}, {"com.android.settings"},
} {
if currentAppPkg == perAppPkg.permissionAppPkgNames {
allowedAppPackage = true
break
}
}
}
if !allowedAppPackage {
s.Fatalf("Failed to launch app: incorrect package(expected: %s, actual: %s)", appPkgName, currentAppPkg)
}
test.Fn(ctx, s, tconn, a, d, appPkgName, updatedAppActivity)
})
}
}
// setUpDevice func setup Chrome on Chromebook.
func setUpDevice(ctx context.Context, s *testing.State, appPkgName, appActivity string) (*chrome.Chrome, *chrome.TestConn, *arc.ARC, string) {
// Setup Chrome.
cr := s.PreValue().(arc.PreData).Chrome
a := s.PreValue().(arc.PreData).ARC
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to create test API connection: ", err)
}
d, err := a.NewUIDevice(ctx)
if err != nil {
s.Fatal("Failed initializing UI Automator: ", err)
}
defer d.Close(ctx)
s.Log("Enable showing ANRs")
if err := a.Command(ctx, "settings", "put", "secure", "anr_show_background", "1").Run(testexec.DumpLogOnError); err != nil {
s.Fatal("Failed to enable showing ANRs: ", err)
}
s.Log("Enable crash dialog")
if err := a.Command(ctx, "settings", "put", "secure", "show_first_crash_dialog_dev_option", "1").Run(testexec.DumpLogOnError); err != nil {
s.Fatal("Failed to enable crash dialog: ", err)
}
s.Log("Installing app")
// Keep the device awake while installing from the play store.
resetKeepAwake, err := power.RequestKeepAwake(ctx, tconn, power.Display)
if err != nil {
s.Fatal("Failed to request the device to keep awake: ", err)
}
defer resetKeepAwake(ctx, tconn)
if err := apps.Launch(ctx, tconn, apps.PlayStore.ID); err != nil {
s.Fatal("Failed to launch Play Store: ", err)
}
if err := playstore.InstallApp(ctx, a, d, appPkgName, &playstore.Options{}); err != nil {
s.Fatal("Failed to install app: ", err)
}
versionName, err := GetAppVersion(ctx, s, a, d, appPkgName)
if err := apps.Close(ctx, tconn, apps.PlayStore.ID); err != nil {
s.Log("Failed to close Play Store: ", err)
}
return cr, tconn, a, versionName
}
// ClamshellFullscreenApp Test launches the app in full screen window and verifies launch successfully without crash or ANR on ARC-P devices
func ClamshellFullscreenApp(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
info, err := ash.GetARCAppWindowInfo(ctx, tconn, appPkgName)
if err != nil {
s.Error("Failed to get window info: ", err)
}
t, ok := arc.Type()
if !ok {
s.Fatal("Unable to determine arc type")
}
// If ARC-P.
if t == arc.Container {
s.Log("Setting the window to fullscreen")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventFullscreen); err != nil {
s.Fatal(" Failed to set the window to fullscreen: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateFullscreen); err != nil {
s.Fatal("The window is not in fullscreen: ", err)
}
if !isNApp(ctx, d) {
if err := restartApp(ctx, d, appPkgName); err != nil {
s.Fatal("Failed to restart app: ", err)
}
}
}
if t == arc.VM {
// If app is launched in maximized state or
// If app doesn't have resize lock feature in normal window mode then set the window to full screen.
uia := uiauto.New(tconn)
button := nodewith.HasClass(centerButtonClassName)
err := uia.WithTimeout(10 * time.Second).WaitUntilExists(button)(ctx)
if info.State == ash.WindowStateMaximized || err != nil {
s.Log("app is in maximized mode or it can be an O4C app. Setting the window to fullscreen")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventFullscreen); err != nil {
s.Error("Failed to set the window to fullscreen: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateFullscreen); err != nil {
s.Error("The window is not in fullscreen: ", err)
}
return
}
// If app has resize lock feature and it is in phone or tablet size.
nodes, err := uia.WithTimeout(10*time.Second).NodesInfo(ctx, button)
if err != nil {
s.Error("Failed to get button node info: ", err)
}
if len(nodes) >= 1 {
info := nodes[0]
defaultState := info.Name
s.Logf("Default state of app is in: %+v", defaultState)
if info.Name == phoneButtonName || info.Name == tabletButtonName {
// CloseSplash to handle got it button.
if err := closeSplash(ctx, tconn); err != nil {
s.Log("CloseSplash doesn't exist: ", err)
}
// Check if the compat-mode button of a fully-locked app is disabled
if err := uia.LeftClick(button)(ctx); err != nil {
s.Fatal(err, "failed to click on the compat-mode button: ", err)
}
// Need some sleep here as we verify that nothing changes.
if err := testing.Sleep(ctx, time.Second); err != nil {
s.Fatal("Failed to sleep after clicking on the compat-mode button: ", err)
}
// Check if compat-mode button is disabled or enabled.
if err := checkVisibility(ctx, tconn, bubbleDialogClassName, false); err == nil {
// If compat-mode button is disabled.
s.Log("The app is non-resizable. Skipping test")
return
}
}
// If compat-mode button is enabled.
if err := selectResizeLockMode(ctx, tconn, appPkgName); err != nil {
s.Fatal("Failed to click on the compat-mode dialog: ", err)
}
if err := handleConfirmDialog(ctx, tconn); err != nil {
s.Log("confirmDialog doesn't exist: ", err)
}
s.Log("Setting the window to fullscreen")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventFullscreen); err != nil {
s.Error("Failed to set the window to fullscreen: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateFullscreen); err != nil {
s.Error("The window is not in fullscreen: ", err)
}
s.Log("Reseting window to normal size")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventNormal); err != nil {
s.Error("Failed to reset window to normal size: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateNormal); err != nil {
s.Error("The window is not normalized: ", err)
}
// Restore the window to the default window state of an app.
selectDefaultWindowState(ctx, s, tconn, appPkgName, defaultState)
}
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// MinimizeRestoreApp Test "minimize and relaunch the app" and verifies app relaunch successfully without crash or ANR.
func MinimizeRestoreApp(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
s.Log("Minimizing the window")
defaultState, err := ash.GetARCAppWindowState(ctx, tconn, appPkgName)
if err != nil {
s.Fatal("Failed to get the default window state: ", err)
}
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventMinimize); err != nil {
s.Error("Failed to minimize the window: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateMinimized); err != nil {
s.Error("The window is not minimized: ", err)
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
s.Log("Restoring the window")
var restoreEvent ash.WMEventType
switch defaultState {
case ash.WindowStateFullscreen:
restoreEvent = ash.WMEventFullscreen
case ash.WindowStateMaximized:
restoreEvent = ash.WMEventMaximize
default:
restoreEvent = ash.WMEventNormal
}
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, restoreEvent); err != nil {
s.Error("Failed to restore the window: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, defaultState); err != nil {
s.Error("The window is not restored: ", err)
}
}
// ClamshellResizeWindow Test "resize and restore back to original state of the app" and verifies app launch successfully without crash or ANR on ARC-P devices.
func ClamshellResizeWindow(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
info, err := ash.GetARCAppWindowInfo(ctx, tconn, appPkgName)
if err != nil {
s.Fatal("Failed to get window info: ", err)
}
goalState := ash.WindowStateMaximized
if info.State == ash.WindowStateFullscreen {
goalState = ash.WindowStateFullscreen
}
tabletModeEnabled, err := ash.TabletModeEnabled(ctx, tconn)
if err != nil {
s.Fatal("Failed to get tablet mode: ", err)
}
if tabletModeEnabled {
s.Log("Device is in tablet mode. Skipping test")
return
}
t, ok := arc.Type()
if !ok {
s.Fatal("Unable to determine arc type")
}
// If ARC-P.
if t == arc.Container {
info, err := ash.GetARCAppWindowInfo(ctx, tconn, appPkgName)
if err != nil {
s.Fatal("Failed to get window info: ", err)
}
s.Logf("App Resize info, info.CanResize %+v", info.CanResize)
if !info.CanResize {
s.Log("This app is not resizable. Skipping test")
return
}
if isNApp(ctx, d) {
s.Log("N-apps start maximized. Reseting window to normal size")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventNormal); err != nil {
s.Error("Failed to reset window to normal size: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateNormal); err != nil {
s.Error("The window is not normalized: ", err)
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
s.Log("Maximizing the window")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventTypeForState(goalState)); err != nil {
s.Log("Failed to maximize the window: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, goalState); err != nil {
s.Log("The window is not maximized: ", err)
}
if !isNApp(ctx, d) {
if err := restartApp(ctx, d, appPkgName); err != nil {
s.Fatal("Failed to restart app: ", err)
}
}
}
// If ARC-VM.
// Handle resize lock feature.
if t == arc.VM {
// If app is launched in maximized or in fullscreen state.
if info.State == ash.WindowStateMaximized || info.State == ash.WindowStateFullscreen {
// Check if app is resizable or not.
s.Logf("App Resize info, info.CanResize %+v", info.CanResize)
if !info.CanResize {
s.Log("This app is not resizable. Skipping test")
return
}
s.Log("Reseting window to normal size")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventNormal); err != nil {
s.Error("Failed to reset window to normal size: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateNormal); err != nil {
s.Error("The window is not normalized: ", err)
}
}
// If app doesn't have resize lock feature in normal window mode then maximize the app.
uia := uiauto.New(tconn)
centerButton := nodewith.HasClass(centerButtonClassName)
if err := uia.WithTimeout(10 * time.Second).Exists(centerButton)(ctx); err != nil {
s.Log("It can be an O4C app. App window is normal. Maximize the window")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventTypeForState(goalState)); err != nil {
s.Error("Failed to maximize the window: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, goalState); err != nil {
s.Error("The window is not maximized: ", err)
}
return
}
// If app has resize lock feature and it is in phone or tablet size.
nodes, err := uia.NodesInfo(ctx, centerButton)
if err != nil {
s.Error("Failed to get node info for center button: ", err)
return
}
if len(nodes) >= 1 {
buttonInfo := nodes[0]
if buttonInfo.Name == phoneButtonName || buttonInfo.Name == tabletButtonName {
s.Log("App is in: ", buttonInfo.Name)
// CloseSplash to handle got it button.
if err := closeSplash(ctx, tconn); err != nil {
s.Log("CloseSplash doesn't exist: ", err)
}
// Check if the compat-mode button of a fully-locked app is disabled.
if err := uia.LeftClick(centerButton)(ctx); err != nil {
s.Fatal("Failed to click on the compat-mode button: ", err)
}
// Need some sleep here as we verify that nothing changes.
if err := testing.Sleep(ctx, time.Second); err != nil {
s.Fatal("Failed to sleep after clicking on the compat-mode button: ", err)
}
// Check if compat-mode button is disabled or enabled.
if err := checkVisibility(ctx, tconn, bubbleDialogClassName, false); err == nil {
// If compat-mode button is disabled.
s.Log("The app is non-resizable. Skipping test")
return
}
}
// If compat-mode button is enabled.
if err := selectResizeLockMode(ctx, tconn, appPkgName); err != nil {
s.Fatal("Failed to click on the compat-mode dialog: ", err)
}
if err := handleConfirmDialog(ctx, tconn); err != nil {
s.Log("confirmDialog doesn't exist: ", err)
}
s.Log("Maximizing the window")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventTypeForState(goalState)); err != nil {
s.Error("Failed to maximize the window: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, goalState); err != nil {
s.Error("The window is not maximized: ", err)
}
}
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// Represents the high-level state of the app from the resize-lock feature's perspective.
type resizeLockMode int
const (
phoneResizeLockMode resizeLockMode = iota
tabletResizeLockMode
resizableResizeLockMode
)
func (mode resizeLockMode) String() string {
switch mode {
case phoneResizeLockMode:
return phoneButtonName
case tabletResizeLockMode:
return tabletButtonName
case resizableResizeLockMode:
return resizableButtonName
default:
return ""
}
}
const (
// Used to (i) find the resize lock mode buttons on the compat-mode menu and (ii) check the state of the compat-mode button.
phoneButtonName = "Phone"
tabletButtonName = "Tablet"
resizableButtonName = "Resizable"
// Currently the automation API doesn't support unique ID, so use the classnames to find the elements of interest.
centerButtonClassName = "FrameCenterButton"
checkBoxClassName = "Checkbox"
bubbleDialogClassName = "BubbleDialogDelegateView"
overlayDialogClassName = "OverlayDialog"
menuItemViewClassName = "MenuItemView"
// A11y names are available for some UI elements.
splashCloseButtonName = "Got it"
confirmButtonName = "Allow"
cancelButtonName = "Cancel"
closeMenuItemViewName = "Close"
)
// ResizeLock Test verifies if app has resize lock feature available or not and verifies if app launch successfully without crash or ANR.
func ResizeLock(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
t, ok := arc.Type()
if !ok {
s.Fatal("Unable to determine arc type")
}
// If ARC-VM.
if t == arc.VM {
if err := checkCompatModeButton(ctx, s, tconn, a, d, appPkgName, appActivity); err != nil {
s.Fatal("Failed to verify the window state and compat mode button availability: ", err)
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
}
// checkCompatModeButton verifies the window state of app and also verifies compat mode button is available or not for the given app.
func checkCompatModeButton(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) error {
info, err := ash.GetARCAppWindowInfo(ctx, tconn, appPkgName)
if err != nil {
s.Fatal("Failed to get window info: ", err)
}
// Check if app is launched in maximized or in fullscreen state.
if info.State == ash.WindowStateMaximized || info.State == ash.WindowStateFullscreen {
s.Log("App is in maximized or full screen state")
// Check if app is resizable or not.
s.Logf("App Resize info, info.CanResize %+v", info.CanResize)
if !info.CanResize {
s.Log("This app is not resizable. Skipping test")
return nil
}
// If resizable, on clicking resize button, check app shows up resize lock feature or not.
s.Log("N-apps start maximized. Reseting window to normal size")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventNormal); err != nil {
s.Error("Failed to reset window to normal size: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateNormal); err != nil {
s.Error("The window is not normalized: ", err)
}
}
// Check for compat-mode button.
uia := uiauto.New(tconn)
nodes, err := uia.WithTimeout(10*time.Second).NodesInfo(ctx, nodewith.HasClass(centerButtonClassName))
if err != nil {
return errors.Wrap(err, "failed to get node info for center button")
}
if len(nodes) == 0 {
s.Log("App isn't resize locked app. It can be an O4C app")
} else {
button := nodes[0]
if button.Name == phoneButtonName || button.Name == tabletButtonName || button.Name == resizableButtonName {
s.Log("It is resize locked app")
s.Logf("button.Name:%s", button.Name)
} else {
return errors.Wrap(err, "failed to find the compat mode options")
}
}
return nil
}
// checkVisibility checks whether the node specified by the given class name exists or not.
func checkVisibility(ctx context.Context, tconn *chrome.TestConn, className string, visible bool) error {
uia := uiauto.New(tconn)
finder := nodewith.HasClass(className).First()
if visible {
return uia.WithTimeout(10 * time.Second).WaitUntilExists(finder)(ctx)
}
return uia.WithTimeout(10 * time.Second).WaitUntilGone(finder)(ctx)
}
// selectResizeLockMode clicks on the resizable lock mode button and clicks on the confirm button.
func selectResizeLockMode(ctx context.Context, tconn *chrome.TestConn, appPkgName string) error {
uia := uiauto.New(tconn)
compatModeMenuDialog := nodewith.Role(role.Window).HasClass(bubbleDialogClassName)
resizeLockModeButton := nodewith.Role(role.MenuItem).Name(resizableButtonName).Ancestor(compatModeMenuDialog)
if err := uia.WithTimeout(10 * time.Second).WaitUntilExists(compatModeMenuDialog)(ctx); err != nil {
return errors.Wrapf(err, "failed to find the compat-mode menu dialog of %s", appPkgName)
}
if err := uia.WithTimeout(10 * time.Second).WaitUntilExists(resizeLockModeButton)(ctx); err != nil {
return errors.Wrapf(err, "failed to find the %s button on the compat mode menu", resizableButtonName)
}
return uia.LeftClick(resizeLockModeButton)(ctx)
}
// selectDefaultWindowState clicks on the default window type which can be phone or tablet button and clicks on the confirm button.
func selectDefaultWindowState(ctx context.Context, s *testing.State, tconn *chrome.TestConn, appPkgName, defaultState string) error {
uia := uiauto.New(tconn)
button := nodewith.HasClass(centerButtonClassName)
nodes, err := uia.WithTimeout(10*time.Second).NodesInfo(ctx, button)
if err == nil && len(nodes) >= 1 {
info := nodes[0]
// If app is in resizable mode.
if info.Name == resizableButtonName {
s.Log("App is in: ", info.Name)
// CloseSplash to handle got it button.
if err := closeSplash(ctx, tconn); err != nil {
s.Log("CloseSplash doesn't exist: ", err)
}
// Check if the compat-mode button of a fully-locked app is disabled
if err := uia.LeftClick(button)(ctx); err != nil {
s.Fatal(err, "failed to click on the compat-mode button: ", err)
}
// Need some sleep here as we verify that nothing changes.
if err := testing.Sleep(ctx, time.Second); err != nil {
s.Fatal("Failed to sleep after clicking on the compat-mode button: ", err)
}
// Check if compat-mode button is disabled or enabled.
if err := checkVisibility(ctx, tconn, bubbleDialogClassName, false); err == nil {
// If compat-mode button is disabled.
s.Log("The app is non-resizable. Skipping test")
return nil
}
}
}
compatModeMenuDialog := nodewith.HasClass(bubbleDialogClassName)
if err := uia.WithTimeout(10 * time.Second).WaitUntilExists(compatModeMenuDialog)(ctx); err != nil {
return errors.Wrapf(err, "failed to find the compat-mode menu dialog of %s", appPkgName)
}
// To get back to the default window state of an app.
defaultWindow := resizableButtonName
if defaultState == "Phone" {
s.Logf("Get back to default window state of an app: %+v", defaultState)
defaultWindow = phoneButtonName
}
if defaultState == "Tablet" {
s.Logf("Get back to default window state of an app: %+v", defaultState)
defaultWindow = tabletButtonName
}
resizeLockModeButton := nodewith.Ancestor(compatModeMenuDialog).Name(defaultWindow)
if err := uia.WaitUntilExists(resizeLockModeButton)(ctx); err != nil {
return errors.Wrapf(err, "failed to find the %s button on the compat mode menu", defaultWindow)
}
return uia.LeftClick(resizeLockModeButton)(ctx)
}
// handleConfirmDialog clicks on allow button for the confirmation dialog.
func handleConfirmDialog(ctx context.Context, tconn *chrome.TestConn) error {
uia := uiauto.New(tconn)
confirmationDialog := nodewith.HasClass(overlayDialogClassName)
if err := uia.WithTimeout(10 * time.Second).WaitUntilExists(confirmationDialog)(ctx); err != nil {
return errors.Wrap(err, "failed to find the resizability confirmation dialog")
}
confirmButton := nodewith.Role(role.Button).Name(confirmButtonName).Ancestor(confirmationDialog)
if err := uia.WithTimeout(10 * time.Second).WaitUntilExists(confirmButton)(ctx); err != nil {
return errors.Wrap(err, "failed to find the confirm button on the compat mode menu")
}
return uia.LeftClick(confirmButton)(ctx)
}
// closeSplash clicks on the close button and closes the splash screen.
func closeSplash(ctx context.Context, tconn *chrome.TestConn) error {
splash := nodewith.HasClass(bubbleDialogClassName)
button := nodewith.Ancestor(splash).HasClass(splashCloseButtonName)
return uiauto.New(tconn).WithTimeout(10*time.Second).LeftClickUntil(button, func(ctx context.Context) error {
return checkVisibility(ctx, tconn, bubbleDialogClassName, false /* visible */)
})(ctx)
}
// TouchAndTextInputs func verify touch and text inputs in the app are working properly without crash or ANR.
func TouchAndTextInputs(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
// Press enter key twice.
if err := d.PressKeyCode(ctx, ui.KEYCODE_ENTER, 0); err != nil {
s.Log("Failed to enter KEYCODE_ENTER: ", err)
}
if err := d.PressKeyCode(ctx, ui.KEYCODE_ENTER, 0); err != nil {
s.Log("Failed to enter KEYCODE_ENTER: ", err)
}
// To perform touch and text inputs.
out, err := a.Command(ctx, "monkey", "--pct-syskeys", "0", "-p", appPkgName, "--pct-touch", "30", "--pct-nav", "10", "--pct-touch", "40", "--pct-nav", "10", "--pct-anyevent", "2", "--throttle", "100", "-v", "50").Output(testexec.DumpLogOnError)
if err != nil {
s.Error("Failed to perform monkey test touch and text inputs: ", err)
}
if err := processMonkeyOutput(string(out)); err != nil {
s.Error("Touch and text inputs are not working properly in the app: ", err)
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// KeyboardNavigations func verifies app perform keyboard navigations successfully without crash or ANR.
func KeyboardNavigations(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
tabletModeEnabled, err := ash.TabletModeEnabled(ctx, tconn)
if err != nil {
s.Fatal("Failed to get tablet mode: ", err)
}
if tabletModeEnabled {
s.Log("Device is in tablet mode. Skipping test")
return
}
// Press enter key twice.
if err := d.PressKeyCode(ctx, ui.KEYCODE_ENTER, 0); err != nil {
s.Log("Failed to enter KEYCODE_ENTER: ", err)
}
if err := d.PressKeyCode(ctx, ui.KEYCODE_ENTER, 0); err != nil {
s.Log("Failed to enter KEYCODE_ENTER: ", err)
}
// To perform keyboard navigations.
out, err := a.Command(ctx, "monkey", "--pct-syskeys", "0", "-p", appPkgName, "--pct-nav", "20", "--pct-majornav", "20", "--pct-nav", "20", "--pct-majornav", "20", "--throttle", "100", "-v", "50").Output(testexec.DumpLogOnError)
if err != nil {
s.Error("Failed to perform monkey test keyboard navigations: ", err)
}
if err := processMonkeyOutput(string(out)); err != nil {
s.Error("Key board navigations such as up/down/left/right are not working properly in the app: ", err)
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// TouchAndPlayVideo func verifies app perform touch and play video successfully without crash or ANR.
func TouchAndPlayVideo(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
// Press enter key twice.
if err := d.PressKeyCode(ctx, ui.KEYCODE_ENTER, 0); err != nil {
s.Log("Failed to enter KEYCODE_ENTER: ", err)
}
if err := d.PressKeyCode(ctx, ui.KEYCODE_ENTER, 0); err != nil {
s.Log("Failed to enter KEYCODE_ENTER: ", err)
}
// To perform touch and play video.
out, err := a.Command(ctx, "monkey", "--pct-syskeys", "0", "-p", appPkgName, "--pct-touch", "60", "--throttle", "100", "-v", "50").Output(testexec.DumpLogOnError)
if err != nil {
s.Error("Failed to perform monkey test touch and play video content: ", err)
}
if err := processMonkeyOutput(string(out)); err != nil {
s.Error("Touch and play videos are not working properly in the app: ", err)
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// TouchviewRotate Test verifies if app performs rotation successfully without crash or ANR.
func TouchviewRotate(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
info, err := ash.GetARCAppWindowInfo(ctx, tconn, appPkgName)
if err != nil {
s.Fatal("Failed to get window info: ", err)
}
s.Logf("App Display ID, info.DisplayID %+v", info.DisplayID)
// Set display orientation to natural state 90 degree.
if err := display.SetDisplayRotationSync(ctx, tconn, info.DisplayID, "Rotate90"); err != nil {
s.Fatal("Failed to set app to 90 rotation: ", err)
} else {
s.Log("Set app to 90 rotation was successful")
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
// Set display orientation to natural state 180 degree.
if err := display.SetDisplayRotationSync(ctx, tconn, info.DisplayID, "Rotate180"); err != nil {
s.Fatal("Failed to set app to 180 rotation: ", err)
} else {
s.Log("Set app to 180 rotation was successful")
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
// Set display orientation to natural state 270 degree.
if err := display.SetDisplayRotationSync(ctx, tconn, info.DisplayID, "Rotate270"); err != nil {
s.Fatal("Failed to set app to 270 rotation: ", err)
} else {
s.Log("Set app to 270 rotation was successful")
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
// Set display orientation to natural state 0 degree.
if err := display.SetDisplayRotationSync(ctx, tconn, info.DisplayID, "Rotate0"); err != nil {
s.Fatal("Failed to set app to 0 rotation: ", err)
} else {
s.Log("Set app to 0 rotation was successful")
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// MouseScrollAction func verifies app perform mouse scroll actions successfully without crash or ANR.
func MouseScrollAction(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
checkForScrollLayout := d.Object(ui.PackageName(appPkgName), ui.Scrollable(true), ui.Focusable(true), ui.Enabled(true))
if err := checkForScrollLayout.WaitForExists(ctx, DefaultUITimeout); err != nil {
s.Log("ScrollLayout doesn't exist. Page is not scrollable and skipping the test: ", err)
return
}
// To perform mouse scroll actions.
out, err := a.Command(ctx, "monkey", "--pct-syskeys", "0", "-p", appPkgName, "--pct-trackball", "100", "--throttle", "100", "-v", "50").Output(testexec.DumpLogOnError)
if err != nil {
s.Error("Failed to perform monkey test mouse scroll: ", err)
}
if err := processMonkeyOutput(string(out)); err != nil {
s.Error("Mouse scroll is not working properly in the app: ", err)
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// TouchScreenScroll Test verifies app perform scrollForward successfully without crash or ANR.
func TouchScreenScroll(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
checkForScrollLayout := d.Object(ui.PackageName(appPkgName), ui.Scrollable(true), ui.Focusable(true), ui.Enabled(true))
if err := checkForScrollLayout.WaitForExists(ctx, DefaultUITimeout); err != nil {
s.Log("ScrollLayout doesn't exist. Page is not scrollable and skipping the test: ", err)
return
}
scrollForwdInfo, err := checkForScrollLayout.ScrollForward(ctx, 50)
if err != nil {
s.Fatal("Failed to scroll forward: ", err)
}
if !scrollForwdInfo {
s.Log("App page can not be scrolled forward anymore")
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// MouseClick func verifies mouse click work successfully in the app without crash or ANR.
func MouseClick(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
var (
xCoordinate int
yCoordinate int
)
tabletModeEnabled, err := ash.TabletModeEnabled(ctx, tconn)
if err != nil {
s.Fatal("Failed to get tablet mode: ", err)
}
if tabletModeEnabled {
s.Log("Device is in tablet mode. Skipping test")
return
}
checkUIElement := d.Object(ui.PackageName(appPkgName), ui.Clickable(true), ui.Focusable(true), ui.Enabled(true))
if err := checkUIElement.WaitForExists(ctx, DefaultUITimeout); err != nil {
s.Log("checkUIElement doesn't exist and skipped mouse click: ", err)
return
}
s.Log("checkUIElement does exists")
if uiElementBounds, err := checkUIElement.GetBounds(ctx); err != nil {
s.Log("Failed to get uiElementBounds and skipped mouse click : ", err)
} else {
s.Log("uiElementBounds: ", uiElementBounds)
xCoordinate = uiElementBounds.Left
s.Log("Xcoordinate: ", xCoordinate)
yCoordinate = uiElementBounds.Top
s.Log("Ycoordinate: ", yCoordinate)
// To perform mouse click.
out, err := a.Command(ctx, "input", "mouse", "tap", strconv.Itoa(xCoordinate), strconv.Itoa(yCoordinate)).Output(testexec.DumpLogOnError)
if err != nil {
s.Fatal("Failed to perform mouse click: ", err)
} else {
s.Log("Performed mouse click: ", string(out))
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
}
// OrientationSize Test verifies orientation size of the app after launch.
func OrientationSize(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
const (
blackBars = "Black bars observed on both sides of an app"
maximizedSize = "Maximized"
phoneSize = "Phone"
tabletSize = "Tablet"
)
t, ok := arc.Type()
if !ok {
s.Fatal("Unable to determine arc type")
}
// If ARC-VM.
if t == arc.VM {
// TODO(b/188816051): Remove ash.TabletModeEnabled(ctx, tconn) if a solution is found for identifying the device in clamshell/ laptop mode using hardware/software dependency.
tabletModeEnabled, err := ash.TabletModeEnabled(ctx, tconn)
if err != nil {
s.Fatal("Failed to get tablet mode: ", err)
}
if tabletModeEnabled {
s.Log("Device is in tablet mode. Skipping test")
return
}
appWidth, appHeight, err := getAppCoordinates(ctx, s, a, d, appPkgName)
if err != nil {
s.Fatal("Failed to get app coordinates: ", err)
}
info, err := d.GetInfo(ctx)
if err != nil {
s.Fatal("Failed to get device display: ", err)
}
deviceDisplayWidth := info.DisplayWidth
deviceDisplayHeight := info.DisplayHeight
windowInfo, err := getAppWindowInfo(ctx, s, a, d, appPkgName)
switch windowInfo {
case "freeform":
if appWidth == deviceDisplayWidth {
testing.ContextLogf(ctx, "Orientation size of an app is %+v and its appWidth %+v appHeight %+v deviceDisplayWidth %+v deviceDisplayHeight %+v ", maximizedSize, appWidth, appHeight, deviceDisplayWidth, deviceDisplayHeight)
} else if appWidth > deviceDisplayWidth/2 && appWidth <= deviceDisplayWidth*3/4 && appWidth != deviceDisplayWidth {
testing.ContextLogf(ctx, "Orientation size of an app is %+v and its appWidth %+v appHeight %+v deviceDisplayWidth %+v deviceDisplayHeight %+v ", tabletSize, appWidth, appHeight, deviceDisplayWidth, deviceDisplayHeight)
} else if appWidth >= deviceDisplayWidth*3/4 && appWidth != deviceDisplayWidth {
testing.ContextLogf(ctx, "Orientation size of an app: %v and its appWidth %+v appHeight %+v deviceDisplayWidth %+v deviceDisplayHeight %+v", tabletSize, appWidth, appHeight, deviceDisplayWidth, deviceDisplayHeight)
} else {
testing.ContextLogf(ctx, "Orientation size of an app is %+v and its appWidth %+v appHeight %+v deviceDisplayWidth %+v deviceDisplayHeight %+v ", phoneSize, appWidth, appHeight, deviceDisplayWidth, deviceDisplayHeight)
}
case "fullscreen":
if appWidth == deviceDisplayWidth {
testing.ContextLogf(ctx, "Orientation size of an app: %v and its appWidth %+v appHeight %+v deviceDisplayWidth %+v deviceDisplayHeight %+v", maximizedSize, appWidth, appHeight, deviceDisplayWidth, deviceDisplayHeight)
} else if appWidth < deviceDisplayWidth && appWidth != deviceDisplayWidth {
testing.ContextLogf(ctx, "appWidth %+v appHeight %+v deviceDisplayWidth %+v deviceDisplayHeight %+v", appWidth, appHeight, deviceDisplayWidth, deviceDisplayHeight)
s.Fatal("Failed to utilize the screen and app is in ", maximizedSize+" size with "+blackBars)
}
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
}
// Largescreenlayout Test verifies if app utilizes large screen after maximizing the app and without crash or ANR.
func Largescreenlayout(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
tabletModeEnabled, err := ash.TabletModeEnabled(ctx, tconn)
if err != nil {
s.Fatal("Failed to get tablet mode: ", err)
}
if !tabletModeEnabled {
s.Log("Setting the window to fullscreen")
if _, err := ash.SetARCAppWindowState(ctx, tconn, appPkgName, ash.WMEventFullscreen); err != nil {
s.Fatal("Failed to set the window to fullscreen: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateFullscreen); err != nil {
s.Fatal("The window is not in fullscreen: ", err)
}
}
appWidth, appHeight, err := getAppCoordinates(ctx, s, a, d, appPkgName)
if err != nil {
s.Fatal("Failed to get app coordinates: ", err)
}
info, err := d.GetInfo(ctx)
if err != nil {
s.Fatal("Failed to get device display: ", err)
}
deviceDisplayWidth := info.DisplayWidth
deviceDisplayHeight := info.DisplayHeight
if appWidth >= deviceDisplayWidth {
testing.ContextLogf(ctx, "App utilizes the device display screen and its appWidth %+v appHeight %+v deviceDisplayWidth %+v deviceDisplayHeight %+v", appWidth, appHeight, deviceDisplayWidth, deviceDisplayHeight)
} else {
testing.ContextLogf(ctx, "appWidth %+v appHeight %+v deviceDisplayWidth %+v deviceDisplayHeight %+v", appWidth, appHeight, deviceDisplayWidth, deviceDisplayHeight)
s.Fatal("Failed to utilize the device display screen and black bars observed on both side of an app")
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// ReOpenWindow Test "close and relaunch the app" and verifies app launch successfully without crash or ANR.
func ReOpenWindow(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
// Create an activity handle.
act, err := arc.NewActivity(a, appPkgName, appActivity)
if err != nil {
s.Fatal("Failed to create new app activity: ", err)
}
defer act.Close()
// Close the app.
if err := a.Command(ctx, "am", "force-stop", appPkgName).Run(testexec.DumpLogOnError); err != nil {
s.Fatal("Failed to stop app before test cases: ", err)
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
// Relaunch the app.
s.Log("Relaunching the app")
if err := act.StartWithDefaultOptions(ctx, tconn); err != nil {
s.Fatal("Failed to restart app: ", err)
}
}
// EscKey Test verifies if app doesn't quit on pressing esc key and without crash or ANR.
func EscKey(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
if err := d.PressKeyCode(ctx, ui.KEYCODE_ESCAPE, 0); err != nil {
s.Fatal("Failed to press KEYCODE_ESCAPE: ", err)
}
if currentAppPkg, err := CurrentAppPackage(ctx, d, s); err != nil {
s.Fatal("Failed to get current app package: ", err)
} else if currentAppPkg != appPkgName && currentAppPkg != "com.google.android.packageinstaller" && currentAppPkg != "com.google.android.gms" && currentAppPkg != "com.google.android.permissioncontroller" {
s.Fatalf("App quits on pressing esc key: package(expected: %s, actual: %s)", appPkgName, currentAppPkg)
}
}
// SplitScreen Test verifies if app supports split screen and check if app performs split screen without crash or ANR.
func SplitScreen(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
displayInfo, err := display.GetInternalInfo(ctx, tconn)
if err != nil {
s.Fatal("Failed to get the internal display info: ", err)
}
s.Logf("displayInfo:%+v", displayInfo.HasTouchSupport)
if !displayInfo.HasTouchSupport {
s.Log("Device doesn't support touchscreen. Skipping test")
return
}
windowInfo, err := ash.GetARCAppWindowInfo(ctx, tconn, appPkgName)
if err != nil {
s.Fatal("Failed to get window info: ", err)
}
s.Logf("App Resize info, info.CanResize %+v", windowInfo.CanResize)
if !windowInfo.CanResize {
s.Log("App doesn't support split screen. Skipping test")
return
}
orientation, err := display.GetOrientation(ctx, tconn)
if err != nil {
s.Fatal("Failed to obtain the orientation info: ", err)
}
s.Logf("Orientation of primary window, orientation.Type %+v", orientation.Type)
//TODO(b/178401320): Remove this if a proper solution is found to perform split screen on portrait oriented apps.
if orientation.Type == display.OrientationPortraitPrimary || orientation.Type == display.OrientationPortraitSecondary {
s.Log("App is in portrait orientation. Skipping test")
return
}
tew, err := input.Touchscreen(ctx)
if err != nil {
s.Fatal("Failed to access to the touch screen: ", err)
}
defer tew.Close()
// Ensure device display is in landscape orientation so that app window snap on
// the left and right.
orientation, err = display.GetOrientation(ctx, tconn)
if err != nil {
s.Fatal("Failed to obtain the orientation info: ", err)
}
s.Logf("orientation.Angle %+v", orientation.Angle)
rotation := -orientation.Angle
tew.SetRotation(rotation)
stw, err := tew.NewSingleTouchWriter()
if err != nil {
s.Fatal("Failed to create a single touch writer: ", err)
}
defer stw.Close()
if err := ash.SetOverviewModeAndWait(ctx, tconn, true); err != nil {
s.Fatal("Failed to enter overview: ", err)
}
const target = 0
if err := dragToSnapFirstOverviewWindow(ctx, s, tconn, tew, stw, target); err != nil {
s.Fatal("Failed to drag window from overview and snap left: ", err)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, appPkgName, ash.WindowStateLeftSnapped); err != nil {
s.Fatal("Failed to wait until window state change to left: ", err)
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
// StylusClick func verifies if stylus click works properly in the app without crash or ANR.
func StylusClick(ctx context.Context, s *testing.State, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, appPkgName, appActivity string) {
var (
xCoordinate int
yCoordinate int
)
info, err := display.GetInternalInfo(ctx, tconn)
if err != nil {
s.Fatal("Failed to get the internal display info: ", err)
}
if !info.HasTouchSupport {
s.Log("Device does not have touch support. Skipping the test")
return
}
checkUIElement := d.Object(ui.Clickable(true), ui.Focusable(true), ui.Enabled(true))
if err := checkUIElement.WaitForExists(ctx, DefaultUITimeout); err != nil {
s.Log("checkUIElement doesn't exist and skipped stylus click: ", err)
return
}
s.Log("checkUIElement does exists")
if uiElementBounds, err := checkUIElement.GetBounds(ctx); err != nil {
s.Log("Failed to get uiElementBounds and skipped stylus click : ", err)
} else {
s.Log("uiElementBounds: ", uiElementBounds)
xCoordinate = uiElementBounds.Left
s.Log("Xcoordinate: ", xCoordinate)
yCoordinate = uiElementBounds.Top
s.Log("Ycoordinate: ", yCoordinate)
// TODO (b/188840879): Remove a.Command(ctx, "input", "stylus", "tap", XCoordinate, YCoordinate) if proper solution is found for it.
// To perform stylus click.
out, err := a.Command(ctx, "input", "stylus", "tap", strconv.Itoa(xCoordinate), strconv.Itoa(yCoordinate)).Output(testexec.DumpLogOnError)
if err != nil {
s.Fatal("Failed to perform stylus click: ", err)
} else {
s.Log("Performed stylus click: ", string(out))
}
DetectAndHandleCloseCrashOrAppNotResponding(ctx, s, d)
}
}
// DetectAndHandleCloseCrashOrAppNotResponding func to handle Crash or ANR.
func DetectAndHandleCloseCrashOrAppNotResponding(ctx context.Context, s *testing.State, d *ui.Device) {
const (
alertTitleCanNotDownloadText = "Can't download app"
alertTitleHasStoppedText = "has stopped"
alertTitleKeepsStoppingText = "keeps stopping"
alertTitleNotRespondingText = "isn't responding"
alertTitleOpenAppAgainText = "Open app again"
shortUITimeout = 2 * time.Second
)
// Check for isn't responding alert title
alertTitleCanNotDownload := d.Object(ui.TextContains(alertTitleCanNotDownloadText))
alertTitleHasStopped := d.Object(ui.TextContains(alertTitleHasStoppedText))
alertTitleKeepsStopping := d.Object(ui.TextContains(alertTitleKeepsStoppingText))
alertTitleNotResponding := d.Object(ui.TextContains(alertTitleNotRespondingText))
alertTitleOpenAppAgain := d.Object(ui.TextContains(alertTitleOpenAppAgainText))
if err := testing.Poll(ctx, func(ctx context.Context) error {
if err := alertTitleNotResponding.Exists(ctx); err == nil {
return testing.PollBreak(errors.Wrap(err, "NotResponding does exist"))
}
if err := alertTitleHasStopped.Exists(ctx); err == nil {
return testing.PollBreak(errors.Wrap(err, "HasStopped does exist"))
}
if err := alertTitleKeepsStopping.Exists(ctx); err == nil {
return testing.PollBreak(errors.Wrap(err, "KeepsStopping does exist"))
}
if err := alertTitleOpenAppAgain.Exists(ctx); err == nil {
return testing.PollBreak(errors.Wrap(err, "OpenAppAgain does exist"))
}
if err := alertTitleCanNotDownload.Exists(ctx); err == nil {
return testing.PollBreak(errors.Wrap(err, "CanNotDownload does exist"))
}
return errors.New("waiting for crash")
}, &testing.PollOptions{Timeout: shortUITimeout}); err != nil && !strings.Contains(err.Error(), "waiting for crash") {
s.Error("The application crashed: ", err)
path := filepath.Join(s.OutDir(), "app-crash-or-anr.png")
if err := screenshot.Capture(ctx, path); err != nil {
s.Log("Screenshot for app-crash-or-anr.png: ", err)
}
handleCrashOrANRDialog(ctx, s, d)
}
}
// handleCrashOrANRDialog func will handle the crash or ANR dialog box
func handleCrashOrANRDialog(ctx context.Context, s *testing.State, d *ui.Device) {
const (
closeAppText = "Close"
okText = "ok"
OpenAppAgainText = "Open app again"
)
// Click on open app again
openAppAgainButton := d.Object(ui.ClassName(AndroidButtonClassName), ui.TextContains(OpenAppAgainText))
if err := openAppAgainButton.WaitForExists(ctx, DefaultUITimeout); err != nil {
s.Error("OpenAppAgainButton doesn't exist: ", err)
} else if err := openAppAgainButton.Click(ctx); err != nil {
s.Fatal("Failed to click on OpenAppAgainButton: ", err)
}
// Click on close app
closeButton := d.Object(ui.ClassName(AndroidButtonClassName), ui.TextContains(closeAppText))
if err := closeButton.WaitForExists(ctx, DefaultUITimeout); err != nil {
s.Error("CloseButton doesn't exist: ", err)
} else if err := closeButton.Click(ctx); err != nil {
s.Fatal("Failed to click on CloseButton: ", err)
}
// Click on ok button
okButton := d.Object(ui.ClassName(AndroidButtonClassName), ui.TextContains(okText))
if err := okButton.WaitForExists(ctx, DefaultUITimeout); err != nil {
s.Error("OkButton doesn't exist: ", err)
} else if err := okButton.Click(ctx); err != nil {
s.Fatal("Failed to click on OkButton: ", err)
}
}
func restartApp(ctx context.Context, d *ui.Device, appPkgName string) error {
const restartButtonResourceID = "android:id/button1"
// Click on restart button.
testing.ContextLog(ctx, "Attempting restart")
restartButton := d.Object(ui.ResourceID(restartButtonResourceID))
if err := restartButton.WaitForExists(ctx, LongUITimeout); err != nil {
return errors.Wrap(err, "restart button does not exist")
}
if err := restartButton.Click(ctx); err != nil {
return errors.Wrap(err, "failed to click on restart button")
}
if _, err := d.WaitForWindowUpdate(ctx, appPkgName, LongUITimeout); err != nil {
return errors.Wrap(err, "failed to wait for window to update")
}
return nil
}
// isNApp func to check if it is an N or pre-N app
func isNApp(ctx context.Context, d *ui.Device) bool {
info, err := d.GetInfo(ctx)
if err != nil {
testing.ContextLog(ctx, "Failed to get app sdk version: ", err)
return false
}
testing.ContextLogf(ctx, "App sdk version %+v", info.SDKInt)
return info.SDKInt >= 24
}
// CurrentAppPackage func to get info on current package name.
func CurrentAppPackage(ctx context.Context, d *ui.Device, s *testing.State) (string, error) {
// Wait for app to launch.
d.WaitForIdle(ctx, ShortUITimeout)
// Check if UiAutomator server is up.
isALive := d.Alive(ctx)
if !isALive {
s.Fatal("UiAutomator server isn't responding: ", isALive)
}
info, err := d.GetInfo(ctx)
if err != nil {
return "", err
}
return info.CurrentPackagename, nil
}
// processMonkeyOutput func parse the output logs of monkey test.
func processMonkeyOutput(output string) error {
applicationNotRespondingErrorMsg := "Application is not responding:"
anrErrorMessage := "ANR"
monkeyTestAbortedErrorMessage := "Monkey aborted due to error."
monkeyTestAbortedErrorMsg := "monkey aborted."
NotRespondingErrorMessage := "NOT RESPONDING:"
for _, line := range strings.Split(output, "\n") {
if strings.Contains(line, monkeyTestAbortedErrorMessage) ||
strings.Contains(line, monkeyTestAbortedErrorMsg) ||
strings.Contains(line, applicationNotRespondingErrorMsg) ||
strings.Contains(line, anrErrorMessage) ||
strings.Contains(line, NotRespondingErrorMessage) {
return errors.New("monkey test aborted: " + line)
}
}
return nil
}
// HandleDialogBoxes func will handle the dialog box
func HandleDialogBoxes(ctx context.Context, s *testing.State, d *ui.Device, appPkgName string) {
const (
allowText = "ALLOW"
agreeText = "Agree"
continueText = "Continue"
cancelText = "Cancel"
gotItText = "Got it"
notNowText = "NOT NOW"
okText = "OK"
okayText = "OKAY"
skipText = "Skip"
whileUsingThisAppButtonText = "WHILE USING THE APP"
)
allowButton := d.Object(ui.TextMatches("(?i)" + allowText))
appverifer := d.Object(ui.PackageName(appPkgName))
agreeButton := d.Object(ui.TextMatches("(?i)" + agreeText))
continueButton := d.Object(ui.TextMatches("(?i)" + continueText))
gotItButton := d.Object(ui.TextMatches("(?i)" + gotItText))
notNowButton := d.Object(ui.TextMatches("(?i)" + notNowText))
okButton := d.Object(ui.TextMatches("(?i)" + okText))
okayButton := d.Object(ui.TextMatches("(?i)" + okayText))
skipButton := d.Object(ui.TextMatches("(?i)" + skipText))
whileUsingThisAppButton := d.Object(ui.TextMatches("(?i)" + whileUsingThisAppButtonText))
cancelButton := d.Object(ui.TextMatches("(?i)" + cancelText))
if err := testing.Poll(ctx, func(ctx context.Context) error {
if err := allowButton.Exists(ctx); err == nil {
s.Log("Click on allowButton")
allowButton.Click(ctx)
}
if err := whileUsingThisAppButton.Exists(ctx); err == nil {
s.Log("Click on whileUsingThisApp")
whileUsingThisAppButton.Click(ctx)
}
if err := notNowButton.Exists(ctx); err == nil {
s.Log("Click on notNowButton")
notNowButton.Click(ctx)
}
if err := agreeButton.Exists(ctx); err == nil {
s.Log("Click on agreeButton")
agreeButton.Click(ctx)
}
if err := okButton.Exists(ctx); err == nil {
s.Log("Click on okButton")
okButton.Click(ctx)
}
if err := okayButton.Exists(ctx); err == nil {
s.Log("Click on okayButton")
okayButton.Click(ctx)
}
if err := skipButton.Exists(ctx); err == nil {
s.Log("Click on skipButton")
skipButton.Click(ctx)
}
if err := continueButton.Exists(ctx); err == nil {
s.Log("Click on continueButton")
continueButton.Click(ctx)
}
if err := gotItButton.Exists(ctx); err == nil {
s.Log("Click on gotItButton")
gotItButton.Click(ctx)
}
if err := cancelButton.Exists(ctx); err == nil {
s.Log("Click on cancelButton")
cancelButton.Click(ctx)
}
return appverifer.Exists(ctx)
}, &testing.PollOptions{Timeout: LongUITimeout}); err != nil {
s.Error("appPkgName doesn't exist: ", err)
}
}
// getAppCoordinates func provides coordinates of the app.
func getAppCoordinates(ctx context.Context, s *testing.State, a *arc.ARC, d *ui.Device, appPkgName string) (int, int, error) {
var (
xCoordinate int
yCoordinate int
)
// To get app activities.
out, err := a.Command(ctx, "am", "stack", "list").Output()
if err != nil {
s.Fatal("Failed to get stack list: ", err)
}
output := string(out)
coordinatePrefix := "bounds="
splitOutput := strings.Split(output, "\n")
for splitLine := range splitOutput {
if strings.Contains(splitOutput[splitLine], appPkgName) {
splitCoordinate := strings.Split(splitOutput[splitLine], " ")
for CoordinateInfo := range splitCoordinate {
if strings.Contains(splitCoordinate[CoordinateInfo], coordinatePrefix) {
s.Log("Coordinates: ", splitCoordinate[CoordinateInfo])
x1coordinateWithOutTrim := strings.Split(splitCoordinate[CoordinateInfo], ",")[0]
x1Coordinate := strings.Split(x1coordinateWithOutTrim, "[")[1]
x1CoordinateValue, err := strconv.Atoi(x1Coordinate)
if err != nil {
s.Fatal("Failed to get x1CoordinateValue: ", err)
}
y1coordinateWithOutTrim := strings.Split(splitCoordinate[CoordinateInfo], ",")[1]
y1Coordinate := strings.Split(y1coordinateWithOutTrim, "]")[0]
y1CoordinateValue, err := strconv.Atoi(y1Coordinate)
if err != nil {
s.Fatal("Failed to get y1CoordinateValue: ", err)
}
x2coordinateWithOutTrim := strings.Split(splitCoordinate[CoordinateInfo], ",")[1]
x2Coordinate := strings.Split(x2coordinateWithOutTrim, "[")[1]
x2CoordinateValue, err := strconv.Atoi(x2Coordinate)
if err != nil {
s.Fatal("Failed to get x2CoordinateValue: ", err)
}
y2coordinateWithOutTrim := strings.Split(splitCoordinate[CoordinateInfo], ",")[2]
y2Coordinate := strings.Split(y2coordinateWithOutTrim, "]")[0]
y2CoordinateValue, err := strconv.Atoi(y2Coordinate)
if err != nil {
s.Fatal("Failed to get y2CoordinateValue: ", err)
}
xCoordinate = x2CoordinateValue - x1CoordinateValue
yCoordinate = y2CoordinateValue - y1CoordinateValue
break
}
}
}
}
return xCoordinate, yCoordinate, err
}
// getAppWindowInfo func provides coordinates of the app.
func getAppWindowInfo(ctx context.Context, s *testing.State, a *arc.ARC, d *ui.Device, appPkgName string) (string, error) {
var windowInfo string
// To get app activities.
out, err := a.Command(ctx, "dumpsys", "activity", "activities").Output()
if err != nil {
s.Fatal("Failed to get dumpsys activity activities: ", err)
}
output := string(out)
TaskRecordPrefix := "* Task"
windowStatePrefixForARCP := "WindowState"
windowStatePrefixForARCR := "mode="
var arcpFound bool
var arcrFound bool
splitOutput := strings.Split(output, "\n")
for splitLine := range splitOutput {
if strings.Contains(splitOutput[splitLine], appPkgName) && strings.Contains(splitOutput[splitLine], TaskRecordPrefix) {
splitWindowInfo := strings.Split(splitOutput[splitLine], " ")
for appWindowInfo := range splitWindowInfo {
if !arcpFound && strings.Contains(splitWindowInfo[appWindowInfo], windowStatePrefixForARCP) {
s.Log("windowInfo raw message ARCP: ", splitWindowInfo[appWindowInfo])
windowInfoForARCP := strings.Split(splitWindowInfo[appWindowInfo], "{")[1]
s.Log("windowInfoARCP: ", windowInfoForARCP)
windowInfo = windowInfoForARCP
arcpFound = true
break
}
if !arcrFound && strings.Contains(splitWindowInfo[appWindowInfo], windowStatePrefixForARCR) {
s.Log("windowInfo raw message ARCR: ", splitWindowInfo[appWindowInfo])
windowInfoForARCR := strings.Split(splitWindowInfo[appWindowInfo], "=")[1]
s.Log("windowInfoARCR: ", windowInfoForARCR)
windowInfo = windowInfoForARCR
arcrFound = true
break
}
}
}
}
return windowInfo, err
}
// dragToSnapFirstOverviewWindow finds the first window in overview, and drags
// to snap it. This function assumes that overview is already active.
func dragToSnapFirstOverviewWindow(ctx context.Context, s *testing.State, tconn *chrome.TestConn, tew *input.TouchscreenEventWriter, stw *input.SingleTouchEventWriter, targetX input.TouchCoord) error {
info, err := display.GetInternalInfo(ctx, tconn)
if err != nil {
return errors.Wrap(err, "failed to get the internal display info")
}
tcc := tew.NewTouchCoordConverter(info.Bounds.Size())
w, err := ash.FindFirstWindowInOverview(ctx, tconn)
if err != nil {
return errors.Wrap(err, "failed to find window in overview grid")
}
centerX, centerY := tcc.ConvertLocation(w.OverviewInfo.Bounds.CenterPoint())
if err := stw.LongPressAt(ctx, centerX, centerY); err != nil {
return errors.Wrap(err, "failed to long-press to start dragging landscape window")
}
s.Logf("Long pressed at (%v, %v)", centerX, centerY)
if err := stw.Swipe(ctx, centerX, centerY, targetX, tew.Height()/2, time.Second); err != nil {
return errors.Wrap(err, "failed to swipe for snapping window to left")
}
s.Logf("Swiped from: (%v, %v) to ( %v, %v, %v)", centerX, centerY, targetX, tew.Height()/2, time.Second)
if err := stw.End(); err != nil {
return errors.Wrap(err, "failed to end swipe")
}
return nil
}
// GetAppVersion provides info on app version.
func GetAppVersion(ctx context.Context, s *testing.State, a *arc.ARC, d *ui.Device, appPkgName string) (string, error) {
var versionNameAfterSplit string
// To get app version name.
out, err := a.Command(ctx, "dumpsys", "package", appPkgName).Output()
if err != nil {
s.Log(err, "could not get dumpsys package")
} else {
versionNamePrefix := "versionName="
output := string(out)
splitOutput := strings.Split(output, "\n")
for splitLine := range splitOutput {
if strings.Contains(splitOutput[splitLine], versionNamePrefix) {
versionNameAfterSplit = strings.Split(splitOutput[splitLine], "=")[1]
s.Log("Version name of ", appPkgName, " is: ", versionNameAfterSplit)
break
}
}
}
return versionNameAfterSplit, err
}
// TabletOnlyModels is a list of tablet only models to be skipped from clamshell mode runs.
var TabletOnlyModels = []string{
"dru",
"krane",
}
// ClamshellOnlyModels is a list of clamshell only models to be skipped from tablet mode runs.
var ClamshellOnlyModels = []string{
"sarien",
"elemi",
"berknip",
"dratini",
// grunt:
"careena",
"kasumi",
"treeya",
"grunt",
"barla",
"aleena",
"liara",
"nuwani",
// octopus:
"bluebird",
"apel",
"blooglet",
"blorb",
"bobba",
"casta",
"dorp",
"droid",
"fleex",
"foob",
"garfour",
"garg",
"laser14",
"lick",
"mimrock",
"nospike",
"orbatrix",
"phaser",
"sparky",
}