blob: 0456859725880e582d94b6ab7397f77628c00776 [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 arc
import (
"context"
"fmt"
"image/color"
"math"
"time"
"chromiumos/tast/common/android/adb"
"chromiumos/tast/ctxutil"
"chromiumos/tast/errors"
"chromiumos/tast/local/android/ui"
"chromiumos/tast/local/arc"
"chromiumos/tast/local/bundles/cros/arc/wm"
"chromiumos/tast/local/chrome"
"chromiumos/tast/local/chrome/ash"
"chromiumos/tast/local/chrome/display"
chromeui "chromiumos/tast/local/chrome/ui"
"chromiumos/tast/local/colorcmp"
"chromiumos/tast/local/input"
"chromiumos/tast/local/media/imgcmp"
"chromiumos/tast/local/screenshot"
"chromiumos/tast/testing"
)
const (
resizeLockTestPkgName = "org.chromium.arc.testapp.resizelock"
resizeLockApkName = "ArcResizeLockTest.apk"
resizeLockMainActivityName = "org.chromium.arc.testapp.resizelock.MainActivity"
resizeLockUnresizableUnspecifiedActivityName = "org.chromium.arc.testapp.resizelock.UnresizableUnspecifiedActivity"
resizeLockUnresizablePortraitActivityName = "org.chromium.arc.testapp.resizelock.UnresizablePortraitActivity"
resizeLockResizableUnspecifiedMaximizedActivityName = "org.chromium.arc.testapp.resizelock.ResizableUnspecifiedMaximizedActivity"
// Verifying splash visbility requires 3 different resize-locked apps.
resizeLock2PkgName = "org.chromium.arc.testapp.resizelock2"
resizeLock3PkgName = "org.chromium.arc.testapp.resizelock3"
resizeLock2ApkName = "ArcResizeLockTest2.apk"
resizeLock3ApkName = "ArcResizeLockTest3.apk"
// 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"
shelfIconClassName = "ash/ShelfAppButton"
menuItemViewClassName = "MenuItemView"
// A11y names are available for some UI elements
splashCloseButtonName = "Got it"
confirmButtonName = "Allow"
cancelButtonName = "Cancel"
appManagementSettingToggleName = "Preset window sizes"
appInfoMenuItemViewName = "App info"
closeMenuItemViewName = "Close"
// Used to identify the shelf icon of interest.
resizeLockAppName = "ArcResizeLockTest"
settingsAppName = "Settings"
// Used in test cases where screenshots are taken.
pixelColorDiffMargin = 5
clientContentColorPixelPercentThreshold = 95
borderColorPixelCountThreshold = 1000
borderWidthPX = 6
)
// Represents the size of a window.
type orientation int
const (
phoneOrientation orientation = iota
tabletOrientation
maximizedOrientation
)
func (mode orientation) String() string {
switch mode {
case phoneOrientation:
return "phone"
case tabletOrientation:
return "tablet"
case maximizedOrientation:
return "maximized"
default:
return "unknown"
}
}
// Represents the high-level state of the app from the resize-lock feature's perspective.
type resizeLockMode int
const (
phoneResizeLockMode resizeLockMode = iota
tabletResizeLockMode
resizableResizeLockMode
nonEligibleResizeLockMode
)
func (mode resizeLockMode) String() string {
switch mode {
case phoneResizeLockMode:
return phoneButtonName
case tabletResizeLockMode:
return tabletButtonName
case resizableResizeLockMode:
return resizableButtonName
default:
return ""
}
}
// Represents the expected behavior and action to take for the resizability confirmation dialog.
type confirmationDialogAction int
const (
dialogActionNoDialog confirmationDialogAction = iota
dialogActionCancel
dialogActionConfirm
dialogActionConfirmWithDoNotAskMeAgainChecked
)
// Represents how to interact with UI.
type inputMethodType int
const (
inputMethodClick inputMethodType = iota
inputMethodKeyEvent
)
func (mode inputMethodType) String() string {
switch mode {
case inputMethodClick:
return "click"
case inputMethodKeyEvent:
return "keyboard"
default:
return "unknown"
}
}
type resizeLockTestFunc func(context.Context, *chrome.TestConn, *arc.ARC, *ui.Device, *chrome.Chrome, *input.KeyboardEventWriter) error
type resizeLockTestCase struct {
name string
fn resizeLockTestFunc
}
// The order of the test cases matters since some persistent chrome-side properties are tested.
// - "Splash" must come first as any launch of the apps affect the test case.
// - CUJ must come next as it tests the behavior of resize confirmation dialog, and once tested the dialog isn't shown in other tests.
var testCases = []resizeLockTestCase{
{
name: "Splash",
fn: testSplash,
},
{
name: "Resize Locked App - CUJ",
fn: testResizeLockedAppCUJ,
},
{
name: "Resize Locked App - Fully Locked",
fn: testFullyLockedApp,
},
{
name: "O4C App",
fn: testO4CApp,
},
{
name: "Unresizable Maximized App",
fn: testUnresizableMaximizedApp,
},
{
name: "Resizable Maximized App",
fn: testResizableMaximizedApp,
},
{
name: "Install from outside of PlayStore",
fn: testAppFromOutsideOfPlayStore,
},
}
func init() {
testing.AddTest(&testing.Test{
Func: ResizeLock,
Desc: "Checks that ARC++ Resize Lock works as expected",
Contacts: []string{"takise@chromium.org", "toshikikikuchi@chromium.org", "arc-framework+tast@google.com"},
Attr: []string{"group:mainline", "informational"},
SoftwareDeps: []string{"chrome", "android_vm"},
Timeout: 5 * time.Minute,
})
}
func ResizeLock(ctx context.Context, s *testing.State) {
// Ensure to enable the finch flag.
cr, err := chrome.New(ctx, chrome.ARCEnabled(), chrome.ExtraArgs("--enable-features=ArcResizeLock"))
if err != nil {
s.Fatal("Failed to start Chrome: ", err)
}
defer cr.Close(ctx)
tconn, err := cr.TestAPIConn(ctx)
if err != nil {
s.Fatal("Failed to create Test API connection: ", err)
}
a, err := arc.New(ctx, s.OutDir())
if err != nil {
s.Fatal("Failed to start ARC: ", err)
}
defer a.Close(ctx)
dev, err := a.NewUIDevice(ctx)
if err != nil {
s.Fatal("Failed to initialize UI Automator: ", err)
}
defer dev.Close(ctx)
dispInfo, err := display.GetPrimaryInfo(ctx, tconn)
if err != nil {
s.Fatal("Failed to get primary display info: ", err)
}
origShelfAlignment, err := ash.GetShelfAlignment(ctx, tconn, dispInfo.ID)
if err != nil {
s.Fatal("Failed to get shelf alignment: ", err)
}
if err := ash.SetShelfAlignment(ctx, tconn, dispInfo.ID, ash.ShelfAlignmentBottom); err != nil {
s.Fatal("Failed to set shelf alignment to Bottom: ", err)
}
// Be nice and restore shelf alignment to its original state on exit.
defer ash.SetShelfAlignment(ctx, tconn, dispInfo.ID, origShelfAlignment)
origShelfBehavior, err := ash.GetShelfBehavior(ctx, tconn, dispInfo.ID)
if err != nil {
s.Fatal("Failed to get shelf behavior: ", err)
}
if err := ash.SetShelfBehavior(ctx, tconn, dispInfo.ID, ash.ShelfBehaviorNeverAutoHide); err != nil {
s.Fatal("Failed to set shelf behavior to Never Auto Hide: ", err)
}
// Be nice and restore shelf behavior to its original state on exit.
defer ash.SetShelfBehavior(ctx, tconn, dispInfo.ID, origShelfBehavior)
tabletModeStatus, err := ash.TabletModeEnabled(ctx, tconn)
if err != nil {
s.Fatal("Failed to get tablet mode: ", err)
}
if err := ash.SetTabletModeEnabled(ctx, tconn, false); err != nil {
s.Fatal("Failed to set device to clamshell mode: ", err)
}
// Be nice and restore tablet mode to its original state on exit.
defer ash.SetTabletModeEnabled(ctx, tconn, tabletModeStatus)
keyboard, err := input.Keyboard(ctx)
if err != nil {
s.Fatal("Failed to create a keyboard: ", err)
}
defer keyboard.Close()
ctxDefer := ctx
ctx, cancel := ctxutil.Shorten(ctx, 5*time.Second)
defer cancel()
for _, app := range []struct {
apkName string
pkgName string
fromPlayStore bool
}{
{wm.APKNameArcWMTestApp23, wm.Pkg23, false},
{wm.APKNameArcWMTestApp24, wm.Pkg24, true},
{wm.APKNameArcWMTestApp24Maximized, wm.Pkg24InMaximizedList, true},
{resizeLockApkName, resizeLockTestPkgName, true},
{resizeLock2ApkName, resizeLock2PkgName, true},
{resizeLock3ApkName, resizeLock3PkgName, true},
} {
if app.fromPlayStore {
if err := a.Install(ctx, arc.APKPath(app.apkName), adb.InstallOptionFromPlayStore); err != nil {
s.Fatal("Failed to install app from PlayStore: ", err)
}
} else {
if err := a.Install(ctx, arc.APKPath(app.apkName)); err != nil {
s.Fatal("Failed to install app from outside of PlayStore: ", err)
}
}
defer a.Uninstall(ctxDefer, app.pkgName)
}
// Place a maximized activity below to ensure that the display has a white background.
// This is necessary because currently checking the visibility of the translucent window border relies on taking a screenshot.
// The WM23 app is used here as the WM24 app is used for testing O4C (Optimized for Chromebook).
activity, err := arc.NewActivity(a, wm.Pkg23, wm.NonResizableLandscapeActivity)
if err != nil {
s.Fatal("Failed to create the WM23 unresizable landscape activity: ", err)
}
defer activity.Close()
if err := activity.Start(ctx, tconn); err != nil {
s.Fatal("Failed to start the WM23 unresizable landscape activity: ", err)
}
defer activity.Stop(ctx, tconn)
for _, test := range testCases {
s.Logf("Running test %q", test.name)
if err := test.fn(ctx, tconn, a, dev, cr, keyboard); err != nil {
path := fmt.Sprintf("%s/screenshot-resize-lock-failed-test-%s.png", s.OutDir(), test.name)
if err := screenshot.CaptureChrome(ctx, cr, path); err != nil {
s.Log("Failed to capture screenshot: ", err)
}
s.Errorf("Failed to run test %s: %v", test.name, err)
}
}
}
// testO4CApp verifies that an O4C app is not resize locked even if it's newly-installed.
func testO4CApp(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, keyboard *input.KeyboardEventWriter) error {
return testNonResizeLocked(ctx, tconn, a, d, cr, keyboard, wm.Pkg24, wm.APKNameArcWMTestApp24, wm.ResizableUnspecifiedActivity, false /* checkRestoreMaximize */)
}
// testUnresizableMaximizedApp verifies that an unresizable, maximized app is not resize locked even if it's newly-installed.
func testUnresizableMaximizedApp(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, keyboard *input.KeyboardEventWriter) error {
return testNonResizeLocked(ctx, tconn, a, d, cr, keyboard, resizeLockTestPkgName, resizeLockApkName, resizeLockUnresizableUnspecifiedActivityName, false /* checkRestoreMaximize */)
}
// testResizableMaximizedApp verifies that a resizable, maximized app is not resize locked even if it's newly-installed.
func testResizableMaximizedApp(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, keyboard *input.KeyboardEventWriter) error {
return testNonResizeLocked(ctx, tconn, a, d, cr, keyboard, resizeLockTestPkgName, resizeLockApkName, resizeLockResizableUnspecifiedMaximizedActivityName, true /* checkRestoreMaximize */)
}
// testAppFromOutsideOfPlayStore verifies that an resize-lock-eligible app installed from outside of PlayStore is not resize locked even if it's newly-installed.
func testAppFromOutsideOfPlayStore(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, keyboard *input.KeyboardEventWriter) error {
return testNonResizeLocked(ctx, tconn, a, d, cr, keyboard, wm.Pkg24InMaximizedList, wm.APKNameArcWMTestApp24Maximized, wm.ResizableUnspecifiedActivity, false /* checkRestoreMaximize */)
}
// testNonResizeLocked verifies that the given app is not resize locked.
func testNonResizeLocked(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, keyboard *input.KeyboardEventWriter, packageName, apkName, activityName string, checkRestoreMaximize bool) error {
activity, err := arc.NewActivity(a, packageName, activityName)
if err != nil {
return errors.Wrapf(err, "failed to create %s", activityName)
}
defer activity.Close()
if err := activity.Start(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to start %s", activityName)
}
defer activity.Stop(ctx, tconn)
// Verify the initial state of the given non-resize-locked app.
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, nonEligibleResizeLockMode, false /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify the resize lock state of %s", activityName)
}
if checkRestoreMaximize {
// Restore the app and verify the app is resize-locked.
if _, err := ash.SetARCAppWindowState(ctx, tconn, packageName, ash.WMEventNormal); err != nil {
return errors.Wrapf(err, "failed to restore %s", activityName)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, packageName, ash.WindowStateNormal); err != nil {
return errors.Wrapf(err, "failed to wait for %s to be restored", activityName)
}
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, tabletResizeLockMode, false /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify resize lock state of %s", activityName)
}
// Make the app resizable to enable maximization.
if err := toggleResizeLockMode(ctx, tconn, a, d, cr, activity, tabletResizeLockMode, resizableResizeLockMode, dialogActionNoDialog, inputMethodClick, keyboard); err != nil {
return errors.Wrapf(err, "failed to change the resize lock mode of %s from tablet to resizable", apkName)
}
// Maximize the app and verify the app is not resize-locked.
if _, err := ash.SetARCAppWindowState(ctx, tconn, packageName, ash.WMEventMaximize); err != nil {
return errors.Wrapf(err, "failed to maximize %s", activityName)
}
if err := ash.WaitForARCAppWindowState(ctx, tconn, packageName, ash.WindowStateMaximized); err != nil {
return errors.Wrapf(err, "failed to wait for %s to be maximized", activityName)
}
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, nonEligibleResizeLockMode, false /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify resize lock state of %s", activityName)
}
}
return nil
}
// testFullyLockedApp verifies that the given app is fully locked.
func testFullyLockedApp(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, keyboard *input.KeyboardEventWriter) error {
activity, err := arc.NewActivity(a, resizeLockTestPkgName, resizeLockUnresizablePortraitActivityName)
if err != nil {
return errors.Wrapf(err, "failed to create %s", resizeLockUnresizablePortraitActivityName)
}
defer activity.Close()
if err := activity.Start(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to start %s", resizeLockUnresizablePortraitActivityName)
}
defer activity.Stop(ctx, tconn)
// Verify the initial state of the given non-resize-locked app.
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, phoneResizeLockMode, false /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify the resize lock state of %s", resizeLockUnresizablePortraitActivityName)
}
// The compat-mode button of a fully-locked app is disabled.
icon, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{ClassName: centerButtonClassName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the compat-mode button")
}
defer icon.Release(ctx)
if err := icon.LeftClick(ctx); err != nil {
return errors.Wrap(err, "failed to click on the compat-mode button")
}
// Need some sleep here as we verify that nothing changes.
if err := testing.Sleep(ctx, time.Second); err != nil {
return errors.Wrap(err, "failed to sleep after clicking on the compat-mode button")
}
if err := checkVisibility(ctx, tconn, bubbleDialogClassName, false); err != nil {
return errors.Wrapf(err, "failed to verify the visibility of the compat-mode menu of %s", activity.ActivityName())
}
// The setting toggle of a fully-locked app is invisible in the app-management page.
if err := openAppManagementSetting(ctx, tconn, resizeLockAppName); err != nil {
return errors.Wrapf(err, "failed to open the app management page of %s", resizeLockAppName)
}
defer closeAppManagementSetting(ctx, tconn)
return chromeui.WaitUntilGone(ctx, tconn, chromeui.FindParams{Name: appManagementSettingToggleName}, 10*time.Second)
}
// testSplash installs 3 different resize-locked app, launches an activity twice, and verifies that the splash screen works as expected.
// The spec of visibility: The splash must be shown twice per user, once per app at most.
func testSplash(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, keyboard *input.KeyboardEventWriter) error {
const (
// The splash must be shown twice per user at most.
showSplashLimit = 2
)
for i, test := range []struct {
apkName string
pkgName string
activityName string
method inputMethodType
}{
{resizeLockApkName, resizeLockTestPkgName, resizeLockMainActivityName, inputMethodClick},
{resizeLock2ApkName, resizeLock2PkgName, resizeLockMainActivityName, inputMethodKeyEvent},
{resizeLock3ApkName, resizeLock3PkgName, resizeLockMainActivityName, inputMethodClick},
} {
activity, err := arc.NewActivity(a, test.pkgName, test.activityName)
if err != nil {
return errors.Wrapf(err, "failed to create %s", test.activityName)
}
defer activity.Close()
if err := activity.Start(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to start %s", test.activityName)
}
defer activity.Stop(ctx, tconn)
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, phoneResizeLockMode, i < showSplashLimit /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify resize lock state of %s", resizeLockMainActivityName)
}
if i < showSplashLimit {
if err := closeSplash(ctx, tconn, test.method, keyboard); err != nil {
return errors.Wrapf(err, "failed to close the splash screen of %s via %s", resizeLockMainActivityName, test.method)
}
}
// Close and reopen the activity, and verify that the splash is not shown on the same app more than once.
if err := activity.Stop(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to stop %s", test.activityName)
}
if err := activity.Start(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to start %s", test.activityName)
}
defer activity.Stop(ctx, tconn)
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, phoneResizeLockMode, false /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify resize lock state of %s", resizeLockMainActivityName)
}
if err := activity.Stop(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to stop %s", test.activityName)
}
}
return nil
}
// testResizeLockedAppCUJ goes though the critical user journey of a resize-locked app via both click and keyboard, and verifies the app behaves expectedly.
func testResizeLockedAppCUJ(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, keyboard *input.KeyboardEventWriter) error {
for _, test := range []struct {
packageName string
apkName string
activityName string
method inputMethodType
}{
{resizeLockTestPkgName, resizeLockApkName, resizeLockMainActivityName, inputMethodClick},
{resizeLock2PkgName, resizeLock2ApkName, resizeLockMainActivityName, inputMethodKeyEvent},
} {
if err := testResizeLockedAppCUJInternal(ctx, tconn, a, d, cr, test.packageName, test.apkName, test.activityName, test.method, keyboard); err != nil {
return errors.Wrapf(err, "failed to run the critical user journey for %s via %s", test.apkName, test.method)
}
}
return nil
}
// testResizeLockedAppCUJInternal goes though the critical user journey of the given resize-locked app via the given input method.
func testResizeLockedAppCUJInternal(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, packageName, apkName, activityName string, method inputMethodType, keyboard *input.KeyboardEventWriter) error {
activity, err := arc.NewActivity(a, packageName, resizeLockMainActivityName)
if err != nil {
return errors.Wrapf(err, "failed to create %s", resizeLockMainActivityName)
}
defer activity.Close()
if err := activity.Start(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to start %s", resizeLockMainActivityName)
}
defer activity.Stop(ctx, tconn)
// Verify the initial state of a normal resize-locked app.
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, phoneResizeLockMode, false /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify resize lock state of %s", resizeLockMainActivityName)
}
for _, test := range []struct {
currentMode resizeLockMode
nextMode resizeLockMode
action confirmationDialogAction
}{
// Check the cancel button does nothing.
{phoneResizeLockMode, resizableResizeLockMode, dialogActionCancel},
// Toggle between Phone and Tablet.
{phoneResizeLockMode, tabletResizeLockMode, dialogActionNoDialog},
{tabletResizeLockMode, phoneResizeLockMode, dialogActionNoDialog},
// Toggle between Phone and Resizable without "Don't ask me again" checked.
{phoneResizeLockMode, resizableResizeLockMode, dialogActionConfirm},
{resizableResizeLockMode, phoneResizeLockMode, dialogActionNoDialog},
// Toggle between Phone and Resizable with "Don't ask me again" checked.
{phoneResizeLockMode, resizableResizeLockMode, dialogActionConfirmWithDoNotAskMeAgainChecked},
{resizableResizeLockMode, phoneResizeLockMode, dialogActionNoDialog},
{phoneResizeLockMode, resizableResizeLockMode, dialogActionNoDialog},
} {
if err := toggleResizeLockMode(ctx, tconn, a, d, cr, activity, test.currentMode, test.nextMode, test.action, method, keyboard); err != nil {
return errors.Wrapf(err, "failed to change the resize lock mode of %s from %s to %s", resizeLockApkName, test.currentMode, test.nextMode)
}
// Verify that relaunching an app doesn't cause any inconsistency.
if err := activity.Stop(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to stop %s", resizeLockMainActivityName)
}
if err := activity.Start(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to start %s", resizeLockMainActivityName)
}
expectedMode := test.nextMode
if test.action == dialogActionCancel {
expectedMode = test.currentMode
}
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, expectedMode, false /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify resize lock state of %s", resizeLockMainActivityName)
}
}
for _, test := range []struct {
currentMode resizeLockMode
nextMode resizeLockMode
}{
{resizableResizeLockMode, phoneResizeLockMode},
{phoneResizeLockMode, resizableResizeLockMode},
{resizableResizeLockMode, phoneResizeLockMode},
} {
// Toggle the resizability state via the Chrome OS setting toggle.
if err := toggleAppManagementSettingToggle(ctx, tconn, a, d, cr, activity, resizeLockAppName, test.currentMode, test.nextMode, method, keyboard); err != nil {
return errors.Wrapf(err, "failed to toggle the resizability state from %s to %s on the Chrome OS settings", test.currentMode, test.nextMode)
}
}
return nil
}
// checkResizeLockState verifies the various properties that depend on resize lock state.
func checkResizeLockState(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, mode resizeLockMode, isSplashVisible bool) error {
resizeLocked := mode == phoneResizeLockMode || mode == tabletResizeLockMode
if err := checkResizability(ctx, tconn, a, d, activity, mode); err != nil {
return errors.Wrapf(err, "failed to verify the resizability of %s", activity.ActivityName())
}
if err := checkVisibility(ctx, tconn, bubbleDialogClassName, isSplashVisible); err != nil {
return errors.Wrapf(err, "failed to verify the visibility of the splash screen on %s", activity.ActivityName())
}
if err := checkCompatModeButton(ctx, tconn, a, d, cr, activity, mode); err != nil {
return errors.Wrapf(err, "failed to verify the type of the compat mode button of %s", activity.ActivityName())
}
if err := checkBorder(ctx, tconn, a, d, cr, activity, resizeLocked /* shouldShowBorder */); err != nil {
return errors.Wrapf(err, "failed to verify the visibility of the resize lock window border of %s", activity.ActivityName())
}
// There's no orientation rule for non-resize-locked apps, so only check the phone and tablet modes.
if mode == tabletResizeLockMode {
if err := checkOrientation(ctx, tconn, a, d, cr, activity, tabletOrientation); err != nil {
return errors.Wrapf(err, "failed to verify %s has tablet orientation", activity.ActivityName())
}
} else if mode == phoneResizeLockMode {
if err := checkOrientation(ctx, tconn, a, d, cr, activity, phoneOrientation); err != nil {
return errors.Wrapf(err, "failed to verify %s has tablet orientation", activity.ActivityName())
}
}
if err := checkMaximizeRestoreButtonVisibility(ctx, tconn, a, d, cr, activity, mode); err != nil {
return errors.Wrapf(err, "failed to verify the visibility of maximize/restore button for %s", activity.ActivityName())
}
return nil
}
// getExpectedResizability returns the resizability based on the resize lock state and the pure resizability of the app.
func getExpectedResizability(activity *arc.Activity, mode resizeLockMode) bool {
// Resize-locked apps are unresizable.
if mode == phoneResizeLockMode || mode == tabletResizeLockMode {
return false
}
// The activity with resizability false in its manifest is unresizable.
if activity.ActivityName() == resizeLockUnresizableUnspecifiedActivityName {
return false
}
return true
}
// checkMaximizeRestoreButtonVisibility verifies the visibility of the maximize/restore button of the given app.
func checkMaximizeRestoreButtonVisibility(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, mode resizeLockMode) error {
return testing.Poll(ctx, func(ctx context.Context) error {
expected := ash.CaptionButtonBack | ash.CaptionButtonMinimize | ash.CaptionButtonClose
// The visibility of the maximize/restore button matches the resizability of the app.
if getExpectedResizability(activity, mode) {
expected |= ash.CaptionButtonMaximizeAndRestore
}
return wm.CompareCaption(ctx, tconn, activity.PackageName(), expected)
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// checkOrientation verifies the orientation of the given app.
func checkOrientation(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, expectedOrientation orientation) error {
return testing.Poll(ctx, func(ctx context.Context) error {
actualOrientation, err := activityOrientation(ctx, tconn, activity)
if err != nil {
return errors.Wrapf(err, "failed to get the current orientation of %s", activity.PackageName())
}
if actualOrientation != expectedOrientation {
errors.Errorf("failed to verify the orientation; want: %s, got: %s", expectedOrientation, actualOrientation)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// activityOrientation returns the current orientation of the given app.
func activityOrientation(ctx context.Context, tconn *chrome.TestConn, activity *arc.Activity) (orientation, error) {
window, err := ash.GetARCAppWindowInfo(ctx, tconn, activity.PackageName())
if err != nil {
return maximizedOrientation, errors.Wrapf(err, "failed to ARC window infomation for package name %s", activity.PackageName())
}
if window.State == ash.WindowStateMaximized || window.State == ash.WindowStateFullscreen {
return maximizedOrientation, nil
}
if window.BoundsInRoot.Width < window.BoundsInRoot.Height {
return phoneOrientation, nil
}
return tabletOrientation, nil
}
// checkCompatModeButton verifies the state of the compat-mode button of the given app.
func checkCompatModeButton(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, mode resizeLockMode) error {
if mode == nonEligibleResizeLockMode {
return checkVisibility(ctx, tconn, centerButtonClassName, false /* visible */)
}
return testing.Poll(ctx, func(ctx context.Context) error {
button, err := chromeui.Find(ctx, tconn, chromeui.FindParams{ClassName: centerButtonClassName})
if err != nil {
return errors.Wrap(err, "failed to find the compat-mode button")
}
button.Release(ctx)
if button.Name != mode.String() {
return errors.Errorf("failed to verify the name of compat-mode button; got: %s, want: %s", button.Name, mode)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// checkClientContent verifies the client content fills the entire window.
// This is useful to check if switching between phone and tablet modes causes any UI glich.
func checkClientContent(ctx context.Context, tconn *chrome.TestConn, cr *chrome.Chrome, activity *arc.Activity) error {
return testing.Poll(ctx, func(ctx context.Context) error {
bounds, err := activity.WindowBounds(ctx)
if err != nil {
return errors.Wrapf(err, "failed to get the window bounds of %s", activity.ActivityName())
}
img, err := screenshot.GrabScreenshot(ctx, cr)
if err != nil {
return testing.PollBreak(err)
}
dispMode, err := ash.PrimaryDisplayMode(ctx, tconn)
if err != nil {
return errors.Wrap(err, "failed to get display mode of the primary display")
}
windowInfo, err := ash.GetARCAppWindowInfo(ctx, tconn, activity.PackageName())
if err != nil {
return errors.Wrapf(err, "failed to get arc app window info for %s", activity.PackageName())
}
captionHeight := int(math.Round(float64(windowInfo.CaptionHeight) * dispMode.DeviceScaleFactor))
totalPixels := (bounds.Height - captionHeight) * bounds.Width
bluePixels := imgcmp.CountPixels(img, color.RGBA{0, 0, 255, 255})
bluePercent := bluePixels * 100 / totalPixels
if bluePercent < clientContentColorPixelPercentThreshold {
return errors.Errorf("failed to verify the number of the blue pixels exceeds the threshold (%d%%); contains %d / %d (%d%%) blue pixels", clientContentColorPixelPercentThreshold, bluePixels, totalPixels, bluePercent)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second, Interval: time.Second})
}
// checkBorder checks whether the special window border for compatibility mode is shown or not.
// This functions takes a screenshot of the display, and counts the number of pixels that are dark gray around the window border.
func checkBorder(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, shouldShowBorder bool) error {
return testing.Poll(ctx, func(ctx context.Context) error {
bounds, err := activity.WindowBounds(ctx)
if err != nil {
return errors.Wrapf(err, "failed to get the window bounds of %s", activity.ActivityName())
}
img, err := screenshot.GrabScreenshot(ctx, cr)
if err != nil {
return testing.PollBreak(err)
}
rect := img.Bounds()
borderColorPixels := 0
for y := rect.Min.Y; y < rect.Max.Y; y++ {
for x := rect.Min.X; x < rect.Max.X; x++ {
onLeftBorder := bounds.Left-borderWidthPX <= x && x < bounds.Left
onTopBorder := bounds.Top-borderWidthPX <= y && y < bounds.Top
onRightBorder := bounds.Right() < x && x <= bounds.Right()+borderWidthPX
onBottomBorder := bounds.Bottom() < y && y <= bounds.Bottom()+borderWidthPX
onBorder := onLeftBorder || onTopBorder || onRightBorder || onBottomBorder
if onBorder && colorcmp.ColorsMatch(img.At(x, y), color.RGBA{155, 155, 155, 255}, pixelColorDiffMargin) {
borderColorPixels++
}
}
}
if shouldShowBorder && borderColorPixels < borderColorPixelCountThreshold {
return errors.Errorf("failed to verify that the window border is visible; contains %d border color pixels", borderColorPixels)
}
if !shouldShowBorder && borderColorPixels > borderColorPixelCountThreshold {
return errors.Errorf("failed to verify that the window border is invisible; contains %d border color pixels", borderColorPixels)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// 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 {
if visible {
return chromeui.WaitUntilExists(ctx, tconn, chromeui.FindParams{ClassName: className}, 10*time.Second)
}
return chromeui.WaitUntilGone(ctx, tconn, chromeui.FindParams{ClassName: className}, 10*time.Second)
}
// checkResizability verifies the given app's resizability.
func checkResizability(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, activity *arc.Activity, mode resizeLockMode) error {
return testing.Poll(ctx, func(ctx context.Context) error {
window, err := ash.GetARCAppWindowInfo(ctx, tconn, activity.PackageName())
if err != nil {
return errors.Wrapf(err, "failed to get the ARC window infomation for package name %s", activity.PackageName())
}
shouldBeResizable := getExpectedResizability(activity, mode)
if window.CanResize != shouldBeResizable {
return errors.Errorf("failed to verify the resizability; got %t, want %t", window.CanResize, shouldBeResizable)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// showCompatModeMenu shows the compat-mode menu via the given method.
func showCompatModeMenu(ctx context.Context, tconn *chrome.TestConn, method inputMethodType, keyboard *input.KeyboardEventWriter) error {
switch method {
case inputMethodClick:
return showCompatModeMenuViaButtonClick(ctx, tconn)
case inputMethodKeyEvent:
return showCompatModeMenuViaKeyboard(ctx, tconn, keyboard)
}
return errors.Errorf("invalid inputMethodType is given: %s", method)
}
// showCompatModeMenuViaButtonClick clicks on the compat-mode button and shows the compat-mode menu.
func showCompatModeMenuViaButtonClick(ctx context.Context, tconn *chrome.TestConn) error {
icon, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{ClassName: centerButtonClassName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the compat-mode button")
}
defer icon.Release(ctx)
if err := icon.LeftClick(ctx); err != nil {
return errors.Wrap(err, "failed to click on the compat-mode button")
}
return checkVisibility(ctx, tconn, bubbleDialogClassName, true /* visible */)
}
// showCompatModeMenuViaKeyboard injects the keyboard shortcut and shows the compat-mode menu.
func showCompatModeMenuViaKeyboard(ctx context.Context, tconn *chrome.TestConn, keyboard *input.KeyboardEventWriter) error {
return testing.Poll(ctx, func(ctx context.Context) error {
if err := keyboard.Accel(ctx, "Search+Alt+C"); err != nil {
return errors.Wrap(err, "failed to inject Search+Alt+C")
}
if err := chromeui.WaitUntilExists(ctx, tconn, chromeui.FindParams{ClassName: bubbleDialogClassName}, 2*time.Second); err != nil {
return errors.Wrap(err, "failed to find the compat-mode dialog")
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// waitForCompatModeMenuToDisappear waits for the compat-mode menu to disappear.
// After one of the resize lock mode buttons are selected, the compat mode menu disappears after a few seconds of delay.
// Can't use chromeui.WaitUntilGone() for this purpose because this function also checks whether the dialog has the "Phone" button or not to ensure that we are checking the correct dialog.
func waitForCompatModeMenuToDisappear(ctx context.Context, tconn *chrome.TestConn) error {
return testing.Poll(ctx, func(ctx context.Context) error {
dialog, err := chromeui.Find(ctx, tconn, chromeui.FindParams{ClassName: bubbleDialogClassName})
if err == nil && dialog != nil {
defer dialog.Release(ctx)
phoneButton, err := dialog.Descendant(ctx, chromeui.FindParams{Name: phoneButtonName})
if err == nil && phoneButton != nil {
phoneButton.Release(ctx)
return errors.Wrap(err, "compat mode menu is sitll visible")
}
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// closeSplash closes the splash screen via the given method.
func closeSplash(ctx context.Context, tconn *chrome.TestConn, method inputMethodType, keyboard *input.KeyboardEventWriter) error {
splash, err := chromeui.Find(ctx, tconn, chromeui.FindParams{ClassName: bubbleDialogClassName})
if err != nil {
return errors.Wrap(err, "failed to find the splash dialog")
}
defer splash.Release(ctx)
switch method {
case inputMethodClick:
return closeSplashViaClick(ctx, tconn, splash)
case inputMethodKeyEvent:
return closeSplashViaKeyboard(ctx, tconn, splash, keyboard)
}
return nil
}
// closeSplashViaKeyboard presses the Enter key and closes the splash screen.
func closeSplashViaKeyboard(ctx context.Context, tconn *chrome.TestConn, splash *chromeui.Node, keyboard *input.KeyboardEventWriter) error {
return testing.Poll(ctx, func(ctx context.Context) error {
if err := keyboard.Accel(ctx, "Enter"); err != nil {
return errors.Wrap(err, "failed to press the Enter key")
}
return checkVisibility(ctx, tconn, bubbleDialogClassName, false /* visible */)
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// closeSplashViaClick clicks on the close button and closes the splash screen.
func closeSplashViaClick(ctx context.Context, tconn *chrome.TestConn, splash *chromeui.Node) error {
return testing.Poll(ctx, func(ctx context.Context) error {
button, err := splash.Descendant(ctx, chromeui.FindParams{Name: splashCloseButtonName})
if err != nil {
return errors.Wrap(err, "failed to find the close button of the splash dialog")
}
defer button.Release(ctx)
if err := button.LeftClick(ctx); err != nil {
return errors.Wrap(err, "failed to click on the close button of the splash dialog")
}
return checkVisibility(ctx, tconn, bubbleDialogClassName, false /* visible */)
}, &testing.PollOptions{Timeout: 10 * time.Second})
}
// toggleResizeLockMode shows the compat-mode menu, selects one of the resize lock mode buttons on the compat-mode menu via the given method, and verifies the post state.
func toggleResizeLockMode(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, currentMode, nextMode resizeLockMode, action confirmationDialogAction, method inputMethodType, keyboard *input.KeyboardEventWriter) error {
preToggleOrientation, err := activityOrientation(ctx, tconn, activity)
if err != nil {
return errors.Wrapf(err, "failed to get the pre-toggle orientation of %s", activity.PackageName())
}
if err := showCompatModeMenu(ctx, tconn, method, keyboard); err != nil {
return errors.Wrapf(err, "failed to show the compat-mode dialog of %s via %s", activity.ActivityName(), method)
}
compatModeMenuDialog, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{ClassName: bubbleDialogClassName}, 10*time.Second)
if err != nil {
return errors.Wrapf(err, "failed to find the compat-mode menu dialog of %s", activity.ActivityName())
}
defer compatModeMenuDialog.Release(ctx)
switch method {
case inputMethodClick:
if err := selectResizeLockModeViaClick(ctx, nextMode, compatModeMenuDialog); err != nil {
return errors.Wrapf(err, "failed to click on the compat-mode dialog of %s via click", activity.ActivityName())
}
case inputMethodKeyEvent:
if err := shiftViaTabAndEnter(ctx, tconn, compatModeMenuDialog, chromeui.FindParams{Name: nextMode.String()}, keyboard); err != nil {
return errors.Wrapf(err, "failed to click on the compat-mode dialog of %s via keyboard", activity.ActivityName())
}
}
expectedMode := nextMode
if action == dialogActionCancel {
expectedMode = currentMode
}
if action != dialogActionNoDialog {
if err := waitForCompatModeMenuToDisappear(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to wait for the compat-mode menu of %s to disappear", activity.ActivityName())
}
confirmationDialog, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{ClassName: overlayDialogClassName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the resizability confirmation dialog")
}
defer confirmationDialog.Release(ctx)
switch method {
case inputMethodClick:
if err := handleConfirmationDialogViaClick(ctx, tconn, nextMode, confirmationDialog, action); err != nil {
return errors.Wrapf(err, "failed to handle the confirmation dialog of %s via click", activity.ActivityName())
}
case inputMethodKeyEvent:
if err := handleConfirmationDialogViaKeyboard(ctx, tconn, nextMode, confirmationDialog, action, keyboard); err != nil {
return errors.Wrapf(err, "failed to handle the confirmation dialog of %s via keyboard", activity.ActivityName())
}
}
}
// The compat-mode dialog stays shown for two seconds by default after resize lock mode is toggled.
// Explicitly close the dialog using the Esc key.
if err := testing.Poll(ctx, func(ctx context.Context) error {
if err := keyboard.Accel(ctx, "Esc"); err != nil {
return errors.Wrap(err, "failed to press the Esc key")
}
return checkVisibility(ctx, tconn, bubbleDialogClassName, false /* visible */)
}, &testing.PollOptions{Timeout: 5 * time.Second}); err != nil {
return errors.Wrap(err, "failed to verify that the resizability confirmation dialog is invisible")
}
postToggleOrientation, err := activityOrientation(ctx, tconn, activity)
if err != nil {
return errors.Wrapf(err, "failed to get the post-toggle orientation of %s", activity.PackageName())
}
if preToggleOrientation != postToggleOrientation {
if err := checkClientContent(ctx, tconn, cr, activity); err != nil {
return errors.Wrapf(err, "failed to verify the client content fills the window of %s", activity.ActivityName())
}
}
return checkResizeLockState(ctx, tconn, a, d, cr, activity, expectedMode, false /* isSplashVisible */)
}
// handleConfirmationDialogViaKeyboard does the given action for the confirmation dialog via keyboard.
func handleConfirmationDialogViaKeyboard(ctx context.Context, tconn *chrome.TestConn, mode resizeLockMode, confirmationDialog *chromeui.Node, action confirmationDialogAction, keyboard *input.KeyboardEventWriter) error {
if action == dialogActionCancel {
return shiftViaTabAndEnter(ctx, tconn, confirmationDialog, chromeui.FindParams{Name: cancelButtonName}, keyboard)
} else if action == dialogActionConfirm || action == dialogActionConfirmWithDoNotAskMeAgainChecked {
if action == dialogActionConfirmWithDoNotAskMeAgainChecked {
if err := shiftViaTabAndEnter(ctx, tconn, confirmationDialog, chromeui.FindParams{ClassName: checkBoxClassName}, keyboard); err != nil {
return errors.Wrap(err, "failed to select the checkbox of the resizability confirmation dialog via keyboard")
}
}
return shiftViaTabAndEnter(ctx, tconn, confirmationDialog, chromeui.FindParams{Name: confirmButtonName}, keyboard)
}
return nil
}
// handleConfirmationDialogViaClick does the given action for the confirmation dialog via click.
func handleConfirmationDialogViaClick(ctx context.Context, tconn *chrome.TestConn, mode resizeLockMode, confirmationDialog *chromeui.Node, action confirmationDialogAction) error {
if action == dialogActionCancel {
cancelButton, err := confirmationDialog.DescendantWithTimeout(ctx, chromeui.FindParams{Name: cancelButtonName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the cancel button on the compat mode menu")
}
return cancelButton.LeftClick(ctx)
} else if action == dialogActionConfirm || action == dialogActionConfirmWithDoNotAskMeAgainChecked {
if action == dialogActionConfirmWithDoNotAskMeAgainChecked {
checkbox, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{ClassName: checkBoxClassName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the checkbox of the resizability confirmation dialog")
}
defer checkbox.Release(ctx)
if err := checkbox.LeftClick(ctx); err != nil {
return errors.Wrap(err, "failed to click on the checkbox of the resizability confirmation dialog")
}
}
confirmButton, err := confirmationDialog.DescendantWithTimeout(ctx, chromeui.FindParams{Name: confirmButtonName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the confirm button on the compat mode menu")
}
return confirmButton.LeftClick(ctx)
}
return nil
}
// selectResizeLockModeViaClick clicks on the given resize lock mode button.
func selectResizeLockModeViaClick(ctx context.Context, mode resizeLockMode, compatModeMenuDialog *chromeui.Node) error {
resizeLockModeButton, err := compatModeMenuDialog.DescendantWithTimeout(ctx, chromeui.FindParams{Name: mode.String()}, 10*time.Second)
if err != nil {
return errors.Wrapf(err, "failed to find the %s button on the compat mode menu", mode)
}
defer resizeLockModeButton.Release(ctx)
return resizeLockModeButton.LeftClick(ctx)
}
// shiftViaTabAndEnter keeps pressing the Tab key until the UI element of interest gets focus, and press the Enter key.
func shiftViaTabAndEnter(ctx context.Context, tconn *chrome.TestConn, parent *chromeui.Node, params chromeui.FindParams, keyboard *input.KeyboardEventWriter) error {
if err := testing.Poll(ctx, func(ctx context.Context) error {
if err := keyboard.Accel(ctx, "Tab"); err != nil {
return errors.Wrap(err, "failed to press the Tab key")
}
var node *chromeui.Node
var err error
if parent != nil {
node, err = parent.DescendantWithTimeout(ctx, params, 10*time.Second)
} else {
node, err = chromeui.FindWithTimeout(ctx, tconn, params, 10*time.Second)
}
if err != nil {
return errors.Wrap(err, "failed to find the node seeking focus")
}
if !node.State[chromeui.StateTypeFocused] {
return errors.New("failed to wait for the node to get focus")
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second}); err != nil {
return errors.Wrap(err, "failed to shift focus to the node to click on")
}
return keyboard.Accel(ctx, "Enter")
}
// toggleAppManagementSettingToggle opens the app-management page for the given app via the shelf icon, toggles the resize lock setting, and verifies the states of the app and the setting toggle.
func toggleAppManagementSettingToggle(ctx context.Context, tconn *chrome.TestConn, a *arc.ARC, d *ui.Device, cr *chrome.Chrome, activity *arc.Activity, appName string, currentMode, nextMode resizeLockMode, method inputMethodType, keyboard *input.KeyboardEventWriter) error {
// This check must be done before opening the Chrome OS settings page so it won't affect the screenshot taken in one of the checks.
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, currentMode, false /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify resize lock state of %s", appName)
}
if err := openAppManagementSetting(ctx, tconn, appName); err != nil {
return errors.Wrapf(err, "failed to open the app management page of %s", appName)
}
if err := checkAppManagementSettingToggleState(ctx, tconn, currentMode); err != nil {
return errors.Wrap(err, "failed to verify the state of the setting toggle before toggling the setting")
}
switch method {
case inputMethodClick:
if err := toggleAppManagementSettingToggleViaClick(ctx, tconn); err != nil {
return errors.Wrap(err, "failed to toggle the resize-lock setting toggle on the Chrome OS settings via click")
}
case inputMethodKeyEvent:
if err := shiftViaTabAndEnter(ctx, tconn, nil, chromeui.FindParams{Name: appManagementSettingToggleName}, keyboard); err != nil {
return errors.Wrap(err, "failed to toggle the resize-lock setting toggle on the Chrome OS settings via keyboard")
}
}
if err := checkAppManagementSettingToggleState(ctx, tconn, nextMode); err != nil {
return errors.Wrap(err, "failed to verify the state of the setting toggle after toggling the setting")
}
if err := closeAppManagementSetting(ctx, tconn); err != nil {
return errors.Wrapf(err, "failed to close the app management page of %s", appName)
}
// This check must be done after closing the Chrome OS settings page so it won't affect the screenshot taken in one of the checks.
if err := checkResizeLockState(ctx, tconn, a, d, cr, activity, nextMode, false /* isSplashVisible */); err != nil {
return errors.Wrapf(err, "failed to verify resize lock state of %s", resizeLockMainActivityName)
}
return nil
}
// toggleAppManagementSettingToggleViaClick toggles the resize-lock setting toggle via click.
func toggleAppManagementSettingToggleViaClick(ctx context.Context, tconn *chrome.TestConn) error {
settingToggle, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{Name: appManagementSettingToggleName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the setting toggle")
}
defer settingToggle.Release(ctx)
return settingToggle.LeftClick(ctx)
}
// openAppManagementSetting opens the app management page if the given app.
func openAppManagementSetting(ctx context.Context, tconn *chrome.TestConn, appName string) error {
resizeLockShelfIcon, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{Name: appName, ClassName: shelfIconClassName}, 10*time.Second)
if err != nil {
return errors.Wrapf(err, "failed to find the shelf icon of %s", appName)
}
defer resizeLockShelfIcon.Release(ctx)
if err := resizeLockShelfIcon.RightClick(ctx); err != nil {
return errors.Wrapf(err, "failed to click on the shelf icon of %s", appName)
}
appInfoMenuItem, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{Name: appInfoMenuItemViewName, ClassName: menuItemViewClassName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the menu item for the app-management page")
}
defer appInfoMenuItem.Release(ctx)
return appInfoMenuItem.LeftClick(ctx)
}
// closeAppManagementSetting closes any open app management page.
func closeAppManagementSetting(ctx context.Context, tconn *chrome.TestConn) error {
settingShelfIcon, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{Name: settingsAppName, ClassName: shelfIconClassName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the shelf icon of the settings app")
}
defer settingShelfIcon.Release(ctx)
if err := settingShelfIcon.RightClick(ctx); err != nil {
return errors.Wrap(err, "failed to click on the shelf icon of the settings app")
}
closeMenuItem, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{Name: closeMenuItemViewName, ClassName: menuItemViewClassName}, 10*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the menu item for closing the settings app")
}
defer closeMenuItem.Release(ctx)
return closeMenuItem.LeftClick(ctx)
}
// checkAppManagementSettingToggleState verifies the resize lock setting state on the app-management page.
// The app management page must be open when this function is called.
func checkAppManagementSettingToggleState(ctx context.Context, tconn *chrome.TestConn, mode resizeLockMode) error {
return testing.Poll(ctx, func(ctx context.Context) error {
settingToggle, err := chromeui.FindWithTimeout(ctx, tconn, chromeui.FindParams{Name: appManagementSettingToggleName}, 2*time.Second)
if err != nil {
return errors.Wrap(err, "failed to find the resize lock setting toggle on the app-management page")
}
defer settingToggle.Release(ctx)
if ((mode == phoneResizeLockMode || mode == tabletResizeLockMode) && settingToggle.Checked == chromeui.CheckedStateFalse) ||
(mode == resizableResizeLockMode && settingToggle.Checked == chromeui.CheckedStateTrue) {
return errors.Errorf("the app-management resize lock setting value (%v) doesn't match the expected curent state (%s)", settingToggle.Checked, mode)
}
return nil
}, &testing.PollOptions{Timeout: 10 * time.Second})
}